├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue_template.yml └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── language-configuration.json ├── package-lock.json ├── package.json ├── res ├── doc │ ├── completion.png │ ├── diagnostic.png │ ├── goto.png │ ├── hover.png │ ├── inactive-regions.png │ ├── inlay-hints.png │ ├── signature.png │ ├── syntax-highlighting.png │ └── variants.png ├── icons │ ├── glsl-icon.svg │ ├── hlsl-icon.svg │ └── wgsl-icon.svg └── logo-shader-validator.png ├── src ├── extension.ts ├── request.ts ├── shaderVariant.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── binary.test.ts │ │ ├── completion.test.ts │ │ ├── diagnostic.test.ts │ │ ├── index.ts │ │ ├── utils.ts │ │ └── version.test.ts ├── validator.ts └── wasm-wasi-lsp.ts ├── syntaxes ├── glsl.tmLanguage.json ├── hlsl.tmLanguage.json └── wgsl.tmLanguage.json ├── test ├── test.frag.glsl ├── test.hlsl └── test.wgsl ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.yml: -------------------------------------------------------------------------------- 1 | name: "Ask a question, report a bug, request a feature, etc." 2 | description: "Ask any question, discuss best practices, report a bug, request a feature." 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | If your issue is related to the features of the extension (goto, diagnostics, enhancement), please post your issue to the [language server repository](https://github.com/antaalt/shader-sense/issues) instead. 8 | 9 | This repository is for the extension only and is mostly UI related issues. 10 | 11 | If you are not sure, feel free to post here, issue might be transfered. 12 | - type: textarea 13 | id: issue 14 | attributes: 15 | label: "Issue:" 16 | placeholder: "Type your issue here..." 17 | validations: 18 | required: true -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Extension 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_call: 9 | inputs: 10 | prerelease: 11 | required: false 12 | type: boolean 13 | default: false 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-24.04 # ubuntu-latest which is still based on 22.04 as of now does not have right GCC lib 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | - name: Install dependencies 25 | run: npm ci 26 | - name: Install VSCE 27 | run: npm i -g vsce 28 | - name: Read package JSON 29 | id: get_package_json 30 | run: | 31 | content=`cat ./package.json` 32 | # the following lines are only required for multi line json 33 | content="${content//'%'/'%25'}" 34 | content="${content//$'\n'/'%0A'}" 35 | content="${content//$'\r'/'%0D'}" 36 | # end of optional handling for multi line json 37 | echo "::set-output name=packageJson::$content" # This is deprecated, should change 38 | # This below should work and is not deprecated but it does not... 39 | #echo "packageJson=${content}" >> $GITHUB_OUTPUT 40 | - name: Display requested server version 41 | run: echo '${{ fromJson(steps.get_package_json.outputs.packageJson).server_version }}' 42 | env: 43 | PACKAGE_JSON: echo $PACKAGE_JSON 44 | - name: Download shader language servers. 45 | uses: robinraju/release-downloader@v1.11 46 | env: 47 | PACKAGE_JSON: echo $PACKAGE_JSON 48 | with: 49 | repository: 'antaalt/shader-sense' 50 | tag: ${{ format('v{0}', fromJson(steps.get_package_json.outputs.packageJson).server_version) }} 51 | fileName: '*.zip' 52 | - name: Create bin folder 53 | run: mkdir ./bin 54 | - name: Copy windows binaries to bin 55 | run: mkdir ./bin/windows && unzip shader-language-server-x86_64-pc-windows-msvc.zip -d ./bin/windows && rm ./shader-language-server-x86_64-pc-windows-msvc.zip 56 | - name: Copy linux binaries to bin 57 | run: mkdir ./bin/linux && unzip shader-language-server-x86_64-unknown-linux-gnu.zip -d ./bin/linux && rm ./shader-language-server-x86_64-unknown-linux-gnu.zip 58 | - name: Copy wasi binaries to bin 59 | run: mkdir ./bin/wasi && unzip shader-language-server-wasm32-wasip1-threads.zip -d ./bin/wasi && rm ./shader-language-server-wasm32-wasip1-threads.zip 60 | - name: Mark server as executable on Linux 61 | run: chmod +x ./bin/linux/shader-language-server 62 | - name: Get server version 63 | run: echo "SERVER_VERSION=$(./bin/linux/shader-language-server --version | sed 's/shader-language-server v//g')" >> $GITHUB_ENV 64 | # VScode need a framebuffer to run test. So create one. 65 | - name: Create framebuffer to run test 66 | run: sudo apt-get install xvfb 67 | - name: Test extension 68 | run: xvfb-run --auto-servernum npm test 69 | - name: Package extension 70 | if: ${{ !inputs.prerelease }} 71 | run: vsce package 72 | - name: Package prerelease extension 73 | if: ${{ inputs.prerelease }} 74 | run: vsce package --pre-release 75 | - name: Get version 76 | run: echo "PACKAGE_VERSION=$(npm pkg get version | sed 's/"//g')" >> $GITHUB_ENV 77 | - name: Check version 78 | run: echo $PACKAGE_VERSION 79 | - name: Copy VSIX 80 | run: mkdir -p ./ext/ && cp ./shader-validator-$PACKAGE_VERSION.vsix ./ext/shader-validator.vsix 81 | - name: Upload artifact 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: extension 85 | path: ./ext/ -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy and publish Extension 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | # https://github.com/marketplace/actions/publish-vs-code-extension 8 | # https://code.visualstudio.com/api/working-with-extensions/continuous-integration 9 | jobs: 10 | build: 11 | uses: ./.github/workflows/build.yml 12 | secrets: inherit # for GH_PAT 13 | with: 14 | prerelease: ${{ github.event.release.prerelease }} 15 | 16 | deploy: 17 | permissions: write-all 18 | needs: build 19 | runs-on: ubuntu-latest 20 | 21 | # Handle secrets: https://docs.github.com/fr/actions/security-guides/using-secrets-in-github-actions 22 | # Get tokens: https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Download VSIX 26 | uses: actions/download-artifact@v4 27 | with: 28 | name: extension 29 | - name: Publish to Open VSX Registry 30 | uses: HaaLeo/publish-vscode-extension@v1 31 | id: publishToOpenVSX 32 | with: 33 | pat: ${{ secrets.OPEN_VSX_PAT }} 34 | extensionFile: shader-validator.vsix 35 | preRelease: ${{ github.event.release.prerelease }} 36 | - name: Publish to Visual Studio Marketplace 37 | uses: HaaLeo/publish-vscode-extension@v1 38 | # Dont publish pre-release here, vscode marketplace dont support semver != major.minor.patch 39 | if: ${{ !github.event.release.prerelease }} 40 | with: 41 | pat: ${{ secrets.MARKETPLACE_PAT }} 42 | registryUrl: https://marketplace.visualstudio.com 43 | extensionFile: shader-validator.vsix 44 | - name: Upload assets to release 45 | run: gh release upload ${{ github.ref_name }} "shader-validator.vsix" 46 | env: 47 | GH_TOKEN: ${{ github.token }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VSCode TS ### 2 | out 3 | dist 4 | node_modules 5 | .vscode-test/ 6 | .vscode-test-web/ 7 | *.vsix 8 | examples/ 9 | 10 | ### Binary ### 11 | # Do not upload binary on Git, uses SHADER_LANGUAGE_SERVER_EXECUTABLE env variable to 12 | # target built version of server, bin folder generated with CI on push. 13 | bin/ 14 | 15 | ### Rust ### 16 | # Generated by Cargo 17 | # will have compiled files and executables 18 | debug/ 19 | target/ 20 | 21 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 22 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 23 | Cargo.lock 24 | 25 | # These are backup files generated by rustfmt 26 | **/*.rs.bk 27 | 28 | # MSVC Windows builds of rustc generate these, which store debugging information 29 | *.pdb -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.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 Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/dist/node/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: compile" 20 | }, 21 | { 22 | "name": "Run Web Extension", 23 | "type": "extensionHost", 24 | "debugWebWorkerHost": true, 25 | "request": "launch", 26 | "runtimeExecutable": "${execPath}", 27 | "args": [ 28 | "--extensionDevelopmentPath=${workspaceFolder}", 29 | "--extensionDevelopmentKind=web" 30 | ], 31 | "outFiles": [ 32 | "${workspaceFolder}/dist/web/**/*.js" 33 | ], 34 | "preLaunchTask": "npm: compile" 35 | }, 36 | { 37 | "name": "Extension Tests", 38 | "type": "extensionHost", 39 | "request": "launch", 40 | "runtimeExecutable": "${execPath}", 41 | "args": [ 42 | "--extensionDevelopmentPath=${workspaceFolder}", 43 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", 44 | "${workspaceRoot}/test" // Active workspace 45 | ], 46 | "outFiles": [ 47 | "${workspaceFolder}/out/test/**/*.js" 48 | ], 49 | "preLaunchTask": "npm: compile" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .vscode-test-web/** 4 | .vscode-test.js 5 | .github/** 6 | src/** 7 | node_modules/** 8 | test/** 9 | out/** 10 | .gitignore 11 | .yarnrc 12 | vsc-extension-quickstart.md 13 | **/tsconfig.json 14 | **/webpack.config.js 15 | **/.eslintrc.json 16 | **/*.map 17 | **/*.ts 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 antaalt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shader validator 2 | 3 | [![extension issues](https://img.shields.io/github/issues/antaalt/shader-validator.svg?label=extension%20issues)](https://github.com/antaalt/shader-validator/issues) 4 | [![server issues](https://img.shields.io/github/issues/antaalt/shader-sense.svg?label=server%20issues)](https://github.com/antaalt/shader-sense/issues) 5 | [![vsmarketplace](https://img.shields.io/visual-studio-marketplace/v/antaalt.shader-validator?color=blue&label=vscode%20marketplace)](https://marketplace.visualstudio.com/items?itemName=antaalt.shader-validator) 6 | [![openVSX registry](https://img.shields.io/open-vsx/v/antaalt/shader-validator?color=purple)](https://open-vsx.org/extension/antaalt/shader-validator) 7 | 8 | This is a vscode extension allowing syntax highlighting, linting & symbol providing for HLSL / GLSL / WGSL shaders. It is using [shader-language-server](https://github.com/antaalt/shader-sense/tree/main/shader-language-server) to lint shaders using common validator API & parse symbols for some code inspection. 9 | 10 | Currently, it support some features and languages: 11 | 12 | - **[Syntax Highlighting](#syntax-highlighting)**: Improved syntax highlighting for code. 13 | - **[Diagnostic](#diagnostics)**: Highlight errors & warning as user type code. 14 | - **[goto](#goto)**: Go to a symbol definition 15 | - **[completion](#autocompletion)**: Suggest completion items 16 | - **[hover](#hover)**: Add tooltip when hovering symbols 17 | - **[signature](#signature)**: Help when selecting a signature 18 | - **[inlay hints](#inlay-hints)**: Add hints to function calls 19 | - **[Variant](#variants)**: Define multiple shader variant entry point & quickly switch between them. 20 | - **[Regions](#regions)**: Detect inactive regions in code due to preprocessor and grey them out. 21 | 22 | |Language|Syntax Highlighting|Diagnostics |User symbols |Built-in symbols|Regions| 23 | |--------|-------------------|------------|-------------|----------------|-------| 24 | |GLSL |✅ |✅(glslang)|✅ |✅ |✅ | 25 | |HLSL |✅ |✅(DXC) |✅ |✅ |✅ | 26 | |WGSL |✅ |✅(Naga) |❌ |❌ |❌ | 27 | 28 | ## Features 29 | 30 | ### Syntax highlighting 31 | 32 | This extension provide improved syntax highlighting for HLSL, GLSL & WGSL than the base one in VS code. 33 | 34 | ![syntax-highlighting](res/doc/syntax-highlighting.png) 35 | 36 | ### Diagnostics 37 | 38 | You cant lint your code in real time through this extension: 39 | 40 | - GLSL relies on Glslang. 41 | - HLSL relies on DirectX shader compiler on desktop, Glslang on the web (see below). 42 | - WGSL relies on Naga. 43 | 44 | ![diagnostic](res/doc/diagnostic.png) 45 | 46 | ### Autocompletion 47 | 48 | The extension will suggest you symbols from your file and intrinsics as you type. 49 | 50 | ![diagnostic](res/doc/completion.png) 51 | 52 | ### Signature 53 | 54 | View available signatures for your function as you type it. 55 | 56 | ![diagnostic](res/doc/signature.png) 57 | 58 | ### Hover 59 | 60 | View informations relative to a symbol by hovering it. 61 | 62 | ![diagnostic](res/doc/hover.png) 63 | 64 | ### Goto 65 | 66 | Go to your declaration definition by clicking on it. 67 | 68 | ![diagnostic](res/doc/goto.png) 69 | 70 | ### Inlay hints 71 | 72 | Add inlay hints to your function calls. 73 | 74 | ![inlay-hints](res/doc/inlay-hints.png) 75 | 76 | You can disable this in settings.json (default pressed is Ctrl+Alt) 77 | ```json 78 | "editor.inlayHints.enabled": "on" 79 | "editor.inlayHints.enabled": "onUnlessPressed" 80 | "editor.inlayHints.enabled": "off" 81 | "editor.inlayHints.enabled": "offUnlessPressed" 82 | ``` 83 | 84 | ### Variants 85 | 86 | Swap shader variant on the fly to change entry point & macro definition. This allow you to define and easily change between the one you have set, affecting regions. For example when you have a lot of entry point in a single shader file, splitted using macros, or want to see the content from your dependencies with the context passed from you main entry point. 87 | 88 | You can then access these variants directly from the dedicated window and then access them by clicking on them. 89 | 90 | A neat feature for big shader codebase with lot of entry point everywhere ! 91 | 92 | ![shader-variant](res/doc/variants.png) 93 | 94 | ### Regions 95 | 96 | Grey out inactive regions depending on currently declared preprocessor & filter symbols. 97 | 98 | ![diagnostic](res/doc/inactive-regions.png) 99 | 100 | ### And much more 101 | 102 | This extension also support some features such as document symbols, workspace symbols... 103 | 104 | ## Extension Settings 105 | 106 | This extension contributes the following settings: 107 | 108 | * `shader-validator.validate`: Enable/disable validation with common API. 109 | * `shader-validator.symbols`: Enable/disable symbol inspection & providers. 110 | * `shader-validator.symbolDiagnostics`: Enable/disable symbol provider debug diagnostics. 111 | * `shader-validator.severity`: Select minimal log severity for linting. 112 | * `shader-validator.includes`: All custom includes for linting. 113 | * `shader-validator.pathRemapping`: All virtual paths. 114 | * `shader-validator.defines`: All custom macros and their values for linting. 115 | * `shader-validator.serverPath`: Use a custom server instead of the bundled one. 116 | 117 | ### HLSL specific settings: 118 | 119 | * `shader-validator.hlsl.shaderModel`: Specify the shader model to target for HLSL 120 | * `shader-validator.hlsl.version`: Specify the HLSL version 121 | * `shader-validator.hlsl.enable16bitTypes`: Enable 16 bit types support with HLSL 122 | 123 | ### GLSL specific settings: 124 | 125 | * `shader-validator.glsl.targetClient`: Specify the OpenGL or Vulkan version for GLSL 126 | * `shader-validator.glsl.spirvVersion`: Specify the SPIRV version to target for GLSL 127 | 128 | ## Platform support 129 | 130 | This extension is supported on every platform, but some limitations are to be expected on some: 131 | - Windows: full feature set. 132 | - Linux: full feature set. 133 | - Mac: Rely on WASI version of server, same as web, see web support for limitations. 134 | 135 | ## Web support 136 | 137 | This extension run on the web on [vscode.dev](https://vscode.dev/). It is relying on the [WebAssembly Execution engine](https://marketplace.visualstudio.com/items?itemName=ms-vscode.wasm-wasi-core). Because of this restriction, we can't use dxc on the web as it does not compile to WASI and instead rely on glslang, which is more limited in linting (Only support some basic features of SM 6.0, while DXC support all newly added SM (current 6.8)). 138 | 139 | ## Credits 140 | 141 | This extension is based on a heavily modified version of PolyMeilex [vscode-wgsl](https://github.com/PolyMeilex/vscode-wgsl) -------------------------------------------------------------------------------- /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 | { "open": "{", "close": "}" }, 17 | { "open": "[", "close": "]" }, 18 | { "open": "(", "close": ")" }, 19 | { "open": "'", "close": "'", "notIn": ["string", "comment"] }, 20 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 21 | { "open": "`", "close": "`", "notIn": ["string", "comment"] }, 22 | { "open": "/**", "close": " */", "notIn": ["string"] } 23 | ], 24 | // By default, VSCode only autclose if there is whitespace after the cursor 25 | "autoCloseBefore": ";:.,=}])>` \n\t", 26 | // symbols that can be used to surround a selection 27 | "surroundingPairs": [ 28 | ["{", "}"], 29 | ["[", "]"], 30 | ["(", ")"], 31 | ["\"", "\""], 32 | ["'", "'"] 33 | ], 34 | "folding": { 35 | "markers": { 36 | "start": "^\\s*//\\s*#?region\\b", 37 | "end": "^\\s*//\\s*#?endregion\\b" 38 | } 39 | }, 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shader-validator", 3 | "displayName": "Shader validator", 4 | "description": "HLSL / GLSL / WGSL language server for vscode", 5 | "icon": "res/logo-shader-validator.png", 6 | "galleryBanner": { 7 | "color": "#9ad0ff", 8 | "theme": "light" 9 | }, 10 | "version": "0.6.3", 11 | "server_version": "0.6.2", 12 | "publisher": "antaalt", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/antaalt/shader-validator" 16 | }, 17 | "engines": { 18 | "vscode": "^1.88.0" 19 | }, 20 | "categories": [ 21 | "Programming Languages" 22 | ], 23 | "keywords": [ 24 | "shader", 25 | "lsp", 26 | "hlsl", 27 | "glsl", 28 | "wgsl" 29 | ], 30 | "activationEvents": [ 31 | "onCommand:shader-validator.validateFile", 32 | "onLanguage:glsl", 33 | "onLanguage:wgsl", 34 | "onLanguage:hlsl" 35 | ], 36 | "main": "./dist/node/extension", 37 | "browser": "./dist/web/extension", 38 | "contributes": { 39 | "languages": [ 40 | { 41 | "id": "hlsl", 42 | "extensions": [ 43 | ".hlsl", 44 | ".hlsli", 45 | ".fx", 46 | ".fxh", 47 | "ush", 48 | "usf" 49 | ], 50 | "icon": { 51 | "light": "./res/icons/hlsl-icon.svg", 52 | "dark": "./res/icons/hlsl-icon.svg" 53 | }, 54 | "configuration": "./language-configuration.json" 55 | }, 56 | { 57 | "id": "glsl", 58 | "extensions": [ 59 | ".glsl", 60 | ".vert", 61 | ".frag", 62 | ".mesh", 63 | ".task", 64 | ".comp", 65 | ".geom", 66 | ".tesc", 67 | ".tese" 68 | ], 69 | "icon": { 70 | "light": "./res/icons/glsl-icon.svg", 71 | "dark": "./res/icons/glsl-icon.svg" 72 | }, 73 | "configuration": "./language-configuration.json" 74 | }, 75 | { 76 | "id": "wgsl", 77 | "extensions": [ 78 | ".wgsl" 79 | ], 80 | "icon": { 81 | "light": "./res/icons/wgsl-icon.svg", 82 | "dark": "./res/icons/wgsl-icon.svg" 83 | }, 84 | "configuration": "./language-configuration.json" 85 | } 86 | ], 87 | "grammars": [ 88 | { 89 | "language": "hlsl", 90 | "scopeName": "source.hlsl", 91 | "path": "./syntaxes/hlsl.tmLanguage.json" 92 | }, 93 | { 94 | "language": "glsl", 95 | "scopeName": "source.glsl", 96 | "path": "./syntaxes/glsl.tmLanguage.json" 97 | }, 98 | { 99 | "language": "wgsl", 100 | "scopeName": "source.wgsl", 101 | "path": "./syntaxes/wgsl.tmLanguage.json" 102 | } 103 | ], 104 | "configuration": [ 105 | { 106 | "title": "Common", 107 | "properties": { 108 | "shader-validator.validate": { 109 | "description": "Validate shader as you type with common validator API.", 110 | "type": "boolean", 111 | "default": true 112 | }, 113 | "shader-validator.symbols": { 114 | "description": "Provide symbol inspection with providers (goto, hover, completion...)", 115 | "type": "boolean", 116 | "default": true 117 | }, 118 | "shader-validator.symbolDiagnostics": { 119 | "description": "Provide diagnostics when symbol provider has issues.", 120 | "type": "boolean", 121 | "default": false 122 | }, 123 | "shader-validator.severity": { 124 | "type": "string", 125 | "description": "Minimum linting severity. Set it lower to display some hints aswell. Might not be supported by all languages.", 126 | "default": "info", 127 | "enum": [ 128 | "none", 129 | "error", 130 | "warning", 131 | "info", 132 | "hint" 133 | ] 134 | }, 135 | "shader-validator.includes": { 136 | "description": "Include paths to look up for includes.", 137 | "type": "array", 138 | "items": { 139 | "type": "string" 140 | }, 141 | "default": [] 142 | }, 143 | "shader-validator.defines": { 144 | "description": "Preprocessor variables and values.", 145 | "type": "object", 146 | "additionalProperties": { 147 | "type": "string" 148 | } 149 | }, 150 | "shader-validator.pathRemapping": { 151 | "description": "Remap virtual path starting with / to an absolute path.", 152 | "type": "object", 153 | "additionalProperties": { 154 | "type": "string" 155 | } 156 | }, 157 | "shader-validator.serverPath": { 158 | "description": "Use an external server instead of the bundled ones.", 159 | "type": "string", 160 | "default": "" 161 | } 162 | } 163 | }, 164 | { 165 | "title": "Hlsl", 166 | "properties": { 167 | "shader-validator.hlsl.shaderModel": { 168 | "type": "string", 169 | "description": "Shader model targeted for DXC HLSL (DXC only support up to sm 6.0).", 170 | "default": "ShaderModel6_8", 171 | "enum": [ 172 | "ShaderModel6", 173 | "ShaderModel6_1", 174 | "ShaderModel6_2", 175 | "ShaderModel6_3", 176 | "ShaderModel6_4", 177 | "ShaderModel6_5", 178 | "ShaderModel6_6", 179 | "ShaderModel6_7", 180 | "ShaderModel6_8" 181 | ], 182 | "enumItemLabels": [ 183 | "sm 6.0", 184 | "sm 6.1", 185 | "sm 6.2", 186 | "sm 6.3", 187 | "sm 6.4", 188 | "sm 6.5", 189 | "sm 6.6", 190 | "sm 6.7", 191 | "sm 6.8" 192 | ] 193 | }, 194 | "shader-validator.hlsl.version": { 195 | "type": "string", 196 | "description": "HLSL version for DXC.", 197 | "default": "V2021", 198 | "enum": [ 199 | "V2016", 200 | "V2017", 201 | "V2018", 202 | "V2021" 203 | ], 204 | "enumItemLabels": [ 205 | "2016", 206 | "2017", 207 | "2018", 208 | "2021" 209 | ] 210 | }, 211 | "shader-validator.hlsl.enable16bitTypes": { 212 | "type": "boolean", 213 | "description": "Enable 16 bits types. Only supported with sm >= 6.2 & HLSL version >= 2018.", 214 | "default": false 215 | } 216 | } 217 | }, 218 | { 219 | "title": "Glsl", 220 | "properties": { 221 | "shader-validator.glsl.targetClient": { 222 | "type": "string", 223 | "description": "Shader client for GLSL.", 224 | "default": "Vulkan1_3", 225 | "enum": [ 226 | "Vulkan1_0", 227 | "Vulkan1_1", 228 | "Vulkan1_2", 229 | "Vulkan1_3", 230 | "OpenGL450" 231 | ], 232 | "enumItemLabels": [ 233 | "Vulkan 1.0", 234 | "Vulkan 1.1", 235 | "Vulkan 1.2", 236 | "Vulkan 1.3", 237 | "OpenGL" 238 | ] 239 | }, 240 | "shader-validator.glsl.spirvVersion": { 241 | "type": "string", 242 | "description": "SPIRV version targeted for GLSL.", 243 | "default": "SPIRV1_6", 244 | "enum": [ 245 | "SPIRV1_0", 246 | "SPIRV1_1", 247 | "SPIRV1_2", 248 | "SPIRV1_3", 249 | "SPIRV1_4", 250 | "SPIRV1_5", 251 | "SPIRV1_6" 252 | ], 253 | "enumItemLabels": [ 254 | "Spirv 1.0", 255 | "Spirv 1.1", 256 | "Spirv 1.2", 257 | "Spirv 1.3", 258 | "Spirv 1.4", 259 | "Spirv 1.5", 260 | "Spirv 1.6" 261 | ] 262 | } 263 | } 264 | }, 265 | { 266 | "title": "Debug", 267 | "properties": { 268 | "shader-validator.trace.server": { 269 | "type": "string", 270 | "scope": "window", 271 | "enum": [ 272 | "off", 273 | "messages", 274 | "verbose" 275 | ], 276 | "enumDescriptions": [ 277 | "No traces", 278 | "Error only", 279 | "Full log" 280 | ], 281 | "default": "off", 282 | "description": "Trace requests to the shader-language-server (this is usually overly verbose and not recommended for regular users)." 283 | } 284 | } 285 | } 286 | ], 287 | "commands": [ 288 | { 289 | "command": "shader-validator.validateFile", 290 | "title": "Validate file", 291 | "category": "Shader validator", 292 | "icon": "$(check)" 293 | }, 294 | { 295 | "command": "shader-validator.dumpAst", 296 | "title": "Dump ast for debug", 297 | "category": "Shader validator", 298 | "icon": "$(list-tree)" 299 | }, 300 | { 301 | "command": "shader-validator.dumpDependency", 302 | "title": "Dump dependency tree for debug", 303 | "category": "Shader validator", 304 | "icon": "$(list-tree)" 305 | }, 306 | { 307 | "command": "shader-validator.addCurrentFile", 308 | "title": "Add current file", 309 | "category": "Shader validator", 310 | "icon": "$(add)", 311 | "enablement": "view == shader-validator-variants" 312 | }, 313 | { 314 | "command": "shader-validator.editMenu", 315 | "title": "Edit", 316 | "category": "Shader validator", 317 | "icon": "$(edit)", 318 | "enablement": "view == shader-validator-variants" 319 | }, 320 | { 321 | "command": "shader-validator.addMenu", 322 | "title": "Add", 323 | "category": "Shader validator", 324 | "icon": "$(add)", 325 | "enablement": "view == shader-validator-variants" 326 | }, 327 | { 328 | "command": "shader-validator.editMenu", 329 | "title": "Edit", 330 | "category": "Shader validator", 331 | "icon": "$(edit)", 332 | "enablement": "view == shader-validator-variants" 333 | }, 334 | { 335 | "command": "shader-validator.deleteMenu", 336 | "title": "Delete", 337 | "category": "Shader validator", 338 | "icon": "$(trash)", 339 | "enablement": "view == shader-validator-variants" 340 | }, 341 | { 342 | "command": "shader-validator.gotoShaderEntryPoint", 343 | "title": "Go to shader entry point", 344 | "category": "Shader validator", 345 | "icon": "$(arrow-right)" 346 | } 347 | ], 348 | "menus": { 349 | "commandPalette": [ 350 | { 351 | "command": "shader-validator.gotoShaderEntryPoint", 352 | "when": "false" 353 | } 354 | ], 355 | "view/title": [ 356 | { 357 | "command": "shader-validator.addCurrentFile", 358 | "when": "view == shader-validator-variants && editorIsOpen", 359 | "group": "navigation" 360 | } 361 | ], 362 | "view/item/context": [ 363 | { 364 | "command": "shader-validator.addMenu", 365 | "group": "inline@1", 366 | "when": "view == shader-validator-variants && (viewItem == file || viewItem == includeList || viewItem == defineList)" 367 | }, 368 | { 369 | "command": "shader-validator.editMenu", 370 | "group": "inline@2", 371 | "when": "view == shader-validator-variants && (viewItem == variant || viewItem == include || viewItem == define || viewItem == stage)" 372 | }, 373 | { 374 | "command": "shader-validator.deleteMenu", 375 | "group": "inline@3", 376 | "when": "view == shader-validator-variants && (viewItem == variant || viewItem == include || viewItem == define || viewItem == file)" 377 | } 378 | ] 379 | }, 380 | "viewsContainers": { 381 | "activitybar": [ 382 | { 383 | "id": "shader-validator", 384 | "title": "Shader validator", 385 | "icon": "res/icons/hlsl-icon.svg" 386 | } 387 | ] 388 | }, 389 | "views": { 390 | "shader-validator": [ 391 | { 392 | "id": "shader-validator-variants", 393 | "name": "Shader variants" 394 | } 395 | ] 396 | }, 397 | "viewsWelcome": [ 398 | { 399 | "view": "shader-validator-variants", 400 | "contents": "In order to setup a shader variant, you can add a currently opened shader file & configure it from here.\n[Add Current File](command:shader-validator.addCurrentFile)\nShader variant allow you to define a context and an entry point for shader for a better validation & symbol providing experience.", 401 | "when": "workbenchState != empty && editorIsOpen" 402 | }, 403 | { 404 | "view": "shader-validator-variants", 405 | "contents": "Open a file to be able to add it as a shader variant.", 406 | "when": "workbenchState != empty && !editorIsOpen" 407 | }, 408 | { 409 | "view": "shader-validator-variants", 410 | "contents": "Open a file to be able to add it as a shader variant.", 411 | "when": "workbenchState == empty" 412 | } 413 | ] 414 | }, 415 | "scripts": { 416 | "vscode:prepublish": "webpack --mode production", 417 | "compile": "webpack --mode development", 418 | "watch": "webpack --mode development --watch", 419 | "package": "webpack --mode production --devtool hidden-source-map", 420 | "open-in-browser": "vscode-test-web --extensionDevelopmentPath=. .", 421 | "pretest": "webpack --mode development && tsc -p . --outDir out", 422 | "test": "node ./out/test/runTest.js", 423 | "lint": "eslint src --ext ts" 424 | }, 425 | "devDependencies": { 426 | "@types/glob": "^8.0.0", 427 | "@types/mocha": "^10.0.1", 428 | "@types/node": "16.x", 429 | "@types/vscode": "1.88.0", 430 | "@typescript-eslint/eslint-plugin": "^5.45.0", 431 | "@typescript-eslint/parser": "^5.45.0", 432 | "@vscode/test-cli": "^0.0.10", 433 | "@vscode/test-electron": "^2.4.1", 434 | "@vscode/test-web": "^0.0.60", 435 | "eslint": "^8.28.0", 436 | "glob": "^8.0.3", 437 | "mocha": "^10.1.0", 438 | "ts-loader": "^9.5.1", 439 | "typescript": "^4.9.3", 440 | "webpack": "^5.93.0", 441 | "webpack-cli": "^5.1.4" 442 | }, 443 | "dependencies": { 444 | "@vscode/wasm-wasi": "^1.0.1", 445 | "os-browserify": "^0.3.0", 446 | "path-browserify": "^1.0.1", 447 | "vscode-languageclient": "^10.0.0-next.3" 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /res/doc/completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/completion.png -------------------------------------------------------------------------------- /res/doc/diagnostic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/diagnostic.png -------------------------------------------------------------------------------- /res/doc/goto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/goto.png -------------------------------------------------------------------------------- /res/doc/hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/hover.png -------------------------------------------------------------------------------- /res/doc/inactive-regions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/inactive-regions.png -------------------------------------------------------------------------------- /res/doc/inlay-hints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/inlay-hints.png -------------------------------------------------------------------------------- /res/doc/signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/signature.png -------------------------------------------------------------------------------- /res/doc/syntax-highlighting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/syntax-highlighting.png -------------------------------------------------------------------------------- /res/doc/variants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/doc/variants.png -------------------------------------------------------------------------------- /res/icons/glsl-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/icons/hlsl-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/icons/wgsl-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /res/logo-shader-validator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antaalt/shader-validator/7625e0f48baa64bc08267d00c256d1c379efb556/res/logo-shader-validator.png -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | 5 | import { createLanguageClient, getServerPlatform, ServerPlatform } from './validator'; 6 | import { dumpAstRequest, dumpDependencyRequest } from './request'; 7 | import { ShaderVariantTreeDataProvider } from './shaderVariant'; 8 | import { DidChangeConfigurationNotification, LanguageClient } from 'vscode-languageclient'; 9 | 10 | export let sidebar: ShaderVariantTreeDataProvider; 11 | 12 | // This method is called when your extension is activated 13 | // Your extension is activated the very first time the command is executed 14 | export async function activate(context: vscode.ExtensionContext) 15 | { 16 | // Install dependencies if running on wasi 17 | const msWasmWasiCoreName = 'ms-vscode.wasm-wasi-core'; 18 | const msWasmWasiCore = vscode.extensions.getExtension(msWasmWasiCoreName); 19 | if (msWasmWasiCore === undefined && getServerPlatform() === ServerPlatform.wasi) 20 | { 21 | const message = 'It is required to install Microsoft WASM WASI core extension for running the shader validator server on wasi. Do you want to install it now?'; 22 | const choice = await vscode.window.showInformationMessage(message, 'Install', 'Not now'); 23 | if (choice === 'Install') { 24 | // Wait for extension to be correctly installed. 25 | await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, 26 | (progress) => { 27 | progress.report({ message: "Installing Microsoft WASM wasi core extension" }); 28 | return new Promise((resolve, reject) => { 29 | vscode.extensions.onDidChange((e) => { 30 | console.assert(vscode.extensions.getExtension(msWasmWasiCoreName) !== undefined, "Failed to load WASM wasi core."); 31 | resolve(); 32 | }); 33 | vscode.commands.executeCommand("workbench.extensions.installExtension", msWasmWasiCoreName); 34 | }); 35 | }, 36 | ); 37 | } else { 38 | vscode.window.showErrorMessage("Extension shader-validator failed to install dependencies. It will not launch the validation server."); 39 | return; // Extension failed to launch. 40 | } 41 | } 42 | 43 | // Create language client 44 | const possiblyNullClient = await createLanguageClient(context); 45 | if (possiblyNullClient === null) { 46 | console.error("Failed to launch shader-validator language server."); 47 | return; 48 | } 49 | let client = possiblyNullClient; 50 | 51 | // Create sidebar 52 | sidebar = new ShaderVariantTreeDataProvider(context, client); 53 | 54 | // Subscribe for dispose 55 | context.subscriptions.push(vscode.Disposable.from(client)); 56 | 57 | // Subscribe commands 58 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.validateFile", (uri: vscode.Uri) => { 59 | //client.sendRequest() 60 | vscode.window.showInformationMessage("Cannot validate file manually for now"); 61 | })); 62 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.dumpAst", () => { 63 | let activeTextEditor = vscode.window.activeTextEditor; 64 | if (activeTextEditor && activeTextEditor.document.uri.scheme === 'file') { 65 | client.sendRequest(dumpAstRequest, { 66 | uri: client.code2ProtocolConverter.asUri(activeTextEditor.document.uri) 67 | }).then((value: string | null) => { 68 | console.info(value); 69 | client.outputChannel.appendLine(value || "No AST to dump"); 70 | }, (reason: any) => { 71 | client.outputChannel.appendLine("Failed to get ast: " + reason); 72 | }); 73 | } else { 74 | client.outputChannel.appendLine("No active file for dumping ast"); 75 | } 76 | })); 77 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.dumpDependency", () => { 78 | let activeTextEditor = vscode.window.activeTextEditor; 79 | if (activeTextEditor && activeTextEditor.document.uri.scheme === 'file') { 80 | client.sendRequest(dumpDependencyRequest, { 81 | uri: client.code2ProtocolConverter.asUri(activeTextEditor.document.uri) 82 | }).then((value: string | null) => { 83 | console.info(value); 84 | client.outputChannel.appendLine(value || "No deps tree to dump"); 85 | }, (reason: any) => { 86 | client.outputChannel.appendLine("Failed to get deps tree: " + reason); 87 | }); 88 | } else { 89 | client.outputChannel.appendLine("No active file for dumping deps tree"); 90 | } 91 | })); 92 | context.subscriptions.push( 93 | vscode.workspace.onDidChangeConfiguration(async (event : vscode.ConfigurationChangeEvent) => { 94 | if (event.affectsConfiguration("shader-validator")) { 95 | if (event.affectsConfiguration("shader-validator.trace.server") || 96 | event.affectsConfiguration("shader-validator.serverPath")) { 97 | let newClient = await createLanguageClient(context); 98 | if (newClient !== null) { 99 | client.dispose(); 100 | client = newClient; 101 | } 102 | } else { 103 | await client.sendNotification(DidChangeConfigurationNotification.type, { 104 | settings: "", 105 | }); 106 | } 107 | } 108 | }) 109 | ); 110 | } 111 | 112 | 113 | // This method is called when your extension is deactivated 114 | export function deactivate(context: vscode.ExtensionContext) { 115 | // Validator should self destruct thanks to vscode.Disposable 116 | } -------------------------------------------------------------------------------- /src/request.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProtocolRequestType, 3 | TextDocumentIdentifier, 4 | TextDocumentRegistrationOptions, 5 | } from "vscode-languageclient"; 6 | 7 | // Request to dump ast to log. 8 | export interface DumpAstParams extends TextDocumentIdentifier {} 9 | export interface DumpAstRegistrationOptions extends TextDocumentRegistrationOptions {} 10 | 11 | export const dumpAstRequest = new ProtocolRequestType('debug/dumpAst'); 12 | 13 | 14 | // Request to dump ast to log. 15 | export interface DumpDependencyParams extends TextDocumentIdentifier {} 16 | export interface DumpDependencyRegistrationOptions extends TextDocumentRegistrationOptions {} 17 | 18 | export const dumpDependencyRequest = new ProtocolRequestType('debug/dumpDependency'); -------------------------------------------------------------------------------- /src/shaderVariant.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { CancellationToken, DocumentSymbol, DocumentSymbolRequest, LanguageClient, ProtocolNotificationType, ProtocolRequestType, Range, SymbolInformation, SymbolKind, TextDocumentIdentifier, TextDocumentItem, TextDocumentRegistrationOptions } from 'vscode-languageclient/node'; 3 | 4 | interface ShaderVariantSerialized { 5 | entryPoint: string, 6 | stage: string | null, 7 | defines: Object, 8 | includes: string[], 9 | } 10 | 11 | function shaderVariantToSerialized(e: ShaderVariant) : ShaderVariantSerialized { 12 | function cameltoPascalCase(s: string) : string { 13 | return String(s[0]).toUpperCase() + String(s).slice(1); 14 | } 15 | return { 16 | entryPoint: e.name, 17 | stage: (e.stage.stage === ShaderStage.auto) ? null : cameltoPascalCase(ShaderStage[e.stage.stage]), 18 | defines: Object.fromEntries(e.defines.defines.map(e => [e.label, e.value])), 19 | includes: e.includes.includes.map(e => e.include) 20 | }; 21 | } 22 | function getActiveFileVariant(file: ShaderVariantFile) : ShaderVariant | null { 23 | return file.variants.find((e: ShaderVariant) => { 24 | return e.isActive; 25 | }) || null; 26 | } 27 | // Notification from client to change shader variant 28 | interface DidChangeShaderVariantParams { 29 | textDocument: TextDocumentIdentifier 30 | shaderVariant: ShaderVariantSerialized | null 31 | } 32 | interface DidChangeShaderVariantRegistrationOptions extends TextDocumentRegistrationOptions {} 33 | 34 | const didChangeShaderVariantNotification = new ProtocolNotificationType('textDocument/didChangeShaderVariant'); 35 | 36 | // Request from server to send file active variant. 37 | interface ShaderVariantParams extends TextDocumentIdentifier {} 38 | interface ShaderVariantRegistrationOptions extends TextDocumentRegistrationOptions {} 39 | 40 | interface ShaderVariantResponse { 41 | shaderVariant: ShaderVariantSerialized | null, 42 | } 43 | const shaderVariantRequest = new ProtocolRequestType('textDocument/shaderVariant'); 44 | 45 | 46 | export type ShaderVariantDefine = { 47 | kind: 'define', 48 | label: string, 49 | value: string, 50 | }; 51 | 52 | export type ShaderVariantDefineList = { 53 | kind: 'defineList', 54 | defines: ShaderVariantDefine[], 55 | }; 56 | 57 | export type ShaderVariantInclude = { 58 | kind: 'include', 59 | include: string, 60 | }; 61 | 62 | export type ShaderVariantIncludeList = { 63 | kind: 'includeList', 64 | includes: ShaderVariantInclude[], 65 | }; 66 | 67 | export enum ShaderStage { 68 | auto, 69 | vertex, 70 | fragment, 71 | compute, 72 | tesselationControl, 73 | tesselationEvaluation, 74 | mesh, 75 | task, 76 | geometry, 77 | rayGeneration, 78 | closestHit, 79 | anyHit, 80 | callable, 81 | miss, 82 | intersect, 83 | } 84 | 85 | export type ShaderVariantStage = { 86 | kind: 'stage', 87 | stage: ShaderStage, 88 | }; 89 | 90 | // This should be shadervariant. 91 | export type ShaderVariant = { 92 | kind: 'variant'; 93 | uri: vscode.Uri; 94 | name: string; 95 | isActive: boolean; 96 | // Per variant data 97 | stage: ShaderVariantStage; 98 | defines: ShaderVariantDefineList; 99 | includes: ShaderVariantIncludeList; 100 | }; 101 | 102 | export type ShaderVariantFile = { 103 | kind: 'file', 104 | uri: vscode.Uri, 105 | variants: ShaderVariant[], 106 | }; 107 | 108 | export type ShaderEntryPoint = { 109 | entryPoint: string, 110 | range: vscode.Range, 111 | }; 112 | 113 | export type ShaderVariantNode = ShaderVariant | ShaderVariantFile | ShaderVariantDefineList | ShaderVariantIncludeList | ShaderVariantDefine | ShaderVariantInclude | ShaderVariantStage; 114 | 115 | const shaderVariantTreeKey : string = 'shader-validator.shader-variant-tree-key'; 116 | 117 | export class ShaderVariantTreeDataProvider implements vscode.TreeDataProvider { 118 | 119 | private onDidChangeTreeDataEmitter: vscode.EventEmitter = new vscode.EventEmitter(); 120 | readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEmitter.event; 121 | 122 | // using vscode.Uri as key does not match well with Memento state storage... 123 | private files: Map; 124 | private tree: vscode.TreeView; 125 | private client: LanguageClient; 126 | private decorator: vscode.TextEditorDecorationType; 127 | private workspaceState: vscode.Memento; 128 | private shaderEntryPointList: Map; 129 | private asyncGoToShaderEntryPoint: Map; 130 | 131 | private load() { 132 | let variants : ShaderVariantFile[] = this.workspaceState.get(shaderVariantTreeKey, []); 133 | this.files = new Map(variants.map((e : ShaderVariantFile) => { 134 | // Seems that serialisation is breaking something, so this is required for uri & range to behave correctly. 135 | e.uri = vscode.Uri.from(e.uri); 136 | for (let variant of e.variants) { 137 | variant.uri = vscode.Uri.from(variant.uri); 138 | } 139 | return [e.uri.path, e]; 140 | })); 141 | } 142 | private save() { 143 | let array = Array.from(this.files.values()); 144 | this.workspaceState.update(shaderVariantTreeKey, array); 145 | } 146 | 147 | constructor(context: vscode.ExtensionContext, client: LanguageClient) { 148 | this.workspaceState = context.workspaceState; 149 | this.files = new Map; 150 | this.load(); 151 | this.shaderEntryPointList = new Map; 152 | this.client = client; 153 | this.tree = vscode.window.createTreeView("shader-validator-variants", { 154 | treeDataProvider: this 155 | // TODO: drag and drop for better ux. 156 | //dragAndDropController: 157 | }); 158 | this.asyncGoToShaderEntryPoint = new Map; 159 | this.tree.onDidChangeCheckboxState((e: vscode.TreeCheckboxChangeEvent) => { 160 | for (let [variant, checkboxState] of e.items) { 161 | if (variant.kind === 'variant') { 162 | if (checkboxState === vscode.TreeItemCheckboxState.Checked) { 163 | // Need to unset other possibles active ones to keep only one entry point active per file. 164 | let file = this.files.get(variant.uri.path); 165 | if (file) { 166 | let needRefresh = false; 167 | for (let otherVariant of file.variants) { 168 | if (otherVariant.isActive) { 169 | needRefresh = true; 170 | otherVariant.isActive = false; 171 | } 172 | } 173 | variant.isActive = true; // checked 174 | if (needRefresh) { 175 | // Refresh file & all its childs 176 | this.refresh(file, file); 177 | } else { 178 | this.refresh(variant, file); 179 | } 180 | } else { 181 | console.warn("Failed to find file ", variant.uri); 182 | variant.isActive = true; // checked 183 | } 184 | } else { 185 | variant.isActive = false; // unchecked 186 | let file = this.files.get(variant.uri.path); 187 | if (file) { 188 | this.refresh(file, file); 189 | } 190 | } 191 | } 192 | } 193 | this.save(); 194 | this.updateDecorations(); 195 | }); 196 | this.decorator = vscode.window.createTextEditorDecorationType({ 197 | // Icon 198 | gutterIconPath: context.asAbsolutePath('./res/icons/hlsl-icon.svg'), 199 | gutterIconSize: "contain", 200 | // Minimap 201 | overviewRulerColor: "rgb(0, 174, 255)", 202 | overviewRulerLane: vscode.OverviewRulerLane.Full, 203 | rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen, 204 | // Border 205 | borderWidth: '1px', 206 | borderStyle: 'solid', 207 | }); 208 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.addCurrentFile", (): void => { 209 | let supportedLangId = ["hlsl", "glsl", "wgsl"]; 210 | if (vscode.window.activeTextEditor && supportedLangId.includes(vscode.window.activeTextEditor.document.languageId)) { 211 | this.open(vscode.window.activeTextEditor.document.uri); 212 | } 213 | this.save(); 214 | })); 215 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.addMenu", (node: ShaderVariantNode): void => { 216 | this.add(node); 217 | this.save(); 218 | })); 219 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.deleteMenu", (node: ShaderVariantNode) => { 220 | this.delete(node); 221 | this.save(); 222 | })); 223 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.editMenu", async (node: ShaderVariantNode) => { 224 | await this.edit(node); 225 | this.save(); 226 | })); 227 | context.subscriptions.push(vscode.commands.registerCommand("shader-validator.gotoShaderEntryPoint", (uri: vscode.Uri, entryPointName: string) => { 228 | this.goToShaderEntryPoint(uri, entryPointName, true); 229 | })); 230 | // Prepare entry point symbol cache 231 | for (let editor of vscode.window.visibleTextEditors) { 232 | if (editor.document.uri.scheme === 'file') { 233 | this.shaderEntryPointList.set(editor.document.uri.path, []); 234 | } 235 | } 236 | context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(document => { 237 | if (document.uri.scheme === 'file') { 238 | this.shaderEntryPointList.set(document.uri.path, []); 239 | } 240 | })); 241 | context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(document => { 242 | this.shaderEntryPointList.delete(document.uri.path); 243 | })); 244 | this.updateDependencies(); 245 | } 246 | 247 | private goToShaderEntryPoint(uri: vscode.Uri, entryPointName: string, defer: boolean) { 248 | let shaderEntryPointList = this.shaderEntryPointList.get(uri.path); 249 | let entryPoint = shaderEntryPointList?.find(e => e.entryPoint === entryPointName); 250 | if (entryPoint) { 251 | vscode.commands.executeCommand('vscode.open', uri, { 252 | selection: entryPoint.range 253 | }); 254 | } else { 255 | let editor = vscode.window.visibleTextEditors.find(e => e.document.uri === uri); 256 | if (editor || !defer) { 257 | // Already opened, but no entry point found. 258 | vscode.window.showWarningMessage(`Failed to find entry point ${entryPointName} for file ${vscode.workspace.asRelativePath(uri)}`); 259 | } else { 260 | // Store request & open the file. Resolve goto on document request 261 | this.asyncGoToShaderEntryPoint.set(uri, entryPointName); 262 | vscode.commands.executeCommand('vscode.open', uri, {}); 263 | } 264 | } 265 | } 266 | 267 | private getFileAndParentNode(node: ShaderVariantNode) : [ShaderVariantFile, ShaderVariantNode | null] | null { 268 | if (node.kind === 'variant') { 269 | let file = this.files.get(node.uri.path); 270 | if (file) { 271 | return [file, null]; // No parent 272 | } 273 | } else if (node.kind === 'define') { 274 | for (let [_, file] of this.files) { 275 | for (let variant of file.variants) { 276 | let index = variant.defines.defines.indexOf(node); 277 | if (index > -1) { 278 | return [file, variant.defines]; 279 | } 280 | } 281 | } 282 | } else if (node.kind === 'defineList') { 283 | for (let [_, file] of this.files) { 284 | for (let variant of file.variants) { 285 | if (variant.defines === node) { 286 | return [file, variant]; 287 | } 288 | } 289 | } 290 | } else if (node.kind === 'stage') { 291 | for (let [_, file] of this.files) { 292 | for (let variant of file.variants) { 293 | if (variant.stage === node) { 294 | return [file, variant]; 295 | } 296 | } 297 | } 298 | } else if (node.kind === 'include') { 299 | for (let [_, file] of this.files) { 300 | for (let variant of file.variants) { 301 | let index = variant.includes.includes.indexOf(node); 302 | if (index > -1) { 303 | return [file, variant.includes]; 304 | } 305 | } 306 | } 307 | } else if (node.kind === 'includeList') { 308 | for (let [_, file] of this.files) { 309 | for (let variant of file.variants) { 310 | if (variant.includes === node) { 311 | return [file, variant]; 312 | } 313 | } 314 | } 315 | } 316 | console.warn("Failed to find file for node ", node); 317 | return null; 318 | } 319 | 320 | public refresh(node: ShaderVariantNode, file: ShaderVariantFile | null, updateFileNode?: boolean) { 321 | this.onDidChangeTreeDataEmitter.fire(); 322 | if (file) { 323 | this.updateDependency(file); 324 | } else { 325 | let result = this.getFileAndParentNode(node); 326 | if (result) { 327 | let [file, parent] = result; 328 | this.updateDependency(file); 329 | } else { 330 | // Something failed here... 331 | this.updateDependencies(); 332 | } 333 | } 334 | } 335 | public refreshAll() { 336 | this.onDidChangeTreeDataEmitter.fire(); 337 | this.updateDependencies(); 338 | } 339 | private requestDocumentSymbol(uri: vscode.Uri) { 340 | // This one seems to get symbol from cache without requesting the server... 341 | //vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", file.uri); 342 | // This one works, but result is not intercepted by vscode & updated... 343 | //this.client.sendRequest(DocumentSymbolRequest.type, { 344 | // textDocument: { 345 | // uri: this.client.code2ProtocolConverter.asUri(file.uri), 346 | // } 347 | //}); 348 | // We have to rely on a dirty hack instead. 349 | // Need to check this does not break anything 350 | // Dirty hack to trigger document symbol update 351 | let visibleEditor = vscode.window.visibleTextEditors.find(e => e.document.uri.path === uri.path); 352 | if (visibleEditor) { 353 | let editor = visibleEditor; 354 | editor.edit(editBuilder => { 355 | for (let iLine = 0; iLine < editor.document.lineCount; iLine++) { 356 | // Find first non-empty line to avoid crashing on empty line with negative position. 357 | let line = editor.document.lineAt(iLine); 358 | if (line.text.length > 0) { 359 | const text = line.text; 360 | const c = line.range.end.character; 361 | // Remove last character of first line and add it back. 362 | editBuilder.delete(new vscode.Range(iLine, c-1, iLine, c)); 363 | editBuilder.insert(new vscode.Position(iLine, c), text[c-1]); 364 | break; 365 | } 366 | } 367 | // All empty lines means no symbols ! 368 | }); 369 | } 370 | } 371 | private updateDependency(file: ShaderVariantFile) { 372 | let fileActiveVariant = getActiveFileVariant(file); 373 | let params : DidChangeShaderVariantParams = { 374 | textDocument: { 375 | uri: this.client.code2ProtocolConverter.asUri(file.uri), 376 | }, 377 | shaderVariant: fileActiveVariant ? shaderVariantToSerialized(fileActiveVariant) : null, 378 | }; 379 | this.client.sendNotification(didChangeShaderVariantNotification, params); 380 | 381 | // Symbols might have changed, so request them as we use this to compute symbols. 382 | this.requestDocumentSymbol(file.uri); 383 | } 384 | public onDocumentSymbols(uri: vscode.Uri, symbols: vscode.DocumentSymbol[]) { 385 | // TODO:TREE: need to recurse child as well. 386 | this.shaderEntryPointList.set(uri.path, symbols.filter(symbol => symbol.kind === vscode.SymbolKind.Function).map(symbol => { 387 | return { 388 | entryPoint: symbol.name, 389 | range: symbol.selectionRange 390 | }; 391 | })); 392 | // Solve async request for goto. 393 | let entryPoint = this.asyncGoToShaderEntryPoint.get(uri); 394 | if (entryPoint) { 395 | this.asyncGoToShaderEntryPoint.delete(uri); 396 | this.goToShaderEntryPoint(uri, entryPoint, false); 397 | } 398 | this.updateDecorations(); 399 | } 400 | private updateDependencies() { 401 | for (let [_, file] of this.files) { 402 | this.updateDependency(file); 403 | } 404 | } 405 | 406 | public getTreeItem(element: ShaderVariantNode): vscode.TreeItem { 407 | if (element.kind === 'variant') { 408 | let item = new vscode.TreeItem(element.name, vscode.TreeItemCollapsibleState.Collapsed); 409 | // Need to use a middleware command because item is not updated on collapse change. 410 | item.command = { 411 | title: "Go to variant", 412 | command: 'shader-validator.gotoShaderEntryPoint', 413 | arguments: [ 414 | element.uri, 415 | element.name 416 | ] 417 | }; 418 | item.description = `[${element.defines.defines.map(d => d.label).join(",")}]`; 419 | item.tooltip = `Shader variant ${element.name}`; 420 | item.checkboxState = element.isActive ? vscode.TreeItemCheckboxState.Checked : vscode.TreeItemCheckboxState.Unchecked; 421 | item.contextValue = element.kind; 422 | return item; 423 | } else if (element.kind === 'file') { 424 | let item = new vscode.TreeItem(vscode.workspace.asRelativePath(element.uri), vscode.TreeItemCollapsibleState.Expanded); 425 | item.description = `${element.variants.length}`; 426 | item.resourceUri = element.uri; 427 | item.tooltip = `File ${element.uri.fsPath}`; 428 | item.iconPath = vscode.ThemeIcon.File; 429 | item.contextValue = element.kind; 430 | return item; 431 | } else if (element.kind === 'defineList') { 432 | let item = new vscode.TreeItem("defines", vscode.TreeItemCollapsibleState.Expanded); 433 | item.description = `${element.defines.length}`; 434 | item.tooltip = `List of defines`, 435 | item.iconPath = new vscode.ThemeIcon('keyboard'); 436 | item.contextValue = element.kind; 437 | return item; 438 | } else if (element.kind === 'includeList') { 439 | let item = new vscode.TreeItem("includes", vscode.TreeItemCollapsibleState.Expanded); 440 | item.description = `${element.includes.length}`; 441 | item.tooltip = `List of includes`, 442 | item.iconPath = new vscode.ThemeIcon('files'); 443 | item.contextValue = element.kind; 444 | return item; 445 | } else if (element.kind === 'define') { 446 | let item = new vscode.TreeItem(element.label, vscode.TreeItemCollapsibleState.None); 447 | item.description = element.value; 448 | item.tooltip = `User defined macro ${element.label} with value ${element.value}`, 449 | item.contextValue = element.kind; 450 | return item; 451 | } else if (element.kind === 'include') { 452 | let item = new vscode.TreeItem(element.include, vscode.TreeItemCollapsibleState.None); 453 | item.description = element.include; 454 | item.tooltip = `User include path ${element.include}`, 455 | item.contextValue = element.kind; 456 | return item; 457 | } else if (element.kind === 'stage') { 458 | let item = new vscode.TreeItem("stage", vscode.TreeItemCollapsibleState.None); 459 | item.description = ShaderStage[element.stage]; 460 | item.tooltip = "The shader stage of this variant. If auto is selected, the server will try to guess the stage, or use generic one when supported by API."; 461 | item.iconPath = new vscode.ThemeIcon('code'); 462 | item.contextValue = element.kind; 463 | return item; 464 | } else { 465 | console.error("Unimplemented kind: ", element); 466 | return undefined!; // unreachable 467 | } 468 | } 469 | 470 | public getChildren(element?: ShaderVariantNode): ShaderVariantNode[] | Thenable { 471 | if (element) { 472 | if (element.kind === 'variant') { 473 | return [element.stage, element.defines, element.includes]; 474 | } else if (element.kind === 'file') { 475 | return element.variants; 476 | } else if (element.kind === 'includeList') { 477 | return element.includes; 478 | } else if (element.kind === 'defineList') { 479 | return element.defines; 480 | } else if (element.kind === 'include') { 481 | return []; 482 | } else if (element.kind === 'define') { 483 | return []; 484 | } else if (element.kind === 'stage') { 485 | return []; 486 | } else { 487 | console.error("Reached unreachable", element); 488 | return undefined!; // unreachable 489 | } 490 | } else { 491 | // Convert to array 492 | return Array.from(this.files.values()); 493 | } 494 | } 495 | 496 | public open(uri: vscode.Uri): void { 497 | if (uri.scheme !== 'file') { 498 | return; 499 | } 500 | let file = this.files.get(uri.path); 501 | if (!file) { 502 | let newFile : ShaderVariantFile = { 503 | kind: 'file', 504 | uri: uri, 505 | variants: [] 506 | }; 507 | this.files.set(uri.path, newFile); 508 | this.refreshAll(); 509 | } 510 | } 511 | public close(uri: vscode.Uri): void { 512 | let file = this.files.get(uri.path); 513 | if (file) { 514 | // We keep it if some variants where defied. 515 | if (file.variants.length === 0) { 516 | this.files.delete(uri.path); 517 | this.refreshAll(); 518 | } 519 | } 520 | } 521 | public add(node: ShaderVariantNode) { 522 | if (node.kind === 'file') { 523 | node.variants.push({ 524 | kind: 'variant', 525 | uri: node.uri, 526 | name: 'main', 527 | isActive: false, 528 | stage: { 529 | kind: 'stage', 530 | stage: ShaderStage.auto 531 | }, 532 | defines: { 533 | kind: 'defineList', 534 | defines:[] 535 | }, 536 | includes: { 537 | kind: 'includeList', 538 | includes:[] 539 | }, 540 | }); 541 | this.refresh(node, node); 542 | } else if (node.kind === 'defineList') { 543 | node.defines.push({ 544 | kind: "define", 545 | label: "MY_MACRO", 546 | value: "0", 547 | }); 548 | this.refresh(node, null); 549 | } else if (node.kind === 'includeList') { 550 | node.includes.push({ 551 | kind: "include", 552 | include: "C:/", 553 | }); 554 | this.refresh(node, null); 555 | } 556 | } 557 | public async edit(node: ShaderVariantNode) { 558 | if (node.kind === 'variant') { 559 | let name = await vscode.window.showInputBox({ 560 | title: "Entry point selection", 561 | value: node.name, 562 | prompt: "Select an entry point name for your variant", 563 | placeHolder: "main" 564 | }); 565 | if (name) { 566 | node.name = name; 567 | this.refresh(node, null); 568 | } 569 | } else if (node.kind === 'define') { 570 | let label = await vscode.window.showInputBox({ 571 | title: "Macro label", 572 | value: node.label, 573 | prompt: "Select a label for you macro.", 574 | placeHolder: "MY_MACRO" 575 | }); 576 | let value = await vscode.window.showInputBox({ 577 | title: "Macro value", 578 | value: node.value, 579 | prompt: "Select a value for you macro.", 580 | placeHolder: "0" 581 | }); 582 | if (label) { 583 | node.label = label; 584 | } 585 | if (value) { 586 | node.value = value; 587 | } 588 | if (value || label) { 589 | this.refresh(node, null); 590 | } 591 | } else if (node.kind === 'include') { 592 | let include = await vscode.window.showInputBox({ 593 | title: "Include path", 594 | value: node.include, 595 | prompt: "Select a path for your include.", 596 | placeHolder: "C:/Users/" 597 | }); 598 | if (include) { 599 | node.include = include; 600 | this.refresh(node, null); 601 | } 602 | } else if (node.kind === 'stage') { 603 | let stage = await vscode.window.showQuickPick( 604 | [ 605 | ShaderStage[ShaderStage.auto], 606 | ShaderStage[ShaderStage.vertex], 607 | ShaderStage[ShaderStage.fragment], 608 | ShaderStage[ShaderStage.compute], 609 | ShaderStage[ShaderStage.tesselationControl], 610 | ShaderStage[ShaderStage.tesselationEvaluation], 611 | ShaderStage[ShaderStage.mesh], 612 | ShaderStage[ShaderStage.task], 613 | ShaderStage[ShaderStage.geometry], 614 | ShaderStage[ShaderStage.rayGeneration], 615 | ShaderStage[ShaderStage.closestHit], 616 | ShaderStage[ShaderStage.anyHit], 617 | ShaderStage[ShaderStage.callable], 618 | ShaderStage[ShaderStage.miss], 619 | ShaderStage[ShaderStage.intersect], 620 | ], 621 | { 622 | title: "Shader stage" 623 | } 624 | ); 625 | if (stage) { 626 | node.stage = ShaderStage[stage as keyof typeof ShaderStage]; 627 | this.refresh(node, null); 628 | } 629 | } 630 | } 631 | public delete(node: ShaderVariantNode) { 632 | if (node.kind === 'file') { 633 | this.files.delete(node.uri.path); 634 | this.refreshAll(); 635 | } else if (node.kind === 'variant') { 636 | let cachedFile = this.files.get(node.uri.path); 637 | if (cachedFile) { 638 | let index = cachedFile.variants.indexOf(node); 639 | if (index > -1) { 640 | cachedFile.variants.splice(index, 1); 641 | this.refresh(cachedFile, cachedFile); 642 | } 643 | } 644 | } else if (node.kind === 'define') { 645 | // Dirty remove, might be costly when lot of elements... 646 | for (let [_, file] of this.files) { 647 | let found = false; 648 | for (let variant of file.variants) { 649 | let index = variant.defines.defines.indexOf(node); 650 | if (index > -1) { 651 | variant.defines.defines.splice(index, 1); 652 | // Refresh variant for description 653 | this.refresh(variant, file); 654 | found = true; 655 | break; 656 | } 657 | } 658 | if (found) { 659 | break; 660 | } 661 | } 662 | } else if (node.kind === 'include') { 663 | // Dirty remove, might be costly when lot of elements... 664 | for (let [uri, file] of this.files) { 665 | let found = false; 666 | for (let variant of file.variants) { 667 | let index = variant.includes.includes.indexOf(node); 668 | if (index > -1) { 669 | variant.includes.includes.splice(index, 1); 670 | this.refresh(variant.includes, file); 671 | found = true; 672 | break; 673 | } 674 | } 675 | if (found) { 676 | break; 677 | } 678 | } 679 | } 680 | } 681 | private updateDecoration(editor: vscode.TextEditor) { 682 | let file = this.files.get(editor.document.uri.path); 683 | let entryPoints = this.shaderEntryPointList.get(editor.document.uri.path); 684 | 685 | if (file && entryPoints) { 686 | let variant = getActiveFileVariant(file); 687 | if (variant) { 688 | let found = false; 689 | for (let entryPoint of entryPoints) { 690 | if (entryPoint.entryPoint === variant.name) { 691 | let decorations : vscode.DecorationOptions[]= []; 692 | decorations.push({ range: entryPoint.range, hoverMessage: variant.name }); 693 | editor.setDecorations(this.decorator, decorations); 694 | found = true; 695 | break; 696 | } 697 | } 698 | if (!found) { 699 | console.info("Entry point not found in ", entryPoints); 700 | editor.setDecorations(this.decorator, []); 701 | } 702 | } else { 703 | console.info("No active variant ", entryPoints); 704 | editor.setDecorations(this.decorator, []); 705 | } 706 | } else { 707 | console.info("No file or entry point ", file, entryPoints); 708 | editor.setDecorations(this.decorator, []); 709 | } 710 | } 711 | private updateDecorations(uri?: vscode.Uri) { 712 | for (let editor of vscode.window.visibleTextEditors) { 713 | if (editor.document.uri.scheme === 'file') { 714 | this.updateDecoration(editor); 715 | } 716 | } 717 | } 718 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ 17 | extensionDevelopmentPath, 18 | extensionTestsPath, 19 | launchArgs: [ 20 | //"--disable-extensions", 21 | path.resolve(__dirname, '../../test/') 22 | ] 23 | }); 24 | } catch (err) { 25 | console.error('Failed to run tests'); 26 | process.exit(1); 27 | } 28 | } 29 | 30 | main(); 31 | -------------------------------------------------------------------------------- /src/test/suite/binary.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | import * as fs from 'fs'; 7 | import * as path from 'path'; 8 | import { getRootFolder } from './utils'; 9 | 10 | function doesBinaryExist(binary : string) : boolean { 11 | let executablePath = path.join(getRootFolder(), "bin", binary); 12 | //console.log(`Checking presence of ${executablePath} from ${process.cwd()}`); 13 | return fs.existsSync(executablePath); 14 | } 15 | 16 | suite('Binary Test Suite', () => { 17 | vscode.window.showInformationMessage('Start all binary tests.'); 18 | suiteTeardown(() => { 19 | vscode.window.showInformationMessage('All binary tests done!'); 20 | }); 21 | 22 | test('Check wasm binary', () => { 23 | assert.ok(doesBinaryExist("wasi/shader-language-server.wasm")); 24 | }); 25 | test('Check windows binary', () => { 26 | assert.ok(doesBinaryExist("windows/shader-language-server.exe")); 27 | // Dxc need these dll or it will crash. 28 | assert.ok(doesBinaryExist("windows/dxcompiler.dll")); 29 | assert.ok(doesBinaryExist("windows/dxil.dll")); 30 | }); 31 | test('Check linux binary', () => { 32 | assert.ok(doesBinaryExist("linux/shader-language-server")); 33 | // Dxc need these dll or it will crash. 34 | assert.ok(doesBinaryExist("linux/libdxcompiler.so")); 35 | assert.ok(doesBinaryExist("linux/libdxil.so")); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/test/suite/completion.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | import { activate } from './utils'; 7 | 8 | suite('Completion Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all completion tests.'); 10 | suiteTeardown(() => { 11 | vscode.window.showInformationMessage('All completion tests done!'); 12 | }); 13 | 14 | test('Complete GLSL code', async () => { 15 | const docUri = await vscode.workspace.findFiles("test.frag.glsl"); 16 | assert.ok(docUri.length > 0); 17 | await testCompletion(docUri[0], new vscode.Position(8, 0), { 18 | items: [ 19 | { label: 'clamp', kind: vscode.CompletionItemKind.Function }, 20 | { label: 'main', kind: vscode.CompletionItemKind.Function }, 21 | { label: 'test', kind: vscode.CompletionItemKind.Function }, 22 | { label: 'res', kind: vscode.CompletionItemKind.Variable }, 23 | ] 24 | }, true); 25 | }).timeout(5000); 26 | 27 | test('Complete HLSL code', async () => { 28 | const docUri = await vscode.workspace.findFiles("test.hlsl"); 29 | assert.ok(docUri.length > 0); 30 | await testCompletion(docUri[0], new vscode.Position(0, 0), { 31 | items: [] 32 | }, false); 33 | }).timeout(5000); 34 | 35 | test('Complete WGSL code', async () => { 36 | const docUri = await vscode.workspace.findFiles("test.wgsl"); 37 | assert.ok(docUri.length > 0); 38 | await testCompletion(docUri[0], new vscode.Position(0, 0), { 39 | items: [] 40 | }, false); 41 | }).timeout(5000); 42 | }); 43 | 44 | async function testCompletion( 45 | docUri: vscode.Uri, 46 | position: vscode.Position, 47 | expectedCompletionList: vscode.CompletionList, 48 | waitServer: boolean 49 | ) { 50 | let data = await activate(docUri, waitServer)!; 51 | 52 | // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion 53 | const actualCompletionList = (await vscode.commands.executeCommand( 54 | 'vscode.executeCompletionItemProvider', 55 | docUri, 56 | position 57 | )) as vscode.CompletionList; 58 | assert.ok(actualCompletionList.items.length >= expectedCompletionList.items.length); 59 | expectedCompletionList.items.forEach((expectedItem : vscode.CompletionItem) => { 60 | // Look into database if we can find them 61 | let item = actualCompletionList.items.find((actualItem) => { 62 | //const actualItem = actualCompletionList.items[i]; 63 | const actualLabel = actualItem.label as vscode.CompletionItemLabel; 64 | return actualLabel.label === expectedItem.label && actualItem.kind === expectedItem.kind; 65 | }); 66 | assert.notStrictEqual(item, undefined, `Failed to find symbol ${expectedItem.label}`); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/test/suite/diagnostic.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | import { activate } from './utils'; 7 | 8 | suite('Diagnostic Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all diagnostics tests.'); 10 | suiteTeardown(() => { 11 | vscode.window.showInformationMessage('All diagnostics tests done!'); 12 | }); 13 | test('Diagnostic GLSL code', async () => { 14 | const docUri = await vscode.workspace.findFiles("test.frag.glsl"); 15 | assert.ok(docUri.length > 0); 16 | await testDiagnostic(docUri[0], true); 17 | }).timeout(5000); 18 | 19 | test('Diagnostic HLSL code', async () => { 20 | const docUri = await vscode.workspace.findFiles("test.hlsl"); 21 | assert.ok(docUri.length > 0); 22 | await testDiagnostic(docUri[0], false); 23 | }).timeout(5000); 24 | 25 | test('Diagnostic WGSL code', async () => { 26 | const docUri = await vscode.workspace.findFiles("test.wgsl"); 27 | assert.ok(docUri.length > 0); 28 | await testDiagnostic(docUri[0], false); 29 | }).timeout(5000); 30 | }); 31 | 32 | async function testDiagnostic( 33 | docUri: vscode.Uri, 34 | waitServer: boolean 35 | ) { 36 | let data = await activate(docUri, waitServer)!; 37 | let diagnostics = vscode.languages.getDiagnostics(docUri); 38 | assert.ok(diagnostics.length === 0); 39 | } 40 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import Mocha from 'mocha'; 3 | import { glob } from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err : any, files : any) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach((f : any) => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run((failures : any) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/suite/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | export function getRootFolder() : string { 5 | // Depending on platform, we have different cwd... 6 | // https://github.com/microsoft/vscode-test/issues/17 7 | return path.join(process.cwd(), process.platform === 'win32' ? "../../" : "./"); 8 | } 9 | 10 | export async function activate(docUri: vscode.Uri, waitServer: boolean) : Promise<[vscode.TextDocument, vscode.TextEditor] | null> { 11 | const ext = vscode.extensions.getExtension('antaalt.shader-validator')!; 12 | await ext.activate(); 13 | try { 14 | let doc = await vscode.workspace.openTextDocument(docUri); 15 | let editor = await vscode.window.showTextDocument(doc); 16 | if (waitServer) { 17 | await sleep(1000); // Wait for server activation 18 | } 19 | return [doc, editor]; 20 | } catch (e) { 21 | console.error(e); 22 | return null; 23 | } 24 | } 25 | 26 | async function sleep(ms: number) { 27 | return new Promise(resolve => setTimeout(resolve, ms)); 28 | } -------------------------------------------------------------------------------- /src/test/suite/version.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | import * as fs from 'fs'; 7 | import * as cp from 'child_process'; 8 | import { getRootFolder } from './utils'; 9 | import { getPlatformBinaryUri, getServerPlatform } from '../../validator'; 10 | 11 | suite('Server version Test Suite', () => { 12 | test('Check server version', () => { 13 | let platform = getServerPlatform(); 14 | let executableUri = getPlatformBinaryUri(vscode.Uri.parse(getRootFolder()), platform); 15 | assert.ok(fs.existsSync(executableUri.fsPath), `Failed to find ${executableUri}`); 16 | let server = cp.spawn(executableUri.fsPath, [ 17 | "--version" 18 | ]); 19 | const version = vscode.extensions.getExtension('antaalt.shader-validator')!.packageJSON.server_version; 20 | const decoder = new TextDecoder('utf-8'); 21 | server.stdout.on('data', (data) => { 22 | const text = decoder.decode(data); 23 | assert.equal(text, "shader-language-server v" + version, `Incompatible version: ${version}`); 24 | }); 25 | server.stderr.on('data', (data) => { 26 | assert.fail(`stderr: ${data}`); 27 | }); 28 | server.on('error', (data) => { 29 | assert.fail(`Error: ${data}`); 30 | }); 31 | }); 32 | }); -------------------------------------------------------------------------------- /src/validator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as cp from "child_process"; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | import { 7 | createStdioOptions, 8 | createUriConverters, 9 | startServer 10 | } from './wasm-wasi-lsp'; // Should import from @vscode/wasm-wasi-lsp, but version not based on last released wasm-wasi version 11 | import { MountPointDescriptor, ProcessOptions, Wasm } from "@vscode/wasm-wasi/v1"; 12 | import { 13 | CloseAction, 14 | CloseHandlerResult, 15 | DidChangeConfigurationNotification, 16 | ErrorAction, 17 | ErrorHandler, 18 | ErrorHandlerResult, 19 | LanguageClient, 20 | LanguageClientOptions, 21 | Message, 22 | Middleware, 23 | ProvideDocumentSymbolsSignature, 24 | ServerOptions, 25 | TransportKind 26 | } from 'vscode-languageclient/node'; 27 | import { sidebar } from "./extension"; 28 | 29 | export enum ServerPlatform { 30 | windows, 31 | linux, 32 | wasi, 33 | } 34 | 35 | export function isRunningOnWeb() : boolean { 36 | // Web environment is detected with no fallback on child process which is not supported there. 37 | return typeof cp.spawn !== 'function' || typeof process === 'undefined'; 38 | } 39 | function getServerVersion(serverPath: string) : string | null { 40 | if (isRunningOnWeb()) { 41 | // Bundled version always used on the web as we cant access external folders. 42 | return "shader-language-server v" + vscode.extensions.getExtension('antaalt.shader-validator')!.packageJSON.server_version; 43 | } else { 44 | if (fs.existsSync(serverPath)) { 45 | const result = cp.execSync(serverPath + " --version"); 46 | const version = result.toString("utf8").trim(); 47 | return version; 48 | } else { 49 | return null; 50 | } 51 | } 52 | } 53 | function isValidVersion(serverVersion: string) { 54 | const requestedServerVersion = vscode.extensions.getExtension('antaalt.shader-validator')!.packageJSON.server_version; 55 | const versionExpected = "shader-language-server v" + requestedServerVersion; 56 | return serverVersion === versionExpected; 57 | } 58 | function getUserServerPath() : string | null { 59 | if (isRunningOnWeb()) { 60 | return null; 61 | } else { 62 | // Check configuration. 63 | let serverPath = vscode.workspace.getConfiguration("shader-validator").get("serverPath"); 64 | if (serverPath && serverPath.length > 0) { 65 | let serverVersion = getServerVersion(serverPath); 66 | if (serverVersion) { 67 | console.info(`shader-validator.serverPath found: ${serverPath}`); 68 | return serverPath; 69 | } else { 70 | console.warn("shader-validator.serverPath not found."); 71 | } 72 | } 73 | // Check environment variables 74 | if (process.env.SHADER_LANGUAGE_SERVER_EXECUTABLE_PATH !== undefined) { 75 | let envPath = process.env.SHADER_LANGUAGE_SERVER_EXECUTABLE_PATH; 76 | let serverVersion = getServerVersion(envPath); 77 | if (serverVersion) { 78 | console.info(`SHADER_LANGUAGE_SERVER_EXECUTABLE_PATH found: ${envPath}`); 79 | return envPath; 80 | } else { 81 | console.warn("SHADER_LANGUAGE_SERVER_EXECUTABLE_PATH server path not found."); 82 | } 83 | } 84 | // Use bundled executables. 85 | console.info("No server path user settings found. Using bundled executable."); 86 | return null; 87 | } 88 | } 89 | function getPlatformBinaryDirectoryPath(extensionUri: vscode.Uri, platform: ServerPlatform) : vscode.Uri { 90 | let serverPath = getUserServerPath(); 91 | if (serverPath) { 92 | return vscode.Uri.file(path.dirname(serverPath)); 93 | } else { 94 | // CI is handling the copy to bin folder to avoid storage of exe on git. 95 | switch (platform) { 96 | case ServerPlatform.windows: 97 | return vscode.Uri.joinPath(extensionUri, "bin/windows/"); 98 | case ServerPlatform.linux: 99 | return vscode.Uri.joinPath(extensionUri, "bin/linux/"); 100 | case ServerPlatform.wasi: 101 | return vscode.Uri.joinPath(extensionUri, "bin/wasi/"); 102 | } 103 | } 104 | } 105 | function getPlatformBinaryName(platform: ServerPlatform) : string { 106 | let serverPath = getUserServerPath(); 107 | if (serverPath) { 108 | return path.basename(serverPath); 109 | } else { 110 | switch (platform) { 111 | case ServerPlatform.windows: 112 | return "shader-language-server.exe"; 113 | case ServerPlatform.linux: 114 | return "shader-language-server"; 115 | case ServerPlatform.wasi: 116 | return "shader-language-server.wasm"; 117 | } 118 | } 119 | } 120 | // Absolute path as uri 121 | export function getPlatformBinaryUri(extensionUri: vscode.Uri, platform: ServerPlatform) : vscode.Uri { 122 | return vscode.Uri.joinPath(getPlatformBinaryDirectoryPath(extensionUri, platform), getPlatformBinaryName(platform)); 123 | } 124 | 125 | export function getServerPlatform() : ServerPlatform { 126 | if (isRunningOnWeb()) { 127 | return ServerPlatform.wasi; 128 | } else { 129 | // Dxc only built for linux x64 & windows x64. Fallback to WASI for every other situations. 130 | switch (process.platform) { 131 | case "win32": 132 | return (process.arch === 'x64') ? ServerPlatform.windows : ServerPlatform.wasi; 133 | case "linux": 134 | return (process.arch === 'x64') ? ServerPlatform.linux : ServerPlatform.wasi; 135 | default: 136 | return ServerPlatform.wasi; 137 | } 138 | } 139 | } 140 | 141 | function getMiddleware() : Middleware { 142 | return { 143 | async provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken, next: ProvideDocumentSymbolsSignature) { 144 | const result = await next(document, token); 145 | if (result) { 146 | // /!\ Type casting need to match server data sent. /!\ 147 | let resultArray = result as vscode.DocumentSymbol[]; 148 | sidebar.onDocumentSymbols(document.uri, resultArray); 149 | } 150 | return result; 151 | }, 152 | }; 153 | } 154 | 155 | class ShaderErrorHandler implements ErrorHandler { 156 | 157 | private readonly restarts: number[]; 158 | private readonly maxRestartCount: number = 5; 159 | 160 | constructor() { 161 | this.restarts = []; 162 | } 163 | 164 | public error(_error: Error, _message: Message, count: number): ErrorHandlerResult { 165 | if (count && count <= 3) { 166 | vscode.window.showErrorMessage("Server encountered an error in transport. Trying to continue..."); 167 | return { action: ErrorAction.Continue }; 168 | } 169 | vscode.window.showErrorMessage("Server encountered an error in transport. Shutting down."); 170 | return { action: ErrorAction.Shutdown }; 171 | } 172 | 173 | public closed(): CloseHandlerResult { 174 | this.restarts.push(Date.now()); 175 | if (this.restarts.length <= this.maxRestartCount) { 176 | vscode.window.showErrorMessage(`Server was unexpectedly closed ${this.restarts.length+1} times. Restarting...`); 177 | return { action: CloseAction.Restart }; 178 | } else { 179 | const diff = this.restarts[this.restarts.length - 1] - this.restarts[0]; 180 | if (diff <= 3 * 60 * 1000) { 181 | // Log from error. 182 | return { action: CloseAction.DoNotRestart, message: `The shader language server crashed ${this.maxRestartCount+1} times in the last 3 minutes. The server will not be restarted. Set shader-validator.trace.server to verbose for more information.` }; 183 | } else { 184 | vscode.window.showErrorMessage(`Server was unexpectedly closed ${this.restarts.length+1} again. Restarting...`); 185 | this.restarts.shift(); 186 | return { action: CloseAction.Restart }; 187 | } 188 | } 189 | } 190 | } 191 | 192 | export async function createLanguageClient(context: vscode.ExtensionContext): Promise { 193 | // Create validator 194 | // Web does not support child process, use wasi instead. 195 | let platform = getServerPlatform(); 196 | if (platform === ServerPlatform.wasi) { 197 | return createLanguageClientWASI(context); 198 | } else { 199 | return createLanguageClientStandard(context, platform); 200 | } 201 | } 202 | async function createLanguageClientStandard(context: vscode.ExtensionContext, platform : ServerPlatform) : Promise { 203 | const channelName = 'Shader language Server'; // For trace option, need same name 204 | const channel = vscode.window.createOutputChannel(channelName); 205 | context.subscriptions.push(channel); 206 | 207 | const executable = getPlatformBinaryUri(context.extensionUri, platform); 208 | const version = getServerVersion(executable.fsPath); 209 | if (!version) { 210 | vscode.window.showErrorMessage(`Server executable not found.`); 211 | return null; 212 | } 213 | if (!isValidVersion(version)) { 214 | vscode.window.showWarningMessage(`${version} is not compatible with this extension (Expecting shader-language-server v${vscode.extensions.getExtension('antaalt.shader-validator')!.packageJSON.server_version}). Server may crash or behave weirdly.`); 215 | } 216 | // Current working directory need to be set to executable for finding DLL. 217 | // But it would be better to have it pointing to workspace. 218 | const cwd = getPlatformBinaryDirectoryPath(context.extensionUri, platform); 219 | console.info(`Executing server ${executable} with working directory ${cwd}`); 220 | const trace = vscode.workspace.getConfiguration("shader-validator").get("trace.server"); 221 | const defaultEnv = {}; 222 | const env = (trace === "verbose") ? { 223 | ...defaultEnv, 224 | "RUST_BACKTRACE": "1", // eslint-disable-line 225 | "RUST_LOG": "shader_language_server=trace,shader_sense=trace", // eslint-disable-line @typescript-eslint/naming-convention 226 | } : (trace === "messages") ? { 227 | ...defaultEnv, 228 | "RUST_BACKTRACE": "1", // eslint-disable-line 229 | "RUST_LOG": "shader_language_server=info,shader_sense=info", // eslint-disable-line @typescript-eslint/naming-convention 230 | } : defaultEnv; 231 | const serverOptions: ServerOptions = { 232 | command: executable.fsPath, 233 | transport: TransportKind.stdio, 234 | options: { 235 | cwd: cwd.fsPath, 236 | env: env 237 | } 238 | }; 239 | const clientOptions: LanguageClientOptions = { 240 | // Register the server for shader documents 241 | documentSelector: [ 242 | { scheme: 'file', language: 'hlsl' }, 243 | { scheme: 'file', language: 'glsl' }, 244 | { scheme: 'file', language: 'wgsl' }, 245 | ], 246 | outputChannel: channel, 247 | middleware: getMiddleware(), 248 | errorHandler: new ShaderErrorHandler() 249 | }; 250 | 251 | let client = new LanguageClient( 252 | 'shader-validator', 253 | channelName, 254 | serverOptions, 255 | clientOptions, 256 | context.extensionMode === vscode.ExtensionMode.Development 257 | ); 258 | 259 | // Start the client. This will also launch the server 260 | await client.start(); 261 | 262 | return client; 263 | } 264 | async function createLanguageClientWASI(context: vscode.ExtensionContext) : Promise { 265 | const channelName = 'Shader language Server WASI'; // For trace option, need same name 266 | const channel = vscode.window.createOutputChannel(channelName); 267 | context.subscriptions.push(channel); 268 | 269 | // Load the WASM API 270 | const wasm: Wasm = await Wasm.load(); 271 | 272 | // Load the WASM module. It is stored alongside the extension's JS code. 273 | // So we can use VS Code's file system API to load it. Makes it 274 | // independent of whether the code runs in the desktop or the web. 275 | const executable = getPlatformBinaryUri(context.extensionUri, ServerPlatform.wasi); 276 | const version = getServerVersion(executable.fsPath); 277 | if (!version) { 278 | vscode.window.showErrorMessage(`Server executable not found.`); 279 | return null; 280 | } 281 | if (!isValidVersion(version)) { 282 | vscode.window.showWarningMessage(`${version} is not compatible with extension (Expecting shader-language-server v${vscode.extensions.getExtension('antaalt.shader-validator')!.packageJSON.server_version}). Server may crash or behave weirdly.`); 283 | } 284 | const serverOptions: ServerOptions = async () => { 285 | // Create virtual file systems to access workspaces from wasi app 286 | const mountPoints: MountPointDescriptor[] = [ 287 | { kind: 'workspaceFolder'}, // Workspaces 288 | ]; 289 | console.info(`Executing wasi server ${executable}`); 290 | const bits = await vscode.workspace.fs.readFile(executable); 291 | const module = await WebAssembly.compile(bits); 292 | 293 | 294 | const trace = vscode.workspace.getConfiguration("shader-validator").get("trace.server"); 295 | const defaultEnv = { 296 | // https://github.com/rust-lang/rust/issues/117440 297 | //"RUST_MIN_STACK": "65535", // eslint-disable-line @typescript-eslint/naming-convention 298 | }; 299 | const env = (trace === "verbose") ? { 300 | ...defaultEnv, 301 | "RUST_BACKTRACE": "1", // eslint-disable-line 302 | "RUST_LOG": "shader-language-server=trace,shader_sense=trace", // eslint-disable-line @typescript-eslint/naming-convention 303 | } : (trace === "messages") ? { 304 | ...defaultEnv, 305 | "RUST_BACKTRACE": "1", // eslint-disable-line 306 | "RUST_LOG": "shader_language_server=info,shader_sense=info", // eslint-disable-line @typescript-eslint/naming-convention 307 | } : defaultEnv; 308 | 309 | const options : ProcessOptions = { 310 | stdio: createStdioOptions(), 311 | env: env, 312 | mountPoints: mountPoints, 313 | trace: true, 314 | }; 315 | // Memory options required by wasm32-wasip1-threads target 316 | const memory : WebAssembly.MemoryDescriptor = { 317 | initial: 160, 318 | maximum: 1024, // Big enough to handle glslang heavy RAM usage. 319 | shared: true 320 | }; 321 | 322 | // Create a WASM process. 323 | const wasmProcess = await wasm.createProcess('shader-validator', module, memory, options); 324 | 325 | // Hook stderr to the output channel 326 | const decoder = new TextDecoder('utf-8'); 327 | wasmProcess.stderr!.onData(data => { 328 | const text = decoder.decode(data); 329 | console.log("Received error:", text); 330 | channel.appendLine("[shader-language-server::error]" + text); 331 | }); 332 | wasmProcess.stdout!.onData(data => { 333 | const text = decoder.decode(data); 334 | console.log("Received data:", text); 335 | channel.appendLine("[shader-language-server::data]" + text); 336 | }); 337 | return startServer(wasmProcess); 338 | }; 339 | 340 | // Now we start client 341 | const clientOptions: LanguageClientOptions = { 342 | documentSelector: [ 343 | { scheme: 'file', language: 'hlsl' }, 344 | { scheme: 'file', language: 'glsl' }, 345 | { scheme: 'file', language: 'wgsl' }, 346 | ], 347 | outputChannel: channel, 348 | uriConverters: createUriConverters(), 349 | traceOutputChannel: channel, 350 | middleware: getMiddleware(), 351 | errorHandler: new ShaderErrorHandler() 352 | }; 353 | 354 | 355 | let client = new LanguageClient( 356 | 'shader-validator', 357 | channelName, 358 | serverOptions, 359 | clientOptions, 360 | context.extensionMode === vscode.ExtensionMode.Development 361 | ); 362 | 363 | // Start the client. This will also launch the server 364 | try { 365 | await client.start(); 366 | } catch (error) { 367 | client.error(`Start failed`, error, 'force'); 368 | } 369 | 370 | return client; 371 | } 372 | -------------------------------------------------------------------------------- /src/wasm-wasi-lsp.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as vscode from 'vscode'; 6 | 7 | import { Readable, WasmProcess, Writable, type Stdio } from '@vscode/wasm-wasi/v1'; 8 | import { Disposable, Emitter, Event, Message, MessageTransports, RAL, ReadableStreamMessageReader, WriteableStreamMessageWriter } from 'vscode-languageclient'; 9 | 10 | class ReadableStreamImpl implements RAL.ReadableStream { 11 | 12 | private readonly errorEmitter: Emitter<[Error, Message | undefined, number | undefined]>; 13 | private readonly closeEmitter: Emitter; 14 | private readonly endEmitter: Emitter; 15 | 16 | private readonly readable: Readable; 17 | 18 | constructor(readable: Readable) { 19 | this.errorEmitter = new Emitter<[Error, Message, number]>(); 20 | this.closeEmitter = new Emitter(); 21 | this.endEmitter = new Emitter(); 22 | this.readable = readable; 23 | } 24 | 25 | public get onData(): Event { 26 | return this.readable.onData; 27 | } 28 | 29 | public get onError(): Event<[Error, Message | undefined, number | undefined]> { 30 | return this.errorEmitter.event; 31 | } 32 | 33 | public fireError(error: any, message?: Message, count?: number): void { 34 | this.errorEmitter.fire([error, message, count]); 35 | } 36 | 37 | public get onClose(): Event { 38 | return this.closeEmitter.event; 39 | } 40 | 41 | public fireClose(): void { 42 | this.closeEmitter.fire(undefined); 43 | } 44 | 45 | public onEnd(listener: () => void): Disposable { 46 | return this.endEmitter.event(listener); 47 | } 48 | 49 | public fireEnd(): void { 50 | this.endEmitter.fire(undefined); 51 | } 52 | } 53 | 54 | type MessageBufferEncoding = RAL.MessageBufferEncoding; 55 | 56 | class WritableStreamImpl implements RAL.WritableStream { 57 | 58 | private readonly errorEmitter: Emitter<[Error, Message | undefined, number | undefined]>; 59 | private readonly closeEmitter: Emitter; 60 | private readonly endEmitter: Emitter; 61 | 62 | private readonly writable: Writable; 63 | 64 | constructor(writable: Writable) { 65 | this.errorEmitter = new Emitter<[Error, Message, number]>(); 66 | this.closeEmitter = new Emitter(); 67 | this.endEmitter = new Emitter(); 68 | this.writable = writable; 69 | } 70 | 71 | public get onError(): Event<[Error, Message | undefined, number | undefined]> { 72 | return this.errorEmitter.event; 73 | } 74 | 75 | public fireError(error: any, message?: Message, count?: number): void { 76 | this.errorEmitter.fire([error, message, count]); 77 | } 78 | 79 | public get onClose(): Event { 80 | return this.closeEmitter.event; 81 | } 82 | 83 | public fireClose(): void { 84 | this.closeEmitter.fire(undefined); 85 | } 86 | 87 | public onEnd(listener: () => void): Disposable { 88 | return this.endEmitter.event(listener); 89 | } 90 | 91 | public fireEnd(): void { 92 | this.endEmitter.fire(undefined); 93 | } 94 | 95 | public write(data: string | Uint8Array, _encoding?: MessageBufferEncoding): Promise { 96 | if (typeof data === 'string') { 97 | return this.writable.write(data, 'utf-8'); 98 | } else { 99 | return this.writable.write(data); 100 | } 101 | } 102 | 103 | public end(): void { 104 | } 105 | } 106 | 107 | export function createStdioOptions(): Stdio { 108 | return { 109 | in: { 110 | kind: 'pipeIn', 111 | }, 112 | out: { 113 | kind: 'pipeOut' 114 | }, 115 | err: { 116 | kind: 'pipeOut' 117 | } 118 | }; 119 | } 120 | 121 | export async function startServer(process: WasmProcess, readable: Readable | undefined = process.stdout, writable: Writable | undefined = process.stdin): Promise { 122 | 123 | if (readable === undefined || writable === undefined) { 124 | throw new Error('Process created without streams or no streams provided.'); 125 | } 126 | 127 | const reader = new ReadableStreamImpl(readable); 128 | const writer = new WritableStreamImpl(writable); 129 | 130 | process.run().then((value) => { 131 | if (value === 0) { 132 | reader.fireEnd(); 133 | } else { 134 | reader.fireError([new Error(`Process exited with code: ${value}`), undefined, undefined]); 135 | } 136 | }, (error) => { 137 | reader.fireError([error, undefined, undefined]); 138 | }); 139 | 140 | return { reader: new ReadableStreamMessageReader(reader), writer: new WriteableStreamMessageWriter(writer), detached: false }; 141 | } 142 | 143 | export function createUriConverters(): { code2Protocol: (value: vscode.Uri) => string; protocol2Code: (value: string) => vscode.Uri } | undefined { 144 | const folders = vscode.workspace.workspaceFolders; 145 | if (folders === undefined || folders.length === 0) { 146 | return undefined; 147 | } 148 | const c2p: Map = new Map(); 149 | const p2c: Map = new Map(); 150 | if (folders.length === 1) { 151 | const folder = folders[0]; 152 | c2p.set(folder.uri.toString(), 'file:///workspace'); 153 | p2c.set('file:///workspace', folder.uri.toString()); 154 | } else { 155 | for (const folder of folders) { 156 | const uri = folder.uri.toString(); 157 | c2p.set(uri, `file:///workspace/${folder.name}`); 158 | p2c.set(`file:///workspace/${folder.name}`, uri); 159 | } 160 | } 161 | return { 162 | code2Protocol: (uri: vscode.Uri) => { 163 | const str = uri.toString(); 164 | for (const key of c2p.keys()) { 165 | if (str.startsWith(key)) { 166 | return str.replace(key, c2p.get(key) ?? ''); 167 | } 168 | } 169 | return str; 170 | }, 171 | protocol2Code: (value: string) => { 172 | for (const key of p2c.keys()) { 173 | if (value.startsWith(key)) { 174 | return vscode.Uri.parse(value.replace(key, p2c.get(key) ?? '')); 175 | } 176 | } 177 | return vscode.Uri.parse(value); 178 | } 179 | }; 180 | } -------------------------------------------------------------------------------- /syntaxes/glsl.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GLSL", 3 | "scopeName": "source.glsl", 4 | "fileTypes": [ "glsl" ], 5 | "author": ["antaalt"], 6 | "uuid": "34900319-4d23-40ed-8f7c-aabf564ec9d1", 7 | "patterns": [ 8 | { 9 | "include": "#line_comments" 10 | }, 11 | { 12 | "include": "#block_comments" 13 | }, 14 | { 15 | "include": "#keywords" 16 | }, 17 | { 18 | "include": "#functions" 19 | }, 20 | { 21 | "include": "#function_calls" 22 | }, 23 | { 24 | "include": "#constants" 25 | }, 26 | { 27 | "include": "#types" 28 | }, 29 | { 30 | "include": "#variables" 31 | }, 32 | { 33 | "include": "#punctuation" 34 | } 35 | ], 36 | "repository": { 37 | "line_comments": { 38 | "comment": "single line comment", 39 | "name": "comment.line.double-slash.glsl", 40 | "match": "\\s*//.*" 41 | }, 42 | "block_comments": { 43 | "patterns": [ 44 | { 45 | "comment": "empty block comments", 46 | "name": "comment.block.glsl", 47 | "match": "/\\*\\*/" 48 | }, 49 | { 50 | "comment": "block documentation comments", 51 | "name": "comment.block.documentation.glsl", 52 | "begin": "/\\*\\*", 53 | "end": "\\*/", 54 | "patterns": [ 55 | { 56 | "include": "#block_comments" 57 | } 58 | ] 59 | }, 60 | { 61 | "comment": "block comments", 62 | "name": "comment.block.glsl", 63 | "begin": "/\\*(?!\\*)", 64 | "end": "\\*/", 65 | "patterns": [ 66 | { 67 | "include": "#block_comments" 68 | } 69 | ] 70 | } 71 | ] 72 | }, 73 | "functions": { 74 | "patterns": [ 75 | { 76 | "comment": "macro function definition", 77 | "name": "meta.function.definition.glsl", 78 | "begin": "\\b([A-Z_][A-Z0-9_]*)(?=[\\s]*\\()", 79 | "beginCaptures": { 80 | "1": { 81 | "name": "constant.character.preprocessor.glsl" 82 | }, 83 | "2": { 84 | "name": "punctuation.brackets.round.glsl" 85 | } 86 | }, 87 | "end": "\\)", 88 | "endCaptures": { 89 | "0": { 90 | "name": "punctuation.brackets.round.glsl" 91 | } 92 | }, 93 | "patterns": [ 94 | { 95 | "include": "#line_comments" 96 | }, 97 | { 98 | "include": "#block_comments" 99 | }, 100 | { 101 | "include": "#keywords" 102 | }, 103 | { 104 | "include": "#function_calls" 105 | }, 106 | { 107 | "include": "#constants" 108 | }, 109 | { 110 | "include": "#types" 111 | }, 112 | { 113 | "include": "#variables" 114 | }, 115 | { 116 | "include": "#punctuation" 117 | } 118 | ] 119 | }, 120 | { 121 | "comment": "function definition", 122 | "name": "meta.function.definition.glsl", 123 | "begin": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)(?=[\\s]*\\()", 124 | "beginCaptures": { 125 | "1": { 126 | "name": "storage.type.glsl" 127 | }, 128 | "2": { 129 | "name": "entity.name.function.glsl" 130 | }, 131 | "4": { 132 | "name": "punctuation.brackets.round.glsl" 133 | } 134 | }, 135 | "end": "\\{", 136 | "endCaptures": { 137 | "0": { 138 | "name": "punctuation.brackets.curly.glsl" 139 | } 140 | }, 141 | "patterns": [ 142 | { 143 | "include": "#line_comments" 144 | }, 145 | { 146 | "include": "#block_comments" 147 | }, 148 | { 149 | "include": "#keywords" 150 | }, 151 | { 152 | "include": "#function_calls" 153 | }, 154 | { 155 | "include": "#constants" 156 | }, 157 | { 158 | "include": "#types" 159 | }, 160 | { 161 | "include": "#variables" 162 | }, 163 | { 164 | "include": "#punctuation" 165 | } 166 | ] 167 | } 168 | ] 169 | }, 170 | "function_calls": { 171 | "patterns": [ 172 | { 173 | "comment": "function/method calls", 174 | "name": "meta.function.call.glsl", 175 | "begin": "((?!([buid]?vec|mat|float|double|uint|int|bool)([1-4][1-4]?)?)[a-zA-Z_][a-zA-Z0-9_]*)(?=[\\s]*\\()", 176 | "beginCaptures": { 177 | "1": { 178 | "name": "entity.name.function.glsl" 179 | }, 180 | "2": { 181 | "name": "punctuation.brackets.round.glsl" 182 | } 183 | }, 184 | "end": "\\)", 185 | "endCaptures": { 186 | "0": { 187 | "name": "punctuation.brackets.round.glsl" 188 | } 189 | }, 190 | "patterns": [ 191 | { 192 | "include": "#line_comments" 193 | }, 194 | { 195 | "include": "#block_comments" 196 | }, 197 | { 198 | "include": "#keywords" 199 | }, 200 | { 201 | "include": "#function_calls" 202 | }, 203 | { 204 | "include": "#constants" 205 | }, 206 | { 207 | "include": "#types" 208 | }, 209 | { 210 | "include": "#variables" 211 | }, 212 | { 213 | "include": "#punctuation" 214 | } 215 | ] 216 | } 217 | ] 218 | }, 219 | "constants": { 220 | "patterns": [ 221 | { 222 | "comment": "decimal float literal", 223 | "name": "constant.numeric.float.glsl", 224 | "match": "(-?\\b[0-9][0-9]*(\\.)[0-9]*)([eE][+-]?[0-9]+|(?i:[fhl]))?\\b", 225 | "captures": { 226 | "1": { 227 | "name": "constant.numeric.float.glsl" 228 | }, 229 | "2": { 230 | "name": "constant.language.attribute.glsl entity.name.tag" 231 | }, 232 | "3": { 233 | "name": "constant.language.attribute.glsl entity.name.tag" 234 | } 235 | } 236 | }, 237 | { 238 | "comment": "decimal literal", 239 | "name": "constant.numeric.decimal.glsl", 240 | "match": "(-?\\b(0x)[0-9a-fA-F]+|\\b(0)[0-9]+|\\b[0-9][0-9]*)((?i:[lu]+))?\\b", 241 | "captures": { 242 | "1": { 243 | "name": "constant.numeric.decimal.glsl" 244 | }, 245 | "2": { 246 | "name": "constant.language.attribute.glsl entity.name.tag" 247 | }, 248 | "3": { 249 | "name": "constant.language.attribute.glsl entity.name.tag" 250 | }, 251 | "4": { 252 | "name": "constant.language.attribute.glsl entity.name.tag" 253 | } 254 | } 255 | }, 256 | { 257 | "comment": "boolean constant", 258 | "name": "constant.language.boolean.glsl", 259 | "match": "\\b(true|false)\\b" 260 | }, 261 | { 262 | "name": "constant.language.boolean.glsl", 263 | "match": "\\b(FALSE|TRUE|NULL)\\b" 264 | }, 265 | { 266 | "comment": "builtin variable", 267 | "name": "constant.language.builtin.glsl", 268 | "match": "\\b(?i:gl_VertexID|gl_InstanceID|gl_DrawID|gl_BaseVertex|gl_BaseInstance)\\b" 269 | }, 270 | { 271 | "comment": "builtin variable tesselation shader", 272 | "name": "constant.language.builtin.tesselation.glsl", 273 | "match": "\\b(?i:gl_PatchVerticesIn|gl_PrimitiveID|gl_Position|gl_InvocationID|gl_PointSize|gl_TessCoord|gl_PatchVerticesIn|gl_ClipDistance|gl_MaxPatchVertices)\\b" 274 | }, 275 | { 276 | "comment": "builtin variable tesselation shader output", 277 | "name": "constant.language.builtin.tesselation.output.glsl", 278 | "match": "\\b(?i:(gl_TessLevelOuter|gl_TessLevelInner|gl_in|gl_out)\\[[0-9]+\\])" 279 | }, 280 | { 281 | "comment": "builtin variable geometry shader", 282 | "name": "constant.language.builtin.geometry.glsl", 283 | "match": "\\b(?i:gl_PrimitiveIDI[0-9]+|gl_InvocationID|gl_Position|gl_InvocationID|gl_PointSize|gl_ViewportIndex|gl_Layer)\\b" 284 | }, 285 | { 286 | "comment": "builtin variable fragment shader", 287 | "name": "constant.language.builtin.fragment.glsl", 288 | "match": "\\b(?i:gl_FragCoord|gl_FrontFacing|gl_PointCoord|gl_SampleID|gl_SamplePosition|gl_SampleMaskIn|gl_SampleMask|gl_FragDepth)\\b" 289 | }, 290 | { 291 | "comment": "builtin variable compute shader", 292 | "name": "constant.language.builtin.compute.glsl", 293 | "match": "\\b(?i:gl_NumWorkGroups|gl_WorkGroupID|gl_LocalInvocationID|gl_GlobalInvocationID|gl_LocalInvocationIndex|gl_WorkGroupSize)\\b" 294 | }, 295 | { 296 | "comment": "builtin variable shader", 297 | "name": "constant.language.builtin.glsl", 298 | "match": "\\b(?i:gl_DepthRangeParameters|gl_DepthRange|gl_NumSamples)\\b" 299 | }, 300 | { 301 | "comment": "string constant", 302 | "name": "string.quoted.double.glsl", 303 | "begin": "[\"']", 304 | "end": "[\"']", 305 | "patterns": [ 306 | { 307 | "name": "constant.character.escape.glsl", 308 | "match": "\\\\." 309 | } 310 | ] 311 | } 312 | ] 313 | }, 314 | "types": { 315 | "comment": "types", 316 | "name": "storage.type.glsl", 317 | "patterns": [ 318 | { 319 | "comment": "scalar Types", 320 | "name": "storage.type.glsl", 321 | "match": "\\b(bool|int|uint|float|double)\\b" 322 | }, 323 | { 324 | "comment": "vector/matrix types", 325 | "name": "storage.type.glsl", 326 | "match": "\\b([biud]?(vec|mat)[1-4](x[1-4])?)\\b" 327 | }, 328 | { 329 | "comment": "legacy sampler type", 330 | "name": "storage.type.sampler.legacy.glsl", 331 | "match": "\\b[biud]?(sampler|sampler1D|sampler2D|sampler3D|samplerCube|sampler2DRect|sampler1DArray|sampler2DArray|samplerCubeArray|samplerBuffer|sampler2DMS|sampler2DMSArray)(Shadow)?\\b" 332 | }, 333 | { 334 | "comment": "texture type", 335 | "name": "storage.type.texture.glsl", 336 | "match": "\\b[biud]?(image1D|image2D|image3D|imageCube|image2DRect|image1DArray|image2DArray|imageCubeArray|imageBuffer|image2DMS|image2DMSArray)\\b" 337 | }, 338 | { 339 | "comment": "Custom type", 340 | "name": "entity.name.type.glsl", 341 | "match": "\\b([A-Z][A-Za-z0-9_]*)\\b" 342 | } 343 | ] 344 | }, 345 | "variables": { 346 | "patterns": [ 347 | { 348 | "comment": "variables", 349 | "name": "variable.other.glsl", 350 | "match": "\\b(?]" 385 | } 386 | ] 387 | }, 388 | "keywords": { 389 | "patterns": [ 390 | { 391 | "comment": "other keywords", 392 | "name": "keyword.control.glsl", 393 | "match": "\\b(const|goto|void|volatile|break|typedef|using|case|continue|default|discard|else|export|do|enum|for|function|if|private|return|switch|attribute|while|workgroup|operator)\\b" 394 | }, 395 | { 396 | "comment": "reserved keywords", 397 | "name": "keyword.control.glsl", 398 | "match": "\\b(try|catch|do|new|long|typeid|public)\\b" 399 | }, 400 | { 401 | "comment": "constant keyword", 402 | "name": "keyword.declaration.glsl", 403 | "match": "\\b(unsigned|signed|cbuffer|tbuffer|namespace|coherent|uniform|restrict|readonly|writeonly|lowp|mediump|highp|precision|varying|unorm|patch)\\b" 404 | }, 405 | { 406 | "comment": "struct keyword", 407 | "name": "keyword.declaration.struct.glsl", 408 | "match": "\\b(struct)\\s+(\\[[a-z]+\\]\\s+)?([a-zA-Z][a-zA-Z0-9_]*)?(\\s+:\\s+[a-zA-Z][a-zA-Z0-9_]*)?", 409 | "captures": { 410 | "1": { 411 | "name": "keyword.declaration.struct.glsl" 412 | }, 413 | "2": { 414 | "name": "constant.language.attribute.glsl" 415 | }, 416 | "3": { 417 | "name": "entity.name.type.glsl" 418 | }, 419 | "5": { 420 | "name": "entity.name.type.glsl" 421 | } 422 | } 423 | 424 | }, 425 | { 426 | "comment": "logical operators", 427 | "name": "keyword.operator.logical.glsl", 428 | "match": "(\\^|\\||\\|\\||&&|<<|>>|!|~|\\*)(?!=)" 429 | }, 430 | { 431 | "comment": "logical AND, borrow references", 432 | "name": "keyword.operator.borrow.and.glsl", 433 | "match": "&(?![&=])" 434 | }, 435 | { 436 | "comment": "assignment operators", 437 | "name": "keyword.operator.assignment.glsl", 438 | "match": "(\\+=|-=|\\*=|/=|%=|\\^=|&=|\\|=|<<=|>>=)" 439 | }, 440 | { 441 | "comment": "single equal", 442 | "name": "keyword.operator.assignment.equal.glsl", 443 | "match": "(?])=(?!=|>)" 444 | }, 445 | { 446 | "comment": "comparison operators", 447 | "name": "keyword.operator.comparison.glsl", 448 | "match": "(=(=)?(?!>)|!=|<=|(?=|>|<)" 449 | }, 450 | { 451 | "comment": "math operators", 452 | "name": "keyword.operator.math.glsl", 453 | "match": "(([+%]|(\\*(?!\\w)))(?!=))|(-(?!>))|(/(?!/))" 454 | }, 455 | { 456 | "comment": "dot access", 457 | "name": "keyword.operator.access.dot.glsl", 458 | "match": "\\.(?!\\.)" 459 | }, 460 | { 461 | "comment": "namespace access", 462 | "name": "keyword.operator.access.colon.glsl", 463 | "match": "\\::(?!\\:)" 464 | }, 465 | { 466 | "comment": "dashrocket, skinny arrow", 467 | "name": "keyword.operator.arrow.skinny.glsl", 468 | "match": "->" 469 | }, 470 | { 471 | "comment": "type modifier", 472 | "name": "keyword.declaration.glsl", 473 | "match": "\\b(column_major|const|export|extern|globallycoherent|groupshared|inline|inout|in|out|precise|row_major|shared|static|uniform|volatile|buffer)\\b" 474 | }, 475 | { 476 | "comment": "compiler operator", 477 | "name": "keyword.declaration.glsl", 478 | "match": "\\b(sizeof|offsetof|static_assert|decltype)\\b" 479 | }, 480 | { 481 | "comment": "type modifier for float", 482 | "name": "keyword.declaration.float.glsl", 483 | "match": "\\b(snorm|unorm)\\b" 484 | }, 485 | { 486 | "comment": "type modifier for storage", 487 | "name": "keyword.declaration.glsl", 488 | "match": "\\b(packoffset|register)\\b" 489 | }, 490 | { 491 | "comment": "type modifier for interpolation", 492 | "name": "keyword.declaration.glsl", 493 | "match": "\\b(flat|noperspective|smooth|centroid|sample|invariant)\\b" 494 | }, 495 | { 496 | "comment": "type modifier for geometry shader", 497 | "name": "keyword.declaration.glsl", 498 | "match": "\\b(lineadj|line|point|triangle|triangleadj)\\b" 499 | }, 500 | { 501 | "comment": "preprocessor", 502 | "name": "keyword.preprocessor.glsl", 503 | "match": "^\\s*#\\s*(define|elif|else|endif|ifdef|ifndef|if|undef|line|error|warning|pragma|INF|version|extension)\\s+([A-Za-z0-9_]+)?", 504 | "captures": { 505 | "1": { 506 | "name": "keyword.preprocessor.glsl" 507 | }, 508 | "2": { 509 | "name": "constant.character.preprocessor.glsl" 510 | } 511 | } 512 | }, 513 | { 514 | "comment": "system include constant", 515 | "name": "keyword.preprocessor.glsl", 516 | "match": "^\\s*#\\s*include\\s+([<\"].*[>\"])", 517 | "captures": { 518 | "1": { 519 | "name": "string.quoted.double.glsl", 520 | "patterns": [ 521 | { 522 | "name": "constant.character.escape.glsl", 523 | "match": "\\\\." 524 | } 525 | ] 526 | } 527 | } 528 | } 529 | ] 530 | } 531 | } 532 | } -------------------------------------------------------------------------------- /syntaxes/hlsl.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HLSL", 3 | "scopeName": "source.hlsl", 4 | "fileTypes": [ "hlsl" ], 5 | "author": ["antaalt"], 6 | "_resources":[ 7 | "https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide", 8 | "https://macromates.com/manual/en/language_grammars#naming-conventions" 9 | ], 10 | "uuid": "75ae6133-98c4-45a5-ac1b-0c31e8af330c", 11 | "patterns": [ 12 | { 13 | "include": "#line_comments" 14 | }, 15 | { 16 | "include": "#block_comments" 17 | }, 18 | { 19 | "include": "#qualifiers" 20 | }, 21 | { 22 | "include": "#preprocessor" 23 | }, 24 | { 25 | "include": "#keywords" 26 | }, 27 | { 28 | "include": "#attributes" 29 | }, 30 | { 31 | "include": "#functions" 32 | }, 33 | { 34 | "include": "#function_calls" 35 | }, 36 | { 37 | "include": "#constants" 38 | }, 39 | { 40 | "include": "#variables" 41 | }, 42 | { 43 | "include": "#types" 44 | }, 45 | { 46 | "include": "#punctuation" 47 | } 48 | ], 49 | "repository": { 50 | "line_comments": { 51 | "comment": "single line comment", 52 | "name": "comment.line.double-slash.hlsl", 53 | "match": "\\s*//.*" 54 | }, 55 | "block_comments": { 56 | "patterns": [ 57 | { 58 | "comment": "empty block comments", 59 | "name": "comment.block.hlsl", 60 | "match": "/\\*\\*/" 61 | }, 62 | { 63 | "comment": "block documentation comments", 64 | "name": "comment.block.documentation.hlsl", 65 | "begin": "/\\*\\*", 66 | "end": "\\*/", 67 | "patterns": [ 68 | { 69 | "include": "#block_comments" 70 | } 71 | ] 72 | }, 73 | { 74 | "comment": "block comments", 75 | "name": "comment.block.hlsl", 76 | "begin": "/\\*(?!\\*)", 77 | "end": "\\*/", 78 | "patterns": [ 79 | { 80 | "include": "#block_comments" 81 | } 82 | ] 83 | } 84 | ] 85 | }, 86 | "attributes": { 87 | "patterns": [ 88 | { 89 | "comment": "attribute declaration", 90 | "name": "constant.language.attribute.hlsl", 91 | "match": "\\[(?i:domain|earlydepthstencil|instance|maxtessfactor|numthreads|outputcontrolpoints|outputtopology|partitioning|patchconstantfunc|flatten|branch|loop|unroll|fastopt|allow_uav_condition|forcecase|call|clipplanes|maxvertexcount|noinline|unknown|rootsignature|raypayload)" 92 | } 93 | ] 94 | }, 95 | "functions": { 96 | "patterns": [ 97 | { 98 | "comment": "function definition", 99 | "name": "meta.function.definition.hlsl", 100 | "begin": "\\b(\\w+)(\\s*\\<\\s*(\\w+)\\s*\\>\\s*)?\\s+(\\w+)(?=[\\s]*\\()", 101 | "beginCaptures": { 102 | "1": { 103 | "name": "entity.name.type.hlsl", 104 | "patterns": [ 105 | { 106 | "include": "#types" 107 | } 108 | ] 109 | }, 110 | "3": { 111 | "name": "entity.name.type.hlsl", 112 | "patterns": [ 113 | { 114 | "include": "#types" 115 | } 116 | ] 117 | }, 118 | "4": { 119 | "name": "entity.name.function.hlsl" 120 | }, 121 | "6": { 122 | "name": "punctuation.brackets.round.hlsl" 123 | } 124 | }, 125 | "end": "\\{", 126 | "endCaptures": { 127 | "0": { 128 | "name": "punctuation.brackets.curly.hlsl" 129 | } 130 | }, 131 | "patterns": [ 132 | { 133 | "include": "#preprocessor" 134 | }, 135 | { 136 | "include": "#line_comments" 137 | }, 138 | { 139 | "include": "#block_comments" 140 | }, 141 | { 142 | "include": "#qualifiers" 143 | }, 144 | { 145 | "include": "#keywords" 146 | }, 147 | { 148 | "include": "#attributes" 149 | }, 150 | { 151 | "include": "#function_calls" 152 | }, 153 | { 154 | "include": "#constants" 155 | }, 156 | { 157 | "include": "#types" 158 | }, 159 | { 160 | "include": "#variables" 161 | }, 162 | { 163 | "include": "#punctuation" 164 | } 165 | ] 166 | } 167 | ] 168 | }, 169 | "function_calls": { 170 | "patterns": [ 171 | { 172 | "comment": "function/method calls", 173 | "name": "meta.function.call.hlsl", 174 | "begin": "([a-zA-Z_][a-zA-Z0-9_]*)(\\s*\\<\\s*(\\w+)\\s*\\>)?(?=[\\s]*\\()", 175 | "beginCaptures": { 176 | "1": { 177 | "name": "entity.name.function.hlsl", 178 | "patterns": [ 179 | { 180 | "include": "#types" 181 | } 182 | ] 183 | }, 184 | "3": { 185 | "name": "entity.name.function.hlsl", 186 | "patterns": [ 187 | { 188 | "include": "#types" 189 | } 190 | ] 191 | }, 192 | "4": { 193 | "name": "punctuation.brackets.round.hlsl" 194 | } 195 | }, 196 | "end": "\\)", 197 | "endCaptures": { 198 | "0": { 199 | "name": "punctuation.brackets.round.hlsl" 200 | } 201 | }, 202 | "patterns": [ 203 | { 204 | "include": "#preprocessor" 205 | }, 206 | { 207 | "include": "#line_comments" 208 | }, 209 | { 210 | "include": "#block_comments" 211 | }, 212 | { 213 | "include": "#qualifiers" 214 | }, 215 | { 216 | "include": "#keywords" 217 | }, 218 | { 219 | "include": "#attributes" 220 | }, 221 | { 222 | "include": "#function_calls" 223 | }, 224 | { 225 | "include": "#constants" 226 | }, 227 | { 228 | "include": "#types" 229 | }, 230 | { 231 | "include": "#variables" 232 | }, 233 | { 234 | "include": "#punctuation" 235 | } 236 | ] 237 | } 238 | ] 239 | }, 240 | "constants": { 241 | "patterns": [ 242 | { 243 | "comment": "decimal float literal", 244 | "name": "constant.numeric.float.hlsl", 245 | "match": "(-?\\b[0-9][0-9]*\\.[0-9]*)([eE][+-]?[0-9]+)?((?i:[fhl]))?\\b", 246 | "captures": { 247 | "1": { 248 | "name": "constant.numeric.float.hlsl" 249 | }, 250 | "2": { 251 | "name": "constant.language.attribute.hlsl entity.name.tag" 252 | }, 253 | "3": { 254 | "name": "constant.language.attribute.hlsl entity.name.tag" 255 | } 256 | } 257 | }, 258 | { 259 | "comment": "decimal literal", 260 | "name": "constant.numeric.decimal.hlsl", 261 | "match": "(-?\\b((0x)[0-9a-fA-F]+|(0)[0-9]+|[0-9]+))([ul]+)?\\b", 262 | "captures": { 263 | "1": { 264 | "name": "constant.numeric.decimal.hlsl" 265 | }, 266 | "2": { 267 | "name": "constant.numeric.decimal.hlsl" 268 | }, 269 | "3": { 270 | "name": "constant.language.attribute.hlsl entity.name.tag" 271 | }, 272 | "4": { 273 | "name": "constant.language.attribute.hlsl entity.name.tag" 274 | }, 275 | "5": { 276 | "name": "constant.language.attribute.hlsl entity.name.tag" 277 | } 278 | } 279 | }, 280 | { 281 | "comment": "boolean constant", 282 | "name": "constant.language.boolean.hlsl", 283 | "match": "\\b(true|false)\\b" 284 | }, 285 | { 286 | "name": "constant.language.boolean.hlsl", 287 | "match": "\\b(FALSE|TRUE|NULL)\\b" 288 | }, 289 | { 290 | "comment": "hlsl semantic", 291 | "name": "constant.language.hlsl", 292 | "match": "(?<=\\:\\s|\\:)(?i:BINORMAL[0-9]*|BLENDINDICES[0-9]*|BLENDWEIGHT[0-9]*|COLOR[0-9]*|NORMAL[0-9]*|POSITIONT|POSITION[0-9]*|PSIZE[0-9]*|TANGENT[0-9]*|TEXCOORD[0-9]*|FOG|TESSFACTOR[0-9]*|VFACE|VPOS|DEPTH[0-9]*)\\b" 293 | }, 294 | { 295 | "comment": "hlsl semantic shader model 4.0", 296 | "name": "constant.language.sm4.hlsl", 297 | "match": "\\b(?<=\\:\\s|\\:)(?i:SV_ClipDistance[0-9]*|SV_CullDistance[0-9]*|SV_Coverage|SV_Depth|SV_DepthGreaterEqual[0-9]*|SV_DepthLessEqual[0-9]*|SV_InstanceID|SV_IsFrontFace|SV_Position|SV_RenderTargetArrayIndex|SV_SampleIndex|SV_StencilRef|SV_Target[0-7]?|SV_VertexID|SV_ViewportArrayIndex)\\b" 298 | }, 299 | { 300 | "comment": "hlsl semantic shader model 5.0", 301 | "name": "constant.language.sm5.hlsl", 302 | "match": "(?<=\\:\\s|\\:)(?i:SV_DispatchThreadID|SV_DomainLocation|SV_GroupID|SV_GroupIndex|SV_GroupThreadID|SV_GSInstanceID|SV_InsideTessFactor|SV_OutputControlPointID|SV_TessFactor)\\b" 303 | }, 304 | { 305 | "comment": "hlsl semantic shader model 5.1", 306 | "name": "constant.language.sm51.hlsl", 307 | "match": "(?<=\\:\\s|\\:)(?i:SV_InnerCoverage|SV_StencilRef)\\b" 308 | }, 309 | { 310 | "comment": "string constant", 311 | "name": "string.quoted.double.hlsl", 312 | "begin": "[\"']", 313 | "end": "[\"']", 314 | "patterns": [ 315 | { 316 | "name": "constant.character.escape.hlsl", 317 | "match": "\\\\." 318 | } 319 | ] 320 | } 321 | ] 322 | }, 323 | "types": { 324 | "comment": "types", 325 | "name": "storage.type.hlsl", 326 | "patterns": [ 327 | { 328 | "comment": "scalar Types", 329 | "name": "storage.type.hlsl", 330 | "match": "\\b(bool|int|uint|dword|half|float|double)\\b" 331 | }, 332 | { 333 | "comment": "minimum precision scalar Types", 334 | "name": "storage.type.hlsl", 335 | "match": "\\b(min16float|min10float|min16int|min12int|min16uint)\\b" 336 | }, 337 | { 338 | "comment": "scalar Types 6.0", 339 | "name": "storage.type.sm60.hlsl", 340 | "match": "\\b(uint64_t|int64_t|uint32_t|int32_t)\\b" 341 | }, 342 | { 343 | "comment": "scalar Types 6.2", 344 | "name": "storage.type.sm62.hlsl", 345 | "match": "\\b(float16_t|uint16_t|int16_t|float32_t|float64_t)\\b" 346 | }, 347 | { 348 | "comment": "vector/matrix types", 349 | "name": "storage.type.hlsl", 350 | "match": "\\b(matrix|vector|(bool|int|uint|half|float|double|dword)[1-4](x[1-4])?)\\b" 351 | }, 352 | { 353 | "comment": "vector/matrix types", 354 | "name": "storage.type.hlsl", 355 | "match": "\\b((min12int|min16float|min16int|min16uint|float16_t|float32_t|float64_t|uint16_t|int16_t|uint32_t|int32_t|uint64_t|int64_t)[1-4](x[1-4])?)\\b" 356 | }, 357 | { 358 | "comment": "string type", 359 | "name": "storage.type.hlsl", 360 | "match": "\\b(string)\\b" 361 | }, 362 | { 363 | "comment": "read hlsl type", 364 | "name": "storage.type.object.hlsl", 365 | "match": "\\b(AppendStructuredBuffer|Buffer|StructuredBuffer|ByteAddressBuffer|ConstantBuffer|TextureBuffer|ConsumeStructuredBuffer|InputPatch|OutputPatch|FeedbackTexture2D|FeedbackTexture2DArray)\\b" 366 | }, 367 | { 368 | "comment": "rasterized order view type", 369 | "name": "storage.type.object.rasterizerordered.hlsl", 370 | "match": "\\b(RasterizerOrderedBuffer|RasterizerOrderedByteAddressBuffer|RasterizerOrderedStructuredBuffer|RasterizerOrderedTexture1D|RasterizerOrderedTexture1DArray|RasterizerOrderedTexture2D|RasterizerOrderedTexture2DArray|RasterizerOrderedTexture3D)\\b" 371 | }, 372 | { 373 | "comment": "read write hlsl type", 374 | "name": "storage.type.object.rw.hlsl", 375 | "match": "\\b(RWBuffer|RWByteAddressBuffer|RWStructuredBuffer|RWTexture1D|RWTexture1DArray|RWTexture2D|RWTexture2DArray|RWTexture3D)\\b" 376 | }, 377 | { 378 | "comment": "raytracing extension", 379 | "name": "storage.type.object.rw.hlsl", 380 | "match": "\\b(RayDesc|BuiltInTriangleIntersectionAttributes|RaytracingAccelerationStructure|GlobalRootSignature)\\b" 381 | }, 382 | { 383 | "comment": "geometry stream type", 384 | "name": "storage.type.object.geometryshader.hlsl", 385 | "match": "\\b(LineStream|PointStream|TriangleStream)\\b" 386 | }, 387 | { 388 | "comment": "legacy sampler type", 389 | "name": "storage.type.sampler.legacy.hlsl", 390 | "match": "\\b(sampler|sampler1D|sampler2D|sampler3D|samplerCUBE|sampler_state)\\b" 391 | }, 392 | { 393 | "comment": "sampler type", 394 | "name": "storage.type.sampler.hlsl", 395 | "match": "\\b(SamplerState|SamplerComparisonState)\\b" 396 | }, 397 | { 398 | "comment": "legacy texture type", 399 | "name": "storage.type.texture.legacy.hlsl", 400 | "match": "\\b(texture2D|textureCUBE)\\b" 401 | }, 402 | { 403 | "comment": "texture type", 404 | "name": "storage.type.texture.hlsl", 405 | "match": "\\b(Texture1D|Texture1DArray|Texture2D|Texture2DArray|Texture2DMS|Texture2DMSArray|Texture3D|TextureCube|TextureCubeArray)\\b" 406 | }, 407 | { 408 | "comment": "blending type", 409 | "name": "storage.type.hlsl", 410 | "match": "\\b(BlendState|DepthStencilState|RasterizerState)\\b" 411 | }, 412 | { 413 | "comment": "technique type", 414 | "name": "storage.type.fx.technique.hlsl", 415 | "match": "\\b(technique|Technique|technique10|technique11|pass)\\b" 416 | } 417 | ] 418 | }, 419 | "variables": { 420 | "patterns": [ 421 | { 422 | "comment": "struct declaration", 423 | "name": "keyword.declaration.struct.hlsl", 424 | "match": "\\b(struct|class|interface|cbuffer|tbuffer)\\s+(\\[[a-z]+\\]\\s+)?([a-zA-Z][a-zA-Z0-9_]*)?(\\s+:\\s+[a-zA-Z][a-zA-Z0-9_]*)?", 425 | "captures": { 426 | "1": { 427 | "name": "keyword.declaration.struct.hlsl" 428 | }, 429 | "2": { 430 | "name": "constant.language.attribute.hlsl" 431 | }, 432 | "3": { 433 | "name": "entity.name.type.hlsl" 434 | } 435 | } 436 | }, 437 | { 438 | "comment": "variable declaration", 439 | "name": "meta.variable.declaration.hlsl", 440 | "match": "\\b((\\w+)\\s+)*(\\w+)(\\s*\\<\\s*((\\w+)[\\s\\,]?)+\\s*\\>)?\\s+(\\w+)\\b", 441 | "captures": { 442 | "2": { 443 | "name": "keyword.declaration.qualifier.hlsl", 444 | "patterns": [ 445 | { 446 | "include": "#qualifiers" 447 | } 448 | ] 449 | }, 450 | "3": { 451 | "name": "entity.name.type.hlsl", 452 | "patterns": [ 453 | { 454 | "include": "#types" 455 | } 456 | ] 457 | }, 458 | "5": { 459 | "name": "entity.name.type.hlsl", 460 | "patterns": [ 461 | { 462 | "include": "#types" 463 | } 464 | ] 465 | }, 466 | "6": { 467 | "name": "variable.other.hlsl" 468 | } 469 | } 470 | } 471 | ] 472 | }, 473 | "punctuation": { 474 | "patterns": [ 475 | { 476 | "comment": "comma", 477 | "name": "punctuation.comma.hlsl", 478 | "match": "," 479 | }, 480 | { 481 | "comment": "curly braces", 482 | "name": "punctuation.brackets.curly.hlsl", 483 | "match": "[{}]" 484 | }, 485 | { 486 | "comment": "parentheses, round brackets", 487 | "name": "punctuation.brackets.round.hlsl", 488 | "match": "[()]" 489 | }, 490 | { 491 | "comment": "semicolon", 492 | "name": "punctuation.semi.hlsl", 493 | "match": ";" 494 | }, 495 | { 496 | "comment": "square brackets", 497 | "name": "punctuation.brackets.square.hlsl", 498 | "match": "[\\[\\]]" 499 | }, 500 | { 501 | "comment": "angle brackets", 502 | "name": "punctuation.brackets.angle.hlsl", 503 | "match": "(?]" 504 | } 505 | ] 506 | }, 507 | "qualifiers": { 508 | "patterns": [ 509 | { 510 | "comment": "other qualifier", 511 | "name": "keyword.qualifier.hlsl", 512 | "match": "\\b(const|volatile|uniform|unorm|unsigned|signed)\\b" 513 | }, 514 | { 515 | "comment": "type modifier for float", 516 | "name": "keyword.declaration.float.hlsl", 517 | "match": "\\b(snorm|unorm)\\b" 518 | }, 519 | { 520 | "comment": "type modifier for interpolation", 521 | "name": "keyword.declaration.hlsl", 522 | "match": "\\b(centroid|linear|nointerpolation|noperspective|sample)\\b" 523 | }, 524 | { 525 | "comment": "type modifier for geometry shader", 526 | "name": "keyword.declaration.hlsl", 527 | "match": "\\b(lineadj|line|point|triangle|triangleadj)\\b" 528 | }, 529 | { 530 | "comment": "common qualifier", 531 | "name": "keyword.qualifier.hlsl", 532 | "match": "\\b(column_major|const|export|extern|globallycoherent|groupshared|inline|inout|in|out|precise|row_major|shared|static|uniform|volatile)\\b" 533 | } 534 | ] 535 | }, 536 | "preprocessor": { 537 | "patterns": [ 538 | { 539 | "comment": "preprocessor", 540 | "name": "keyword.preprocessor.hlsl", 541 | "match": "^\\s*#\\s*(if|elif|else|endif|ifdef|ifndef|undef|include|line|error|warning|pragma|INF)\\s+([\\w\\(\\)]+)?", 542 | "captures": { 543 | "1": { 544 | "name": "keyword.other.preprocessor.hlsl" 545 | }, 546 | "2": { 547 | "name": "variable.other.hlsl", 548 | "patterns": [ 549 | { 550 | "comment": "defined macro", 551 | "match": "(defined)\\(\\w+\\)", 552 | "captures": { 553 | "1": { 554 | "name": "entity.name.function" 555 | } 556 | } 557 | }, 558 | { 559 | "include": "#line_comments" 560 | }, 561 | { 562 | "include": "#block_comments" 563 | }, 564 | { 565 | "include": "#keywords" 566 | }, 567 | { 568 | "include": "#attributes" 569 | }, 570 | { 571 | "include": "#function_calls" 572 | }, 573 | { 574 | "include": "#constants" 575 | }, 576 | { 577 | "include": "#types" 578 | }, 579 | { 580 | "include": "#variables" 581 | }, 582 | { 583 | "include": "#punctuation" 584 | } 585 | ] 586 | } 587 | } 588 | }, 589 | { 590 | "comment": "define preprocessor", 591 | "name": "keyword.preprocessor.hlsl", 592 | "match": "^\\s*#\\s*(define)\\s+(\\w+)(\\s*\\(\\s*[\\w\\,\\s]+\\s*\\))?\\s+(.*)?", 593 | "captures": { 594 | "1": { 595 | "name": "keyword.preprocessor.hlsl" 596 | }, 597 | "2": { 598 | "name": "entity.name.function.preprocessor.hlsl" 599 | }, 600 | "3": { 601 | "name": "variable.other.hlsl" 602 | }, 603 | "4": { 604 | "name": "variable.other.hlsl", 605 | "patterns": [ 606 | { 607 | "include": "#line_comments" 608 | }, 609 | { 610 | "include": "#block_comments" 611 | }, 612 | { 613 | "include": "#keywords" 614 | }, 615 | { 616 | "include": "#attributes" 617 | }, 618 | { 619 | "include": "#function_calls" 620 | }, 621 | { 622 | "include": "#constants" 623 | }, 624 | { 625 | "include": "#types" 626 | }, 627 | { 628 | "include": "#variables" 629 | }, 630 | { 631 | "include": "#punctuation" 632 | } 633 | ] 634 | } 635 | } 636 | } 637 | ] 638 | }, 639 | "keywords": { 640 | "patterns": [ 641 | { 642 | "comment": "other keywords", 643 | "name": "keyword.control.hlsl", 644 | "match": "\\b(goto|void|break|typedef|using|case|continue|default|discard|else|export|do|enum|for|function|if|private|return|switch|while|workgroup|operator)\\b" 645 | }, 646 | { 647 | "comment": "reserved keywords", 648 | "name": "keyword.control.hlsl", 649 | "match": "\\b(try|catch|do|new|long|typeid|public|__imag|__real)\\b" 650 | }, 651 | { 652 | "comment": "type keyword", 653 | "name": "keyword.declaration.type.hlsl", 654 | "match": "\\b(typename|template|namespace)\\b" 655 | }, 656 | { 657 | "comment": "logical operators", 658 | "name": "keyword.operator.logical.hlsl", 659 | "match": "(\\^|\\||\\|\\||&&|<<|>>|!|~|\\*)(?!=)" 660 | }, 661 | { 662 | "comment": "logical AND, borrow references", 663 | "name": "keyword.operator.borrow.and.hlsl", 664 | "match": "&(?![&=])" 665 | }, 666 | { 667 | "comment": "assignment operators", 668 | "name": "keyword.operator.assignment.hlsl", 669 | "match": "(\\+=|-=|\\*=|/=|%=|\\^=|&=|\\|=|<<=|>>=)" 670 | }, 671 | { 672 | "comment": "single equal", 673 | "name": "keyword.operator.assignment.equal.hlsl", 674 | "match": "(?])=(?!=|>)" 675 | }, 676 | { 677 | "comment": "comparison operators", 678 | "name": "keyword.operator.comparison.hlsl", 679 | "match": "(=(=)?(?!>)|!=|<=|(?=|>|<)" 680 | }, 681 | { 682 | "comment": "math operators", 683 | "name": "keyword.operator.math.hlsl", 684 | "match": "(([+%]|(\\*(?!\\w)))(?!=))|(-(?!>))|(/(?!/))" 685 | }, 686 | { 687 | "comment": "dot access", 688 | "name": "keyword.operator.access.dot.hlsl", 689 | "match": "\\.(?!\\.)" 690 | }, 691 | { 692 | "comment": "namespace access", 693 | "name": "keyword.operator.access.colon.hlsl", 694 | "match": "\\::(?!\\:)" 695 | }, 696 | { 697 | "comment": "dashrocket, skinny arrow", 698 | "name": "keyword.operator.arrow.skinny.hlsl", 699 | "match": "->" 700 | }, 701 | { 702 | "comment": "compiler operator", 703 | "name": "keyword.declaration.hlsl", 704 | "match": "\\b(sizeof|offsetof|static_assert|decltype|__decltype|_Static_assert)\\b" 705 | }, 706 | { 707 | "comment": "type modifier for storage", 708 | "name": "keyword.declaration.hlsl", 709 | "match": "\\b(packoffset|register)\\b" 710 | } 711 | ] 712 | } 713 | } 714 | } -------------------------------------------------------------------------------- /syntaxes/wgsl.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WGSL", 3 | "scopeName": "source.wgsl", 4 | "fileTypes": [ "wgsl" ], 5 | "author": ["antaalt", "PolyMeilex"], 6 | "uuid": "746210e7-d00f-4583-b571-f4cbb6b4f73e", 7 | "patterns": [ 8 | { 9 | "include": "#line_comments" 10 | }, 11 | { 12 | "include": "#block_comments" 13 | }, 14 | { 15 | "include": "#keywords" 16 | }, 17 | { 18 | "include": "#attributes" 19 | }, 20 | { 21 | "include": "#functions" 22 | }, 23 | { 24 | "include": "#function_calls" 25 | }, 26 | { 27 | "include": "#constants" 28 | }, 29 | { 30 | "include": "#types" 31 | }, 32 | { 33 | "include": "#variables" 34 | }, 35 | { 36 | "include": "#punctuation" 37 | } 38 | ], 39 | "repository": { 40 | "line_comments": { 41 | "comment": "single line comment", 42 | "name": "comment.line.double-slash.wgsl", 43 | "match": "\\s*//.*" 44 | }, 45 | "block_comments": { 46 | "patterns": [ 47 | { 48 | "comment": "empty block comments", 49 | "name": "comment.block.wgsl", 50 | "match": "/\\*\\*/" 51 | }, 52 | { 53 | "comment": "block documentation comments", 54 | "name": "comment.block.documentation.wgsl", 55 | "begin": "/\\*\\*", 56 | "end": "\\*/", 57 | "patterns": [ 58 | { 59 | "include": "#block_comments" 60 | } 61 | ] 62 | }, 63 | { 64 | "comment": "block comments", 65 | "name": "comment.block.wgsl", 66 | "begin": "/\\*(?!\\*)", 67 | "end": "\\*/", 68 | "patterns": [ 69 | { 70 | "include": "#block_comments" 71 | } 72 | ] 73 | } 74 | ] 75 | }, 76 | "attributes": { 77 | "patterns": [ 78 | { 79 | "comment": "attribute declaration", 80 | "name": "meta.attribute.wgsl", 81 | "match": "(@)([A-Za-z_]+)", 82 | "captures": { 83 | "1": { 84 | "name": "keyword.operator.attribute.at" 85 | }, 86 | "2": { 87 | "name": "entity.name.attribute.wgsl" 88 | } 89 | } 90 | } 91 | ] 92 | }, 93 | "functions": { 94 | "patterns": [ 95 | { 96 | "comment": "function definition", 97 | "name": "meta.function.definition.wgsl", 98 | "begin": "\\b(fn)\\s+([A-Za-z0-9_]+)((\\()|(<))", 99 | "beginCaptures": { 100 | "1": { 101 | "name": "keyword.other.fn.wgsl" 102 | }, 103 | "2": { 104 | "name": "entity.name.function.wgsl" 105 | }, 106 | "4": { 107 | "name": "punctuation.brackets.round.wgsl" 108 | } 109 | }, 110 | "end": "\\{", 111 | "endCaptures": { 112 | "0": { 113 | "name": "punctuation.brackets.curly.wgsl" 114 | } 115 | }, 116 | "patterns": [ 117 | { 118 | "include": "#line_comments" 119 | }, 120 | { 121 | "include": "#block_comments" 122 | }, 123 | { 124 | "include": "#keywords" 125 | }, 126 | { 127 | "include": "#attributes" 128 | }, 129 | { 130 | "include": "#function_calls" 131 | }, 132 | { 133 | "include": "#constants" 134 | }, 135 | { 136 | "include": "#types" 137 | }, 138 | { 139 | "include": "#variables" 140 | }, 141 | { 142 | "include": "#punctuation" 143 | } 144 | ] 145 | } 146 | ] 147 | }, 148 | "function_calls": { 149 | "patterns": [ 150 | { 151 | "comment": "function/method calls", 152 | "name": "meta.function.call.wgsl", 153 | "begin": "([A-Za-z0-9_]+)(\\()", 154 | "beginCaptures": { 155 | "1": { 156 | "name": "entity.name.function.wgsl" 157 | }, 158 | "2": { 159 | "name": "punctuation.brackets.round.wgsl" 160 | } 161 | }, 162 | "end": "\\)", 163 | "endCaptures": { 164 | "0": { 165 | "name": "punctuation.brackets.round.wgsl" 166 | } 167 | }, 168 | "patterns": [ 169 | { 170 | "include": "#line_comments" 171 | }, 172 | { 173 | "include": "#block_comments" 174 | }, 175 | { 176 | "include": "#keywords" 177 | }, 178 | { 179 | "include": "#attributes" 180 | }, 181 | { 182 | "include": "#function_calls" 183 | }, 184 | { 185 | "include": "#constants" 186 | }, 187 | { 188 | "include": "#types" 189 | }, 190 | { 191 | "include": "#variables" 192 | }, 193 | { 194 | "include": "#punctuation" 195 | } 196 | ] 197 | } 198 | ] 199 | }, 200 | "constants": { 201 | "patterns": [ 202 | { 203 | "comment": "decimal float literal", 204 | "name": "constant.numeric.float.wgsl", 205 | "match": "(-?\\b[0-9][0-9]*\\.[0-9][0-9]*)([eE][+-]?[0-9]+)?\\b" 206 | }, 207 | { 208 | "comment": "int literal", 209 | "name": "constant.numeric.decimal.wgsl", 210 | "match": "-?\\b0x[0-9a-fA-F]+\\b|\\b0\\b|-?\\b[1-9][0-9]*\\b" 211 | }, 212 | { 213 | "comment": "uint literal", 214 | "name": "constant.numeric.decimal.wgsl", 215 | "match": "\\b0x[0-9a-fA-F]+u\\b|\\b0u\\b|\\b[1-9][0-9]*u\\b" 216 | }, 217 | { 218 | "comment": "boolean constant", 219 | "name": "constant.language.boolean.wgsl", 220 | "match": "\\b(true|false)\\b" 221 | } 222 | ] 223 | }, 224 | "types": { 225 | "comment": "types", 226 | "name": "storage.type.wgsl", 227 | "patterns": [ 228 | { 229 | "comment": "scalar Types", 230 | "name": "storage.type.wgsl", 231 | "match": "\\b(bool|i32|u32|f32)\\b" 232 | }, 233 | { 234 | "comment": "reserved scalar Types", 235 | "name": "storage.type.wgsl", 236 | "match": "\\b(i64|u64|f64)\\b" 237 | }, 238 | { 239 | "comment": "vector type aliasses", 240 | "name": "storage.type.wgsl", 241 | "match": "\\b(vec2i|vec3i|vec4i|vec2u|vec3u|vec4u|vec2f|vec3f|vec4f|vec2h|vec3h|vec4h)\\b" 242 | }, 243 | { 244 | "comment": "matrix type aliasses", 245 | "name": "storage.type.wgsl", 246 | "match": "\\b(mat2x2f|mat2x3f|mat2x4f|mat3x2f|mat3x3f|mat3x4f|mat4x2f|mat4x3f|mat4x4f|mat2x2h|mat2x3h|mat2x4h|mat3x2h|mat3x3h|mat3x4h|mat4x2h|mat4x3h|mat4x4h)\\b" 247 | }, 248 | { 249 | "comment": "vector/matrix types", 250 | "name": "storage.type.wgsl", 251 | "match": "\\b(vec[2-4]|mat[2-4]x[2-4])\\b" 252 | }, 253 | { 254 | "comment": "atomic types", 255 | "name": "storage.type.wgsl", 256 | "match": "\\b(atomic)\\b" 257 | }, 258 | { 259 | "comment": "array types", 260 | "name": "storage.type.wgsl", 261 | "match": "\\b(array)\\b" 262 | }, 263 | { 264 | "comment": "Custom type", 265 | "name": "entity.name.type.wgsl", 266 | "match": "\\b([A-Z][A-Za-z0-9]*)\\b" 267 | } 268 | ] 269 | }, 270 | "variables": { 271 | "patterns": [ 272 | { 273 | "comment": "variables", 274 | "name": "variable.other.wgsl", 275 | "match": "\\b(?]" 310 | } 311 | ] 312 | }, 313 | "keywords": { 314 | "patterns": [ 315 | { 316 | "comment": "other keywords", 317 | "name": "keyword.control.wgsl", 318 | "match": "\\b(bitcast|block|break|case|continue|continuing|default|discard|else|elseif|enable|fallthrough|for|function|if|loop|private|read|read_write|return|storage|switch|uniform|while|workgroup|write)\\b" 319 | }, 320 | { 321 | "comment": "reserved keywords", 322 | "name": "keyword.control.wgsl", 323 | "match": "\\b(asm|const|do|enum|handle|mat|premerge|regardless|typedef|unless|using|vec|void)\\b" 324 | }, 325 | { 326 | "comment": "storage keywords", 327 | "name": "keyword.other.wgsl storage.type.wgsl", 328 | "match": "\\b(let|var)\\b" 329 | }, 330 | { 331 | "comment": "type keyword", 332 | "name": "keyword.declaration.type.wgsl storage.type.wgsl", 333 | "match": "\\b(type)\\b" 334 | }, 335 | { 336 | "comment": "enum keyword", 337 | "name": "keyword.declaration.enum.wgsl storage.type.wgsl", 338 | "match": "\\b(enum)\\b" 339 | }, 340 | { 341 | "comment": "struct keyword", 342 | "name": "keyword.declaration.struct.wgsl storage.type.wgsl", 343 | "match": "\\b(struct)\\b" 344 | }, 345 | { 346 | "comment": "fn", 347 | "name": "keyword.other.fn.wgsl", 348 | "match": "\\bfn\\b" 349 | }, 350 | { 351 | "comment": "logical operators", 352 | "name": "keyword.operator.logical.wgsl", 353 | "match": "(\\^|\\||\\|\\||&&|<<|>>|!)(?!=)" 354 | }, 355 | { 356 | "comment": "logical AND, borrow references", 357 | "name": "keyword.operator.borrow.and.wgsl", 358 | "match": "&(?![&=])" 359 | }, 360 | { 361 | "comment": "assignment operators", 362 | "name": "keyword.operator.assignment.wgsl", 363 | "match": "(\\+=|-=|\\*=|/=|%=|\\^=|&=|\\|=|<<=|>>=)" 364 | }, 365 | { 366 | "comment": "single equal", 367 | "name": "keyword.operator.assignment.equal.wgsl", 368 | "match": "(?])=(?!=|>)" 369 | }, 370 | { 371 | "comment": "comparison operators", 372 | "name": "keyword.operator.comparison.wgsl", 373 | "match": "(=(=)?(?!>)|!=|<=|(?=)" 374 | }, 375 | { 376 | "comment": "math operators", 377 | "name": "keyword.operator.math.wgsl", 378 | "match": "(([+%]|(\\*(?!\\w)))(?!=))|(-(?!>))|(/(?!/))" 379 | }, 380 | { 381 | "comment": "dot access", 382 | "name": "keyword.operator.access.dot.wgsl", 383 | "match": "\\.(?!\\.)" 384 | }, 385 | { 386 | "comment": "dashrocket, skinny arrow", 387 | "name": "keyword.operator.arrow.skinny.wgsl", 388 | "match": "->" 389 | } 390 | ] 391 | } 392 | } 393 | } -------------------------------------------------------------------------------- /test/test.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | uint test(uint nthNumber) { 4 | return 42; 5 | } 6 | void main() { 7 | 8 | uint res = test(0); 9 | 10 | } -------------------------------------------------------------------------------- /test/test.hlsl: -------------------------------------------------------------------------------- 1 | uint test(uint nthNumber) { 2 | return 42; 3 | } 4 | void main() { 5 | uint res = test(0); 6 | 7 | } -------------------------------------------------------------------------------- /test/test.wgsl: -------------------------------------------------------------------------------- 1 | fn test(nthNumber : u32) -> u32 { 2 | return 42u; 3 | } 4 | fn main() { 5 | var res = test(0u); 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "es2022", 5 | "outDir": "out", 6 | "skipLibCheck": true, 7 | "lib": [ 8 | "es2022", 9 | "WebWorker" // Required to find WebAssembly.compile 10 | ], 11 | "types": ["node", "vscode"], 12 | "sourceMap": true, 13 | "rootDir": "src", 14 | "strict": true /* enable all strict type-checking options */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | /* eslint-disable @typescript-eslint/naming-convention */ 3 | 4 | 'use strict'; 5 | 6 | const path = require('path'); 7 | const webpack = require('webpack'); 8 | 9 | /**@type {import('webpack').Configuration}*/ 10 | const webConfig = { 11 | target: 'webworker', // vscode extensions run in webworker context for VS Code web 12 | 13 | entry: './src/extension.ts', // the entry point of this extension 14 | output: { 15 | // the bundle is stored in the 'dist' folder (check package.json) 16 | path: path.resolve(__dirname, 'dist/web'), 17 | filename: 'extension.js', 18 | libraryTarget: 'commonjs2', 19 | devtoolModuleFilenameTemplate: '../[resource-path]' 20 | }, 21 | devtool: 'source-map', 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed 24 | }, 25 | resolve: { 26 | // support reading TypeScript and JavaScript files 27 | mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules 28 | extensions: ['.ts', '.js'], 29 | alias: { 30 | // provides alternate implementation for node module and source files 31 | }, 32 | fallback: { 33 | // Webpack 5 no longer polyfills Node.js core modules automatically. 34 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 35 | // for the list of Node.js core module polyfills. 36 | fs: false, // No filesystem in browser. 37 | path: require.resolve('path-browserify'), 38 | os: require.resolve('os-browserify/browser'), 39 | child_process: false // No child process in browser. Use it as a way to detect if running on web. 40 | } 41 | }, 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.ts$/, 46 | exclude: /node_modules/, 47 | use: ['ts-loader'] 48 | } 49 | ] 50 | } 51 | }; 52 | /**@type {import('webpack').Configuration}*/ 53 | const nodeConfig = { 54 | target: 'node', 55 | 56 | entry: './src/extension.ts', 57 | output: { 58 | // the bundle is stored in the 'dist' folder (check package.json) 59 | path: path.resolve(__dirname, 'dist/node'), 60 | filename: 'extension.js', 61 | libraryTarget: 'commonjs2', 62 | devtoolModuleFilenameTemplate: '../[resource-path]' 63 | }, 64 | devtool: 'source-map', 65 | externals: { 66 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed 67 | }, 68 | resolve: { 69 | // support reading TypeScript and JavaScript files 70 | mainFields: ['main'], 71 | extensions: ['.ts', '.js'], 72 | alias: { 73 | // provides alternate implementation for node module and source files 74 | }, 75 | fallback: { 76 | } 77 | }, 78 | module: { 79 | rules: [ 80 | { 81 | test: /\.ts$/, 82 | exclude: /node_modules/, 83 | use: ['ts-loader'] 84 | } 85 | ] 86 | } 87 | }; 88 | module.exports = [webConfig, nodeConfig]; --------------------------------------------------------------------------------