├── .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 | [](https://github.com/antaalt/shader-validator/issues)
4 | [](https://github.com/antaalt/shader-sense/issues)
5 | [](https://marketplace.visualstudio.com/items?itemName=antaalt.shader-validator)
6 | [](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 | 
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 | 
45 |
46 | ### Autocompletion
47 |
48 | The extension will suggest you symbols from your file and intrinsics as you type.
49 |
50 | 
51 |
52 | ### Signature
53 |
54 | View available signatures for your function as you type it.
55 |
56 | 
57 |
58 | ### Hover
59 |
60 | View informations relative to a symbol by hovering it.
61 |
62 | 
63 |
64 | ### Goto
65 |
66 | Go to your declaration definition by clicking on it.
67 |
68 | 
69 |
70 | ### Inlay hints
71 |
72 | Add inlay hints to your function calls.
73 |
74 | 
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 | 
93 |
94 | ### Regions
95 |
96 | Grey out inactive regions depending on currently declared preprocessor & filter symbols.
97 |
98 | 
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 |
--------------------------------------------------------------------------------
/res/icons/hlsl-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/icons/wgsl-icon.svg:
--------------------------------------------------------------------------------
1 |
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];
--------------------------------------------------------------------------------