├── .cargo └── config ├── .editorconfig ├── .gitattributes ├── .github ├── codecov.yaml ├── dependabot.yaml └── workflows │ ├── main.yaml │ └── release.yaml ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── clippy.toml ├── crates ├── browser │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── cli │ ├── Cargo.toml │ └── src │ │ └── bin │ │ └── main.rs ├── languages │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── error.rs │ │ ├── language.rs │ │ └── lib.rs ├── macros │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── server │ ├── Cargo.toml │ ├── build.rs │ ├── src │ │ ├── core.rs │ │ ├── core │ │ │ ├── document.rs │ │ │ ├── error.rs │ │ │ ├── lock.rs │ │ │ ├── map.rs │ │ │ ├── reference.rs │ │ │ ├── session.rs │ │ │ └── text.rs │ │ ├── handler.rs │ │ ├── handler │ │ │ └── text_document.rs │ │ ├── lib.rs │ │ ├── metadata.rs │ │ ├── provider.rs │ │ └── provider │ │ │ ├── text_document.rs │ │ │ └── text_document │ │ │ ├── document_symbol.rs │ │ │ ├── document_symbol │ │ │ ├── wast.rs │ │ │ └── wat.rs │ │ │ ├── publish_diagnostics.rs │ │ │ ├── publish_diagnostics │ │ │ ├── wast.rs │ │ │ └── wat.rs │ │ │ ├── semantic_tokens.rs │ │ │ └── semantic_tokens │ │ │ ├── builder.rs │ │ │ ├── wast.rs │ │ │ └── wat.rs │ └── tests │ │ └── all │ │ ├── lsp.rs │ │ └── main.rs ├── syntax │ ├── Cargo.toml │ └── src │ │ ├── language.rs │ │ ├── language │ │ ├── wast.rs │ │ └── wat.rs │ │ ├── lib.rs │ │ ├── node.rs │ │ ├── node │ │ └── walker.rs │ │ └── range.rs └── testing │ ├── Cargo.toml │ └── src │ ├── jsonrpc.rs │ ├── lib.rs │ ├── lsp.rs │ └── service.rs ├── fuzz ├── Cargo.toml ├── README.md └── fuzz_targets │ └── lsp │ └── text_document │ └── did_open.rs ├── rust-toolchain ├── rustfmt.toml └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | 4 | [build] 5 | rustflags = "--cfg=web_sys_unstable_apis" 6 | 7 | [target.wasm32-unknown-unknown] 8 | runner = "wasm-bindgen-test-runner" 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | 14 | [*.rs] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.png filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | # skip ci if the last commit contains the appropriate tag 13 | skip-commit: 14 | name: Conditionally skip ci 15 | runs-on: ubuntu-latest 16 | steps: 17 | - if: "contains(github.event.head_commit.message, '[skip-ci]') 18 | || contains(github.event.head_commit.message, '[skip ci]') 19 | || contains(github.event.head_commit.message, '[ci-skip]') 20 | || contains(github.event.head_commit.message, '[ci skip]')" 21 | run: exit 78 22 | 23 | # verify that Cargo.lock passes audit 24 | cargo-audit: 25 | name: Run cargo audit 26 | needs: [skip-commit] 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Fetch latest release version of cargo-audit 31 | run: | 32 | mkdir -p .github/caching 33 | cargo search cargo-audit | grep '^cargo-audit' | awk '{gsub(/"/,"",$3); print $3}' > .github/caching/cargo-audit.lock 34 | - name: Cache cargo-audit/bin 35 | id: cache-cargo-audit 36 | uses: actions/cache@v1 37 | with: 38 | path: ${{ runner.tool_cache }}/cargo-audit/bin 39 | key: cargo-audit-bin-${{ hashFiles('.github/caching/cargo-audit.lock') }} 40 | - name: Install cargo-audit 41 | if: "steps.cache-cargo-audit.outputs.cache-hit != 'true'" 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: install 45 | args: --root ${{ runner.tool_cache }}/cargo-audit --force cargo-audit 46 | - run: echo "${{ runner.tool_cache }}/cargo-audit/bin" >> $GITHUB_PATH 47 | - run: cargo audit 48 | 49 | # verify that project passes clippy lints 50 | cargo-clippy: 51 | name: Run cargo clippy 52 | needs: [skip-commit] 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v2 56 | - name: Install Rust toolchain 57 | uses: actions-rs/toolchain@v1 58 | with: 59 | profile: minimal 60 | components: clippy 61 | - name: Run cargo xtask init 62 | uses: actions-rs/cargo@v1 63 | with: 64 | command: xtask 65 | args: init --with-corpus 66 | - name: Run cargo clippy 67 | uses: actions-rs/clippy-check@v1 68 | with: 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | args: --all-targets --examples --bins --lib --tests -- -D warnings 71 | 72 | # build the documentation 73 | cargo-docs: 74 | name: Run cargo docs 75 | needs: [skip-commit] 76 | runs-on: ubuntu-latest 77 | env: 78 | RUST_TOOLCHAIN: nightly 79 | steps: 80 | - uses: actions/checkout@v2 81 | - name: Install Rust toolchain 82 | uses: actions-rs/toolchain@v1 83 | with: 84 | profile: minimal 85 | toolchain: ${{ env.RUST_TOOLCHAIN }} 86 | override: true 87 | - name: Run cargo xtask init 88 | uses: actions-rs/cargo@v1 89 | with: 90 | command: xtask 91 | args: init 92 | - name: Run cargo xtask doc 93 | uses: actions-rs/cargo@v1 94 | with: 95 | command: xtask 96 | args: doc -- --no-deps --package wasm-lsp-server 97 | - uses: peaceiris/actions-gh-pages@v3 98 | with: 99 | github_token: ${{ secrets.GITHUB_TOKEN }} 100 | publish_dir: ./target/doc 101 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 102 | 103 | # verify that code is formatted 104 | cargo-fmt: 105 | name: Run cargo fmt 106 | needs: [skip-commit] 107 | runs-on: ubuntu-latest 108 | env: 109 | RUST_TOOLCHAIN: nightly 110 | steps: 111 | - uses: actions/checkout@v2 112 | - name: Install Rust toolchain 113 | uses: actions-rs/toolchain@v1 114 | with: 115 | profile: minimal 116 | toolchain: ${{ env.RUST_TOOLCHAIN }} 117 | override: true 118 | components: rustfmt 119 | - name: Run cargo fmt 120 | uses: actions-rs/cargo@v1 121 | with: 122 | toolchain: ${{ env.RUST_TOOLCHAIN }} 123 | command: fmt 124 | args: --all -- --check 125 | 126 | # verify that tests pass and calculate coverage with tarpaulin 127 | cargo-test-coverage: 128 | name: Run cargo tarpaulin 129 | needs: [skip-commit] 130 | runs-on: ubuntu-latest 131 | env: 132 | RUST_TOOLCHAIN: nightly 133 | steps: 134 | - uses: actions/checkout@v2 135 | - name: Install Rust toolchain 136 | uses: actions-rs/toolchain@v1 137 | with: 138 | profile: minimal 139 | toolchain: ${{ env.RUST_TOOLCHAIN }} 140 | override: true 141 | - name: Run cargo xtask init 142 | uses: actions-rs/cargo@v1 143 | with: 144 | command: xtask 145 | args: init --with-corpus 146 | - name: Fetch latest release version of cargo-tarpaulin 147 | run: | 148 | mkdir -p .github/caching 149 | curl -sL https://api.github.com/repos/xd009642/tarpaulin/releases/latest | jq -r '.name' > .github/caching/cargo-tarpaulin.lock 150 | - name: Cache cargo-tarpaulin/bin 151 | id: cache-cargo-tarpaulin 152 | uses: actions/cache@v1 153 | with: 154 | path: ${{ runner.tool_cache }}/cargo-tarpaulin/bin 155 | key: cargo-tarpaulin-bin-${{ hashFiles('.github/caching/cargo-tarpaulin.lock') }} 156 | - name: Install cargo-tarpaulin 157 | if: "steps.cache-cargo-tarpaulin.outputs.cache-hit != 'true'" 158 | uses: actions-rs/cargo@v1 159 | with: 160 | command: install 161 | args: --root ${{ runner.tool_cache }}/cargo-tarpaulin --force cargo-tarpaulin 162 | - run: echo "${{ runner.tool_cache }}/cargo-tarpaulin/bin" >> $GITHUB_PATH 163 | - name: Run cargo xtask tarpaulin 164 | uses: actions-rs/cargo@v1 165 | with: 166 | command: xtask 167 | args: tarpaulin 168 | - name: Upload to codecov.io 169 | uses: codecov/codecov-action@v1 170 | with: 171 | token: ${{ secrets.CODECOV_TOKEN }} 172 | fail_ci_if_error: true 173 | 174 | # verify that tests pass 175 | cargo-test: 176 | name: Run cargo test 177 | needs: [skip-commit] 178 | strategy: 179 | matrix: 180 | os: [macos-latest, ubuntu-latest, windows-latest] 181 | runs-on: ${{ matrix.os }} 182 | steps: 183 | - uses: actions/checkout@v2 184 | - name: Install Rust toolchain 185 | uses: actions-rs/toolchain@v1 186 | with: 187 | toolchain: nightly 188 | profile: minimal 189 | - name: Run cargo xtask init 190 | uses: actions-rs/cargo@v1 191 | with: 192 | command: xtask 193 | args: init --with-corpus 194 | - name: Run cargo xtask test 195 | uses: actions-rs/cargo@v1 196 | with: 197 | command: xtask 198 | args: test 199 | - name: Run cargo xtask test-cli 200 | uses: actions-rs/cargo@v1 201 | with: 202 | command: xtask 203 | args: test-cli 204 | 205 | # verify that tests pass for wasm target 206 | cargo-test-wasm: 207 | name: Run cargo test (wasm) 208 | needs: [skip-commit] 209 | runs-on: ubuntu-latest 210 | env: 211 | WASM_BINDGEN_CLI_VERSION: "0.2.81" 212 | steps: 213 | - uses: actions/checkout@v2 214 | - name: Install Rust toolchain (wasm32-unknown-unknown) 215 | uses: actions-rs/toolchain@v1 216 | with: 217 | target: wasm32-unknown-unknown 218 | toolchain: stable 219 | profile: minimal 220 | override: true 221 | - name: Run cargo xtask init 222 | uses: actions-rs/cargo@v1 223 | with: 224 | command: xtask 225 | args: init --with-corpus 226 | - name: Cache wasm-bindgen-cli/bin 227 | id: cache-wasm-bindgen-cli 228 | uses: actions/cache@v1 229 | with: 230 | path: ${{ runner.tool_cache }}/wasm-bindgen-cli/bin 231 | key: wasm-bindgen-cli-bin-${{ hashFiles('.github/caching/wasm-bindgen-cli.lock') }} 232 | - name: Install Rust toolchain (native) 233 | if: "steps.cache-wasm-bindgen-cli.outputs.cache-hit != 'true'" 234 | uses: actions-rs/toolchain@v1 235 | with: 236 | toolchain: stable 237 | profile: minimal 238 | - name: Install wasm-bindgen-cli 239 | if: "steps.cache-wasm-bindgen-cli.outputs.cache-hit != 'true'" 240 | uses: actions-rs/cargo@v1 241 | with: 242 | command: install 243 | args: --root ${{ runner.tool_cache }}/wasm-bindgen-cli --force wasm-bindgen-cli --version ${{ env.WASM_BINDGEN_CLI_VERSION }} 244 | - name: Configure $PATH for wasm-bindgen-cli 245 | run: echo "${{ runner.tool_cache }}/wasm-bindgen-cli/bin" >> $GITHUB_PATH 246 | - name: Run cargo xtask test 247 | uses: actions-rs/cargo@v1 248 | with: 249 | command: xtask 250 | args: test -- --target wasm32-unknown-unknown 251 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | 10 | jobs: 11 | create_release: 12 | name: Create release entry 13 | if: startsWith(github.ref, 'refs/tags') 14 | runs-on: ubuntu-latest 15 | outputs: 16 | upload_url: ${{ steps.create_release.outputs.upload_url }} 17 | steps: 18 | - name: Create GitHub release entry 19 | id: create_release 20 | uses: actions/create-release@v1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | tag_name: ${{ github.ref }} 25 | release_name: Release ${{ github.ref }} 26 | draft: false 27 | prerelease: false 28 | 29 | deploy_release: 30 | name: Deploy release asset for ${{ matrix.os }} 31 | needs: create_release 32 | if: startsWith(github.ref, 'refs/tags') 33 | runs-on: ${{ matrix.os }} 34 | strategy: 35 | matrix: 36 | name: [linux, windows, macos] 37 | include: 38 | - name: linux 39 | os: ubuntu-latest 40 | artifact_name: wasm-lsp 41 | asset_name: wasm-lsp-linux 42 | - name: windows 43 | os: windows-latest 44 | artifact_name: wasm-lsp.exe 45 | asset_name: wasm-lsp-windows 46 | - name: macos 47 | os: macos-latest 48 | artifact_name: wasm-lsp 49 | asset_name: wasm-lsp-macos 50 | steps: 51 | - uses: actions/checkout@v2 52 | - name: Install Rust toolchain 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | profile: minimal 56 | - name: Run cargo xtask init 57 | uses: actions-rs/cargo@v1 58 | with: 59 | command: xtask 60 | args: init --with-corpus 61 | - name: Run cargo xtask build 62 | uses: actions-rs/cargo@v1 63 | with: 64 | command: xtask 65 | args: build -- --release 66 | - name: Create release asset 67 | run: Compress-Archive -DestinationPath ${{ matrix.asset_name }}.zip -Path target/release/${{ matrix.artifact_name }} 68 | shell: pwsh 69 | - name: Deploy release asset to GitHub 70 | id: upload-release-asset 71 | uses: actions/upload-release-asset@v1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | with: 75 | upload_url: ${{ needs.create_release.outputs.upload_url }} 76 | asset_path: ./${{ matrix.asset_name }}.zip 77 | asset_name: ${{ matrix.asset_name }}.zip 78 | asset_content_type: application/zip 79 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/tree-sitter-wasm"] 2 | path = vendor/tree-sitter-wasm 3 | url = https://github.com/wasm-lsp/tree-sitter-wasm 4 | [submodule "vendor/corpus"] 5 | path = vendor/corpus 6 | url = https://github.com/wasm-lsp/corpus 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project is governed by the Contributor Covenant version 2.0 (https://www.contributor-covenant.org/version/2/0/code_of_conduct). 2 | 3 | All contributors and participants agree to abide by its terms. 4 | 5 | To report violations, send an email to darinmorrison+conduct@gmail.com. 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.release] 2 | codegen-units = 1 3 | lto = "fat" 4 | opt-level = "z" 5 | 6 | [workspace] 7 | members = [ 8 | "crates/browser", 9 | "crates/cli", 10 | "crates/languages", 11 | "crates/macros", 12 | "crates/server", 13 | "crates/syntax", 14 | "crates/testing", 15 | "fuzz", 16 | "xtask", 17 | ] 18 | default-members = ["crates/cli"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

wasm-lsp-server

3 |

4 | A language server implementation for WebAssembly 5 |

6 |

7 | 10 | 13 | 16 |

17 |
18 | 19 | ## Status 20 | 21 | The server is still in an early state. It is usable but many advanced features have not yet been implemented. 22 | 23 | ## Usage 24 | 25 | The server has not yet had a stable release. You can build and install it locally if you would like to experiment with it in the meantime. 26 | 27 | ### Installing the Server 28 | 29 | #### Prebuilt Binaries 30 | 31 | The easiest way to install the server is to grab one of the prebuilt binaries under [releases](https://github.com/wasm-lsp/wasm-lsp-server/releases). 32 | 33 | #### Building from Source 34 | 35 | First ensure you have the [rust toolchain](https://rustup.rs/) installed, then proceed as follows: 36 | 37 | ```bash 38 | git clone https://github.com/wasm-lsp/wasm-lsp-server 39 | cd wasm-lsp-server 40 | cargo xtask init 41 | cargo xtask install 42 | ``` 43 | 44 | ##### Selecting the Async Runtime 45 | 46 | The server is runtime agnostic and can be configured to run on [`async-std`](https://github.com/async-rs/async-std), [`futures`](https://github.com/rust-lang/futures-rs), [`smol`](https://github.com/smol-rs/smol), or [`tokio`](https://github.com/tokio-rs/tokio). 47 | 48 | The table below describes how to select a runtime. The `tokio` runtime is selected by default. 49 | 50 | | runtime | command | 51 | | ----------- | ----------------------------------------- | 52 | | `async-std` | `cargo xtask install --runtime=async-std` | 53 | | `futures` | `cargo xtask install --runtime=futures` | 54 | | `smol` | `cargo xtask install --runtime=smol` | 55 | | `tokio` | `cargo xtask install --runtime=tokio` | 56 | 57 | ### Installing the Client Extension 58 | 59 | Once the server is installed you can install the Visual Studio Code [client extension](https://github.com/wasm-lsp/vscode-wasm). 60 | 61 | ## Supported Document Types 62 | 63 | | extension | supported | kind | 64 | | :-------: | --------- | ---------------------------------------------------------------------------------------------------------------- | 65 | | `.wat` | ☑ | [WebAssembly module definition](https://github.com/WebAssembly/spec/tree/master/interpreter#s-expression-syntax) | 66 | | `.wast` | ☑ | [WebAssembly script](https://github.com/WebAssembly/spec/tree/master/interpreter#scripts) | 67 | 68 | ## Supported WebAssembly Proposals 69 | 70 | The server also supports parsing WebAssembly modules that use the following features: 71 | 72 | #### Phase 4 (Standardization) 73 | 74 | - ☑ [bulk-memory-operations](https://github.com/WebAssembly/bulk-memory-operations) 75 | - ☑ [reference-types](https://github.com/WebAssembly/reference-types) 76 | 77 | #### Phase 3 (Implementation) 78 | 79 | - ☑ [annotations](https://github.com/WebAssembly/annotations) 80 | - ☑ [multi-memory](https://github.com/WebAssembly/multi-memory) 81 | - ☑ [simd](https://github.com/WebAssembly/simd) 82 | 83 | #### Phase 2 (Specification) 84 | 85 | - ☑ [exception-handling](https://github.com/WebAssembly/exception-handling) 86 | - ☑ [function-references](https://github.com/WebAssembly/function-references) 87 | - ☑ [threads](https://github.com/WebAssembly/threads) 88 | 89 | #### Phase 1 (Proposal) 90 | 91 | Nothing planned. 92 | 93 | #### Phase 0 (Pre-Proposal) 94 | 95 | Nothing planned. 96 | 97 | ## Language Server Feature Support 98 | 99 | - ☑ document parsing via [wasm tree-sitter grammars](https://github.com/wasm-lsp/tree-sitter-wasm) 100 | - ☑ document symbol provider 101 | - ☑ syntax error diagnostics provider 102 | - ☑ incremental document synchronization 103 | 104 | ## Language Server Feature Roadmap 105 | 106 | - ☐ code action provider 107 | - ☐ code lens provider 108 | - ☐ completion provider 109 | - ☐ definition provider 110 | - ☐ document formatting (full and ranged) provider 111 | - ☐ document highlight provider 112 | - ☐ hover provider 113 | - ☐ references provider 114 | - ☐ workspace symbol provider 115 | - ☐ semantic tokens provider 116 | - ☐ signature help provider 117 | - ☐ document validation 118 | - ☐ integration with existing wasm toolchains 119 | - ☐ implementation of the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) 120 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm-lsp/wasm-lsp-server/35004c3b7f18704cf413d0e0a3c835d885f025c4/clippy.toml -------------------------------------------------------------------------------- /crates/browser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "wasm-lsp-browser" 4 | version = "0.0.0" 5 | authors = ["silvanshade "] 6 | license = "Apache-2.0 WITH LLVM-exception" 7 | readme = "README.md" 8 | repository = "https://github.com/wasm-lsp/wasm-lsp-server" 9 | keywords = [] 10 | description = """ 11 | A web browser interface for the WebAssembly language server. 12 | """ 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [features] 18 | default = [] 19 | 20 | [lib] 21 | crate-type = ["cdylib", "rlib"] 22 | 23 | [dependencies] 24 | console_error_panic_hook = "0.1.7" 25 | wasm-lsp-languages = { version = "0.0", path = "../languages" } 26 | wasm-lsp-server = { version = "0.0", path = "../server", default-features = false } 27 | futures = "0.3" 28 | js-sys = "0.3" 29 | tower-lsp = { version = "0.17", default-features = false, features = ["runtime-agnostic"] } 30 | tree-sitter = { version = "*", package = "tree-sitter-facade" } 31 | wasm-bindgen = "=0.2.81" 32 | wasm-bindgen-futures = { version = "0.4", features = ["futures-core-03-stream"] } 33 | wasm-streams = "0.2" 34 | web-tree-sitter-sys = "0.6" 35 | 36 | [dependencies.web-sys] 37 | version = "0.3" 38 | features = [ 39 | "ReadableStream", 40 | "WritableStream", 41 | ] 42 | -------------------------------------------------------------------------------- /crates/browser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(unsafe_code)] 3 | 4 | use futures::stream::TryStreamExt; 5 | use tower_lsp::{LspService, Server}; 6 | use wasm_bindgen::{prelude::*, JsCast}; 7 | use wasm_bindgen_futures::{stream::JsStream, JsFuture}; 8 | 9 | #[wasm_bindgen] 10 | pub struct ServerConfig { 11 | into_server: js_sys::AsyncIterator, 12 | from_server: web_sys::WritableStream, 13 | } 14 | 15 | #[wasm_bindgen] 16 | impl ServerConfig { 17 | #[wasm_bindgen(constructor)] 18 | pub fn new(into_server: js_sys::AsyncIterator, from_server: web_sys::WritableStream) -> Self { 19 | Self { 20 | into_server, 21 | from_server, 22 | } 23 | } 24 | } 25 | 26 | // NOTE: we don't use web_sys::ReadableStream for input here because on the 27 | // browser side we need to use a ReadableByteStreamController to construct it 28 | // and so far only Chromium-based browsers support that functionality. 29 | 30 | // NOTE: input needs to be an AsyncIterator specifically 31 | #[wasm_bindgen] 32 | pub async fn serve(config: ServerConfig) -> Result<(), JsValue> { 33 | console_error_panic_hook::set_once(); 34 | 35 | let ServerConfig { 36 | into_server, 37 | from_server, 38 | } = config; 39 | 40 | JsFuture::from(web_tree_sitter_sys::Parser::init()) 41 | .await 42 | .expect("failed to initialize tree-sitter"); 43 | 44 | #[rustfmt::skip] 45 | #[cfg(target_arch = "wasm32")] 46 | let languages = wasm_lsp_server::core::SessionLanguages { 47 | wast: wasm_lsp_languages::language::wast().await.unwrap(), 48 | wat : wasm_lsp_languages::language::wat ().await.unwrap(), 49 | }; 50 | 51 | #[rustfmt::skip] 52 | #[cfg(not(target_arch = "wasm32"))] 53 | let languages = wasm_lsp_server::core::SessionLanguages { 54 | wast: wasm_lsp_languages::language::wast(), 55 | wat : wasm_lsp_languages::language::wat (), 56 | }; 57 | 58 | let stdin = JsStream::from(into_server); 59 | let stdin = stdin 60 | .map_ok(|value| { 61 | value 62 | .dyn_into::() 63 | .expect("could not cast stream item to Uint8Array") 64 | .to_vec() 65 | }) 66 | .map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other)) 67 | .into_async_read(); 68 | 69 | let stdout = JsCast::unchecked_into::(from_server); 70 | let stdout = wasm_streams::WritableStream::from_raw(stdout); 71 | let stdout = stdout.try_into_async_write().map_err(|err| err.0)?; 72 | 73 | let (service, socket) = LspService::new(|client| wasm_lsp_server::Server::new(languages, client).unwrap()); 74 | Server::new(stdin, stdout, socket).serve(service).await; 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "wasm-lsp-cli" 4 | version = "0.0.0" 5 | authors = ["silvanshade "] 6 | license = "Apache-2.0 WITH LLVM-exception" 7 | readme = "README.md" 8 | repository = "https://github.com/wasm-lsp/wasm-lsp-server" 9 | keywords = [] 10 | description = """ 11 | A command-line interface for the WebAssembly language server. 12 | """ 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [[bin]] 18 | name = "wasm-lsp" 19 | path = "src/bin/main.rs" 20 | 21 | [features] 22 | default = [] 23 | 24 | [build-dependencies] 25 | anyhow = "1.0" 26 | built = { version = "0.5", features = ["git2"] } 27 | 28 | [dependencies] 29 | anyhow = "1.0" 30 | blocking = "1.0" 31 | clap = "3.1" 32 | env_logger = "0.9" 33 | futures = "0.3" 34 | tower-lsp = { version = "0.17", default-features = false, features = ["runtime-agnostic"] } 35 | wasm-lsp-languages = { version = "0.0", path = "../languages" } 36 | wasm-lsp-server = { version = "0.0", path = "../server", default-features = false } 37 | 38 | [dev-dependencies] 39 | predicates = "2.1" 40 | 41 | [target.'cfg(any(target_os = "wasi", not(target_arch = "wasm32")))'.dev-dependencies] 42 | assert_cmd = "2.0" 43 | -------------------------------------------------------------------------------- /crates/cli/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | //! Command-line interface for the WebAssembly Language Server 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | 7 | use clap::Command; 8 | use tower_lsp::{LspService, Server}; 9 | 10 | fn cli() { 11 | use wasm_lsp_server::metadata; 12 | Command::new(metadata::PKG_NAME) 13 | .author(metadata::PKG_AUTHORS) 14 | .version(metadata::PKG_VERSION) 15 | .about(metadata::PKG_DESCRIPTION) 16 | .get_matches(); 17 | } 18 | 19 | fn main() -> anyhow::Result<()> { 20 | run()?; 21 | Ok(()) 22 | } 23 | 24 | /// Run the server with the futures runtime. 25 | fn run() -> anyhow::Result<()> { 26 | env_logger::try_init()?; 27 | cli(); 28 | futures::executor::block_on(async { 29 | #[rustfmt::skip] 30 | #[cfg(target_arch = "wasm32")] 31 | let languages = wasm_lsp_server::core::SessionLanguages { 32 | wast: wasm_lsp_languages::language::wast().await?, 33 | wat : wasm_lsp_languages::language::wat ().await?, 34 | }; 35 | #[rustfmt::skip] 36 | #[cfg(not(target_arch = "wasm32"))] 37 | let languages = wasm_lsp_server::core::SessionLanguages { 38 | wast: wasm_lsp_languages::language::wast(), 39 | wat : wasm_lsp_languages::language::wat (), 40 | }; 41 | let (service, socket) = LspService::new(|client| wasm_lsp_server::Server::new(languages, client).unwrap()); 42 | let stdin = blocking::Unblock::new(std::io::stdin()); 43 | let stdout = blocking::Unblock::new(std::io::stdout()); 44 | Server::new(stdin, stdout, socket).serve(service).await; 45 | Ok(()) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /crates/languages/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "wasm-lsp-languages" 4 | version = "0.0.0" 5 | authors = ["silvanshade "] 6 | license = "Apache-2.0 WITH LLVM-exception" 7 | readme = "README.md" 8 | repository = "https://github.com/wasm-lsp/wasm-lsp-server" 9 | keywords = [] 10 | description = """ 11 | Tree-sitter languages for the WebAssembly language server. 12 | """ 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [build-dependencies] 18 | anyhow = "1.0" 19 | cc = "1.0" 20 | 21 | [dependencies] 22 | anyhow = "1.0" 23 | thiserror = "1.0" 24 | 25 | [dependencies.tree-sitter] 26 | package = "tree-sitter-facade" 27 | version = "0.4" 28 | 29 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tree-sitter-sys] 30 | package = "tree-sitter" 31 | version = "0.20" 32 | 33 | [target.'cfg(target_arch = "wasm32")'.dependencies] 34 | futures = "0.3" 35 | wasm-bindgen = { version = "=0.2.81", features = ["strict-macro"] } 36 | wasm-bindgen-futures = "0.4" 37 | web-tree-sitter-sys = "0.6" 38 | -------------------------------------------------------------------------------- /crates/languages/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | fn compile_tree_sitter_grammars() { 4 | let dir = Path::new("../../vendor/tree-sitter-wasm"); 5 | 6 | println!("cargo:rerun-if-changed={:?}", dir.join("wast/src/parser.c")); 7 | let mut cc = cc::Build::new(); 8 | cc.include(dir.join("wast/src")); 9 | cc.file(dir.join("wast/src/parser.c")); 10 | cc.compile("tree-sitter-wast"); 11 | 12 | println!("cargo:rerun-if-changed={:?}", dir.join("wat/src/parser.c")); 13 | let mut cc = cc::Build::new(); 14 | cc.include(dir.join("wat/src")); 15 | cc.file(dir.join("wat/src/parser.c")); 16 | cc.compile("tree-sitter-wat"); 17 | } 18 | 19 | fn main() -> anyhow::Result<()> { 20 | if std::env::var("CARGO_CFG_TARGET_ARCH")? != "wasm32" { 21 | compile_tree_sitter_grammars(); 22 | } 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /crates/languages/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Functionality related to runtime errors. 2 | 3 | use std::path::PathBuf; 4 | use thiserror::Error; 5 | 6 | /// Runtime errors for the WebAssembly parsers. 7 | #[allow(clippy::enum_variant_names)] 8 | #[derive(Debug, Error)] 9 | pub enum Error { 10 | /// Error that occurs when parsing an invalid language-id string. 11 | #[error("InvalidLanguageId: {0}")] 12 | InvalidLanguageId(String), 13 | /// Error that occurs when [`std::ffi::OsStr::to_str`] returns `None`. 14 | #[error("OsStrToStrFailed")] 15 | OsStrToStrFailed, 16 | /// Error that occurs when [`std::path::Path::extension`] returns `None`. 17 | #[error("PathExtensionFailed: {0}")] 18 | PathExtensionFailed(PathBuf), 19 | } 20 | -------------------------------------------------------------------------------- /crates/languages/src/language.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | /// Tree-sitter language for the `.wast` grammar. 4 | #[cfg(not(target_arch = "wasm32"))] 5 | pub fn wast() -> tree_sitter::Language { 6 | #[allow(unsafe_code)] 7 | let inner = unsafe { crate::tree_sitter_wast() }; 8 | inner.into() 9 | } 10 | 11 | /// Tree-sitter language for the `.wast` grammar. 12 | #[cfg(target_arch = "wasm32")] 13 | pub async fn wast() -> anyhow::Result { 14 | use anyhow::anyhow; 15 | use wasm_bindgen::JsCast; 16 | use wasm_bindgen_futures::JsFuture; 17 | let bytes: &[u8] = include_bytes!("../../../vendor/tree-sitter-wasm/wast/tree-sitter-wast.wasm"); 18 | let promise = web_tree_sitter_sys::Language::load_bytes(&bytes.into()); 19 | let future = JsFuture::from(promise); 20 | let value = future 21 | .await 22 | .map_err(|_| anyhow!("failed to load tree-sitter-wast.wasm"))?; 23 | let inner = value.unchecked_into::(); 24 | let result = inner.into(); 25 | Ok(result) 26 | } 27 | 28 | /// Tree-sitter language for the `.wat` grammar. 29 | #[cfg(not(target_arch = "wasm32"))] 30 | pub fn wat() -> tree_sitter::Language { 31 | #[allow(unsafe_code)] 32 | let inner = unsafe { crate::tree_sitter_wat() }; 33 | inner.into() 34 | } 35 | 36 | /// Tree-sitter language for the `.wat` grammar. 37 | #[cfg(target_arch = "wasm32")] 38 | pub async fn wat() -> anyhow::Result { 39 | use anyhow::anyhow; 40 | use wasm_bindgen::JsCast; 41 | use wasm_bindgen_futures::JsFuture; 42 | let bytes: &[u8] = include_bytes!("../../../vendor/tree-sitter-wasm/wat/tree-sitter-wat.wasm"); 43 | let promise = web_tree_sitter_sys::Language::load_bytes(&bytes.into()); 44 | let future = JsFuture::from(promise); 45 | let value = future 46 | .await 47 | .map_err(|_| anyhow!("failed to load tree-sitter-wat.wasm"))?; 48 | let inner = value.unchecked_into::(); 49 | let result = inner.into(); 50 | Ok(result) 51 | } 52 | 53 | /// Languages supported by the server. 54 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 55 | pub enum Language { 56 | /// The `.wast` language. 57 | Wast, 58 | /// The `.wat` language. 59 | Wat, 60 | } 61 | 62 | impl Language { 63 | /// Compute the language id string for the given language. 64 | pub fn id(&self) -> &str { 65 | match self { 66 | Language::Wast => "wasm.wast", 67 | Language::Wat => "wasm.wat", 68 | } 69 | } 70 | } 71 | 72 | impl TryFrom<&str> for Language { 73 | type Error = anyhow::Error; 74 | 75 | fn try_from(language_id: &str) -> anyhow::Result { 76 | use crate::error::Error; 77 | match language_id { 78 | "wasm.wast" => Ok(Language::Wast), 79 | "wasm.wat" => Ok(Language::Wat), 80 | _ => Err(Error::InvalidLanguageId(language_id.into()).into()), 81 | } 82 | } 83 | } 84 | 85 | impl TryFrom<&Path> for Language { 86 | type Error = anyhow::Error; 87 | 88 | fn try_from(path: &Path) -> anyhow::Result { 89 | use crate::error::Error; 90 | let file_ext = path 91 | .extension() 92 | .ok_or_else(|| Error::PathExtensionFailed(path.into()))?; 93 | let file_ext = file_ext.to_str().ok_or(Error::OsStrToStrFailed)?; 94 | let language_id = format!("wasm.{}", file_ext); 95 | Language::try_from(language_id.as_str()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/languages/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Tree-sitter languages for the WebAssembly language server. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | 7 | #[cfg(not(target_arch = "wasm32"))] 8 | extern { 9 | #[allow(dead_code)] 10 | #[doc(hidden)] 11 | fn tree_sitter_wast() -> tree_sitter_sys::Language; 12 | 13 | #[allow(dead_code)] 14 | #[doc(hidden)] 15 | fn tree_sitter_wat() -> tree_sitter_sys::Language; 16 | } 17 | 18 | mod error; 19 | 20 | /// Functions for creating [`tree-sitter::Language`]. 21 | pub mod language; 22 | -------------------------------------------------------------------------------- /crates/macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "wasm-lsp-macros" 4 | version = "0.0.0" 5 | authors = ["silvanshade "] 6 | license = "Apache-2.0 WITH LLVM-exception" 7 | readme = "README.md" 8 | repository = "https://github.com/wasm-lsp/wasm-lsp-server" 9 | keywords = [] 10 | description = """ 11 | Internal macros for the WebAssembly language server implementation. 12 | """ 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [dependencies] 21 | anyhow = "1.0" 22 | glob = "0.3" 23 | heck = "0.4" 24 | proc-macro2 = "1.0" 25 | quote = "1.0" 26 | syn = "1.0" 27 | thiserror = "1.0" 28 | wasm-lsp-languages = { version = "0.0", path = "../languages" } 29 | 30 | [dependencies.tree-sitter-sys] 31 | package = "tree-sitter" 32 | version = "0.20" 33 | -------------------------------------------------------------------------------- /crates/macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Macros for the WASM language server. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | 7 | use glob::glob; 8 | use proc_macro::TokenStream; 9 | use quote::quote; 10 | 11 | mod corpus_tests { 12 | use syn::parse::{Parse, ParseStream}; 13 | 14 | mod keyword { 15 | syn::custom_keyword!(corpus); 16 | syn::custom_keyword!(include); 17 | syn::custom_keyword!(exclude); 18 | syn::custom_keyword!(handler); 19 | } 20 | 21 | pub(crate) struct MacroInput { 22 | pub(crate) corpus: syn::Ident, 23 | pub(crate) include: String, 24 | pub(crate) exclude: Vec, 25 | pub(crate) handler: syn::Expr, 26 | } 27 | 28 | impl Parse for MacroInput { 29 | fn parse(input: ParseStream) -> syn::parse::Result { 30 | input.parse::()?; 31 | input.parse::()?; 32 | let corpus = input.parse()?; 33 | input.parse::()?; 34 | 35 | input.parse::()?; 36 | input.parse::()?; 37 | let include = input.parse::()?.value(); 38 | input.parse::()?; 39 | 40 | let mut exclude = vec![]; 41 | if input.peek(keyword::exclude) { 42 | input.parse::()?; 43 | input.parse::()?; 44 | let paths = { 45 | let content; 46 | syn::bracketed!(content in input); 47 | content.parse_terminated::(|b| b.parse())? 48 | }; 49 | exclude = paths.into_iter().map(|s| s.value()).collect(); 50 | input.parse::()?; 51 | } 52 | 53 | input.parse::()?; 54 | input.parse::()?; 55 | let handler = input.parse()?; 56 | input.parse::().ok(); 57 | 58 | Ok(MacroInput { 59 | corpus, 60 | include, 61 | exclude, 62 | handler, 63 | }) 64 | } 65 | } 66 | } 67 | 68 | /// Generate tests from a corpus of wasm modules on the filesystem. 69 | /// 70 | /// # Arguments 71 | /// 72 | /// * `corpus` - name of the generated submodule containing the individual tests 73 | /// * `include` - glob pattern of files to include for testing 74 | /// * `exclude` - array of file names to exclude from testing 75 | /// 76 | /// # Examples 77 | /// 78 | /// ``` 79 | /// corpus_tests! { 80 | /// corpus: annotations, 81 | /// include: "vendor/corpus/vendor/WebAssembly/annotations/test/core/*.wast", 82 | /// exclude: ["annotations.wast"], 83 | /// } 84 | /// ``` 85 | #[proc_macro] 86 | pub fn corpus_tests(input: TokenStream) -> TokenStream { 87 | let corpus_tests::MacroInput { 88 | corpus, 89 | include, 90 | exclude, 91 | handler, 92 | } = syn::parse_macro_input!(input as corpus_tests::MacroInput); 93 | // Compute a string representation for the corpus name. 94 | let corpus_name = corpus.to_string(); 95 | let corpus_name = corpus_name.as_str(); 96 | 97 | // Compute the paths from the glob pattern. 98 | let paths = glob(&include).unwrap(); 99 | 100 | // Prepare the vector of syntax items; these items are the individual test 101 | // functions that will be enclosed in the generated test submodule. 102 | let mut content = vec![]; 103 | 104 | for path in paths { 105 | // Ensure the path is canonicalized and absolute 106 | let path = path.unwrap().canonicalize().unwrap(); 107 | let path_name = path.to_str().unwrap(); 108 | let file_name = path.file_name().unwrap().to_str().unwrap(); 109 | 110 | // Skip the file if contained in the exclude list; otherwise continue. 111 | if !exclude.contains(&String::from(file_name)) { 112 | let file_stem = path.file_stem().unwrap().to_str().unwrap(); 113 | let test_name = heck::ToSnakeCase::to_snake_case(file_stem); 114 | let test_name = format!("r#{}", test_name); 115 | 116 | // Compute the test identifier. 117 | let test = syn::parse_str::(&test_name).unwrap(); 118 | 119 | // Generate the individual test function for the given file. 120 | let item = quote! { 121 | #[test] 122 | fn #test() { 123 | #handler(#corpus_name, #path_name); 124 | } 125 | }; 126 | content.push(item); 127 | } 128 | } 129 | 130 | // Generate the enclosing test submodule for the given corpus. 131 | let module = quote! { 132 | mod #corpus { 133 | // Include the test functions generated from the corpus files. 134 | #(#content)* 135 | } 136 | }; 137 | 138 | module.into() 139 | } 140 | 141 | mod language { 142 | use syn::parse::{Parse, ParseStream}; 143 | use wasm_lsp_languages::language; 144 | 145 | pub(crate) struct Language(pub(crate) language::Language); 146 | 147 | impl Parse for Language { 148 | fn parse(input: ParseStream) -> syn::parse::Result { 149 | let language = input.parse::()?.value(); 150 | let language = language::Language::try_from(language.as_str()); 151 | let language = language.map_err(|_| input.error("invalid language identifier"))?; 152 | Ok(Language(language)) 153 | } 154 | } 155 | } 156 | 157 | mod field_ids { 158 | use syn::parse::{Parse, ParseStream}; 159 | 160 | mod keyword { 161 | syn::custom_keyword!(language); 162 | syn::custom_keyword!(fields); 163 | } 164 | 165 | pub(crate) struct Field { 166 | pub(crate) ident: syn::Ident, 167 | pub(crate) name: String, 168 | } 169 | 170 | impl Parse for Field { 171 | fn parse(input: ParseStream) -> syn::parse::Result { 172 | let content; 173 | syn::parenthesized!(content in input); 174 | let ident = content.parse()?; 175 | content.parse::()?; 176 | let name = content.parse::()?.value(); 177 | Ok(Field { ident, name }) 178 | } 179 | } 180 | 181 | pub(crate) struct MacroInput { 182 | pub(crate) language: super::language::Language, 183 | pub(crate) fields: Vec, 184 | } 185 | 186 | impl Parse for MacroInput { 187 | fn parse(input: ParseStream) -> syn::parse::Result { 188 | input.parse::()?; 189 | input.parse::()?; 190 | let language = input.parse()?; 191 | input.parse::()?; 192 | 193 | input.parse::()?; 194 | input.parse::()?; 195 | let fields = { 196 | let content; 197 | syn::bracketed!(content in input); 198 | content 199 | .parse_terminated::(|b| b.parse())? 200 | .into_iter() 201 | .collect() 202 | }; 203 | input.parse::().ok(); 204 | 205 | Ok(MacroInput { language, fields }) 206 | } 207 | } 208 | } 209 | 210 | #[allow(missing_docs)] 211 | #[proc_macro] 212 | pub fn field_ids(input: TokenStream) -> TokenStream { 213 | use wasm_lsp_languages::language; 214 | 215 | let macro_input = syn::parse_macro_input!(input as field_ids::MacroInput); 216 | 217 | #[allow(unsafe_code)] 218 | let language = match macro_input.language.0 { 219 | language::Language::Wast => language::wast(), 220 | language::Language::Wat => language::wat(), 221 | }; 222 | 223 | let mut content = vec![]; 224 | 225 | for field in macro_input.fields { 226 | let ident = field.ident; 227 | let name = field.name.as_str(); 228 | let value = language.field_id_for_name(name).unwrap(); 229 | let item = quote! { 230 | pub const #ident: u16 = #value; 231 | }; 232 | content.push(item); 233 | } 234 | 235 | let result = quote! { 236 | #(#content)* 237 | }; 238 | 239 | result.into() 240 | } 241 | 242 | mod node_kind_ids { 243 | use syn::parse::{Parse, ParseStream}; 244 | 245 | mod keyword { 246 | syn::custom_keyword!(language); 247 | syn::custom_keyword!(node_kinds); 248 | } 249 | 250 | pub(crate) struct NodeKind { 251 | pub(crate) ident: syn::Ident, 252 | pub(crate) kind: String, 253 | pub(crate) named: bool, 254 | } 255 | 256 | impl Parse for NodeKind { 257 | fn parse(input: ParseStream) -> syn::parse::Result { 258 | let content; 259 | syn::parenthesized!(content in input); 260 | let ident = content.parse()?; 261 | content.parse::()?; 262 | let kind = content.parse::()?.value(); 263 | content.parse::()?; 264 | let named = content.parse::()?.value(); 265 | Ok(NodeKind { ident, kind, named }) 266 | } 267 | } 268 | 269 | pub(crate) struct MacroInput { 270 | pub(crate) language: super::language::Language, 271 | pub(crate) node_kinds: Vec, 272 | } 273 | 274 | impl Parse for MacroInput { 275 | fn parse(input: ParseStream) -> syn::parse::Result { 276 | input.parse::()?; 277 | input.parse::()?; 278 | let language = input.parse()?; 279 | input.parse::()?; 280 | 281 | input.parse::()?; 282 | input.parse::()?; 283 | let node_kinds = { 284 | let content; 285 | syn::bracketed!(content in input); 286 | content 287 | .parse_terminated::(|b| b.parse())? 288 | .into_iter() 289 | .collect() 290 | }; 291 | input.parse::().ok(); 292 | 293 | Ok(MacroInput { language, node_kinds }) 294 | } 295 | } 296 | } 297 | 298 | #[allow(missing_docs)] 299 | #[proc_macro] 300 | pub fn node_kind_ids(input: TokenStream) -> TokenStream { 301 | use wasm_lsp_languages::language; 302 | 303 | let macro_input = syn::parse_macro_input!(input as node_kind_ids::MacroInput); 304 | 305 | #[allow(unsafe_code)] 306 | let language = match macro_input.language.0 { 307 | language::Language::Wast => language::wast(), 308 | language::Language::Wat => language::wat(), 309 | }; 310 | 311 | let mut content = vec![]; 312 | 313 | for node_kind in macro_input.node_kinds { 314 | let ident = node_kind.ident; 315 | let kind = node_kind.kind.as_str(); 316 | let value = language.id_for_node_kind(kind, node_kind.named); 317 | let item = quote! { 318 | pub const #ident: u16 = #value; 319 | }; 320 | content.push(item); 321 | } 322 | 323 | let result = quote! { 324 | #(#content)* 325 | }; 326 | 327 | result.into() 328 | } 329 | -------------------------------------------------------------------------------- /crates/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "wasm-lsp-server" 4 | version = "0.0.0" 5 | authors = ["silvanshade "] 6 | license = "Apache-2.0 WITH LLVM-exception" 7 | readme = "README.md" 8 | repository = "https://github.com/wasm-lsp/wasm-lsp-server" 9 | keywords = [] 10 | description = """ 11 | A language server implementation for WebAssembly. 12 | """ 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [features] 18 | default = [] 19 | goldenfiles = [] 20 | 21 | [build-dependencies] 22 | anyhow = "1.0" 23 | built = { version = "0.5", features = ["git2"] } 24 | 25 | [dependencies] 26 | anyhow = "1.0" 27 | async-lock = "2.3" 28 | async-trait = "0.1" 29 | bytes = "1.0" 30 | env_logger = "0.9" 31 | futures = "0.3" 32 | log = "0.4" 33 | lsp = { version = "0.93", package = "lsp-types" } 34 | lsp-text = { version = "0.5", features = ["tree-sitter"] } 35 | ropey = "1.2" 36 | serde_json = "1.0" 37 | thiserror = "1.0" 38 | tower-lsp = { version = "0.17", default-features = false, features = ["runtime-agnostic"] } 39 | wasm-lsp-languages = { version = "0.0", path = "../languages" } 40 | wasm-lsp-syntax = { version = "0.0", path = "../syntax" } 41 | 42 | [dependencies.tree-sitter] 43 | package = "tree-sitter-facade" 44 | version = "0.4" 45 | 46 | [dev-dependencies] 47 | criterion = "0.3" 48 | futures-test = "0.3" 49 | glob = "0.3" 50 | indoc = "1.0" 51 | testing = { package = "wasm-lsp-testing", version = "0.0", path = "../testing" } 52 | wasm-lsp-macros = { version = "0.0", path = "../macros" } 53 | 54 | [target.'cfg(any(target_os = "wasi", not(target_arch = "wasm32")))'.dev-dependencies] 55 | 56 | goldenfile = "1.0" 57 | 58 | [package.metadata.cargo-udeps.ignore] 59 | normal = ["env_logger"] 60 | -------------------------------------------------------------------------------- /crates/server/build.rs: -------------------------------------------------------------------------------- 1 | fn collect_metadata() -> anyhow::Result<()> { 2 | built::write_built_file()?; 3 | Ok(()) 4 | } 5 | 6 | fn main() -> anyhow::Result<()> { 7 | collect_metadata()?; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /crates/server/src/core.rs: -------------------------------------------------------------------------------- 1 | //! Core definitions for server functionality. 2 | 3 | /// Definitions related to LSP documents. 4 | pub mod document; 5 | 6 | /// Definitions related to runtime errors. 7 | pub mod error; 8 | 9 | /// Definitions related to session hashmaps. 10 | pub mod map; 11 | 12 | /// Definitions related to lock structures for session hashmaps. 13 | pub mod lock; 14 | 15 | /// Definitions related to references for session hashmaps. 16 | pub mod reference; 17 | 18 | /// Definitions related to the LSP session. 19 | pub mod session; 20 | 21 | /// Definitions related to working with textual content. 22 | pub mod text; 23 | 24 | pub use document::*; 25 | pub use error::*; 26 | pub use lock::*; 27 | pub use map::*; 28 | pub use reference::*; 29 | pub use session::*; 30 | pub use text::*; 31 | pub use wasm_lsp_languages::language::Language; 32 | pub use wasm_lsp_syntax::{language, node, range}; 33 | -------------------------------------------------------------------------------- /crates/server/src/core/document.rs: -------------------------------------------------------------------------------- 1 | //! Definitions related to LSP documents. 2 | 3 | use crate::core::{self}; 4 | use async_lock::Mutex; 5 | use lsp_text::{RopeExt, TextEdit}; 6 | use std::sync::Arc; 7 | 8 | /// Documents for the LSP session. 9 | pub struct Document { 10 | /// The language-id of the document. 11 | pub language: core::Language, 12 | /// The textual content of the document. 13 | pub content: ropey::Rope, 14 | /// The active parser for the document. 15 | pub parser: tree_sitter::Parser, 16 | /// The current syntax tree for the document. 17 | pub tree: tree_sitter::Tree, 18 | } 19 | 20 | impl Document { 21 | /// Create a new [`Document`] given [`lsp::DidOpenTextDocumentParams`]. 22 | pub fn open( 23 | session: Arc, 24 | params: lsp::DidOpenTextDocumentParams, 25 | ) -> anyhow::Result> { 26 | let language = core::Language::try_from(params.text_document.language_id.as_str())?; 27 | let mut parser = tree_sitter::Parser::new()?; 28 | match language { 29 | core::Language::Wast => parser.set_language(&session.languages.wast)?, 30 | core::Language::Wat => parser.set_language(&session.languages.wat)?, 31 | }; 32 | 33 | let content = ropey::Rope::from(params.text_document.text); 34 | let result = { 35 | let content = content.clone(); 36 | let byte_idx = 0; 37 | let callback = content.chunk_walker(byte_idx).callback_adapter_for_tree_sitter(); 38 | let old_tree = None; 39 | parser.parse_with(callback, old_tree)? 40 | }; 41 | Ok(result.map(|tree| core::Document { 42 | language, 43 | content, 44 | parser, 45 | tree, 46 | })) 47 | } 48 | 49 | /// 50 | pub async fn change<'changes>( 51 | session: Arc, 52 | uri: &lsp::Url, 53 | content: &ropey::Rope, 54 | edits: &[TextEdit<'changes>], 55 | ) -> anyhow::Result> { 56 | let result = { 57 | let parser = session.get_mut_parser(uri).await?; 58 | let mut parser = parser.lock().await; 59 | 60 | let callback = { 61 | let mut content = content.clone(); 62 | content.shrink_to_fit(); 63 | let byte_idx = 0; 64 | content.chunk_walker(byte_idx).callback_adapter_for_tree_sitter() 65 | }; 66 | 67 | let old_tree = session.get_mut_tree(uri).await?; 68 | let mut old_tree = old_tree.lock().await; 69 | 70 | for edit in edits { 71 | old_tree.edit(&edit.input_edit); 72 | } 73 | 74 | parser.parse_with(callback, Some(&*old_tree))? 75 | }; 76 | 77 | if let Some(tree) = result { 78 | { 79 | let tree = tree.clone(); 80 | *session.get_mut_tree(uri).await?.value_mut() = Mutex::new(tree); 81 | } 82 | Ok(Some(tree)) 83 | } else { 84 | Ok(None) 85 | } 86 | } 87 | 88 | /// Return the language-id and textual content portion of the [`Document`]. 89 | pub fn text(&self) -> core::Text { 90 | core::Text { 91 | language: self.language, 92 | content: self.content.clone(), 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/server/src/core/error.rs: -------------------------------------------------------------------------------- 1 | //! Definitions related to runtime errors. 2 | 3 | use thiserror::Error; 4 | 5 | /// Runtime errors for the LSP server. 6 | #[allow(clippy::enum_variant_names)] 7 | #[derive(Debug, Eq, Error, PartialEq)] 8 | pub enum Error { 9 | /// Error that occurs when [`core::Session.client`] is accessed and is `None`. 10 | #[error("ClientNotInitialzed")] 11 | ClientNotInitialized, 12 | /// Error that occurs when a session resource is requested and does not exist. 13 | #[error("core::SessionResourceNotFound: kind={kind:?}, uri={uri:?}")] 14 | SessionResourceNotFound { 15 | /// The kind of the requested session resource. 16 | kind: crate::core::session::SessionResourceKind, 17 | /// The URL of the requested session resource. 18 | uri: lsp::Url, 19 | }, 20 | } 21 | 22 | /// Wrapper struct for converting [`anyhow::Error`] into [`tower_lsp::jsonrpc::Error`]. 23 | pub struct IntoJsonRpcError(pub anyhow::Error); 24 | 25 | impl From for tower_lsp::jsonrpc::Error { 26 | fn from(error: IntoJsonRpcError) -> Self { 27 | let mut rpc_error = tower_lsp::jsonrpc::Error::internal_error(); 28 | rpc_error.data = Some(serde_json::to_value(format!("{}", error.0)).unwrap()); 29 | rpc_error 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::{Error, IntoJsonRpcError}; 36 | 37 | #[test] 38 | fn from() { 39 | let error = Error::ClientNotInitialized; 40 | let error = error.into(); 41 | 42 | let mut expected = tower_lsp::jsonrpc::Error::internal_error(); 43 | expected.data = Some(serde_json::to_value(format!("{}", error)).unwrap()); 44 | 45 | let actual: tower_lsp::jsonrpc::Error = IntoJsonRpcError(error).into(); 46 | 47 | assert_eq!(expected, actual); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/server/src/core/lock.rs: -------------------------------------------------------------------------------- 1 | pub use async_lock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; 2 | -------------------------------------------------------------------------------- /crates/server/src/core/map.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use async_trait::async_trait; 4 | use core::{ 5 | borrow::Borrow, 6 | hash::{BuildHasher, Hash}, 7 | }; 8 | use std::collections::{hash_map::RandomState, HashMap}; 9 | 10 | pub type Map = crate::core::RwLock, S>>; 11 | 12 | #[async_trait] 13 | pub trait MapExt<'a, K, V, S> 14 | where 15 | K: Send + Sync + Eq + Hash, 16 | V: Send + Sync, 17 | S: Send + Sync + Clone + BuildHasher, 18 | { 19 | async fn get(&'a self, key: &'a Q) -> Option> 20 | where 21 | K: Borrow, 22 | Q: Eq + Hash + ?Sized + Sync; 23 | 24 | async fn get_mut(&'a self, key: &'a Q) -> Option> 25 | where 26 | K: Borrow, 27 | Q: Eq + Hash + ?Sized + Sync; 28 | 29 | async fn insert(&self, key: K, value: V) -> Option; 30 | 31 | async fn remove(&self, key: &Q) -> Option<(K, V)> 32 | where 33 | K: Borrow, 34 | Q: Eq + Hash + ?Sized + Sync; 35 | } 36 | 37 | #[async_trait] 38 | impl<'a, K, V, S> MapExt<'a, K, V, S> for Map 39 | where 40 | K: Send + Sync + Eq + Hash, 41 | V: Send + Sync, 42 | S: Send + Sync + Clone + BuildHasher, 43 | { 44 | async fn get(&'a self, key: &'a Q) -> Option> 45 | where 46 | K: Borrow, 47 | Q: Eq + Hash + ?Sized + Sync, 48 | { 49 | let guard = self.read().await; 50 | 51 | if let Some((key, value)) = guard.get_key_value(key) { 52 | #[allow(unsafe_code)] 53 | unsafe { 54 | let key: *const K = key; 55 | let value: *const V = value.get(); 56 | Some(crate::core::Ref::new(guard, key, value)) 57 | } 58 | } else { 59 | None 60 | } 61 | } 62 | 63 | async fn get_mut(&'a self, key: &'a Q) -> Option> 64 | where 65 | K: Borrow, 66 | Q: Eq + Hash + ?Sized + Sync, 67 | { 68 | let guard = self.write().await; 69 | 70 | if let Some((key, value)) = guard.get_key_value(key) { 71 | #[allow(unsafe_code)] 72 | unsafe { 73 | let key: *const K = key; 74 | let value: *mut V = value.as_ptr(); 75 | Some(crate::core::RefMut::new(guard, key, value)) 76 | } 77 | } else { 78 | None 79 | } 80 | } 81 | 82 | async fn insert(&self, key: K, value: V) -> Option { 83 | self.write() 84 | .await 85 | .insert(key, crate::core::SharedValue::new(value)) 86 | .map(crate::core::SharedValue::into_inner) 87 | } 88 | 89 | async fn remove(&self, key: &Q) -> Option<(K, V)> 90 | where 91 | K: Borrow, 92 | Q: Eq + Hash + ?Sized + Sync, 93 | { 94 | self.write().await.remove_entry(key).map(|(k, v)| (k, v.into_inner())) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /crates/server/src/core/reference.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use core::{ 4 | cell::UnsafeCell, 5 | hash::Hash, 6 | ops::{Deref, DerefMut}, 7 | }; 8 | use std::{ 9 | collections::{hash_map::RandomState, HashMap}, 10 | hash::BuildHasher, 11 | }; 12 | 13 | /// A simple wrapper around `T` 14 | /// 15 | /// This is to prevent UB when using `HashMap::get_key_value`, because 16 | /// `HashMap` doesn't expose an api to get the key and value, where 17 | /// the value is a `&mut T`. 18 | /// 19 | /// See [#10](https://github.com/xacrimon/dashmap/issues/10) for details 20 | #[repr(transparent)] 21 | pub struct SharedValue { 22 | value: UnsafeCell, 23 | } 24 | 25 | impl Clone for SharedValue { 26 | fn clone(&self) -> Self { 27 | let inner = self.get().clone(); 28 | Self { 29 | value: UnsafeCell::new(inner), 30 | } 31 | } 32 | } 33 | 34 | #[allow(unsafe_code)] 35 | unsafe impl Send for SharedValue { 36 | } 37 | 38 | #[allow(unsafe_code)] 39 | unsafe impl Sync for SharedValue { 40 | } 41 | 42 | impl SharedValue { 43 | /// Create a new `SharedValue` 44 | pub const fn new(value: T) -> Self { 45 | Self { 46 | value: UnsafeCell::new(value), 47 | } 48 | } 49 | 50 | /// Get a shared reference to `T` 51 | pub fn get(&self) -> &T { 52 | #[allow(unsafe_code)] 53 | unsafe { 54 | &*self.value.get() 55 | } 56 | } 57 | 58 | /// Get an unique reference to `T` 59 | pub fn get_mut(&mut self) -> &mut T { 60 | #[allow(unsafe_code)] 61 | unsafe { 62 | &mut *self.value.get() 63 | } 64 | } 65 | 66 | /// Unwraps the value 67 | pub fn into_inner(self) -> T { 68 | self.value.into_inner() 69 | } 70 | 71 | /// Get a mutable raw pointer to the underlying value 72 | pub(crate) fn as_ptr(&self) -> *mut T { 73 | self.value.get() 74 | } 75 | } 76 | 77 | pub struct Ref<'a, K, V, S = RandomState> { 78 | #[allow(unused)] 79 | guard: crate::core::RwLockReadGuard<'a, HashMap, S>>, 80 | key: *const K, 81 | value: *const V, 82 | } 83 | 84 | impl<'a, K, V, S> Ref<'a, K, V, S> 85 | where 86 | K: Eq + Hash, 87 | S: BuildHasher, 88 | { 89 | #[allow(unsafe_code)] 90 | pub(crate) unsafe fn new( 91 | guard: crate::core::RwLockReadGuard<'a, HashMap, S>>, 92 | key: *const K, 93 | value: *const V, 94 | ) -> Self { 95 | Self { guard, key, value } 96 | } 97 | 98 | pub fn key(&self) -> &K { 99 | self.pair().0 100 | } 101 | 102 | pub fn value(&self) -> &V { 103 | self.pair().1 104 | } 105 | 106 | pub fn pair(&self) -> (&K, &V) { 107 | #[allow(unsafe_code)] 108 | unsafe { 109 | (&*self.key, &*self.value) 110 | } 111 | } 112 | } 113 | 114 | #[allow(unsafe_code)] 115 | unsafe impl<'a, K, V, S> Send for Ref<'a, K, V, S> 116 | where 117 | K: Eq + Hash + Sync, 118 | S: BuildHasher, 119 | V: Sync, 120 | { 121 | } 122 | #[allow(unsafe_code)] 123 | unsafe impl<'a, K, V, S> Sync for Ref<'a, K, V, S> 124 | where 125 | K: Eq + Hash + Sync, 126 | S: BuildHasher, 127 | V: Sync, 128 | { 129 | } 130 | 131 | impl<'a, K, V, S> std::fmt::Debug for Ref<'a, K, V, S> 132 | where 133 | K: std::fmt::Debug + Eq + Hash, 134 | S: BuildHasher, 135 | V: std::fmt::Debug, 136 | { 137 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 138 | f.debug_struct("Ref") 139 | .field("k", &self.key) 140 | .field("v", &self.value) 141 | .finish() 142 | } 143 | } 144 | 145 | impl<'a, K, V, S> Deref for Ref<'a, K, V, S> 146 | where 147 | K: Eq + Hash, 148 | S: BuildHasher, 149 | { 150 | type Target = V; 151 | 152 | fn deref(&self) -> &V { 153 | self.value() 154 | } 155 | } 156 | 157 | pub struct RefMut<'a, K, V, S = RandomState> { 158 | #[allow(unused)] 159 | guard: crate::core::RwLockWriteGuard<'a, HashMap, S>>, 160 | k: *const K, 161 | v: *mut V, 162 | } 163 | 164 | impl<'a, K, V, S> RefMut<'a, K, V, S> 165 | where 166 | K: Eq + Hash, 167 | S: BuildHasher, 168 | { 169 | #[allow(unsafe_code)] 170 | pub(crate) unsafe fn new( 171 | guard: crate::core::RwLockWriteGuard<'a, HashMap, S>>, 172 | k: *const K, 173 | v: *mut V, 174 | ) -> Self { 175 | Self { guard, k, v } 176 | } 177 | 178 | pub fn key(&self) -> &K { 179 | self.pair().0 180 | } 181 | 182 | pub fn value(&self) -> &V { 183 | self.pair().1 184 | } 185 | 186 | pub fn value_mut(&mut self) -> &mut V { 187 | self.pair_mut().1 188 | } 189 | 190 | pub fn pair(&self) -> (&K, &V) { 191 | #[allow(unsafe_code)] 192 | unsafe { 193 | (&*self.k, &*self.v) 194 | } 195 | } 196 | 197 | pub fn pair_mut(&mut self) -> (&K, &mut V) { 198 | #[allow(unsafe_code)] 199 | unsafe { 200 | (&*self.k, &mut *self.v) 201 | } 202 | } 203 | } 204 | 205 | #[allow(unsafe_code)] 206 | unsafe impl<'a, K, V, S> Send for RefMut<'a, K, V, S> 207 | where 208 | K: Eq + Hash + Sync, 209 | S: BuildHasher, 210 | V: Sync, 211 | { 212 | } 213 | #[allow(unsafe_code)] 214 | unsafe impl<'a, K, V, S> Sync for RefMut<'a, K, V, S> 215 | where 216 | K: Eq + Hash + Sync, 217 | S: BuildHasher, 218 | V: Sync, 219 | { 220 | } 221 | 222 | impl<'a, K, V, S> std::fmt::Debug for RefMut<'a, K, V, S> 223 | where 224 | K: std::fmt::Debug + Eq + Hash, 225 | S: BuildHasher, 226 | V: std::fmt::Debug, 227 | { 228 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 229 | f.debug_struct("RefMut") 230 | .field("k", &self.k) 231 | .field("v", &self.v) 232 | .finish() 233 | } 234 | } 235 | 236 | impl<'a, K, V, S> Deref for RefMut<'a, K, V, S> 237 | where 238 | K: Eq + Hash, 239 | S: BuildHasher, 240 | { 241 | type Target = V; 242 | 243 | fn deref(&self) -> &V { 244 | self.value() 245 | } 246 | } 247 | 248 | impl<'a, K, V, S> DerefMut for RefMut<'a, K, V, S> 249 | where 250 | K: Eq + Hash, 251 | S: BuildHasher, 252 | { 253 | fn deref_mut(&mut self) -> &mut V { 254 | self.value_mut() 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /crates/server/src/core/session.rs: -------------------------------------------------------------------------------- 1 | //! Definitions related to the LSP session. 2 | 3 | use crate::core::MapExt; 4 | use async_lock::{Mutex, RwLock}; 5 | 6 | /// The LSP server session. This contains the relevant state for workspace. 7 | pub struct Session { 8 | /// The pre-loaded tree-sitter languages for `.wast` and `.wat`. 9 | pub languages: SessionLanguages, 10 | /// The current server LSP capabilities configuration. 11 | pub server_capabilities: RwLock, 12 | /// The current client LSP capabilities configuration. 13 | pub client_capabilities: RwLock>, 14 | client: Option, 15 | texts: crate::core::Map, 16 | parsers: crate::core::Map>, 17 | trees: crate::core::Map>, 18 | } 19 | 20 | impl Session { 21 | /// Create a new [`Session`]. 22 | pub fn new(languages: SessionLanguages, client: Option) -> anyhow::Result { 23 | let server_capabilities = RwLock::new(crate::Server::capabilities()); 24 | let client_capabilities = RwLock::new(Default::default()); 25 | let texts = crate::core::Map::default(); 26 | let parsers = crate::core::Map::default(); 27 | let trees = crate::core::Map::default(); 28 | Ok(Session { 29 | languages, 30 | server_capabilities, 31 | client_capabilities, 32 | client, 33 | texts, 34 | parsers, 35 | trees, 36 | }) 37 | } 38 | 39 | /// Retrieve the handle for the LSP client. 40 | pub fn client(&self) -> anyhow::Result<&tower_lsp::Client> { 41 | self.client 42 | .as_ref() 43 | .ok_or_else(|| crate::core::Error::ClientNotInitialized.into()) 44 | } 45 | 46 | /// Insert a [`crate::core::Document`] into the [`Session`]. 47 | pub async fn insert_document(&self, uri: lsp::Url, document: crate::core::Document) -> anyhow::Result<()> { 48 | let text = document.text(); 49 | let result = self.texts.insert(uri.clone(), text).await; 50 | debug_assert!(result.is_none()); 51 | let result = self.parsers.insert(uri.clone(), Mutex::new(document.parser)).await; 52 | debug_assert!(result.is_none()); 53 | let result = self.trees.insert(uri, Mutex::new(document.tree)).await; 54 | debug_assert!(result.is_none()); 55 | Ok(()) 56 | } 57 | 58 | /// Remove a [`crate::core::Document`] from the [`Session`]. 59 | pub async fn remove_document(&self, uri: &lsp::Url) -> anyhow::Result<()> { 60 | let result = self.texts.remove(uri).await; 61 | debug_assert!(result.is_some()); 62 | let result = self.parsers.remove(uri).await; 63 | debug_assert!(result.is_some()); 64 | let result = self.trees.remove(uri).await; 65 | debug_assert!(result.is_some()); 66 | Ok(()) 67 | } 68 | 69 | /// Retrieve the LSP semantic tokens legend. 70 | pub async fn semantic_tokens_legend(&self) -> Option { 71 | let capabilities = self.server_capabilities.read().await; 72 | if let Some(capabilities) = &capabilities.semantic_tokens_provider { 73 | match capabilities { 74 | lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(options) => Some(options.legend.clone()), 75 | lsp::SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(options) => { 76 | Some(options.semantic_tokens_options.legend.clone()) 77 | }, 78 | } 79 | } else { 80 | None 81 | } 82 | } 83 | 84 | /// Get a reference to the [`crate::core::Text`] for a [`crate::core::Document`] in the 85 | /// [`Session`]. 86 | pub async fn get_text<'a>( 87 | &'a self, 88 | uri: &'a lsp::Url, 89 | ) -> anyhow::Result> { 90 | self.texts.get(uri).await.ok_or_else(|| { 91 | let kind = SessionResourceKind::Document; 92 | let uri = uri.clone(); 93 | crate::core::Error::SessionResourceNotFound { kind, uri }.into() 94 | }) 95 | } 96 | 97 | /// Get a mutable reference to the [`crate::core::Text`] for a [`crate::core::Document`] in the 98 | /// [`Session`]. 99 | pub async fn get_mut_text<'a>( 100 | &'a self, 101 | uri: &'a lsp::Url, 102 | ) -> anyhow::Result> { 103 | self.texts.get_mut(uri).await.ok_or_else(|| { 104 | let kind = SessionResourceKind::Document; 105 | let uri = uri.clone(); 106 | crate::core::Error::SessionResourceNotFound { kind, uri }.into() 107 | }) 108 | } 109 | 110 | /// Get a mutable reference to the [`tree_sitter::Parser`] for a [`crate::core::Document`] in 111 | /// the [`Session`]. 112 | pub async fn get_mut_parser<'a>( 113 | &'a self, 114 | uri: &'a lsp::Url, 115 | ) -> anyhow::Result>> { 116 | self.parsers.get_mut(uri).await.ok_or_else(|| { 117 | let kind = SessionResourceKind::Parser; 118 | let uri = uri.clone(); 119 | crate::core::Error::SessionResourceNotFound { kind, uri }.into() 120 | }) 121 | } 122 | 123 | /// Get a reference to the [`tree_sitter::Tree`] for a [`crate::core::Document`] in the 124 | /// [`Session`]. 125 | pub async fn get_tree<'a>( 126 | &'a self, 127 | uri: &'a lsp::Url, 128 | ) -> anyhow::Result>> { 129 | self.trees.get(uri).await.ok_or_else(|| { 130 | let kind = SessionResourceKind::Tree; 131 | let uri = uri.clone(); 132 | crate::core::Error::SessionResourceNotFound { kind, uri }.into() 133 | }) 134 | } 135 | 136 | /// Get a mutable reference to the [`tree_sitter::Tree`] for a [`crate::core::Document`] in the 137 | /// [`Session`]. 138 | pub async fn get_mut_tree<'a>( 139 | &'a self, 140 | uri: &'a lsp::Url, 141 | ) -> anyhow::Result>> { 142 | self.trees.get_mut(uri).await.ok_or_else(|| { 143 | let kind = SessionResourceKind::Tree; 144 | let uri = uri.clone(); 145 | crate::core::Error::SessionResourceNotFound { kind, uri }.into() 146 | }) 147 | } 148 | } 149 | 150 | /// Pre-loaded tree-sitter languages for `.wast` and `.wat`. 151 | pub struct SessionLanguages { 152 | /// Pre-loaded tree-sitter language for `.wast`. 153 | pub wast: tree_sitter::Language, 154 | /// Pre-loaded tree-sitter language for `.wat`. 155 | pub wat: tree_sitter::Language, 156 | } 157 | 158 | /// A tag representing of the kinds of session resource. 159 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 160 | pub enum SessionResourceKind { 161 | /// A tag representing a [`crate::core::Document`]. 162 | Document, 163 | /// A tag representing a [`tree_sitter::Parser`]. 164 | Parser, 165 | /// A tag representing a [`tree_sitter::Tree`]. 166 | Tree, 167 | } 168 | -------------------------------------------------------------------------------- /crates/server/src/core/text.rs: -------------------------------------------------------------------------------- 1 | //! Definitions related to working with textual content. 2 | 3 | /// Convenience struct for packaging the language-id and textual content of a [`core::Document`]. 4 | pub struct Text { 5 | /// The language-id of the [`core::Document`]. 6 | pub language: crate::core::Language, 7 | /// The textual content of the [`core::Document`]. 8 | pub content: ropey::Rope, 9 | } 10 | 11 | impl Text { 12 | /// Create a new [`Text`] from a language-id and some textual content. 13 | pub fn new( 14 | language_id: impl TryInto, 15 | text: impl AsRef, 16 | ) -> anyhow::Result { 17 | let text = text.as_ref(); 18 | let language = language_id.try_into()?; 19 | let content = ropey::Rope::from_str(text); 20 | Ok(Text { language, content }) 21 | } 22 | } 23 | 24 | impl From for Text { 25 | fn from(value: crate::core::Document) -> Self { 26 | value.text() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/server/src/handler.rs: -------------------------------------------------------------------------------- 1 | //! LSP message handler functions. 2 | 3 | /// LSP message handler functions for `textDocument/*`. 4 | pub mod text_document; 5 | 6 | use std::sync::Arc; 7 | 8 | /// LSP message handler function for `initialize`. 9 | pub async fn initialize(session: Arc, params: lsp::InitializeParams) -> lsp::InitializeResult { 10 | // Received the client capabilities and store them in the server session 11 | *session.client_capabilities.write().await = Some(params.capabilities); 12 | // Retrieve the server capabilities for the response to the client 13 | let capabilities = session.server_capabilities.read().await.clone(); 14 | lsp::InitializeResult { 15 | capabilities, 16 | ..lsp::InitializeResult::default() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/server/src/handler/text_document.rs: -------------------------------------------------------------------------------- 1 | use lsp_text::RopeExt; 2 | use std::sync::Arc; 3 | 4 | /// LSP message handler function for `textDocument/didChange`. 5 | pub async fn did_change( 6 | session: Arc, 7 | params: lsp::DidChangeTextDocumentParams, 8 | ) -> anyhow::Result<()> { 9 | let uri = ¶ms.text_document.uri; 10 | let mut text = session.get_mut_text(uri).await?; 11 | 12 | let edits = params 13 | .content_changes 14 | .iter() 15 | .map(|change| text.content.build_edit(change)) 16 | .collect::, _>>()?; 17 | 18 | for edit in &edits { 19 | text.content.apply_edit(edit); 20 | } 21 | 22 | if let Some(tree) = crate::core::Document::change(session.clone(), uri, &text.content, &edits).await? { 23 | let diagnostics = crate::provider::text_document::diagnostics(&tree, &text); 24 | let version = Default::default(); 25 | session 26 | .client()? 27 | .publish_diagnostics(uri.clone(), diagnostics, version) 28 | .await; 29 | } 30 | 31 | Ok(()) 32 | } 33 | 34 | /// LSP message handler function for `textDocument/didClose`. 35 | pub async fn did_close( 36 | session: Arc, 37 | params: lsp::DidCloseTextDocumentParams, 38 | ) -> anyhow::Result<()> { 39 | let uri = params.text_document.uri; 40 | session.remove_document(&uri).await?; 41 | let diagnostics = Default::default(); 42 | let version = Default::default(); 43 | session.client()?.publish_diagnostics(uri, diagnostics, version).await; 44 | Ok(()) 45 | } 46 | 47 | /// LSP message handler function for `textDocument/didOpen`. 48 | pub async fn did_open( 49 | session: Arc, 50 | params: lsp::DidOpenTextDocumentParams, 51 | ) -> anyhow::Result<()> { 52 | let uri = params.text_document.uri.clone(); 53 | if let Some(document) = crate::core::Document::open(session.clone(), params)? { 54 | let tree = document.tree.clone(); 55 | let text = document.text(); 56 | session.insert_document(uri.clone(), document).await?; 57 | let diagnostics = crate::provider::text_document::diagnostics(&tree, &text); 58 | let version = Default::default(); 59 | session.client()?.publish_diagnostics(uri, diagnostics, version).await; 60 | } else { 61 | log::warn!("'textDocument/didOpen' failed :: uri: {:#?}", uri); 62 | } 63 | Ok(()) 64 | } 65 | 66 | /// LSP message handler function for `textDocument/documentSymbol`. 67 | pub async fn document_symbol( 68 | session: Arc, 69 | params: lsp::DocumentSymbolParams, 70 | ) -> anyhow::Result> { 71 | crate::provider::text_document::document_symbol(session, params).await 72 | } 73 | 74 | /// LSP message handler function for `textDocument/semanticTokens/*`. 75 | pub mod semantic_tokens { 76 | use std::sync::Arc; 77 | 78 | /// LSP message handler function for `textDocument/semanticTokens/full`. 79 | pub async fn full( 80 | session: Arc, 81 | params: lsp::SemanticTokensParams, 82 | ) -> anyhow::Result> { 83 | crate::provider::text_document::semantic_tokens::full(session, params).await 84 | } 85 | 86 | /// LSP message handler function for `textDocument/semanticTokens/range`. 87 | pub async fn range( 88 | session: Arc, 89 | params: lsp::SemanticTokensRangeParams, 90 | ) -> anyhow::Result> { 91 | crate::provider::text_document::semantic_tokens::range(session, params).await 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/server/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The implementation of the WebAssembly language server. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | #![allow(clippy::if_same_then_else)] 7 | #![allow(clippy::needless_lifetimes)] 8 | 9 | /// Core definitions for server functionality. 10 | pub mod core; 11 | 12 | /// LSP message handler functions. 13 | pub mod handler; 14 | 15 | /// Build-time metadata for the server. 16 | pub mod metadata; 17 | 18 | /// LSP feature provider functions. 19 | pub mod provider; 20 | 21 | use std::sync::Arc; 22 | use tower_lsp::jsonrpc; 23 | 24 | /// The WebAssembly language server structure. 25 | pub struct Server { 26 | /// Reference to the LSP client. 27 | pub client: tower_lsp::Client, 28 | /// Reference to the LSP session. 29 | pub session: Arc, 30 | } 31 | 32 | impl Server { 33 | /// Create a new [`Server`] instance. 34 | pub fn new(languages: crate::core::SessionLanguages, client: tower_lsp::Client) -> anyhow::Result { 35 | let session = Arc::new(crate::core::Session::new(languages, Some(client.clone()))?); 36 | Ok(Server { client, session }) 37 | } 38 | 39 | /// Convenience function for building [`lsp::ServerCapabilities`] for [Server]. 40 | pub fn capabilities() -> lsp::ServerCapabilities { 41 | let document_symbol_provider = Some(lsp::OneOf::Left(true)); 42 | 43 | // let semantic_tokens_provider = { 44 | // let token_types = vec![ 45 | // lsp::SemanticTokenType::COMMENT, 46 | // lsp::SemanticTokenType::FUNCTION, 47 | // lsp::SemanticTokenType::KEYWORD, 48 | // lsp::SemanticTokenType::NAMESPACE, 49 | // lsp::SemanticTokenType::OPERATOR, 50 | // lsp::SemanticTokenType::PARAMETER, 51 | // lsp::SemanticTokenType::STRING, 52 | // lsp::SemanticTokenType::TYPE, 53 | // lsp::SemanticTokenType::TYPE_PARAMETER, 54 | // lsp::SemanticTokenType::VARIABLE, 55 | // ]; 56 | // let token_modifiers = Default::default(); 57 | 58 | // let options = lsp::SemanticTokensOptions { 59 | // legend: lsp::SemanticTokensLegend { 60 | // token_types, 61 | // token_modifiers, 62 | // }, 63 | // range: Some(true), 64 | // full: Some(lsp::SemanticTokensFullOptions::Bool(true)), 65 | // ..Default::default() 66 | // }; 67 | // Some(lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(options)) 68 | // }; 69 | 70 | let text_document_sync = { 71 | let options = lsp::TextDocumentSyncOptions { 72 | open_close: Some(true), 73 | change: Some(lsp::TextDocumentSyncKind::INCREMENTAL), 74 | ..Default::default() 75 | }; 76 | Some(lsp::TextDocumentSyncCapability::Options(options)) 77 | }; 78 | 79 | lsp::ServerCapabilities { 80 | text_document_sync, 81 | document_symbol_provider, 82 | // semantic_tokens_provider, 83 | ..Default::default() 84 | } 85 | } 86 | } 87 | 88 | #[tower_lsp::async_trait] 89 | impl tower_lsp::LanguageServer for Server { 90 | async fn initialize(&self, params: lsp::InitializeParams) -> jsonrpc::Result { 91 | let session = self.session.clone(); 92 | let result = crate::handler::initialize(session, params).await; 93 | Ok(result) 94 | } 95 | 96 | async fn initialized(&self, _: lsp::InitializedParams) { 97 | let typ = lsp::MessageType::INFO; 98 | let message = "WebAssembly language server initialized!"; 99 | self.client.log_message(typ, message).await; 100 | } 101 | 102 | async fn shutdown(&self) -> jsonrpc::Result<()> { 103 | Ok(()) 104 | } 105 | 106 | async fn did_open(&self, params: lsp::DidOpenTextDocumentParams) { 107 | let session = self.session.clone(); 108 | crate::handler::text_document::did_open(session, params).await.unwrap() 109 | } 110 | 111 | async fn did_change(&self, params: lsp::DidChangeTextDocumentParams) { 112 | let session = self.session.clone(); 113 | crate::handler::text_document::did_change(session, params) 114 | .await 115 | .unwrap() 116 | } 117 | 118 | async fn did_close(&self, params: lsp::DidCloseTextDocumentParams) { 119 | let session = self.session.clone(); 120 | crate::handler::text_document::did_close(session, params).await.unwrap() 121 | } 122 | 123 | async fn document_symbol( 124 | &self, 125 | params: lsp::DocumentSymbolParams, 126 | ) -> jsonrpc::Result> { 127 | let session = self.session.clone(); 128 | let result = crate::handler::text_document::document_symbol(session, params).await; 129 | Ok(result.map_err(crate::core::IntoJsonRpcError)?) 130 | } 131 | 132 | async fn semantic_tokens_full( 133 | &self, 134 | params: lsp::SemanticTokensParams, 135 | ) -> jsonrpc::Result> { 136 | let session = self.session.clone(); 137 | let result = crate::handler::text_document::semantic_tokens::full(session, params).await; 138 | Ok(result.map_err(crate::core::IntoJsonRpcError)?) 139 | } 140 | 141 | async fn semantic_tokens_range( 142 | &self, 143 | params: lsp::SemanticTokensRangeParams, 144 | ) -> jsonrpc::Result> { 145 | let session = self.session.clone(); 146 | let result = crate::handler::text_document::semantic_tokens::range(session, params).await; 147 | Ok(result.map_err(crate::core::IntoJsonRpcError)?) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /crates/server/src/metadata.rs: -------------------------------------------------------------------------------- 1 | //! Build-time metadata for the server. 2 | 3 | #![allow(dead_code)] 4 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 5 | -------------------------------------------------------------------------------- /crates/server/src/provider.rs: -------------------------------------------------------------------------------- 1 | //! LSP feature provider functions. 2 | 3 | /// Provider definitions for LSP `textDocument/documentSymbol` 4 | pub mod text_document; 5 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document.rs: -------------------------------------------------------------------------------- 1 | /// Provider definitions for LSP `textDocument/documentSymbol` 2 | pub mod document_symbol; 3 | 4 | /// Provider definitions for LSP `textDocument/publishDiagnostics`. 5 | pub mod publish_diagnostics; 6 | 7 | /// Provider definitions for LSP `textDocument/semanticTokens/*` 8 | pub mod semantic_tokens; 9 | 10 | pub use document_symbol::document_symbol; 11 | pub use publish_diagnostics::*; 12 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/document_symbol.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use crate::core::{self}; 4 | use lsp_text::RopeExt; 5 | use std::sync::Arc; 6 | 7 | /// Provider definitions for LSP `textDocument/documentSymbol` for `.wast` documents. 8 | pub mod wast; 9 | 10 | /// Provider definitions for LSP `textDocument/documentSymbol` for `.wat` documents. 11 | pub mod wat; 12 | 13 | /// Encodes data for constructing upcoming DocumentSymbols. 14 | #[derive(Clone, Debug)] 15 | pub(self) struct Data<'tree> { 16 | /// The tree-sitter Node to be processed as a symbol. 17 | pub node: tree_sitter::Node<'tree>, 18 | /// Number of (possibly filtered) children to be processed for the symbol. 19 | pub children_count: usize, 20 | /// The kind of document entity the symbol represents. 21 | pub kind: lsp::SymbolKind, 22 | /// The name hint for the symbol (used for anonymous entities). 23 | pub name_hint: &'static str, 24 | } 25 | 26 | /// Encodes actions for loop iterations when processing tree-sitter nodes. 27 | #[derive(Debug)] 28 | pub(self) enum Work<'tree> { 29 | /// Construct a DocumentSymbol and pop the data stack. 30 | Data, 31 | /// Add a tree-sitter node to remaining nodes to process. 32 | Node(tree_sitter::Node<'tree>), 33 | } 34 | 35 | /// Convenience type for packaging a (symbol) name with an lsp range and selection range. 36 | #[derive(Clone, Debug)] 37 | pub(self) struct SymbolRange { 38 | /// The name (identifier) of the symbol. 39 | pub name: String, 40 | /// The (node-enclosing) range of the symbol. 41 | pub range: lsp::Range, 42 | /// The (identifier-enclosing) range of the symbol. 43 | pub selection_range: lsp::Range, 44 | } 45 | 46 | /// Compute the name and ranges for a document symbol given tree-sitter node data. 47 | pub(self) fn symbol_range( 48 | content: &ropey::Rope, 49 | node: tree_sitter::Node, 50 | name_hint: &'static str, 51 | field_id: u16, 52 | ) -> SymbolRange { 53 | let name; 54 | let range = content.tree_sitter_range_to_lsp_range(node.range()); 55 | let selection_range; 56 | if let Some(inner_node) = node.child_by_field_id(field_id) { 57 | name = content.utf8_text_for_tree_sitter_node(&inner_node).into(); 58 | selection_range = content.tree_sitter_range_to_lsp_range(inner_node.range()); 59 | } else { 60 | name = format!("<{}@{}:{}>", name_hint, range.start.line + 1, range.start.character + 1); 61 | selection_range = range; 62 | } 63 | 64 | SymbolRange { 65 | name, 66 | range, 67 | selection_range, 68 | } 69 | } 70 | 71 | /// Provider function for LSP `textDocument/documentSymbol`. 72 | pub async fn document_symbol( 73 | session: Arc, 74 | params: lsp::DocumentSymbolParams, 75 | ) -> anyhow::Result> { 76 | let uri = params.text_document.uri.clone(); 77 | let text = session.get_text(&uri).await?; 78 | let session = session.clone(); 79 | let response = match text.language { 80 | core::Language::Wast => self::wast::document_symbol(session, params, &text.content).await?, 81 | core::Language::Wat => self::wat::document_symbol(session, params, &text.content).await?, 82 | }; 83 | Ok(response) 84 | } 85 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/document_symbol/wast.rs: -------------------------------------------------------------------------------- 1 | //! Provider definitions for LSP `textDocument/documentSymbol` for `.wast` documents. 2 | 3 | use crate::{ 4 | core::{self, language::wast, node::NodeExt}, 5 | provider::text_document::document_symbol::{symbol_range, Data, SymbolRange, Work}, 6 | }; 7 | use std::sync::Arc; 8 | 9 | /// Provider function for LSP `textDocument/documentSymbol` for `.wast` documents. 10 | pub async fn document_symbol( 11 | session: Arc, 12 | params: lsp::DocumentSymbolParams, 13 | content: &ropey::Rope, 14 | ) -> anyhow::Result> { 15 | // Vector to collect document symbols into as they are constructed. 16 | let mut syms: Vec = vec![]; 17 | 18 | // Prepare the syntax tree. 19 | let tree = session.get_tree(¶ms.text_document.uri).await?; 20 | let tree = tree.lock().await; 21 | let node = tree.root_node(); 22 | 23 | // Prepare the stack machine: 24 | // data: contains data for constructing upcoming DocumentSymbols 25 | // work: contains remaining tree_sitter nodes to process 26 | let mut data: Vec = vec![]; 27 | let mut work: Vec = vec![Work::Node(node)]; 28 | 29 | // The stack machine work loop. 30 | while let Some(next) = work.pop() { 31 | match next { 32 | // Construct a DocumentSymbol and pop data stack 33 | Work::Data => { 34 | if let Some(Data { 35 | node, 36 | children_count, 37 | kind, 38 | name_hint, 39 | }) = data.pop() 40 | { 41 | let SymbolRange { 42 | name, 43 | range, 44 | selection_range, 45 | } = { symbol_range(content, node, name_hint, wast::field::IDENTIFIER) }; 46 | 47 | #[allow(deprecated)] 48 | let sym = lsp::DocumentSymbol { 49 | children: if syms.is_empty() { 50 | None 51 | } else { 52 | // Drain the syms array by the number of children nodes we counted for this DocumentSymbol. 53 | // This allows us to properly reconstruct symbol nesting. 54 | let children = syms.drain(syms.len() - children_count ..); 55 | let children = children.rev(); 56 | Some(children.collect()) 57 | }, 58 | deprecated: Default::default(), 59 | detail: Default::default(), 60 | kind, 61 | name: name.to_string(), 62 | range, 63 | selection_range, 64 | tags: Default::default(), 65 | }; 66 | syms.push(sym); 67 | } 68 | }, 69 | 70 | Work::Node(node) if wast::kind::ROOT == node.kind_id() => { 71 | let mut cursor = node.walk(); 72 | let children = node 73 | .children(&mut cursor) 74 | .filter(|it| [wast::kind::COMMAND, wast::kind::MODULE_FIELD].contains(&it.kind_id())) 75 | .map(Work::Node); 76 | work.extend(children); 77 | }, 78 | 79 | Work::Node(node) if wast::kind::COMMAND == node.kind_id() => { 80 | let mut cursor = node.walk(); 81 | let children = node 82 | .children(&mut cursor) 83 | .filter(|it| [wast::kind::SCRIPT_MODULE].contains(&it.kind_id())) 84 | .map(Work::Node); 85 | work.extend(children); 86 | }, 87 | 88 | Work::Node(node) if wast::kind::MODULE == node.kind_id() => { 89 | work.push(Work::Data); 90 | 91 | let mut children_count = 0; 92 | for child in node.children(&mut node.walk()) { 93 | if child.matches_subtypes(wast::kind::MODULE_FIELD, wast::grouped::MODULE_FIELDS) { 94 | work.push(Work::Node(child)); 95 | children_count += 1; 96 | } 97 | } 98 | 99 | data.push(Data { 100 | node, 101 | children_count, 102 | kind: lsp::SymbolKind::MODULE, 103 | name_hint: "module", 104 | }); 105 | }, 106 | 107 | Work::Node(node) if wast::kind::MODULE_FIELD == node.kind_id() => { 108 | let mut cursor = node.walk(); 109 | let children = node 110 | .children(&mut cursor) 111 | .filter(|it| wast::grouped::MODULE_FIELDS.contains(&it.kind_id())) 112 | .map(Work::Node); 113 | work.extend(children); 114 | }, 115 | 116 | Work::Node(node) if wast::kind::MODULE_FIELD_DATA == node.kind_id() => { 117 | work.push(Work::Data); 118 | data.push(Data { 119 | node, 120 | children_count: 0, 121 | kind: lsp::SymbolKind::KEY, 122 | name_hint: "data", 123 | }); 124 | }, 125 | 126 | Work::Node(node) if wast::kind::MODULE_FIELD_ELEM == node.kind_id() => { 127 | work.push(Work::Data); 128 | data.push(Data { 129 | node, 130 | children_count: 0, 131 | kind: lsp::SymbolKind::FIELD, 132 | name_hint: "elem", 133 | }); 134 | }, 135 | 136 | Work::Node(node) if wast::kind::MODULE_FIELD_FUNC == node.kind_id() => { 137 | work.push(Work::Data); 138 | data.push(Data { 139 | node, 140 | children_count: 0, 141 | kind: lsp::SymbolKind::FUNCTION, 142 | name_hint: "func", 143 | }); 144 | }, 145 | 146 | Work::Node(node) if wast::kind::MODULE_FIELD_GLOBAL == node.kind_id() => { 147 | work.push(Work::Data); 148 | data.push(Data { 149 | node, 150 | children_count: 0, 151 | kind: lsp::SymbolKind::EVENT, 152 | name_hint: "global", 153 | }); 154 | }, 155 | 156 | Work::Node(node) if wast::kind::MODULE_FIELD_MEMORY == node.kind_id() => { 157 | work.push(Work::Data); 158 | data.push(Data { 159 | node, 160 | children_count: 0, 161 | kind: lsp::SymbolKind::ARRAY, 162 | name_hint: "memory", 163 | }); 164 | }, 165 | 166 | Work::Node(node) if wast::kind::MODULE_FIELD_TABLE == node.kind_id() => { 167 | work.push(Work::Data); 168 | data.push(Data { 169 | node, 170 | children_count: 0, 171 | kind: lsp::SymbolKind::INTERFACE, 172 | name_hint: "table", 173 | }); 174 | }, 175 | 176 | Work::Node(node) if wast::kind::MODULE_FIELD_TYPE == node.kind_id() => { 177 | work.push(Work::Data); 178 | data.push(Data { 179 | node, 180 | children_count: 0, 181 | kind: lsp::SymbolKind::TYPE_PARAMETER, 182 | name_hint: "type", 183 | }); 184 | }, 185 | 186 | Work::Node(node) if wast::kind::SCRIPT_MODULE == node.kind_id() => { 187 | let mut cursor = node.walk(); 188 | let children = node 189 | .children(&mut cursor) 190 | .filter(|it| [wast::kind::MODULE].contains(&it.kind_id())) 191 | .map(Work::Node); 192 | work.extend(children); 193 | }, 194 | 195 | _ => {}, 196 | } 197 | } 198 | // Reverse the syms vec so that document symbols are returned in the correct order. Note that 199 | // children nodes are reversed _as the symbols are nested_. 200 | syms.reverse(); 201 | 202 | Ok(Some(lsp::DocumentSymbolResponse::Nested(syms))) 203 | } 204 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/document_symbol/wat.rs: -------------------------------------------------------------------------------- 1 | //! Provider definitions for LSP `textDocument/documentSymbol` for `.wat` documents. 2 | 3 | use crate::{ 4 | core::{self, language::wat, node::NodeExt}, 5 | provider::text_document::document_symbol::{symbol_range, Data, SymbolRange, Work}, 6 | }; 7 | use std::sync::Arc; 8 | 9 | /// Provider function for LSP `textDocument/documentSymbol` for `.wat` documents. 10 | pub async fn document_symbol( 11 | session: Arc, 12 | params: lsp::DocumentSymbolParams, 13 | content: &ropey::Rope, 14 | ) -> anyhow::Result> { 15 | // Vector to collect document symbols into as they are constructed. 16 | let mut syms: Vec = vec![]; 17 | 18 | // Prepare the syntax tree. 19 | let tree = session.get_tree(¶ms.text_document.uri).await?; 20 | let tree = tree.lock().await; 21 | let node = tree.root_node(); 22 | 23 | // Prepare the stack machine: 24 | // data: contains data for constructing upcoming DocumentSymbols 25 | // work: contains remaining tree_sitter nodes to process 26 | let mut data: Vec = vec![]; 27 | let mut work: Vec = vec![Work::Node(node)]; 28 | 29 | // The stack machine work loop. 30 | while let Some(next) = work.pop() { 31 | match next { 32 | // Construct a DocumentSymbol and pop data stack 33 | Work::Data => { 34 | if let Some(Data { 35 | node, 36 | children_count, 37 | kind, 38 | name_hint, 39 | }) = data.pop() 40 | { 41 | let SymbolRange { 42 | name, 43 | range, 44 | selection_range, 45 | } = { symbol_range(content, node, name_hint, wat::field::IDENTIFIER) }; 46 | 47 | #[allow(deprecated)] 48 | let sym = lsp::DocumentSymbol { 49 | children: if syms.is_empty() { 50 | None 51 | } else { 52 | // Drain the syms array by the number of children nodes we counted for this DocumentSymbol. 53 | // This allows us to properly reconstruct symbol nesting. 54 | let children = syms.drain(syms.len() - children_count ..); 55 | let children = children.rev(); 56 | Some(children.collect()) 57 | }, 58 | deprecated: Default::default(), 59 | detail: Default::default(), 60 | kind, 61 | name: name.to_string(), 62 | range, 63 | selection_range, 64 | tags: Default::default(), 65 | }; 66 | syms.push(sym); 67 | } 68 | }, 69 | 70 | Work::Node(node) if wat::kind::ROOT == node.kind_id() => { 71 | let mut cursor = node.walk(); 72 | let commands = node 73 | .children(&mut cursor) 74 | .filter(|it| [wat::kind::MODULE, wat::kind::MODULE_FIELD].contains(&it.kind_id())) 75 | .map(Work::Node); 76 | work.extend(commands); 77 | }, 78 | 79 | Work::Node(node) if wat::kind::MODULE == node.kind_id() => { 80 | work.push(Work::Data); 81 | 82 | let mut children_count = 0; 83 | for child in node.children(&mut node.walk()) { 84 | if child.matches_subtypes(wat::kind::MODULE_FIELD, wat::grouped::MODULE_FIELDS) { 85 | work.push(Work::Node(child)); 86 | children_count += 1; 87 | } 88 | } 89 | 90 | data.push(Data { 91 | node, 92 | children_count, 93 | kind: lsp::SymbolKind::MODULE, 94 | name_hint: "module", 95 | }); 96 | }, 97 | 98 | Work::Node(node) if wat::kind::MODULE_FIELD == node.kind_id() => { 99 | let mut cursor = node.walk(); 100 | let children = node 101 | .children(&mut cursor) 102 | .filter(|it| wat::grouped::MODULE_FIELDS.contains(&it.kind_id())) 103 | .map(Work::Node); 104 | work.extend(children); 105 | }, 106 | 107 | Work::Node(node) if wat::kind::MODULE_FIELD_DATA == node.kind_id() => { 108 | work.push(Work::Data); 109 | data.push(Data { 110 | node, 111 | children_count: 0, 112 | kind: lsp::SymbolKind::KEY, 113 | name_hint: "data", 114 | }); 115 | }, 116 | 117 | Work::Node(node) if wat::kind::MODULE_FIELD_ELEM == node.kind_id() => { 118 | work.push(Work::Data); 119 | data.push(Data { 120 | node, 121 | children_count: 0, 122 | kind: lsp::SymbolKind::FIELD, 123 | name_hint: "elem", 124 | }); 125 | }, 126 | 127 | Work::Node(node) if wat::kind::MODULE_FIELD_FUNC == node.kind_id() => { 128 | work.push(Work::Data); 129 | data.push(Data { 130 | node, 131 | children_count: 0, 132 | kind: lsp::SymbolKind::FUNCTION, 133 | name_hint: "func", 134 | }); 135 | }, 136 | 137 | Work::Node(node) if wat::kind::MODULE_FIELD_GLOBAL == node.kind_id() => { 138 | work.push(Work::Data); 139 | data.push(Data { 140 | node, 141 | children_count: 0, 142 | kind: lsp::SymbolKind::EVENT, 143 | name_hint: "global", 144 | }); 145 | }, 146 | 147 | Work::Node(node) if wat::kind::MODULE_FIELD_MEMORY == node.kind_id() => { 148 | work.push(Work::Data); 149 | data.push(Data { 150 | node, 151 | children_count: 0, 152 | kind: lsp::SymbolKind::ARRAY, 153 | name_hint: "memory", 154 | }); 155 | }, 156 | 157 | Work::Node(node) if wat::kind::MODULE_FIELD_TABLE == node.kind_id() => { 158 | work.push(Work::Data); 159 | data.push(Data { 160 | node, 161 | children_count: 0, 162 | kind: lsp::SymbolKind::INTERFACE, 163 | name_hint: "table", 164 | }); 165 | }, 166 | 167 | Work::Node(node) if wat::kind::MODULE_FIELD_TYPE == node.kind_id() => { 168 | work.push(Work::Data); 169 | data.push(Data { 170 | node, 171 | children_count: 0, 172 | kind: lsp::SymbolKind::TYPE_PARAMETER, 173 | name_hint: "type", 174 | }); 175 | }, 176 | 177 | _ => {}, 178 | } 179 | } 180 | // Reverse the syms vec so that document symbols are returned in the correct order. Note that 181 | // children nodes are reversed _as the symbols are nested_. 182 | syms.reverse(); 183 | 184 | Ok(Some(lsp::DocumentSymbolResponse::Nested(syms))) 185 | } 186 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/publish_diagnostics.rs: -------------------------------------------------------------------------------- 1 | /// Provider definitions for LSP `textDocument/publishDiagnostics` for `.wast` documents. 2 | pub mod wast; 3 | 4 | /// Provider definitions for LSP `textDocument/publishDiagnostics` for `.wat` documents. 5 | pub mod wat; 6 | 7 | /// Provider function for LSP `textDocument/publishDiagnostics`. 8 | pub fn diagnostics(tree: &tree_sitter::Tree, text: &crate::core::Text) -> Vec { 9 | match text.language { 10 | crate::core::Language::Wast => wast::diagnostics(tree, &text.content), 11 | crate::core::Language::Wat => wat::diagnostics(tree, &text.content), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/publish_diagnostics/wast.rs: -------------------------------------------------------------------------------- 1 | //! Provider definitions for LSP `textDocument/publishDiagnostics` for `.wast` documents. 2 | 3 | use crate::core::{self, node::TraceNodeWalker, range::RangeExt}; 4 | use lsp_text::RopeExt; 5 | 6 | /// Provider function for LSP `textDocument/publishDiagnostics` for `.wast` documents. 7 | pub fn diagnostics(tree: &tree_sitter::Tree, content: &ropey::Rope) -> Vec { 8 | let mut diagnostics = vec![]; 9 | let mut walker = { 10 | let language = core::Language::Wast; 11 | let node = tree.root_node(); 12 | TraceNodeWalker::new(language, node) 13 | }; 14 | 15 | let mut previous = walker.node(); 16 | let mut covering_error_range = None::; 17 | 18 | loop { 19 | if walker.done { 20 | break; 21 | } 22 | 23 | let current = walker.node(); 24 | 25 | if current.is_error() { 26 | let range = current.range(); 27 | match covering_error_range { 28 | Some(ref error_range) if error_range.contains(&range) => { 29 | previous = current; 30 | walker.goto_next(); 31 | continue; 32 | }, 33 | _ => { 34 | covering_error_range = Some(range.clone()); 35 | }, 36 | } 37 | let message = String::from("ERROR node"); 38 | let range = content.tree_sitter_range_to_lsp_range(range); 39 | let severity = Some(lsp::DiagnosticSeverity::ERROR); 40 | diagnostics.push(lsp::Diagnostic { 41 | range, 42 | severity, 43 | message, 44 | ..Default::default() 45 | }); 46 | previous = current; 47 | walker.goto_next(); 48 | continue; 49 | } 50 | 51 | if current.is_missing() { 52 | let range = current.range(); 53 | match covering_error_range { 54 | Some(ref error_range) if error_range.contains(&range) => { 55 | previous = current; 56 | walker.goto_next(); 57 | continue; 58 | }, 59 | _ => { 60 | covering_error_range = Some(range.clone()); 61 | }, 62 | } 63 | let message = format!(r#"expected "{}" after "{}""#, current.kind(), previous.kind()); 64 | let range = content.tree_sitter_range_to_lsp_range(range); 65 | let severity = Some(lsp::DiagnosticSeverity::ERROR); 66 | diagnostics.push(lsp::Diagnostic { 67 | range, 68 | severity, 69 | message, 70 | ..Default::default() 71 | }); 72 | previous = current; 73 | walker.goto_next(); 74 | continue; 75 | } 76 | 77 | // catch all case 78 | previous = current; 79 | walker.goto_next(); 80 | } 81 | 82 | diagnostics.reverse(); 83 | diagnostics 84 | } 85 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/publish_diagnostics/wat.rs: -------------------------------------------------------------------------------- 1 | //! Provider definitions for LSP `textDocument/publishDiagnostics` for `.wat` documents. 2 | 3 | use crate::core::{self, node::TraceNodeWalker, range::RangeExt}; 4 | use lsp_text::RopeExt; 5 | 6 | /// Provider function for LSP `textDocument/publishDiagnostics` for `.wat` documents. 7 | pub fn diagnostics(tree: &tree_sitter::Tree, content: &ropey::Rope) -> Vec { 8 | let mut diagnostics = vec![]; 9 | let mut walker = { 10 | let language = core::Language::Wat; 11 | let node = tree.root_node(); 12 | TraceNodeWalker::new(language, node) 13 | }; 14 | 15 | let mut previous = walker.node(); 16 | let mut covering_error_range = None::; 17 | 18 | loop { 19 | if walker.done { 20 | break; 21 | } 22 | 23 | let current = walker.node(); 24 | 25 | if current.is_error() { 26 | let range = current.range(); 27 | match covering_error_range { 28 | Some(ref error_range) if error_range.contains(&range) => { 29 | previous = current; 30 | walker.goto_next(); 31 | continue; 32 | }, 33 | _ => { 34 | covering_error_range = Some(range.clone()); 35 | }, 36 | } 37 | let message = String::from("ERROR node"); 38 | let range = content.tree_sitter_range_to_lsp_range(range); 39 | let severity = Some(lsp::DiagnosticSeverity::ERROR); 40 | diagnostics.push(lsp::Diagnostic { 41 | range, 42 | severity, 43 | message, 44 | ..Default::default() 45 | }); 46 | previous = current; 47 | walker.goto_next(); 48 | continue; 49 | } 50 | 51 | if current.is_missing() { 52 | let range = current.range(); 53 | match covering_error_range { 54 | Some(ref error_range) if error_range.contains(&range) => { 55 | previous = current; 56 | walker.goto_next(); 57 | continue; 58 | }, 59 | _ => { 60 | covering_error_range = Some(range.clone()); 61 | }, 62 | } 63 | let message = format!(r#"expected "{}" after "{}""#, current.kind(), previous.kind()); 64 | let range = content.tree_sitter_range_to_lsp_range(range); 65 | let severity = Some(lsp::DiagnosticSeverity::ERROR); 66 | diagnostics.push(lsp::Diagnostic { 67 | range, 68 | severity, 69 | message, 70 | ..Default::default() 71 | }); 72 | previous = current; 73 | walker.goto_next(); 74 | continue; 75 | } 76 | 77 | // catch all case 78 | previous = current; 79 | walker.goto_next(); 80 | } 81 | 82 | diagnostics.reverse(); 83 | diagnostics 84 | } 85 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/semantic_tokens.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{self, Language}; 2 | use std::sync::Arc; 3 | 4 | /// Definitions for the semantic tokens builder used during tokenization. 5 | pub mod builder; 6 | 7 | pub mod wast; 8 | pub mod wat; 9 | 10 | /// LSP message handler function for `textDocument/semanticTokens/full`. 11 | pub async fn full( 12 | session: Arc, 13 | params: lsp::SemanticTokensParams, 14 | ) -> anyhow::Result> { 15 | let uri = params.text_document.uri.clone(); 16 | let text = session.get_text(&uri).await?; 17 | let response = match text.language { 18 | Language::Wast => wast::full(session.clone(), params, &text.content).await?, 19 | Language::Wat => wat::full(session.clone(), params, &text.content).await?, 20 | }; 21 | Ok(response) 22 | } 23 | 24 | /// LSP message handler function for `textDocument/semanticTokens/range`. 25 | pub async fn range( 26 | session: Arc, 27 | params: lsp::SemanticTokensRangeParams, 28 | ) -> anyhow::Result> { 29 | let uri = params.text_document.uri.clone(); 30 | let text = session.get_text(&uri).await?; 31 | let response = match text.language { 32 | Language::Wast => wast::range(session.clone(), params, &text.content).await?, 33 | Language::Wat => wat::range(session.clone(), params, &text.content).await?, 34 | }; 35 | Ok(response) 36 | } 37 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/semantic_tokens/builder.rs: -------------------------------------------------------------------------------- 1 | //! Definitions for the semantic tokens builder used during tokenization. 2 | 3 | use anyhow::anyhow; 4 | use lsp::SemanticTokensFullDeltaResult; 5 | use lsp_text::RopeExt; 6 | use std::collections::HashMap; 7 | 8 | /// Manages tokenization state for encoding semantic token data. 9 | #[derive(Clone, Debug)] 10 | pub struct SemanticTokensBuilder<'text, 'tree> { 11 | content: &'text ropey::Rope, 12 | id: u128, 13 | prev_row: u32, 14 | prev_col: u32, 15 | prev_data: Option>, 16 | data: Vec, 17 | token_modifier_map: HashMap<&'tree lsp::SemanticTokenModifier, u32>, 18 | token_type_map: HashMap<&'tree lsp::SemanticTokenType, u32>, 19 | has_legend: bool, 20 | } 21 | 22 | impl<'text, 'tree> SemanticTokensBuilder<'text, 'tree> { 23 | /// Create a new [`SemanticTokensBuilder`]. 24 | pub fn new(content: &'text ropey::Rope, legend: Option<&'tree lsp::SemanticTokensLegend>) -> anyhow::Result { 25 | use std::time::{SystemTime, UNIX_EPOCH}; 26 | 27 | let mut token_modifier_map = HashMap::new(); 28 | let mut token_type_map = HashMap::new(); 29 | let mut has_legend = false; 30 | 31 | if let Some(legend) = legend { 32 | has_legend = true; 33 | 34 | for (i, token_type) in legend.token_types.iter().enumerate() { 35 | let _ = token_type_map.insert(token_type, i as u32); 36 | } 37 | 38 | for (i, token_modifier) in legend.token_modifiers.iter().enumerate() { 39 | let _ = token_modifier_map.insert(token_modifier, i as u32); 40 | } 41 | } 42 | 43 | Ok(Self { 44 | content, 45 | id: SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(), 46 | prev_row: Default::default(), 47 | prev_col: Default::default(), 48 | prev_data: Default::default(), 49 | data: Default::default(), 50 | token_modifier_map, 51 | token_type_map, 52 | has_legend, 53 | }) 54 | } 55 | 56 | /// Build the [`lsp::SemanticTokens`] data from the tokenization state. 57 | pub fn build(&mut self) -> lsp::SemanticTokens { 58 | self.prev_data = None; 59 | lsp::SemanticTokens { 60 | result_id: Some(self.id()), 61 | data: self.data.clone(), 62 | } 63 | } 64 | 65 | /// Build the [`lsp::SemanticTokensFullDeltaResult`] data from the current tokenization state. 66 | pub fn build_delta(&mut self) -> anyhow::Result { 67 | if let Some(prev_data) = &self.prev_data { 68 | let mut start_idx = 0; 69 | while start_idx < self.data.len() 70 | && start_idx < prev_data.len() 71 | && prev_data[start_idx] == self.data[start_idx] 72 | { 73 | start_idx += 1; 74 | } 75 | 76 | if start_idx < self.data.len() && start_idx < prev_data.len() { 77 | let mut end_idx = 0; 78 | while end_idx < self.data.len() 79 | && end_idx < prev_data.len() 80 | && prev_data[prev_data.len() - 1 - end_idx] == self.data[self.data.len() - 1 - end_idx] 81 | { 82 | end_idx += 1; 83 | } 84 | 85 | let edit = { 86 | let start = u32::try_from(start_idx)?; 87 | let delete_count = u32::try_from(prev_data.len() - end_idx - start_idx)?; 88 | let data = Some(self.data[start_idx .. self.data.len() - end_idx].to_vec()); 89 | lsp::SemanticTokensEdit { 90 | start, 91 | delete_count, 92 | data, 93 | } 94 | }; 95 | 96 | let tokens_delta = lsp::SemanticTokensDelta { 97 | result_id: Some(self.id()), 98 | edits: vec![edit], 99 | }; 100 | 101 | Ok(lsp::SemanticTokensFullDeltaResult::TokensDelta(tokens_delta)) 102 | } else if start_idx < self.data.len() { 103 | let edit = { 104 | let start = u32::try_from(start_idx)?; 105 | let delete_count = 0; 106 | let data = Some(self.data[start_idx ..].to_vec()); 107 | lsp::SemanticTokensEdit { 108 | start, 109 | delete_count, 110 | data, 111 | } 112 | }; 113 | 114 | let tokens_delta = lsp::SemanticTokensDelta { 115 | result_id: Some(self.id()), 116 | edits: vec![edit], 117 | }; 118 | 119 | Ok(lsp::SemanticTokensFullDeltaResult::TokensDelta(tokens_delta)) 120 | } else if start_idx < prev_data.len() { 121 | let edit = { 122 | let start = u32::try_from(start_idx)?; 123 | let delete_count = u32::try_from(prev_data.len() - start_idx)?; 124 | let data = None; 125 | lsp::SemanticTokensEdit { 126 | start, 127 | delete_count, 128 | data, 129 | } 130 | }; 131 | 132 | let tokens_delta = lsp::SemanticTokensDelta { 133 | result_id: Some(self.id()), 134 | edits: vec![edit], 135 | }; 136 | 137 | Ok(lsp::SemanticTokensFullDeltaResult::TokensDelta(tokens_delta)) 138 | } else { 139 | let tokens_delta = lsp::SemanticTokensDelta { 140 | result_id: Some(self.id()), 141 | edits: vec![], 142 | }; 143 | 144 | Ok(lsp::SemanticTokensFullDeltaResult::TokensDelta(tokens_delta)) 145 | } 146 | } else { 147 | self.prev_data = None; 148 | let semantic_tokens = lsp::SemanticTokens { 149 | result_id: Some(self.id()), 150 | data: self.data.clone(), 151 | }; 152 | Ok(SemanticTokensFullDeltaResult::Tokens(semantic_tokens)) 153 | } 154 | } 155 | 156 | /// Return the ID for the current tokenization state. 157 | pub fn id(&self) -> String { 158 | self.id.to_string() 159 | } 160 | 161 | /// Rollback tokenization state to previous data. 162 | pub fn prev_result(&mut self, id: &str) -> anyhow::Result<()> { 163 | if self.id() == id { 164 | self.prev_data = Some(self.data.clone()); 165 | } 166 | self.reset() 167 | } 168 | 169 | /// Push and encode a token into the tokenization state. 170 | pub fn push( 171 | &mut self, 172 | node: tree_sitter::Node, 173 | token_type: &lsp::SemanticTokenType, 174 | token_modifiers: Option>, 175 | ) -> anyhow::Result<()> { 176 | if !self.has_legend { 177 | return Err(anyhow!("Legend must be provided in constructor")); 178 | } 179 | 180 | if node.has_error() { 181 | return Ok(()); 182 | } 183 | 184 | let range = self.content.tree_sitter_range_to_lsp_range(node.range()); 185 | 186 | if range.start.line != range.end.line { 187 | return Err(anyhow!("`range` cannot span multiple lines")); 188 | } 189 | 190 | if let Some(&n_token_type) = self.token_type_map.get(token_type) { 191 | let line = range.start.line; 192 | let char = range.start.character; 193 | let length = range.end.character - range.start.character; 194 | 195 | let mut n_token_modifiers = 0; 196 | 197 | if let Some(token_modifiers) = token_modifiers { 198 | for token_modifier in token_modifiers { 199 | if let Some(n_token_modifier) = self.token_modifier_map.get(token_modifier) { 200 | n_token_modifiers |= 1 << n_token_modifier; 201 | } else { 202 | return Err(anyhow!("`token_modifier` is not in the provided legend")); 203 | } 204 | } 205 | } 206 | 207 | self.push_encoded(line, char, length, n_token_type, n_token_modifiers); 208 | } else { 209 | return Err(anyhow!("`token_type` is not in the provided legend")); 210 | } 211 | 212 | Ok(()) 213 | } 214 | 215 | /// Push a token in encoded form into the tokenization state. 216 | pub fn push_encoded(&mut self, row: u32, col: u32, len: u32, token_type: u32, token_mods: u32) { 217 | let mut push_row = row; 218 | let mut push_col = col; 219 | 220 | if !self.data.is_empty() { 221 | push_row -= self.prev_row; 222 | if push_row == 0 { 223 | push_col -= self.prev_col; 224 | } 225 | } 226 | 227 | let semantic_token = lsp::SemanticToken { 228 | delta_line: push_row, 229 | delta_start: push_col, 230 | length: len, 231 | token_type, 232 | token_modifiers_bitset: token_mods, 233 | }; 234 | 235 | self.data.push(semantic_token); 236 | 237 | self.prev_row = row; 238 | self.prev_col = col; 239 | } 240 | 241 | /// Reset tokenization state to defaults. 242 | pub fn reset(&mut self) -> anyhow::Result<()> { 243 | use std::time::{SystemTime, UNIX_EPOCH}; 244 | self.id = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(); 245 | self.prev_row = Default::default(); 246 | self.prev_col = Default::default(); 247 | self.prev_data = Default::default(); 248 | self.data = Default::default(); 249 | Ok(()) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /crates/server/src/provider/text_document/semantic_tokens/wat.rs: -------------------------------------------------------------------------------- 1 | //! Semantic tokens provider definitions for ".wat" files. 2 | 3 | use super::builder::SemanticTokensBuilder; 4 | use crate::core::{self, language::wat, node::BasicNodeWalker, Language}; 5 | use anyhow::anyhow; 6 | use lsp_text::RopeExt; 7 | use std::sync::Arc; 8 | 9 | pub(crate) async fn full( 10 | session: Arc, 11 | params: lsp::SemanticTokensParams, 12 | content: &ropey::Rope, 13 | ) -> anyhow::Result> { 14 | let params = { 15 | let uri = params.text_document.uri.clone(); 16 | let tree = session.get_tree(&uri).await?; 17 | lsp::SemanticTokensRangeParams { 18 | work_done_progress_params: params.work_done_progress_params, 19 | partial_result_params: params.partial_result_params, 20 | text_document: params.text_document, 21 | range: { 22 | let tree = tree.lock().await; 23 | let node = tree.root_node(); 24 | content.tree_sitter_range_to_lsp_range(node.range()) 25 | }, 26 | } 27 | }; 28 | 29 | let result = range(session, params, content).await?.map(|result| match result { 30 | lsp::SemanticTokensRangeResult::Tokens(tokens) => lsp::SemanticTokensResult::Tokens(tokens), 31 | lsp::SemanticTokensRangeResult::Partial(partial) => lsp::SemanticTokensResult::Partial(partial), 32 | }); 33 | 34 | Ok(result) 35 | } 36 | 37 | pub(crate) async fn range( 38 | session: Arc, 39 | params: lsp::SemanticTokensRangeParams, 40 | content: &ropey::Rope, 41 | ) -> anyhow::Result> { 42 | let legend = session.semantic_tokens_legend().await; 43 | let legend = legend.as_ref(); 44 | 45 | let tree = session.get_tree(¶ms.text_document.uri).await?; 46 | let tree = tree.lock().await; 47 | 48 | if let Some(node) = { 49 | let range = content.lsp_range_to_tree_sitter_range(params.range)?; 50 | let start = range.start_point(); 51 | let end = range.end_point(); 52 | tree.root_node().descendant_for_point_range(start, end) 53 | } { 54 | let mut handler = Handler::new(content, legend, node)?; 55 | 56 | loop { 57 | if handler.walker.done { 58 | break; 59 | } 60 | 61 | // handle "root" 62 | if wat::kind::ROOT == handler.walker.kind() { 63 | handler.root(); 64 | continue; 65 | } 66 | 67 | // handle {"comment_block", "comment_block_annot", "comment_line", "comment_line_annot"} 68 | if wat::kind::COMMENT_BLOCK == handler.walker.kind() { 69 | // NOTE: We ignore these for now since we can't highlight multiline tokens. 70 | // handler.comment_block()?; 71 | continue; 72 | } else if wat::kind::COMMENT_BLOCK_ANNOT == handler.walker.kind() { 73 | // NOTE: We ignore these for now since we can't highlight multiline tokens. 74 | // handler.comment_block_annot()?; 75 | continue; 76 | } else if wat::kind::COMMENT_LINE == handler.walker.kind() { 77 | handler.comment_line()?; 78 | continue; 79 | } else if wat::kind::COMMENT_LINE_ANNOT == handler.walker.kind() { 80 | handler.comment_line_annot()?; 81 | continue; 82 | } 83 | 84 | // handle "module" 85 | if wat::kind::MODULE == handler.walker.kind() { 86 | handler.module()?; 87 | continue; 88 | } 89 | 90 | // handle "_module_field" 91 | if wat::kind::MODULE_FIELD_DATA == handler.walker.kind() { 92 | handler.module_field_data()?; 93 | continue; 94 | } else if wat::kind::MODULE_FIELD_ELEM == handler.walker.kind() { 95 | handler.module_field_elem()?; 96 | continue; 97 | } else if wat::kind::MODULE_FIELD_EXPORT == handler.walker.kind() { 98 | handler.module_field_export()?; 99 | continue; 100 | } else if wat::kind::MODULE_FIELD_FUNC == handler.walker.kind() { 101 | handler.module_field_func()?; 102 | continue; 103 | } else if wat::kind::MODULE_FIELD_GLOBAL == handler.walker.kind() { 104 | handler.module_field_global()?; 105 | continue; 106 | } else if wat::kind::MODULE_FIELD_IMPORT == handler.walker.kind() { 107 | handler.module_field_import()?; 108 | continue; 109 | } else if wat::kind::MODULE_FIELD_MEMORY == handler.walker.kind() { 110 | handler.module_field_memory()?; 111 | continue; 112 | } else if wat::kind::MODULE_FIELD_START == handler.walker.kind() { 113 | handler.module_field_start()?; 114 | continue; 115 | } else if wat::kind::MODULE_FIELD_TABLE == handler.walker.kind() { 116 | handler.module_field_table()?; 117 | continue; 118 | } else if wat::kind::MODULE_FIELD_TYPE == handler.walker.kind() { 119 | handler.module_field_type()?; 120 | continue; 121 | } 122 | 123 | // FIXME: catch all case 124 | handler.walker.goto_next(); 125 | } 126 | 127 | let tokens = handler.builder.build(); 128 | let result = lsp::SemanticTokensRangeResult::Tokens(tokens); 129 | 130 | Ok(Some(result)) 131 | } else { 132 | Err(anyhow!("Could not obtain tree node for given range")) 133 | } 134 | } 135 | 136 | // Move to the next appropriate node in the syntax tree. 137 | struct Handler<'text, 'tree> { 138 | builder: SemanticTokensBuilder<'text, 'tree>, 139 | walker: BasicNodeWalker<'tree>, 140 | } 141 | 142 | impl<'text, 'tree> Handler<'text, 'tree> { 143 | fn new( 144 | content: &'text ropey::Rope, 145 | legend: Option<&'tree lsp::SemanticTokensLegend>, 146 | node: tree_sitter::Node<'tree>, 147 | ) -> anyhow::Result { 148 | let language = Language::Wat; 149 | let builder = SemanticTokensBuilder::new(content, legend)?; 150 | let walker = BasicNodeWalker::new(language, node); 151 | Ok(Self { builder, walker }) 152 | } 153 | 154 | // fn comment_block(&mut self) -> anyhow::Result<()> { 155 | // let node = self.walker.node(); 156 | // let token_type = &lsp::SemanticTokenType::COMMENT; 157 | // let token_modifiers = Default::default(); 158 | // self.builder.push(node, token_type, token_modifiers)?; 159 | 160 | // self.walker.goto_next(); 161 | 162 | // Ok(()) 163 | // } 164 | 165 | // fn comment_block_annot(&mut self) -> anyhow::Result<()> { 166 | // let node = self.walker.node(); 167 | // let token_type = &lsp::SemanticTokenType::COMMENT; 168 | // let token_modifiers = Default::default(); 169 | // self.builder.push(node, token_type, token_modifiers)?; 170 | 171 | // self.walker.goto_next(); 172 | 173 | // Ok(()) 174 | // } 175 | 176 | fn comment_line(&mut self) -> anyhow::Result<()> { 177 | let node = self.walker.node(); 178 | let token_type = &lsp::SemanticTokenType::COMMENT; 179 | let token_modifiers = Default::default(); 180 | self.builder.push(node, token_type, token_modifiers)?; 181 | 182 | self.walker.goto_next(); 183 | 184 | Ok(()) 185 | } 186 | 187 | fn comment_line_annot(&mut self) -> anyhow::Result<()> { 188 | let node = self.walker.node(); 189 | let token_type = &lsp::SemanticTokenType::COMMENT; 190 | let token_modifiers = Default::default(); 191 | self.builder.push(node, token_type, token_modifiers)?; 192 | 193 | self.walker.goto_next(); 194 | 195 | Ok(()) 196 | } 197 | 198 | fn export(&mut self) -> anyhow::Result<()> { 199 | // "(" 200 | self.walker.goto_first_child(); 201 | 202 | // "export" 203 | self.walker.goto_next_sibling(); 204 | { 205 | let node = self.walker.node(); 206 | let token_type = &lsp::SemanticTokenType::KEYWORD; 207 | let token_modifiers = Default::default(); 208 | self.builder.push(node, token_type, token_modifiers)?; 209 | } 210 | 211 | // $.name 212 | self.walker.goto_next_sibling(); 213 | { 214 | let node = self.walker.node(); 215 | let token_type = &lsp::SemanticTokenType::STRING; 216 | let token_modifiers = Default::default(); 217 | self.builder.push(node, token_type, token_modifiers)?; 218 | } 219 | 220 | // skip ")" 221 | self.walker.goto_next_sibling(); 222 | 223 | self.walker.goto_next(); 224 | 225 | Ok(()) 226 | } 227 | 228 | fn import(&mut self) -> anyhow::Result<()> { 229 | // "(" 230 | self.walker.goto_first_child(); 231 | 232 | // "import" 233 | self.walker.goto_next_sibling(); 234 | { 235 | let node = self.walker.node(); 236 | let token_type = &lsp::SemanticTokenType::KEYWORD; 237 | let token_modifiers = Default::default(); 238 | self.builder.push(node, token_type, token_modifiers)?; 239 | } 240 | 241 | // $.name 242 | self.walker.goto_next_sibling(); 243 | { 244 | let node = self.walker.node(); 245 | let token_type = &lsp::SemanticTokenType::STRING; 246 | let token_modifiers = Default::default(); 247 | self.builder.push(node, token_type, token_modifiers)?; 248 | } 249 | 250 | // $.name 251 | self.walker.goto_next_sibling(); 252 | { 253 | let node = self.walker.node(); 254 | let token_type = &lsp::SemanticTokenType::STRING; 255 | let token_modifiers = Default::default(); 256 | self.builder.push(node, token_type, token_modifiers)?; 257 | } 258 | 259 | // skip ")" 260 | self.walker.goto_next_sibling(); 261 | 262 | self.walker.goto_next(); 263 | 264 | Ok(()) 265 | } 266 | 267 | fn module(&mut self) -> anyhow::Result<()> { 268 | if let Some(node) = self.walker.node().child(1) { 269 | let token_type = &lsp::SemanticTokenType::KEYWORD; 270 | let token_modifiers = Default::default(); 271 | self.builder.push(node, token_type, token_modifiers)?; 272 | } 273 | 274 | self.walker.goto_next(); 275 | 276 | Ok(()) 277 | } 278 | 279 | fn module_field_data(&mut self) -> anyhow::Result<()> { 280 | if let Some(node) = self.walker.node().child(1) { 281 | let token_type = &lsp::SemanticTokenType::KEYWORD; 282 | let token_modifiers = Default::default(); 283 | self.builder.push(node, token_type, token_modifiers)?; 284 | } 285 | 286 | self.walker.goto_next(); 287 | 288 | Ok(()) 289 | } 290 | 291 | fn module_field_elem(&mut self) -> anyhow::Result<()> { 292 | if let Some(node) = self.walker.node().child(1) { 293 | let token_type = &lsp::SemanticTokenType::KEYWORD; 294 | let token_modifiers = Default::default(); 295 | self.builder.push(node, token_type, token_modifiers)?; 296 | } 297 | 298 | self.walker.goto_next(); 299 | 300 | Ok(()) 301 | } 302 | 303 | fn module_field_export(&mut self) -> anyhow::Result<()> { 304 | if let Some(node) = self.walker.node().child(1) { 305 | let token_type = &lsp::SemanticTokenType::KEYWORD; 306 | let token_modifiers = Default::default(); 307 | self.builder.push(node, token_type, token_modifiers)?; 308 | } 309 | 310 | self.walker.goto_next(); 311 | 312 | Ok(()) 313 | } 314 | 315 | fn module_field_func(&mut self) -> anyhow::Result<()> { 316 | // "(" 317 | self.walker.goto_first_child(); 318 | 319 | // "func" 320 | self.walker.goto_next_sibling(); 321 | { 322 | let node = self.walker.node(); 323 | let token_type = &lsp::SemanticTokenType::KEYWORD; 324 | let token_modifiers = Default::default(); 325 | self.builder.push(node, token_type, token_modifiers)?; 326 | self.walker.goto_next_sibling(); 327 | } 328 | 329 | // optional($.identifier) 330 | if wat::kind::IDENTIFIER == self.walker.kind() { 331 | let node = self.walker.node(); 332 | let token_type = &lsp::SemanticTokenType::FUNCTION; 333 | let token_modifiers = Default::default(); 334 | self.builder.push(node, token_type, token_modifiers)?; 335 | self.walker.goto_next_sibling(); 336 | } 337 | 338 | // repeat($.export) 339 | while wat::kind::EXPORT == self.walker.kind() { 340 | self.export()?; 341 | } 342 | 343 | // optional($.import) 344 | if wat::kind::IMPORT == self.walker.kind() { 345 | self.import()?; 346 | } 347 | 348 | // optional($.type_use) 349 | if wat::kind::TYPE_USE == self.walker.kind() { 350 | self.type_use()?; 351 | } 352 | 353 | self.walker.goto_next(); 354 | 355 | Ok(()) 356 | } 357 | 358 | fn module_field_global(&mut self) -> anyhow::Result<()> { 359 | // "(" 360 | self.walker.goto_first_child(); 361 | 362 | // "global" 363 | self.walker.goto_next_sibling(); 364 | { 365 | let node = self.walker.node(); 366 | let token_type = &lsp::SemanticTokenType::KEYWORD; 367 | let token_modifiers = Default::default(); 368 | self.builder.push(node, token_type, token_modifiers)?; 369 | self.walker.goto_next_sibling(); 370 | } 371 | 372 | // optional($.identifier) 373 | if wat::kind::IDENTIFIER == self.walker.kind() { 374 | let node = self.walker.node(); 375 | let token_type = &lsp::SemanticTokenType::FUNCTION; 376 | let token_modifiers = Default::default(); 377 | self.builder.push(node, token_type, token_modifiers)?; 378 | self.walker.goto_next_sibling(); 379 | } 380 | 381 | // repeat($.export) 382 | while wat::kind::EXPORT == self.walker.kind() { 383 | self.export()?; 384 | } 385 | 386 | // optional($.import) 387 | if wat::kind::IMPORT == self.walker.kind() { 388 | self.import()?; 389 | } 390 | 391 | self.walker.goto_next(); 392 | 393 | Ok(()) 394 | } 395 | 396 | fn module_field_import(&mut self) -> anyhow::Result<()> { 397 | if let Some(node) = self.walker.node().child(1) { 398 | let token_type = &lsp::SemanticTokenType::KEYWORD; 399 | let token_modifiers = Default::default(); 400 | self.builder.push(node, token_type, token_modifiers)?; 401 | } 402 | 403 | self.walker.goto_next(); 404 | 405 | Ok(()) 406 | } 407 | 408 | fn module_field_memory(&mut self) -> anyhow::Result<()> { 409 | if let Some(node) = self.walker.node().child(1) { 410 | let token_type = &lsp::SemanticTokenType::KEYWORD; 411 | let token_modifiers = Default::default(); 412 | self.builder.push(node, token_type, token_modifiers)?; 413 | } 414 | 415 | self.walker.goto_next(); 416 | 417 | Ok(()) 418 | } 419 | 420 | fn module_field_start(&mut self) -> anyhow::Result<()> { 421 | if let Some(node) = self.walker.node().child(1) { 422 | let token_type = &lsp::SemanticTokenType::KEYWORD; 423 | let token_modifiers = Default::default(); 424 | self.builder.push(node, token_type, token_modifiers)?; 425 | } 426 | 427 | self.walker.goto_next(); 428 | 429 | Ok(()) 430 | } 431 | 432 | fn module_field_table(&mut self) -> anyhow::Result<()> { 433 | if let Some(node) = self.walker.node().child(1) { 434 | let token_type = &lsp::SemanticTokenType::KEYWORD; 435 | let token_modifiers = Default::default(); 436 | self.builder.push(node, token_type, token_modifiers)?; 437 | } 438 | 439 | self.walker.goto_next(); 440 | 441 | Ok(()) 442 | } 443 | 444 | fn module_field_type(&mut self) -> anyhow::Result<()> { 445 | if let Some(node) = self.walker.node().child(1) { 446 | let token_type = &lsp::SemanticTokenType::KEYWORD; 447 | let token_modifiers = Default::default(); 448 | self.builder.push(node, token_type, token_modifiers)?; 449 | } 450 | 451 | self.walker.goto_next(); 452 | 453 | Ok(()) 454 | } 455 | 456 | fn root(&mut self) { 457 | self.walker.goto_next(); 458 | } 459 | 460 | fn type_use(&mut self) -> anyhow::Result<()> { 461 | // "(" 462 | self.walker.goto_first_child(); 463 | 464 | // "type" 465 | self.walker.goto_next_sibling(); 466 | { 467 | let node = self.walker.node(); 468 | let token_type = &lsp::SemanticTokenType::KEYWORD; 469 | let token_modifiers = Default::default(); 470 | self.builder.push(node, token_type, token_modifiers)?; 471 | } 472 | 473 | // $.index 474 | self.walker.goto_next_sibling(); 475 | { 476 | let node = self.walker.node(); 477 | let token_type = &lsp::SemanticTokenType::VARIABLE; 478 | let token_modifiers = Default::default(); 479 | self.builder.push(node, token_type, token_modifiers)?; 480 | } 481 | 482 | // skip ")" 483 | self.walker.goto_parent(); 484 | 485 | self.walker.goto_next(); 486 | 487 | Ok(()) 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /crates/server/tests/all/main.rs: -------------------------------------------------------------------------------- 1 | mod lsp; 2 | -------------------------------------------------------------------------------- /crates/syntax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "wasm-lsp-syntax" 4 | version = "0.0.0" 5 | authors = ["silvanshade "] 6 | license = "Apache-2.0 WITH LLVM-exception" 7 | readme = "README.md" 8 | repository = "https://github.com/wasm-lsp/wasm-lsp-server" 9 | keywords = [] 10 | description = """ 11 | Syntax related definitions for the WebAssembly language server. 12 | """ 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [dependencies] 18 | anyhow = "1.0" 19 | thiserror = "1.0" 20 | wasm-lsp-languages = { version = "0.0", path = "../languages" } 21 | wasm-lsp-macros = { version = "0.0", path = "../macros" } 22 | 23 | [dependencies.tree-sitter] 24 | package = "tree-sitter-facade" 25 | version = "0.4" 26 | 27 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tree-sitter-sys] 28 | package = "tree-sitter" 29 | version = "0.20" 30 | 31 | [target.'cfg(target_arch = "wasm32")'.dependencies] 32 | wasm-bindgen = { version = "=0.2.81", features = ["strict-macro"] } 33 | wasm-bindgen-futures = "0.4" 34 | 35 | [target.'cfg(target_arch = "wasm32")'.dependencies.futures] 36 | version = "0.3" 37 | 38 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-tree-sitter-sys] 39 | version = "0.6" 40 | -------------------------------------------------------------------------------- /crates/syntax/src/language.rs: -------------------------------------------------------------------------------- 1 | //! Functionality related to [`tree_sitter::Language`]. 2 | 3 | /// Functions for working with the `.wast` grammar. 4 | pub mod wast; 5 | 6 | /// Functions for working with the `.wat` grammar. 7 | pub mod wat; 8 | 9 | /// Comment nodes for both the `.wast` and `.wat` grammar. 10 | pub const COMMENT_NODES: &[u16] = &[ 11 | wast::kind::COMMENT_BLOCK_ANNOT, 12 | wast::kind::COMMENT_BLOCK, 13 | wast::kind::COMMENT_LINE_ANNOT, 14 | wast::kind::COMMENT_LINE, 15 | wat::kind::COMMENT_BLOCK_ANNOT, 16 | wat::kind::COMMENT_BLOCK, 17 | wat::kind::COMMENT_LINE_ANNOT, 18 | wat::kind::COMMENT_LINE, 19 | ]; 20 | -------------------------------------------------------------------------------- /crates/syntax/src/language/wast.rs: -------------------------------------------------------------------------------- 1 | //! Functions for working with the `.wast` grammar. 2 | 3 | pub mod field { 4 | #![allow(missing_docs)] 5 | 6 | wasm_lsp_macros::field_ids! { 7 | language: "wasm.wast", 8 | fields: [ 9 | (IDENTIFIER, "identifier"), 10 | ], 11 | } 12 | } 13 | 14 | pub mod kind { 15 | #![allow(missing_docs)] 16 | 17 | wasm_lsp_macros::node_kind_ids! { 18 | language: "wasm.wast", 19 | node_kinds: [ 20 | (ACTION_GET, "action_get", true), 21 | (ACTION_INVOKE, "action_invoke", true), 22 | (ACTION, "action", true), 23 | (ALIGN_OFFSET_VALUE, "align_offset_value", true), 24 | (ALIGN_VALUE, "align_value", true), 25 | (ANNOTATION_PARENS, "annotation_parens", true), 26 | (ANNOTATION_PART, "annotation_part", true), 27 | (ANNOTATION, "annotation", true), 28 | (ASSERT_EXHAUSTION, "assert_exhaustion", true), 29 | (ASSERT_INVALID, "assert_invalid", true), 30 | (ASSERT_MALFORMED, "assert_malformed", true), 31 | (ASSERT_RETURN_ARITHMETIC_NAN, "assert_return_arithmetic_nan", true), 32 | (ASSERT_RETURN_CANONICAL_NAN, "assert_return_canonical_nan", true), 33 | (ASSERT_RETURN, "assert_return", true), 34 | (ASSERT_TRAP_ACTION, "assert_trap_action", true), 35 | (ASSERT_TRAP_MODULE, "assert_trap_module", true), 36 | (ASSERT_UNLINKABLE, "assert_unlinkable", true), 37 | (ASSERTION, "assertion", true), 38 | (BLOCK_BLOCK, "block_block", true), 39 | (BLOCK_IF, "block_if", true), 40 | (BLOCK_LOOP, "block_loop", true), 41 | (COMMAND, "command", true), 42 | (COMMENT_BLOCK_ANNOT, "comment_block_annot", true), 43 | (COMMENT_BLOCK, "comment_block", true), 44 | (COMMENT_LINE_ANNOT, "comment_line_annot", true), 45 | (COMMENT_LINE, "comment_line", true), 46 | (DEC_FLOAT, "dec_float", true), 47 | (DEC_NAT, "dec_nat", true), 48 | (ELEM_EXPR_EXPR, "elem_expr_expr", true), 49 | (ELEM_EXPR_ITEM, "elem_expr_item", true), 50 | (ELEM_EXPR, "elem_expr", true), 51 | (ELEM_KIND, "elem_kind", true), 52 | (ELEM_LIST, "elem_list", true), 53 | (ESCAPE_SEQUENCE, "escape_sequence", true), 54 | (EXPORT_DESC_FUNC, "export_desc_func", true), 55 | (EXPORT_DESC_GLOBAL, "export_desc_global", true), 56 | (EXPORT_DESC_MEMORY, "export_desc_memory", true), 57 | (EXPORT_DESC_TABLE, "export_desc_table", true), 58 | (EXPORT_DESC, "export_desc", true), 59 | (EXPORT, "export", true), 60 | (EXPR_PLAIN_CONST, "expr_plain_const", true), 61 | (EXPR, "expr", true), 62 | (EXPR1_BLOCK, "expr1_block", true), 63 | (EXPR1_CALL, "expr1_call", true), 64 | (EXPR1_IF, "expr1_if", true), 65 | (EXPR1_LOOP, "expr1_loop", true), 66 | (EXPR1_PLAIN, "expr1_plain", true), 67 | (EXPR1, "expr1", true), 68 | (FLOAT, "float", true), 69 | (FUNC_LOCALS_MANY, "func_locals_many", true), 70 | (FUNC_LOCALS_ONE, "func_locals_one", true), 71 | (FUNC_LOCALS, "func_locals", true), 72 | (FUNC_TYPE_PARAMS_MANY, "func_type_params_many", true), 73 | (FUNC_TYPE_PARAMS_ONE, "func_type_params_one", true), 74 | (FUNC_TYPE_PARAMS, "func_type_params", true), 75 | (FUNC_TYPE_RESULTS, "func_type_results", true), 76 | (FUNC_TYPE, "func_type", true), 77 | (GLOBAL_TYPE_IMM, "global_type_imm", true), 78 | (GLOBAL_TYPE_MUT, "global_type_mut", true), 79 | (GLOBAL_TYPE, "global_type", true), 80 | (HEX_FLOAT, "hex_float", true), 81 | (HEX_NAT, "hex_nat", true), 82 | (IDENTIFIER, "identifier", true), 83 | (IF_BLOCK, "if_block", true), 84 | (IMPORT_DESC_FUNC_TYPE, "import_desc_func_type", true), 85 | (IMPORT_DESC_GLOBAL_TYPE, "import_desc_global_type", true), 86 | (IMPORT_DESC_MEMORY_TYPE, "import_desc_memory_type", true), 87 | (IMPORT_DESC_TABLE_TYPE, "import_desc_table_type", true), 88 | (IMPORT_DESC_TYPE_USE, "import_desc_type_use", true), 89 | (IMPORT_DESC, "import_desc", true), 90 | (IMPORT, "import", true), 91 | (INDEX, "index", true), 92 | (INSTR_BLOCK, "instr_block", true), 93 | (INSTR_CALL, "instr_call", true), 94 | (INSTR_LIST, "instr_list", true), 95 | (INSTR_PLAIN, "instr_plain", true), 96 | (INSTR, "instr", true), 97 | (INT, "int", true), 98 | (LIMITS, "limits", true), 99 | (LITERAL_NAN_ARITHMETIC, "literal_nan_arithmetic", true), 100 | (LITERAL_NAN_CANONICAL, "literal_nan_canonical", true), 101 | (LITERAL_NAN, "literal_nan", true), 102 | (MEMORY_FIELDS_DATA, "memory_fields_data", true), 103 | (MEMORY_FIELDS_TYPE, "memory_fields_type", true), 104 | (MEMORY_TYPE, "memory_type", true), 105 | (MEMORY_USE, "memory_use", true), 106 | (META_INPUT, "meta_input", true), 107 | (META_OUTPUT, "meta_output", true), 108 | (META_SCRIPT, "meta_script", true), 109 | (META, "meta", true), 110 | (MODULE_FIELD_DATA, "module_field_data", true), 111 | (MODULE_FIELD_ELEM, "module_field_elem", true), 112 | (MODULE_FIELD_EXPORT, "module_field_export", true), 113 | (MODULE_FIELD_FUNC, "module_field_func", true), 114 | (MODULE_FIELD_GLOBAL, "module_field_global", true), 115 | (MODULE_FIELD_IMPORT, "module_field_import", true), 116 | (MODULE_FIELD_MEMORY, "module_field_memory", true), 117 | (MODULE_FIELD_START, "module_field_start", true), 118 | (MODULE_FIELD_TABLE, "module_field_table", true), 119 | (MODULE_FIELD_TYPE, "module_field_type", true), 120 | (MODULE_FIELD, "module_field", true), 121 | (MODULE, "module", true), 122 | (NAME, "name", true), 123 | (NAN, "nan", true), 124 | (NAT, "nat", true), 125 | (NUM_TYPE_F32, "num_type_f32", true), 126 | (NUM_TYPE_F64, "num_type_f64", true), 127 | (NUM_TYPE_I32, "num_type_i32", true), 128 | (NUM_TYPE_I64, "num_type_i64", true), 129 | (NUM_TYPE_V128, "num_type_v128", true), 130 | (NUM, "num", true), 131 | (OFFSET_CONST_EXPR, "offset_const_expr", true), 132 | (OFFSET_EXPR, "offset_expr", true), 133 | (OFFSET_VALUE, "offset_value", true), 134 | (OFFSET, "offset", true), 135 | (OP_CONST_REF, "op_const_ref", true), 136 | (OP_CONST, "op_const", true), 137 | (OP_FUNC_BIND, "op_func_bind", true), 138 | (OP_INDEX_OPT_OFFSET_OPT_ALIGN_OPT, "op_index_opt_offset_opt_align_opt", true), 139 | (OP_INDEX_OPT, "op_index_opt", true), 140 | (OP_INDEX, "op_index", true), 141 | (OP_LET, "op_let", true), 142 | (OP_NULLARY, "op_nullary", true), 143 | (OP_SELECT, "op_select", true), 144 | (OP_SIMD_CONST, "op_simd_const", true), 145 | (OP_SIMD_LANE, "op_simd_lane", true), 146 | (OP_SIMD_OFFSET_OPT_ALIGN_OPT, "opt_simd_offset_opt_align_opt", true), 147 | (OP_TABLE_COPY, "op_table_copy", true), 148 | (OP_TABLE_INIT, "op_table_init", true), 149 | (REF_KIND, "ref_kind", true), 150 | (REF_TYPE_EXTERNREF, "ref_type_externref", true), 151 | (REF_TYPE_FUNCREF, "ref_type_funcref", true), 152 | (REF_TYPE_REF, "ref_type_ref", true), 153 | (REF_TYPE, "ref_type", true), 154 | (REGISTER, "register", true), 155 | (RESERVED, "reserved", true), 156 | (RESULT_CONST_NAN, "result_const_nan", true), 157 | (RESULT_CONST, "result_const", true), 158 | (RESULT_REF_EXTERN, "result_ref_extern", true), 159 | (RESULT_REF_FUNC, "result_ref_func", true), 160 | (RESULT_REF_NULL, "result_ref_null", true), 161 | (RESULT, "result", true), 162 | (ROOT, "ROOT", true), 163 | (SCRIPT_MODULE_BINARY, "script_module_binary", true), 164 | (SCRIPT_MODULE_QUOTE, "script_module_quote", true), 165 | (SCRIPT_MODULE, "script_module", true), 166 | (SHARE, "share", true), 167 | (STRING, "string", true), 168 | (TABLE_FIELDS_ELEM, "table_fields_elem", true), 169 | (TABLE_FIELDS_TYPE, "table_fields_type", true), 170 | (TABLE_TYPE, "table_type", true), 171 | (TABLE_USE, "table_use", true), 172 | (TYPE_FIELD, "type_field", true), 173 | (TYPE_USE, "type_use", true), 174 | (VALUE_TYPE_NUM_TYPE, "value_type_num_type", true), 175 | (VALUE_TYPE_REF_TYPE, "value_type_ref_type", true), 176 | (VALUE_TYPE, "value_type", true), 177 | ] 178 | } 179 | 180 | pub mod token { 181 | #![allow(missing_docs)] 182 | 183 | wasm_lsp_macros::node_kind_ids! { 184 | language: "wasm.wast", 185 | node_kinds: [ 186 | (ALIGN, "align", false), 187 | (ASSERT_EXHAUSTION, "assert_exhaustion", false), 188 | (ASSERT_INVALID, "assert_invalid", false), 189 | (ASSERT_MALFORMED, "assert_malformed", false), 190 | (ASSERT_RETURN_ARITHMETIC_NAN, "assert_return_arithmetic_nan", false), 191 | (ASSERT_RETURN_CANONICAL_NAN, "assert_return_canonical_nan", false), 192 | (ASSERT_RETURN, "assert_return", false), 193 | (ASSERT_TRAP, "assert_trap", false), 194 | (ASSERT_UNLINKABLE, "assert_unlinkable", false), 195 | (BINARY, "binary", false), 196 | (BLOCK, "block", false), 197 | (BR_TABLE, "br_table", false), 198 | (CALL_INDIRECT, "call_indirect", false), 199 | (DATA, "data", false), 200 | (DECLARE, "declare", false), 201 | (DOLLAR_SIGN, "$", false), 202 | (ELEM, "elem", false), 203 | (ELSE, "else", false), 204 | (END, "end", false), 205 | (EQUALS, "=", false), 206 | (EXPORT, "export", false), 207 | (EXTERNREF, "externref", false), 208 | (F32, "f32", false), 209 | (F64, "f64", false), 210 | (FULL_STOP, ".", false), 211 | (FUNC, "func", false), 212 | (FUNCREF, "funcref", false), 213 | (GET, "get", false), 214 | (GLOBAL, "global", false), 215 | (I32, "i32", false), 216 | (I64, "i64", false), 217 | (IF, "if", false), 218 | (IMPORT, "import", false), 219 | (INF, "inf", false), 220 | (INPUT, "input", false), 221 | (INVOKE, "invoke", false), 222 | (ITEM, "item", false), 223 | (LOCAL, "local", false), 224 | (LOOP, "loop", false), 225 | (LPAREN_AMPERSAND, "(@", false), 226 | (LPAREN_SEMICOLON, "(;", false), 227 | (LPAREN, "(", false), 228 | (MEMORY, "memory", false), 229 | (MUT, "mut", false), 230 | (OFFSET, "offset", false), 231 | (OUTPUT, "output", false), 232 | (PARAM, "param", false), 233 | (QUOTE, "quote", false), 234 | (REF, "ref", false), 235 | (REGISTER, "register", false), 236 | (RESULT, "result", false), 237 | (REVERSE_SOLIDUS_REVERSE_SOLIDUS, "\\", false), 238 | (RPAREN, ")", false), 239 | (SCRIPT, "script", false), 240 | (SEMICOLON_SEMICOLON, ";;", false), 241 | (TABLE, "table", false), 242 | (THEN, "then", false), 243 | (TYPE, "type", false), 244 | (V128, "v128", false), 245 | ], 246 | } 247 | } 248 | } 249 | 250 | pub mod grouped { 251 | #![allow(missing_docs)] 252 | 253 | pub const MODULE_FIELDS: &[u16] = &[ 254 | super::kind::MODULE_FIELD_DATA, 255 | super::kind::MODULE_FIELD_ELEM, 256 | super::kind::MODULE_FIELD_FUNC, 257 | super::kind::MODULE_FIELD_GLOBAL, 258 | super::kind::MODULE_FIELD_MEMORY, 259 | super::kind::MODULE_FIELD_TABLE, 260 | super::kind::MODULE_FIELD_TYPE, 261 | ]; 262 | } 263 | -------------------------------------------------------------------------------- /crates/syntax/src/language/wat.rs: -------------------------------------------------------------------------------- 1 | //! Functions for working with the `.wat` grammar. 2 | 3 | pub mod field { 4 | #![allow(missing_docs)] 5 | 6 | wasm_lsp_macros::field_ids! { 7 | language: "wasm.wat", 8 | fields: [ 9 | (IDENTIFIER, "identifier"), 10 | ], 11 | } 12 | } 13 | 14 | pub mod kind { 15 | #![allow(missing_docs)] 16 | 17 | wasm_lsp_macros::node_kind_ids! { 18 | language: "wasm.wat", 19 | node_kinds: [ 20 | (ALIGN_OFFSET_VALUE, "align_offset_value", true), 21 | (ALIGN_VALUE, "align_value", true), 22 | (ANNOTATION_PARENS, "annotation_parens", true), 23 | (ANNOTATION_PART, "annotation_part", true), 24 | (ANNOTATION, "annotation", true), 25 | (BLOCK_BLOCK, "block_block", true), 26 | (BLOCK_IF, "block_if", true), 27 | (BLOCK_LOOP, "block_loop", true), 28 | (COMMENT_BLOCK_ANNOT, "comment_block_annot", true), 29 | (COMMENT_BLOCK, "comment_block", true), 30 | (COMMENT_LINE_ANNOT, "comment_line_annot", true), 31 | (COMMENT_LINE, "comment_line", true), 32 | (DEC_FLOAT, "dec_float", true), 33 | (DEC_NAT, "dec_nat", true), 34 | (ELEM_EXPR_EXPR, "elem_expr_expr", true), 35 | (ELEM_EXPR_ITEM, "elem_expr_item", true), 36 | (ELEM_EXPR, "elem_expr", true), 37 | (ELEM_KIND, "elem_kind", true), 38 | (ELEM_LIST, "elem_list", true), 39 | (ESCAPE_SEQUENCE, "escape_sequence", true), 40 | (EXPORT_DESC_FUNC, "export_desc_func", true), 41 | (EXPORT_DESC_GLOBAL, "export_desc_global", true), 42 | (EXPORT_DESC_MEMORY, "export_desc_memory", true), 43 | (EXPORT_DESC_TABLE, "export_desc_table", true), 44 | (EXPORT_DESC, "export_desc", true), 45 | (EXPORT, "export", true), 46 | (EXPR, "expr", true), 47 | (EXPR1_BLOCK, "expr1_block", true), 48 | (EXPR1_CALL, "expr1_call", true), 49 | (EXPR1_IF, "expr1_if", true), 50 | (EXPR1_LOOP, "expr1_loop", true), 51 | (EXPR1_PLAIN, "expr1_plain", true), 52 | (EXPR1, "expr1", true), 53 | (FLOAT, "float", true), 54 | (FUNC_LOCALS_MANY, "func_locals_many", true), 55 | (FUNC_LOCALS_ONE, "func_locals_one", true), 56 | (FUNC_LOCALS, "func_locals", true), 57 | (FUNC_TYPE_PARAMS_MANY, "func_type_params_many", true), 58 | (FUNC_TYPE_PARAMS_ONE, "func_type_params_one", true), 59 | (FUNC_TYPE_PARAMS, "func_type_params", true), 60 | (FUNC_TYPE_RESULTS, "func_type_results", true), 61 | (FUNC_TYPE, "func_type", true), 62 | (GLOBAL_TYPE_IMM, "global_type_imm", true), 63 | (GLOBAL_TYPE_MUT, "global_type_mut", true), 64 | (GLOBAL_TYPE, "global_type", true), 65 | (HEX_FLOAT, "hex_float", true), 66 | (HEX_NAT, "hex_nat", true), 67 | (IDENTIFIER, "identifier", true), 68 | (IF_BLOCK, "if_block", true), 69 | (IMPORT_DESC_FUNC_TYPE, "import_desc_func_type", true), 70 | (IMPORT_DESC_GLOBAL_TYPE, "import_desc_global_type", true), 71 | (IMPORT_DESC_MEMORY_TYPE, "import_desc_memory_type", true), 72 | (IMPORT_DESC_TABLE_TYPE, "import_desc_table_type", true), 73 | (IMPORT_DESC_TYPE_USE, "import_desc_type_use", true), 74 | (IMPORT_DESC, "import_desc", true), 75 | (IMPORT, "import", true), 76 | (INDEX, "index", true), 77 | (INSTR_BLOCK, "instr_block", true), 78 | (INSTR_CALL, "instr_call", true), 79 | (INSTR_LIST, "instr_list", true), 80 | (INSTR_PLAIN, "instr_plain", true), 81 | (INSTR, "instr", true), 82 | (INT, "int", true), 83 | (LIMITS, "limits", true), 84 | (MEMORY_FIELDS_DATA, "memory_fields_data", true), 85 | (MEMORY_FIELDS_TYPE, "memory_fields_type", true), 86 | (MEMORY_TYPE, "memory_type", true), 87 | (MEMORY_USE, "memory_use", true), 88 | (MODULE_FIELD_DATA, "module_field_data", true), 89 | (MODULE_FIELD_ELEM, "module_field_elem", true), 90 | (MODULE_FIELD_EXPORT, "module_field_export", true), 91 | (MODULE_FIELD_FUNC, "module_field_func", true), 92 | (MODULE_FIELD_GLOBAL, "module_field_global", true), 93 | (MODULE_FIELD_IMPORT, "module_field_import", true), 94 | (MODULE_FIELD_MEMORY, "module_field_memory", true), 95 | (MODULE_FIELD_START, "module_field_start", true), 96 | (MODULE_FIELD_TABLE, "module_field_table", true), 97 | (MODULE_FIELD_TYPE, "module_field_type", true), 98 | (MODULE_FIELD, "module_field", true), 99 | (MODULE, "module", true), 100 | (NAME, "name", true), 101 | (NAN, "nan", true), 102 | (NAT, "nat", true), 103 | (NUM_TYPE_F32, "num_type_f32", true), 104 | (NUM_TYPE_F64, "num_type_f64", true), 105 | (NUM_TYPE_I32, "num_type_i32", true), 106 | (NUM_TYPE_I64, "num_type_i64", true), 107 | (NUM_TYPE_V128, "num_type_v128", true), 108 | (NUM, "num", true), 109 | (OFFSET_CONST_EXPR, "offset_const_expr", true), 110 | (OFFSET_EXPR, "offset_expr", true), 111 | (OFFSET_VALUE, "offset_value", true), 112 | (OFFSET, "offset", true), 113 | (OP_CONST, "op_const", true), 114 | (OP_FUNC_BIND, "op_func_bind", true), 115 | (OP_INDEX_OPT_OFFSET_OPT_ALIGN_OPT, "op_index_opt_offset_opt_align_opt", true), 116 | (OP_INDEX_OPT, "op_index_opt", true), 117 | (OP_INDEX, "op_index", true), 118 | (OP_LET, "op_let", true), 119 | (OP_NULLARY, "op_nullary", true), 120 | (OP_SELECT, "op_select", true), 121 | (OP_SIMD_CONST, "op_simd_const", true), 122 | (OP_SIMD_LANE, "op_simd_lane", true), 123 | (OP_SIMD_OFFSET_OPT_ALIGN_OPT, "opt_simd_offset_opt_align_opt", true), 124 | (OP_TABLE_COPY, "op_table_copy", true), 125 | (OP_TABLE_INIT, "op_table_init", true), 126 | (REF_KIND, "ref_kind", true), 127 | (REF_TYPE_EXTERNREF, "ref_type_externref", true), 128 | (REF_TYPE_FUNCREF, "ref_type_funcref", true), 129 | (REF_TYPE_REF, "ref_type_ref", true), 130 | (REF_TYPE, "ref_type", true), 131 | (RESERVED, "reserved", true), 132 | (ROOT, "ROOT", true), 133 | (SHARE, "share", true), 134 | (STRING, "string", true), 135 | (TABLE_FIELDS_ELEM, "table_fields_elem", true), 136 | (TABLE_FIELDS_TYPE, "table_fields_type", true), 137 | (TABLE_TYPE, "table_type", true), 138 | (TABLE_USE, "table_use", true), 139 | (TYPE_FIELD, "type_field", true), 140 | (TYPE_USE, "type_use", true), 141 | (VALUE_TYPE_NUM_TYPE, "value_type_num_type", true), 142 | (VALUE_TYPE_REF_TYPE, "value_type_ref_type", true), 143 | (VALUE_TYPE, "value_type", true), 144 | ], 145 | } 146 | 147 | pub mod token { 148 | #![allow(missing_docs)] 149 | 150 | wasm_lsp_macros::node_kind_ids! { 151 | language: "wasm.wat", 152 | node_kinds: [ 153 | (ALIGN, "align", false), 154 | (ASSERT_EXHAUSTION, "assert_exhaustion", false), 155 | (ASSERT_INVALID, "assert_invalid", false), 156 | (ASSERT_MALFORMED, "assert_malformed", false), 157 | (ASSERT_RETURN_ARITHMETIC_NAN, "assert_return_arithmetic_nan", false), 158 | (ASSERT_RETURN_CANONICAL_NAN, "assert_return_canonical_nan", false), 159 | (ASSERT_RETURN, "assert_return", false), 160 | (ASSERT_TRAP, "assert_trap", false), 161 | (ASSERT_UNLINKABLE, "assert_unlinkable", false), 162 | (BINARY, "binary", false), 163 | (BLOCK, "block", false), 164 | (BR_TABLE, "br_table", false), 165 | (CALL_INDIRECT, "call_indirect", false), 166 | (DATA, "data", false), 167 | (DECLARE, "declare", false), 168 | (DOLLAR_SIGN, "$", false), 169 | (ELEM, "elem", false), 170 | (ELSE, "else", false), 171 | (END, "end", false), 172 | (EQUALS, "=", false), 173 | (EXPORT, "export", false), 174 | (EXTERNREF, "externref", false), 175 | (F32, "f32", false), 176 | (F64, "f64", false), 177 | (FULL_STOP, ".", false), 178 | (FUNC, "func", false), 179 | (FUNCREF, "funcref", false), 180 | (GET, "get", false), 181 | (GLOBAL, "global", false), 182 | (I32, "i32", false), 183 | (I64, "i64", false), 184 | (IF, "if", false), 185 | (IMPORT, "import", false), 186 | (INF, "inf", false), 187 | (INPUT, "input", false), 188 | (INVOKE, "invoke", false), 189 | (ITEM, "item", false), 190 | (LOCAL, "local", false), 191 | (LOOP, "loop", false), 192 | (LPAREN_AMPERSAND, "(@", false), 193 | (LPAREN_SEMICOLON, "(;", false), 194 | (LPAREN, "(", false), 195 | (MEMORY, "memory", false), 196 | (MUT, "mut", false), 197 | (OFFSET, "offset", false), 198 | (OUTPUT, "output", false), 199 | (PARAM, "param", false), 200 | (QUOTE, "quote", false), 201 | (REF, "ref", false), 202 | (REGISTER, "register", false), 203 | (RESULT, "result", false), 204 | (REVERSE_SOLIDUS_REVERSE_SOLIDUS, "\\", false), 205 | (RPAREN, ")", false), 206 | (SCRIPT, "script", false), 207 | (SEMICOLON_SEMICOLON, ";;", false), 208 | (TABLE, "table", false), 209 | (THEN, "then", false), 210 | (TYPE, "type", false), 211 | (V128, "v128", false), 212 | ], 213 | } 214 | } 215 | } 216 | 217 | pub mod grouped { 218 | #![allow(missing_docs)] 219 | 220 | pub const MODULE_FIELDS: &[u16] = &[ 221 | super::kind::MODULE_FIELD_DATA, 222 | super::kind::MODULE_FIELD_ELEM, 223 | super::kind::MODULE_FIELD_FUNC, 224 | super::kind::MODULE_FIELD_GLOBAL, 225 | super::kind::MODULE_FIELD_MEMORY, 226 | super::kind::MODULE_FIELD_TABLE, 227 | super::kind::MODULE_FIELD_TYPE, 228 | ]; 229 | } 230 | -------------------------------------------------------------------------------- /crates/syntax/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Parsers for the WebAssembly language server. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_docs)] 5 | #![deny(unsafe_code)] 6 | 7 | /// Functionality related to [`tree_sitter::Language`]. 8 | pub mod language; 9 | 10 | /// Functionality related to [`tree_sitter::Node`]. 11 | pub mod node; 12 | 13 | /// Functionality related to [`tree_sitter::Range`]. 14 | pub mod range; 15 | -------------------------------------------------------------------------------- /crates/syntax/src/node.rs: -------------------------------------------------------------------------------- 1 | mod walker; 2 | 3 | pub use walker::*; 4 | 5 | /// Utility trait for working with [`tree_sitter::Node`]. 6 | pub trait NodeExt { 7 | /// Predicate to determine if a supertype node matches a given subtype node kind. 8 | fn matches_subtypes(&self, supertype_id: u16, subtype_ids: &[u16]) -> bool; 9 | } 10 | 11 | impl<'tree> NodeExt for tree_sitter::Node<'tree> { 12 | fn matches_subtypes(&self, supertype_id: u16, subtype_ids: &[u16]) -> bool { 13 | if let Some(child) = self.named_child(0) { 14 | supertype_id == self.kind_id() && subtype_ids.contains(&child.kind_id()) 15 | } else { 16 | false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/syntax/src/node/walker.rs: -------------------------------------------------------------------------------- 1 | use wasm_lsp_languages::language::Language; 2 | 3 | #[allow(missing_docs)] 4 | pub mod context { 5 | use tree_sitter::Node; 6 | 7 | pub trait Context<'tree> { 8 | type Level; 9 | 10 | fn new() -> Self; 11 | 12 | fn pop(&mut self) -> Option; 13 | 14 | fn push(&mut self, level: Self::Level); 15 | 16 | fn push_ancestor(&mut self, ancestor: Node<'tree>, prefixed: Vec>); 17 | 18 | fn push_prefix(&mut self, prefix: Node<'tree>); 19 | 20 | fn reverse(&mut self); 21 | } 22 | 23 | pub mod basic { 24 | use std::convert::Infallible; 25 | use tree_sitter::Node; 26 | 27 | #[derive(Debug, Clone, Eq, Hash, PartialEq)] 28 | pub struct Level<'tree> { 29 | phantom: core::marker::PhantomData<&'tree Infallible>, 30 | } 31 | 32 | #[derive(Debug, Default, Clone, Eq, Hash, PartialEq)] 33 | pub struct Context<'tree> { 34 | phantom: core::marker::PhantomData<&'tree Infallible>, 35 | } 36 | 37 | impl<'tree> super::Context<'tree> for Context<'tree> { 38 | type Level = Level<'tree>; 39 | 40 | fn new() -> Self { 41 | Self::default() 42 | } 43 | 44 | fn pop(&mut self) -> Option { 45 | None 46 | } 47 | 48 | fn push(&mut self, _: Self::Level) { 49 | } 50 | 51 | fn push_ancestor(&mut self, _: Node<'tree>, _: Vec>) { 52 | } 53 | 54 | fn push_prefix(&mut self, _: Node<'tree>) { 55 | } 56 | 57 | fn reverse(&mut self) { 58 | } 59 | } 60 | } 61 | 62 | pub mod trace { 63 | use tree_sitter::Node; 64 | 65 | #[derive(Debug, Clone, Eq, Hash, PartialEq)] 66 | pub struct Level<'tree> { 67 | ancestor: Node<'tree>, 68 | prefixed: Vec>, 69 | } 70 | 71 | /// The current node context. 72 | #[derive(Debug, Default, Clone, Eq, Hash, PartialEq)] 73 | pub struct Context<'tree> { 74 | stack: Vec>, 75 | } 76 | 77 | impl<'tree> super::Context<'tree> for Context<'tree> { 78 | type Level = Level<'tree>; 79 | 80 | fn new() -> Self { 81 | Self::default() 82 | } 83 | 84 | fn pop(&mut self) -> Option { 85 | self.stack.pop() 86 | } 87 | 88 | fn push(&mut self, level: Self::Level) { 89 | self.stack.push(level); 90 | } 91 | 92 | fn push_ancestor(&mut self, ancestor: Node<'tree>, prefixed: Vec>) { 93 | let level = Level { ancestor, prefixed }; 94 | self.stack.push(level); 95 | } 96 | 97 | fn push_prefix(&mut self, prefix: Node<'tree>) { 98 | if let Some(level) = self.stack.last_mut() { 99 | level.prefixed.push(prefix); 100 | } else { 101 | unreachable!("NodeWalkerContext::push_prefix should never be callable wihout an active level"); 102 | } 103 | } 104 | 105 | fn reverse(&mut self) { 106 | self.stack.reverse(); 107 | } 108 | } 109 | } 110 | } 111 | 112 | pub use context::Context; 113 | 114 | #[allow(missing_docs)] 115 | pub struct NodeWalker<'tree, C> { 116 | language: Language, 117 | pub context: C, 118 | cursor: tree_sitter::TreeCursor<'tree>, 119 | pub done: bool, 120 | } 121 | 122 | impl<'tree, C: Context<'tree>> NodeWalker<'tree, C> { 123 | /// Create a new [NodeWalker]. 124 | #[inline] 125 | pub fn new(language: Language, node: tree_sitter::Node<'tree>) -> Self { 126 | let context = C::new(); 127 | let cursor = node.walk(); 128 | let done = false; 129 | let mut walker = Self { 130 | language, 131 | context, 132 | cursor, 133 | done, 134 | }; 135 | walker.reconstruct_stack(); 136 | walker 137 | } 138 | 139 | /// Move the cursor to the first child node. 140 | #[inline] 141 | pub fn goto_first_child(&mut self) -> bool { 142 | let ancestor = self.cursor.node(); 143 | let moved = self.cursor.goto_first_child(); 144 | if moved { 145 | let prefixed = Default::default(); 146 | self.context.push_ancestor(ancestor, prefixed); 147 | } 148 | moved 149 | } 150 | 151 | /// Move the cursor to the next sibling node. 152 | #[inline] 153 | pub fn goto_next_sibling(&mut self) -> bool { 154 | let prefix = self.cursor.node(); 155 | let moved = self.cursor.goto_next_sibling(); 156 | if moved { 157 | self.context.push_prefix(prefix); 158 | } 159 | moved 160 | } 161 | 162 | /// Move cursor to the next accessible node. 163 | #[inline] 164 | #[allow(clippy::needless_late_init)] 165 | pub fn goto_next(&mut self) -> bool { 166 | let mut moved; 167 | 168 | // First try to descend to the first child node. 169 | moved = self.goto_first_child(); 170 | if !moved { 171 | // Otherwise try to move to the next sibling node. 172 | moved = self.goto_next_sibling(); 173 | if !moved { 174 | moved = self.goto_next_ancestor_sibling(); 175 | } 176 | } 177 | 178 | moved 179 | } 180 | 181 | /// Move cursor to the next accessible node that has an error. 182 | #[inline] 183 | pub fn goto_next_has_error(&mut self) -> bool { 184 | let node = self.cursor.node(); 185 | let mut moved; 186 | 187 | // Only descend if the current node has an error in the subtree. 188 | if node.has_error() && !crate::language::COMMENT_NODES.contains(&node.kind_id()) { 189 | moved = self.goto_next(); 190 | } else { 191 | // Otherwise try to move to the next sibling node. 192 | moved = self.goto_next_sibling(); 193 | if !moved { 194 | moved = self.goto_next_ancestor_sibling(); 195 | } 196 | } 197 | 198 | moved 199 | } 200 | 201 | /// Move the cursor to the next ancestor sibling node. 202 | #[inline] 203 | pub fn goto_next_ancestor_sibling(&mut self) -> bool { 204 | let mut moved; 205 | let mut finished = true; 206 | 207 | // Otherwise continue to ascend to parent nodes... 208 | loop { 209 | moved = self.goto_parent(); 210 | if moved { 211 | // ... until we can move to a sibling node. 212 | if self.goto_next_sibling() { 213 | finished = false; 214 | break; 215 | } 216 | } else { 217 | break; 218 | } 219 | } 220 | 221 | self.done = finished; 222 | moved 223 | } 224 | 225 | /// Move the cursor to the parent node. 226 | #[inline] 227 | pub fn goto_parent(&mut self) -> bool { 228 | let moved = self.cursor.goto_parent(); 229 | if moved { 230 | self.context.pop(); 231 | } 232 | moved 233 | } 234 | 235 | /// Return the current node's kind id. 236 | #[inline] 237 | pub fn kind(&self) -> u16 { 238 | self.cursor.node().kind_id() 239 | } 240 | 241 | /// Return the current node for the cursor. 242 | #[inline] 243 | pub fn node(&self) -> tree_sitter::Node<'tree> { 244 | self.cursor.node() 245 | } 246 | 247 | /// Reconstruct the context stack from the current node position. 248 | #[inline] 249 | fn reconstruct_stack(&mut self) { 250 | use crate::language::{wast, wat}; 251 | use Language::{Wast, Wat}; 252 | 253 | let language = self.language; 254 | let node = self.node(); 255 | let kind = node.kind_id(); 256 | 257 | // Reconstruct the stack by traversing upward if the current node isn't ROOT. 258 | if (language == Wast && wast::kind::ROOT != kind) || (language == Wat && wat::kind::ROOT != kind) { 259 | let cursor = &mut node.walk(); 260 | loop { 261 | let previous = self.node(); 262 | if self.goto_parent() { 263 | let ancestor = self.node(); 264 | let prefixed = ancestor 265 | .children(cursor) 266 | .take_while(|node| node.id() != previous.id()) 267 | .collect(); 268 | self.context.push_ancestor(ancestor, prefixed) 269 | } else { 270 | break; 271 | } 272 | } 273 | 274 | self.context.reverse(); 275 | self.cursor.reset(node); 276 | } 277 | } 278 | } 279 | 280 | #[allow(missing_docs)] 281 | pub type BasicNodeWalker<'tree> = NodeWalker<'tree, context::basic::Context<'tree>>; 282 | 283 | #[allow(missing_docs)] 284 | pub type TraceNodeWalker<'tree> = NodeWalker<'tree, context::trace::Context<'tree>>; 285 | -------------------------------------------------------------------------------- /crates/syntax/src/range.rs: -------------------------------------------------------------------------------- 1 | /// Utility trait for working with [`tree_sitter::Range`]. 2 | pub trait RangeExt { 3 | /// Predicate to determine if one range contains another range 4 | fn contains(&self, other: &tree_sitter::Range) -> bool; 5 | } 6 | 7 | impl RangeExt for tree_sitter::Range { 8 | fn contains(&self, other: &tree_sitter::Range) -> bool { 9 | self.start_byte() <= other.start_byte() && other.end_byte() <= self.end_byte() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/testing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "wasm-lsp-testing" 4 | version = "0.0.0" 5 | authors = ["silvanshade "] 6 | license = "Apache-2.0 WITH LLVM-exception" 7 | readme = "README.md" 8 | repository = "https://github.com/wasm-lsp/wasm-lsp-server" 9 | keywords = [] 10 | description = """ 11 | Testing support framework for the WebAssembly language server. 12 | """ 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [dependencies] 18 | anyhow = "1.0" 19 | serde_json = { version = "1.0", features = ["preserve_order"] } 20 | tower-lsp = { version = "0.17", default-features = false } 21 | tower-test = "0.4.0" 22 | wasm-lsp-languages = { version = "0.0", path = "../languages" } 23 | wasm-lsp-server = { version = "0.0", path = "../server" } 24 | -------------------------------------------------------------------------------- /crates/testing/src/jsonrpc.rs: -------------------------------------------------------------------------------- 1 | pub mod error { 2 | use serde_json::{json, Value}; 3 | 4 | pub fn invalid_request() -> Value { 5 | json!({ 6 | "jsonrpc": "2.0", 7 | "error": { 8 | "code": tower_lsp::jsonrpc::ErrorCode::InvalidRequest.code(), 9 | "message": "Invalid request", 10 | }, 11 | "id": 1, 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/testing/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(unsafe_code)] 3 | 4 | pub mod jsonrpc; 5 | pub mod lsp; 6 | pub mod service; 7 | -------------------------------------------------------------------------------- /crates/testing/src/lsp.rs: -------------------------------------------------------------------------------- 1 | pub mod exit { 2 | use serde_json::{json, Value}; 3 | 4 | pub fn notification() -> Value { 5 | json!({ 6 | "jsonrpc": "2.0", 7 | "method": "exit", 8 | }) 9 | } 10 | } 11 | 12 | pub mod initialize { 13 | use serde_json::{json, Value}; 14 | 15 | pub fn request() -> Value { 16 | json!({ 17 | "jsonrpc": "2.0", 18 | "method": "initialize", 19 | "params": { 20 | "capabilities":{}, 21 | }, 22 | "id": 1, 23 | }) 24 | } 25 | 26 | pub fn response() -> Value { 27 | json!({ 28 | "jsonrpc": "2.0", 29 | "result": { 30 | "capabilities": wasm_lsp_server::Server::capabilities(), 31 | }, 32 | "id": 1, 33 | }) 34 | } 35 | } 36 | 37 | pub mod initialized { 38 | use serde_json::{json, Value}; 39 | 40 | pub fn notification() -> Value { 41 | json!({ 42 | "jsonrpc": "2.0", 43 | "method": "initialized", 44 | "params": {}, 45 | }) 46 | } 47 | } 48 | 49 | pub mod shutdown { 50 | use serde_json::{json, Value}; 51 | 52 | pub fn request() -> Value { 53 | json!({ 54 | "jsonrpc": "2.0", 55 | "method": "shutdown", 56 | "id": 1, 57 | }) 58 | } 59 | 60 | pub fn response() -> Value { 61 | json!({ 62 | "jsonrpc": "2.0", 63 | "result": null, 64 | "id": 1, 65 | }) 66 | } 67 | } 68 | 69 | pub mod text_document { 70 | pub mod did_change { 71 | 72 | pub mod notification { 73 | use serde_json::{json, Value}; 74 | use tower_lsp::lsp_types::*; 75 | 76 | pub fn entire>(uri: &Url, text: S) -> Value { 77 | json!({ 78 | "jsonrpc": "2.0", 79 | "method": "textDocument/didChange", 80 | "params": { 81 | "textDocument": { 82 | "uri": uri, 83 | }, 84 | "contentChanges": [ 85 | { 86 | "text": text.as_ref(), 87 | } 88 | ], 89 | }, 90 | }) 91 | } 92 | } 93 | } 94 | 95 | pub mod did_close { 96 | use serde_json::{json, Value}; 97 | use tower_lsp::lsp_types::*; 98 | 99 | pub fn notification(uri: &Url) -> Value { 100 | json!({ 101 | "jsonrpc": "2.0", 102 | "method": "textDocument/didClose", 103 | "params": { 104 | "textDocument": { 105 | "uri": uri, 106 | }, 107 | }, 108 | }) 109 | } 110 | } 111 | 112 | pub mod did_open { 113 | use serde_json::{json, Value}; 114 | use tower_lsp::lsp_types::*; 115 | 116 | pub fn notification, T: AsRef>(uri: &Url, language_id: S, version: i64, text: T) -> Value { 117 | json!({ 118 | "jsonrpc": "2.0", 119 | "method": "textDocument/didOpen", 120 | "params": { 121 | "textDocument": { 122 | "uri": uri, 123 | "languageId": language_id.as_ref(), 124 | "version": version, 125 | "text": text.as_ref(), 126 | }, 127 | }, 128 | }) 129 | } 130 | } 131 | 132 | pub mod document_symbol { 133 | use serde_json::{json, Value}; 134 | use tower_lsp::lsp_types::*; 135 | 136 | pub fn request(uri: &Url) -> Value { 137 | json!({ 138 | "jsonrpc": "2.0", 139 | "method": "textDocument/documentSymbol", 140 | "params": { 141 | "textDocument": { 142 | "uri": uri, 143 | }, 144 | }, 145 | "id": 1, 146 | }) 147 | } 148 | 149 | pub fn response(response: DocumentSymbolResponse) -> Value { 150 | json!({ 151 | "jsonrpc": "2.0", 152 | "result": response, 153 | "id": 1, 154 | }) 155 | } 156 | } 157 | 158 | pub mod hover { 159 | use serde_json::{json, Value}; 160 | use tower_lsp::lsp_types::*; 161 | 162 | pub fn request(uri: &Url, position: Position) -> Value { 163 | json!({ 164 | "jsonrpc": "2.0", 165 | "method": "textDocument/hover", 166 | "params": { 167 | "textDocument": { 168 | "uri": uri, 169 | }, 170 | "position": position, 171 | }, 172 | "id": 1, 173 | }) 174 | } 175 | 176 | pub fn response() -> Value { 177 | json!({ 178 | "jsonrpc": "2.0", 179 | "result": { 180 | }, 181 | "id": 1, 182 | }) 183 | } 184 | } 185 | 186 | pub mod publish_diagnostics { 187 | use serde_json::{json, Value}; 188 | use tower_lsp::lsp_types::*; 189 | 190 | pub fn notification(uri: &Url, diagnostics: &[Diagnostic]) -> Value { 191 | json!({ 192 | "jsonrpc": "2.0", 193 | "method": "textDocument/publishDiagnostics", 194 | "params": { 195 | "uri": uri, 196 | "diagnostics": diagnostics, 197 | }, 198 | }) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /crates/testing/src/service.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use tower_lsp::{ClientSocket, LspService}; 3 | use tower_test::mock::Spawn; 4 | use wasm_lsp_server::Server; 5 | 6 | pub async fn send( 7 | service: &mut tower_test::mock::Spawn>, 8 | request: &serde_json::Value, 9 | ) -> Result, tower_lsp::ExitedError> { 10 | let request = serde_json::from_value(request.clone()).unwrap(); 11 | let response = service.call(request).await?; 12 | let response = response.and_then(|x| serde_json::to_value(x).ok()); 13 | Ok(response) 14 | } 15 | 16 | pub async fn spawn() -> anyhow::Result<(tower_test::mock::Spawn>, ClientSocket)> { 17 | #[rustfmt::skip] 18 | #[cfg(target_arch = "wasm32")] 19 | let languages = wasm_lsp_server::core::SessionLanguages { 20 | wast: wasm_lsp_languages::language::wast().await?, 21 | wat : wasm_lsp_languages::language::wat ().await?, 22 | }; 23 | #[rustfmt::skip] 24 | #[cfg(not(target_arch = "wasm32"))] 25 | let languages = wasm_lsp_server::core::SessionLanguages { 26 | wast: wasm_lsp_languages::language::wast(), 27 | wat : wasm_lsp_languages::language::wat (), 28 | }; 29 | let (service, socket) = LspService::new(move |client| { 30 | let server = wasm_lsp_server::Server::new(languages, client); 31 | server.unwrap() 32 | }); 33 | Ok((Spawn::new(service), socket)) 34 | } 35 | 36 | #[macro_export] 37 | macro_rules! assert_status { 38 | ($service:expr, $status:expr) => { 39 | assert_eq!($service.poll_ready(), std::task::Poll::Ready($status)); 40 | }; 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! assert_exchange { 45 | ($service:expr, $request:expr, $response:expr) => { 46 | assert_eq!(testing::service::send($service, $request).await, $response); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | publish = false 3 | edition = "2021" 4 | name = "wasm-lsp-fuzz" 5 | version = "0.0.0" 6 | authors = ["silvanshade "] 7 | license = "Apache-2.0 WITH LLVM-exception" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [[bin]] 13 | name = "lsp_textDocument-didOpen" 14 | path = "fuzz_targets/lsp/text_document/did_open.rs" 15 | bench = false 16 | doc = false 17 | test = false 18 | 19 | [features] 20 | default = [] 21 | 22 | [dependencies] 23 | blocking = "1.0" 24 | futures = "0.3" 25 | libfuzzer-sys = "0.4" 26 | lsp = { version = "0.93", package = "lsp-types" } 27 | serde_json = "1.0" 28 | testing = { package = "wasm-lsp-testing", version = "0.0", path = "../crates/testing" } 29 | tower-lsp = { version = "0.17", default-features = false, features = ["runtime-agnostic"] } 30 | wasm-lsp-server = { version = "0.0", path = "../crates/server", default-features = false } 31 | wasm-smith = "0.11" 32 | wasmprinter = "0.2" 33 | -------------------------------------------------------------------------------- /fuzz/README.md: -------------------------------------------------------------------------------- 1 | # wasm-lsp-fuzz 2 | 3 | The `wasm-lsp-fuzz` subcrate is used for fuzzing the WebAssembly language server. 4 | 5 | ## Usage 6 | 7 | First, you need to have `cargo-fuzz` installed. 8 | 9 | ### Listing the fuzz targets 10 | 11 | List the different fuzz targets with the following command: 12 | 13 | ```bash 14 | cargo fuzz list 15 | ``` 16 | 17 | ### Running the fuzz target 18 | 19 | Run the fuzz target with the following command: 20 | 21 | ```bash 22 | cargo +nightly fuzz run FUZZ_TARGET_NAME 1> /dev/null 23 | ``` 24 | 25 | Currently this only works on linux targets. 26 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/lsp/text_document/did_open.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![no_main] 3 | 4 | use futures::stream::StreamExt; 5 | use libfuzzer_sys::fuzz_target; 6 | use serde_json::Value; 7 | use wasm_smith::Module; 8 | 9 | fuzz_target!(|module: Module| { 10 | let future = async { 11 | let (mut service, mut messages) = testing::service::spawn().await.unwrap(); 12 | let service = &mut service; 13 | 14 | let wasm = module.to_bytes(); 15 | let uri = lsp::Url::parse("inmemory:///test").unwrap(); 16 | let language_id = "wasm.wast"; 17 | let text = wasmprinter::print_bytes(wasm).unwrap(); 18 | 19 | println!("{}", text); 20 | 21 | testing::assert_status!(service, Ok(())); 22 | let request = &testing::lsp::initialize::request(); 23 | let response = Some(testing::lsp::initialize::response()); 24 | testing::assert_exchange!(service, request, Ok(response)); 25 | 26 | testing::assert_status!(service, Ok(())); 27 | let notification = &testing::lsp::text_document::did_open::notification(&uri, language_id, 1, text); 28 | let status = None::; 29 | testing::assert_exchange!(service, notification, Ok(status)); 30 | 31 | let message = messages.next().await.unwrap(); 32 | let actual = serde_json::to_value(&message).unwrap(); 33 | let expected = testing::lsp::text_document::publish_diagnostics::notification(&uri, &[]); 34 | assert_eq!(actual, expected); 35 | }; 36 | 37 | futures::executor::block_on(future); 38 | }); 39 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | stable 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # binop_separator = "Front" 2 | # blank_lines_lower_bound = 0 3 | # blank_lines_upper_bound = 1 4 | # brace_style = "SameLineWhere" 5 | # color = "Auto" 6 | # combine_control_expr = true 7 | comment_width = 100 8 | condense_wildcard_suffixes = true 9 | # control_brace_style = "AlwaysSameLine" 10 | # disable_all_formatting = false 11 | edition = "2021" 12 | empty_item_single_line = false 13 | # enum_discrim_align_threshold = 0 14 | error_on_line_overflow = true 15 | error_on_unformatted = true 16 | # fn_args_layout = "Tall" 17 | # fn_single_line = false 18 | force_explicit_abi = false 19 | # force_multiline_blocks = false 20 | format_code_in_doc_comments = true 21 | format_macro_matchers = true 22 | # format_macro_bodies = true 23 | # format_strings = false 24 | # hard_tabs = false 25 | # hide_parse_errors = false 26 | # ignore = [] 27 | imports_granularity = "Crate" 28 | # imports_indent = "Block" 29 | imports_layout = "HorizontalVertical" 30 | # indent_style = "Block" 31 | # inline_attribute_width = 0 32 | # license_template_path = "" 33 | # match_arm_blocks = true 34 | match_block_trailing_comma = true 35 | max_width = 120 36 | # merge_derives = true 37 | newline_style = "Unix" 38 | normalize_comments = true 39 | normalize_doc_attributes = true 40 | overflow_delimited_expr = true 41 | # remove_nested_parens = true 42 | reorder_impl_items = true 43 | # reorder_imports = true 44 | # reorder_modules = true 45 | # report_fixme = "Never" 46 | # report_todo = "Never" 47 | # required_version = "CARGO_PKG_VERSION" 48 | # skip_children = false 49 | # space_after_colon = true 50 | # space_before_colon = false 51 | spaces_around_ranges = true 52 | # struct_field_align_threshold = 0 53 | # struct_lit_single_line = true 54 | # tab_spaces = 4 55 | # trailing_comma = "Vertical" 56 | # trailing_semicolon = true 57 | # type_punctuation_density = "Wide" 58 | unstable_features = true 59 | use_field_init_shorthand = true 60 | # use_small_heuristics = "Max" 61 | use_try_shorthand = true 62 | version = "Two" 63 | # where_single_line = false 64 | wrap_comments = true 65 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | publish = false 3 | edition = "2021" 4 | name = "xtask" 5 | version = "0.0.0" 6 | authors = ["silvanshade "] 7 | 8 | [dependencies] 9 | dunce = "1.0" 10 | pico-args = "0.5" 11 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![deny(unsafe_code)] 3 | 4 | type Fallible = Result>; 5 | 6 | fn main() -> Fallible<()> { 7 | let help = r#" 8 | xtask 9 | 10 | USAGE: 11 | xtask [SUBCOMMAND] 12 | 13 | FLAGS: 14 | -h, --help Prints help information 15 | 16 | SUBCOMMANDS: 17 | build 18 | build-wasm 19 | check 20 | clippy 21 | doc 22 | format 23 | help Prints this message or the help of the subcommand(s) 24 | init 25 | install 26 | tarpaulin 27 | test 28 | test-cli 29 | udeps 30 | "# 31 | .trim(); 32 | 33 | let mut args: Vec<_> = std::env::args_os().collect(); 34 | // remove "xtask" argument 35 | args.remove(0); 36 | 37 | let cargo_args = if let Some(dash_dash) = args.iter().position(|arg| arg == "--") { 38 | let c = args.drain(dash_dash + 1 ..).collect(); 39 | args.pop(); 40 | c 41 | } else { 42 | Vec::new() 43 | }; 44 | 45 | let mut args = pico_args::Arguments::from_vec(args); 46 | 47 | let result = match args.subcommand()?.as_deref() { 48 | None => { 49 | if args.contains(["-h", "--help"]) { 50 | println!("{}\n", help); 51 | } 52 | Ok(()) 53 | }, 54 | Some("build") => subcommand::cargo::build(&mut args, cargo_args), 55 | Some("build-wasm") => subcommand::cargo::build_wasm(&mut args, cargo_args), 56 | Some("check") => subcommand::cargo::check(&mut args, cargo_args), 57 | Some("clippy") => subcommand::cargo::clippy(&mut args, cargo_args), 58 | Some("doc") => subcommand::cargo::doc(&mut args, cargo_args), 59 | Some("format") => subcommand::cargo::format(&mut args, cargo_args), 60 | Some("init") => subcommand::init(&mut args), 61 | Some("install") => subcommand::cargo::install(&mut args, cargo_args), 62 | Some("tarpaulin") => subcommand::cargo::tarpaulin(&mut args, cargo_args), 63 | Some("test") => subcommand::cargo::test(&mut args, cargo_args), 64 | Some("test-cli") => subcommand::cargo::test_cli(&mut args, cargo_args), 65 | Some("udeps") => subcommand::cargo::udeps(&mut args, cargo_args), 66 | Some("help") => { 67 | println!("{}\n", help); 68 | Ok(()) 69 | }, 70 | Some(subcommand) => Err(format!("unknown subcommand: {}", subcommand).into()), 71 | }; 72 | crate::util::handle_result(result); 73 | 74 | let result = crate::util::handle_unused(&args); 75 | crate::util::handle_result(result); 76 | 77 | Ok(()) 78 | } 79 | 80 | mod metadata { 81 | use std::path::{Path, PathBuf}; 82 | 83 | pub fn cargo() -> crate::Fallible { 84 | // NOTE: we use the cargo wrapper rather than the binary reported through the "CARGO" environment 85 | // variable because we need to be able to invoke cargo with different toolchains (e.g., +nightly) 86 | Ok(String::from("cargo")) 87 | } 88 | 89 | pub fn project_root() -> PathBuf { 90 | Path::new(&env!("CARGO_MANIFEST_DIR")) 91 | .ancestors() 92 | .nth(1) 93 | .unwrap() 94 | .to_path_buf() 95 | } 96 | } 97 | 98 | mod subcommand { 99 | pub mod cargo { 100 | use crate::metadata; 101 | use std::process::Command; 102 | 103 | // Run `cargo build` with custom options. 104 | pub fn build(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 105 | let help = r#" 106 | xtask-build 107 | 108 | USAGE: 109 | xtask build 110 | 111 | FLAGS: 112 | -h, --help Prints help information 113 | --rebuild-parsers Rebuild tree-sitter parsers 114 | -- '...' Extra arguments to pass to the cargo command 115 | "# 116 | .trim(); 117 | 118 | if args.contains(["-h", "--help"]) { 119 | println!("{}\n", help); 120 | return Ok(()); 121 | } 122 | 123 | if args.contains("--rebuild-parsers") { 124 | crate::util::tree_sitter::rebuild_parsers()?; 125 | } 126 | 127 | crate::util::handle_unused(args)?; 128 | 129 | let cargo = metadata::cargo()?; 130 | let mut cmd = Command::new(cargo); 131 | cmd.current_dir(metadata::project_root()); 132 | cmd.args(&["build"]); 133 | cmd.args(&["--package", "wasm-lsp-cli"]); 134 | cmd.args(cargo_args); 135 | cmd.status()?; 136 | 137 | Ok(()) 138 | } 139 | 140 | // Run `cargo build-wasm` with custom options. 141 | pub fn build_wasm(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 142 | let help = r#" 143 | xtask-build 144 | 145 | USAGE: 146 | xtask build-wasm 147 | 148 | FLAGS: 149 | -h, --help Prints help information 150 | --rebuild-parsers Rebuild tree-sitter parsers 151 | -- '...' Extra arguments to pass to the cargo command 152 | "# 153 | .trim(); 154 | 155 | if args.contains(["-h", "--help"]) { 156 | println!("{}\n", help); 157 | return Ok(()); 158 | } 159 | 160 | if args.contains("--rebuild-parsers") { 161 | crate::util::tree_sitter::rebuild_parsers()?; 162 | } 163 | 164 | crate::util::handle_unused(args)?; 165 | 166 | let cargo = metadata::cargo()?; 167 | let mut cmd = Command::new(cargo); 168 | cmd.current_dir(metadata::project_root()); 169 | cmd.env("RUSTFLAGS", "--cfg=web_sys_unstable_apis"); 170 | cmd.args(&["build"]); 171 | cmd.args(&["--package", "wasm-lsp-browser"]); 172 | cmd.args(&["--target", "wasm32-unknown-unknown"]); 173 | cmd.args(cargo_args); 174 | cmd.status()?; 175 | 176 | Ok(()) 177 | } 178 | 179 | // Run `cargo check` with custom options. 180 | pub fn check(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 181 | let help = r#" 182 | xtask-check 183 | 184 | USAGE: 185 | xtask check 186 | 187 | FLAGS: 188 | -h, --help Prints help information 189 | -- '...' Extra arguments to pass to the cargo command 190 | "# 191 | .trim(); 192 | 193 | if args.contains(["-h", "--help"]) { 194 | println!("{}\n", help); 195 | return Ok(()); 196 | } 197 | 198 | crate::util::handle_unused(args)?; 199 | 200 | let cargo = metadata::cargo()?; 201 | let mut cmd = Command::new(cargo); 202 | cmd.current_dir(metadata::project_root()); 203 | cmd.env("RUSTFLAGS", "-Dwarnings --cfg=web_sys_unstable_apis"); 204 | cmd.args(&["check"]); 205 | cmd.args(&["--all-targets"]); 206 | cmd.args(&["--package", "xtask"]); 207 | cmd.args(&["--package", "wasm-lsp-browser"]); 208 | cmd.args(&["--package", "wasm-lsp-cli"]); 209 | cmd.args(&["--package", "wasm-lsp-languages"]); 210 | cmd.args(&["--package", "wasm-lsp-macros"]); 211 | cmd.args(&["--package", "wasm-lsp-server"]); 212 | cmd.args(&["--package", "wasm-lsp-syntax"]); 213 | cmd.args(&["--package", "wasm-lsp-testing"]); 214 | if cfg!(target_os = "linux") { 215 | cmd.args(&["--package", "wasm-lsp-fuzz"]); 216 | } 217 | cmd.args(cargo_args); 218 | cmd.status()?; 219 | 220 | Ok(()) 221 | } 222 | 223 | // Run `cargo clippy` with custom options. 224 | pub fn clippy(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 225 | let help = r#" 226 | xtask-clippy 227 | 228 | USAGE: 229 | xtask clippy 230 | 231 | FLAGS: 232 | -h, --help Prints help information 233 | -- '...' Extra arguments to pass to the cargo command 234 | "# 235 | .trim(); 236 | 237 | if args.contains(["-h", "--help"]) { 238 | println!("{}\n", help); 239 | return Ok(()); 240 | } 241 | 242 | crate::util::handle_unused(args)?; 243 | 244 | let cargo = metadata::cargo()?; 245 | let mut cmd = Command::new(cargo); 246 | cmd.current_dir(metadata::project_root()); 247 | cmd.env("RUSTFLAGS", "-Dwarnings --cfg=web_sys_unstable_apis"); 248 | cmd.args(&["+nightly", "clippy"]); 249 | cmd.args(&["--all-targets"]); 250 | cmd.args(&["--package", "xtask"]); 251 | cmd.args(&["--package", "wasm-lsp-browser"]); 252 | cmd.args(&["--package", "wasm-lsp-cli"]); 253 | cmd.args(&["--package", "wasm-lsp-languages"]); 254 | cmd.args(&["--package", "wasm-lsp-macros"]); 255 | cmd.args(&["--package", "wasm-lsp-server"]); 256 | cmd.args(&["--package", "wasm-lsp-syntax"]); 257 | cmd.args(&["--package", "wasm-lsp-testing"]); 258 | if cfg!(target_os = "linux") { 259 | cmd.args(&["--package", "wasm-lsp-fuzz"]); 260 | } 261 | cmd.args(cargo_args); 262 | cmd.args(&["--", "-D", "warnings"]); 263 | cmd.status()?; 264 | 265 | Ok(()) 266 | } 267 | 268 | // Run `cargo doc` with custom options. 269 | pub fn doc(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 270 | let help = r#" 271 | xtask-doc 272 | 273 | USAGE: 274 | xtask doc 275 | 276 | FLAGS: 277 | -h, --help Prints help information 278 | -- '...' Extra arguments to pass to the cargo command 279 | "# 280 | .trim(); 281 | 282 | if args.contains(["-h", "--help"]) { 283 | println!("{}\n", help); 284 | return Ok(()); 285 | } 286 | 287 | crate::util::handle_unused(args)?; 288 | 289 | let cargo = metadata::cargo()?; 290 | let mut cmd = Command::new(cargo); 291 | cmd.current_dir(metadata::project_root()); 292 | cmd.args(&["+nightly", "doc"]); 293 | cmd.args(cargo_args); 294 | cmd.status()?; 295 | 296 | Ok(()) 297 | } 298 | 299 | // Run `cargo format` with custom options. 300 | pub fn format(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 301 | let help = r#" 302 | xtask-format 303 | 304 | USAGE: 305 | xtask format 306 | 307 | FLAGS: 308 | -h, --help Prints help information 309 | -- '...' Extra arguments to pass to the cargo command 310 | "# 311 | .trim(); 312 | 313 | if args.contains(["-h", "--help"]) { 314 | println!("{}\n", help); 315 | return Ok(()); 316 | } 317 | 318 | crate::util::handle_unused(args)?; 319 | 320 | let cargo = metadata::cargo()?; 321 | let mut cmd = Command::new(cargo); 322 | cmd.current_dir(metadata::project_root()); 323 | cmd.args(&["+nightly", "fmt", "--all"]); 324 | cmd.args(cargo_args); 325 | cmd.status()?; 326 | 327 | Ok(()) 328 | } 329 | 330 | // Run `cargo install` with custom options. 331 | pub fn install(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 332 | let help = r#" 333 | xtask-install 334 | 335 | USAGE: 336 | xtask install 337 | 338 | FLAGS: 339 | -h, --help Prints help information 340 | --rebuild-parsers Rebuild tree-sitter parsers 341 | -- '...' Extra arguments to pass to the cargo command 342 | "# 343 | .trim(); 344 | 345 | if args.contains(["-h", "--help"]) { 346 | println!("{}\n", help); 347 | return Ok(()); 348 | } 349 | 350 | if args.contains("--rebuild-parsers") { 351 | crate::util::tree_sitter::rebuild_parsers()?; 352 | } 353 | 354 | crate::util::handle_unused(args)?; 355 | 356 | let cargo = metadata::cargo()?; 357 | let mut cmd = Command::new(cargo); 358 | let mut path = metadata::project_root(); 359 | path.push("crates"); 360 | path.push("cli"); 361 | cmd.current_dir(path); 362 | cmd.args(&["install"]); 363 | cmd.args(&["--path", "."]); 364 | cmd.args(cargo_args); 365 | cmd.status()?; 366 | 367 | Ok(()) 368 | } 369 | 370 | // Run `cargo tarpaulin` with custom options. 371 | pub fn tarpaulin(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 372 | let help = r#" 373 | xtask-tarpaulin 374 | 375 | USAGE: 376 | xtask tarpaulin 377 | 378 | FLAGS: 379 | -h, --help Prints help information 380 | --rebuild-parsers Rebuild tree-sitter parsers 381 | -- '...' Extra arguments to pass to the cargo command 382 | "# 383 | .trim(); 384 | 385 | if args.contains(["-h", "--help"]) { 386 | println!("{}\n", help); 387 | return Ok(()); 388 | } 389 | 390 | if args.contains("--rebuild-parsers") { 391 | crate::util::tree_sitter::rebuild_parsers()?; 392 | } 393 | 394 | crate::util::handle_unused(args)?; 395 | 396 | let cargo = metadata::cargo()?; 397 | let mut cmd = Command::new(cargo); 398 | cmd.current_dir(metadata::project_root()); 399 | cmd.args(&["+nightly", "tarpaulin"]); 400 | cmd.args(&["--out", "Xml"]); 401 | cmd.args(&[ 402 | "--packages", 403 | "xtask", 404 | "wasm-lsp-browser", 405 | "wasm-lsp-cli", 406 | "wasm-lsp-languages", 407 | "wasm-lsp-macros", 408 | "wasm-lsp-server", 409 | "wasm-lsp-syntax", 410 | "wasm-lsp-testing", 411 | ]); 412 | cmd.args(&[ 413 | "--exclude-files", 414 | "xtask", 415 | "crates/macros", 416 | "crates/server/src/bin", 417 | "crates/server/src/cli.rs", 418 | "crates/testing", 419 | "tests", 420 | "vendor", 421 | "**/stdio2.h", 422 | "**/string_fortified.h", 423 | ]); 424 | cmd.args(cargo_args); 425 | cmd.status()?; 426 | 427 | Ok(()) 428 | } 429 | 430 | // Run `cargo test` with custom options. 431 | pub fn test(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 432 | let help = r#" 433 | xtask-test 434 | 435 | USAGE: 436 | xtask test 437 | 438 | FLAGS: 439 | -h, --help Prints help information 440 | --rebuild-parsers Rebuild tree-sitter parsers 441 | -- '...' Extra arguments to pass to the cargo command 442 | "# 443 | .trim(); 444 | 445 | if args.contains(["-h", "--help"]) { 446 | println!("{}\n", help); 447 | return Ok(()); 448 | } 449 | 450 | if args.contains("--rebuild-parsers") { 451 | crate::util::tree_sitter::rebuild_parsers()?; 452 | } 453 | 454 | crate::util::handle_unused(args)?; 455 | 456 | let cargo = metadata::cargo()?; 457 | let mut cmd = Command::new(cargo); 458 | cmd.current_dir(metadata::project_root()); 459 | cmd.env("RUSTFLAGS", "-Dwarnings --cfg=web_sys_unstable_apis"); 460 | cmd.args(&["test"]); 461 | cmd.args(&["--examples", "--lib", "--tests"]); 462 | cmd.args(&["--package", "wasm-lsp-languages"]); 463 | cmd.args(&["--package", "wasm-lsp-server"]); 464 | cmd.args(&["--package", "wasm-lsp-syntax"]); 465 | cmd.args(&["--package", "wasm-lsp-testing"]); 466 | if cfg!(target_os = "linux") { 467 | cmd.args(&["--package", "wasm-lsp-fuzz"]); 468 | } 469 | cmd.args(cargo_args); 470 | cmd.status()?; 471 | 472 | Ok(()) 473 | } 474 | 475 | // Run `cargo test-cli` with custom options. 476 | pub fn test_cli(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 477 | let help = r#" 478 | xtask-test-cli 479 | 480 | USAGE: 481 | xtask test-cli 482 | 483 | FLAGS: 484 | -h, --help Prints help information 485 | --rebuild-parsers Rebuild tree-sitter parsers 486 | -- '...' Extra arguments to pass to the cargo command 487 | "# 488 | .trim(); 489 | 490 | if args.contains(["-h", "--help"]) { 491 | println!("{}\n", help); 492 | return Ok(()); 493 | } 494 | 495 | if args.contains("--rebuild-parsers") { 496 | crate::util::tree_sitter::rebuild_parsers()?; 497 | } 498 | 499 | crate::util::handle_unused(args)?; 500 | 501 | let cargo = metadata::cargo()?; 502 | let mut cmd = Command::new(cargo); 503 | cmd.current_dir(metadata::project_root()); 504 | cmd.env("RUSTFLAGS", "-Dwarnings"); 505 | cmd.args(&["test"]); 506 | cmd.args(&["--bins"]); 507 | cmd.args(&["--package", "wasm-lsp-cli"]); 508 | cmd.args(cargo_args); 509 | cmd.status()?; 510 | 511 | Ok(()) 512 | } 513 | 514 | // Run `cargo udeps` with custom options. 515 | pub fn udeps(args: &mut pico_args::Arguments, cargo_args: Vec) -> crate::Fallible<()> { 516 | let help = r#" 517 | xtask-udep 518 | 519 | USAGE: 520 | xtask udeps 521 | 522 | FLAGS: 523 | -h, --help Prints help information 524 | -- '...' Extra arguments to pass to the cargo command 525 | "# 526 | .trim(); 527 | 528 | if args.contains(["-h", "--help"]) { 529 | println!("{}\n", help); 530 | return Ok(()); 531 | } 532 | 533 | crate::util::handle_unused(args)?; 534 | 535 | let cargo = metadata::cargo()?; 536 | let mut cmd = Command::new(cargo); 537 | cmd.current_dir(metadata::project_root()); 538 | cmd.args(&["+nightly", "udeps"]); 539 | cmd.args(&["--all-targets"]); 540 | cmd.args(&["--package", "xtask"]); 541 | cmd.args(&["--package", "wasm-lsp-browser"]); 542 | cmd.args(&["--package", "wasm-lsp-cli"]); 543 | cmd.args(&["--package", "wasm-lsp-languages"]); 544 | cmd.args(&["--package", "wasm-lsp-macros"]); 545 | cmd.args(&["--package", "wasm-lsp-server"]); 546 | cmd.args(&["--package", "wasm-lsp-syntax"]); 547 | cmd.args(&["--package", "wasm-lsp-testing"]); 548 | if cfg!(target_os = "linux") { 549 | cmd.args(&["--package", "wasm-lsp-fuzz"]); 550 | } 551 | cmd.args(cargo_args); 552 | cmd.status()?; 553 | 554 | Ok(()) 555 | } 556 | } 557 | 558 | use crate::metadata; 559 | use std::{path::Path, process::Command}; 560 | 561 | // Initialize submodules (e.g., for tree-sitter grammars and test suites) 562 | pub fn init(args: &mut pico_args::Arguments) -> crate::Fallible<()> { 563 | let help = r#" 564 | xtask-init 565 | 566 | USAGE: 567 | xtask init 568 | 569 | FLAGS: 570 | -h, --help Prints help information 571 | --with-corpus 572 | "# 573 | .trim(); 574 | 575 | if args.contains(["-h", "--help"]) { 576 | println!("{}\n", help); 577 | return Ok(()); 578 | } 579 | 580 | let with_corpus = args.contains("--with-corpus"); 581 | 582 | crate::util::handle_unused(args)?; 583 | 584 | // initialize "vendor/tree-sitter-wasm" submodule 585 | let submodule = Path::new("vendor/tree-sitter-wasm").to_str().unwrap(); 586 | let mut cmd = Command::new("git"); 587 | cmd.current_dir(metadata::project_root()); 588 | cmd.args(&["submodule", "update", "--init", "--depth", "1", "--", submodule]); 589 | cmd.status()?; 590 | 591 | if with_corpus { 592 | // initialize "vendor/corpus" submodule 593 | let submodule = Path::new("vendor/corpus").to_str().unwrap(); 594 | let mut cmd = Command::new("git"); 595 | cmd.current_dir(metadata::project_root()); 596 | cmd.args(&["submodule", "update", "--init", "--depth", "1", "--", submodule]); 597 | cmd.status()?; 598 | 599 | // initialize "vendor/corpus/..." submodules 600 | let mut cmd = Command::new("git"); 601 | let mut path = metadata::project_root(); 602 | path.push("vendor"); 603 | path.push("corpus"); 604 | cmd.current_dir(path); 605 | cmd.args(&["submodule", "update", "--init", "--depth", "1"]); 606 | cmd.status()?; 607 | } 608 | 609 | Ok(()) 610 | } 611 | } 612 | 613 | mod util { 614 | pub(super) fn handle_result(result: crate::Fallible) { 615 | if let Err(err) = result { 616 | println!("Error :: {}", err); 617 | std::process::exit(1); 618 | } 619 | } 620 | 621 | pub(super) fn handle_unused(args: &pico_args::Arguments) -> crate::Fallible<()> { 622 | use std::borrow::Borrow; 623 | let unused = args.clone().finish(); 624 | if !unused.is_empty() { 625 | let mut message = String::new(); 626 | for str in unused { 627 | message.push(' '); 628 | message.push_str(str.to_string_lossy().borrow()); 629 | } 630 | Err(format!("unrecognized arguments '{}'", message).into()) 631 | } else { 632 | Ok(()) 633 | } 634 | } 635 | 636 | pub mod tree_sitter { 637 | use crate::metadata; 638 | use std::{ 639 | path::PathBuf, 640 | process::{Command, Stdio}, 641 | }; 642 | 643 | // Rebuild tree-sitter parsers if necessary. 644 | pub fn rebuild_parsers() -> crate::Fallible<()> { 645 | // Configure the project root path. 646 | let root_path = metadata::project_root(); 647 | let root_path = root_path.to_str().unwrap(); 648 | 649 | // Configure the tree-sitter directory path. 650 | let tree_sitter_path = [root_path, "vendor", "tree-sitter-wasm"].iter().collect::(); 651 | let tree_sitter_path = tree_sitter_path.to_str().unwrap(); 652 | 653 | // Configure the tree-sitter cli binary path. 654 | let tree_sitter_cli_path = [tree_sitter_path, "node_modules", ".bin", "tree-sitter"] 655 | .iter() 656 | .collect::(); 657 | let tree_sitter_cli_path = tree_sitter_cli_path.to_str().unwrap(); 658 | 659 | // Check if the tree-sitter cli binary is available. 660 | let mut cmd; 661 | if cfg!(target_os = "windows") { 662 | cmd = Command::new("cmd"); 663 | cmd.args(&["/C", format!("{} --help", tree_sitter_cli_path).as_ref()]); 664 | } else { 665 | cmd = Command::new("sh"); 666 | cmd.args(&["-c", format!("{} --help", tree_sitter_cli_path).as_ref()]); 667 | }; 668 | cmd.stdout(Stdio::null()); 669 | cmd.stderr(Stdio::null()); 670 | 671 | // Run `npm ci` first if `tree-sitter` binary is not available. 672 | if !cmd.status()?.success() { 673 | let mut cmd; 674 | if cfg!(target_os = "windows") { 675 | cmd = Command::new("cmd"); 676 | cmd.args(&["/C", "npm ci"]); 677 | } else { 678 | cmd = Command::new("sh"); 679 | cmd.args(&["-c", "npm ci"]); 680 | } 681 | cmd.current_dir(tree_sitter_path); 682 | cmd.stdout(Stdio::null()); 683 | cmd.stderr(Stdio::null()); 684 | cmd.status()?; 685 | } 686 | 687 | // Iterate through the different grammar types. 688 | for grammar in &["wast", "wat"] { 689 | // Configure the grammar directory path. 690 | let grammar_path = [tree_sitter_path, grammar].iter().collect::(); 691 | let grammar_path = dunce::canonicalize(grammar_path)?; 692 | let grammar_path = grammar_path.to_str().unwrap(); 693 | 694 | let commands = format!("cd {} && {} generate", grammar_path, tree_sitter_cli_path); 695 | let mut cmd; 696 | if cfg!(target_os = "windows") { 697 | cmd = Command::new("cmd"); 698 | cmd.args(&["/C", commands.as_ref()]); 699 | } else { 700 | cmd = Command::new("sh"); 701 | cmd.args(&["-c", commands.as_ref()]); 702 | } 703 | let status = cmd.status()?; 704 | if !status.success() { 705 | panic!("failed to regenerate parser: {}", grammar); 706 | } 707 | } 708 | 709 | Ok(()) 710 | } 711 | } 712 | } 713 | --------------------------------------------------------------------------------