├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── buffers.rs ├── completions.rs ├── display.rs ├── ls.rs ├── main.rs ├── query.rs └── server.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | linker = "rust-lld.exe" 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | os: 5 | required: true 6 | type: string 7 | triplet: 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ inputs.os }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: dtolnay/rust-toolchain@stable 17 | - uses: Swatinem/rust-cache@v2 18 | with: 19 | shared-key: release-${{ inputs.os }} 20 | - run: cargo build --release 21 | - name: Append system triplet to binary name 22 | if: ${{ inputs.os != 'windows-latest' }} 23 | run: mv ./target/release/redscript-ide ./target/release/redscript-ide-${{ inputs.triplet }} 24 | - uses: actions/upload-artifact@v4 25 | name: Upload Windows artifact 26 | if: ${{ inputs.os == 'windows-latest' }} 27 | with: 28 | path: target/release/redscript-ide.exe 29 | name: artifact-${{ inputs.os }} 30 | if-no-files-found: error 31 | - uses: actions/upload-artifact@v4 32 | name: Upload non-Windows artifact 33 | if: ${{ inputs.os != 'windows-latest' }} 34 | with: 35 | path: target/release/redscript-ide-${{ inputs.triplet }} 36 | name: artifact-${{ inputs.os }} 37 | if-no-files-found: error 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["*.*.x"] 6 | pull_request: 7 | branches: ["*.*.x"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | lint: 14 | name: Run clippy 15 | runs-on: windows-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: dtolnay/rust-toolchain@stable 19 | - uses: Swatinem/rust-cache@v2 20 | with: 21 | shared-key: debug 22 | - run: cargo clippy -- -Dwarnings 23 | 24 | build: 25 | name: Build and upload an artifact for ${{ matrix.name }} 26 | uses: ./.github/workflows/build.yml 27 | strategy: 28 | matrix: 29 | os: 30 | - windows-latest 31 | - macos-14 32 | - macos-latest 33 | - ubuntu-latest 34 | include: 35 | - os: windows-latest 36 | name: Windows 37 | triplet: x86_64-pc-windows-msvc 38 | - os: macos-14 39 | name: MacOS Silicon 40 | triplet: aarch64-apple-darwin 41 | - os: macos-latest 42 | name: MacOS Intel 43 | triplet: x86_64-apple-darwin 44 | - os: ubuntu-latest 45 | name: Linux 46 | triplet: x86_64-unknown-linux-gnu 47 | with: 48 | os: ${{ matrix.os }} 49 | triplet: ${{ matrix.triplet }} 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | - "v[0-9]+.[0-9]+.[0-9]+-*" 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | prepare: 14 | name: Build and upload a release artifact for ${{ matrix.name }} 15 | uses: ./.github/workflows/build.yml 16 | strategy: 17 | matrix: 18 | os: 19 | - windows-latest 20 | - macos-14 21 | - macos-latest 22 | - ubuntu-latest 23 | include: 24 | - os: windows-latest 25 | name: Windows 26 | triplet: x86_64-pc-windows-msvc 27 | - os: macos-14 28 | name: MacOS Silicon 29 | triplet: aarch64-apple-darwin 30 | - os: macos-latest 31 | name: MacOS Intel 32 | triplet: x86_64-apple-darwin 33 | - os: ubuntu-latest 34 | name: Linux 35 | triplet: x86_64-unknown-linux-gnu 36 | with: 37 | os: ${{ matrix.os }} 38 | triplet: ${{ matrix.triplet }} 39 | publish: 40 | name: Release artifacts 41 | needs: prepare 42 | runs-on: ubuntu-latest 43 | permissions: 44 | contents: write 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/download-artifact@v4 48 | with: 49 | path: artifacts 50 | merge-multiple: true 51 | - env: 52 | GH_TOKEN: ${{ github.token }} 53 | run: | 54 | FLAGS=(--generate-notes) 55 | ARTIFACTS=$(find ./artifacts -type f) 56 | 57 | if echo ${{ github.ref_name }} | grep -E 'v[0-9]+\.[0-9]+\.[0-9]+-.+'; then 58 | FLAGS+=(--prerelease) 59 | fi 60 | 61 | gh release create ${{ github.ref_name }} ${FLAGS[@]} $ARTIFACTS 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | unstable_features = true 3 | use_field_init_shorthand = true 4 | imports_granularity = "Module" 5 | reorder_imports = true 6 | group_imports = "StdExternalCrate" 7 | reorder_impl_items = true 8 | reorder_modules = true 9 | edition = "2024" 10 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.12" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "aliasable" 28 | version = "0.1.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 31 | 32 | [[package]] 33 | name = "allocator-api2" 34 | version = "0.2.21" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 37 | 38 | [[package]] 39 | name = "anyhow" 40 | version = "1.0.98" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 43 | 44 | [[package]] 45 | name = "autocfg" 46 | version = "1.4.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 49 | 50 | [[package]] 51 | name = "bitfield-struct" 52 | version = "0.11.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8" 55 | dependencies = [ 56 | "proc-macro2", 57 | "quote", 58 | "syn", 59 | ] 60 | 61 | [[package]] 62 | name = "bitflags" 63 | version = "1.3.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "2.9.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 72 | 73 | [[package]] 74 | name = "borrow-or-share" 75 | version = "0.2.2" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" 78 | 79 | [[package]] 80 | name = "bstr" 81 | version = "1.12.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 84 | dependencies = [ 85 | "memchr", 86 | "serde", 87 | ] 88 | 89 | [[package]] 90 | name = "byte" 91 | version = "0.2.7" 92 | source = "git+https://github.com/jac3km4/byte?rev=da71833#da71833efec765732f5d08dec6b78a0c7d1de82c" 93 | dependencies = [ 94 | "byte_derive", 95 | ] 96 | 97 | [[package]] 98 | name = "byte_derive" 99 | version = "0.2.7" 100 | source = "git+https://github.com/jac3km4/byte?rev=da71833#da71833efec765732f5d08dec6b78a0c7d1de82c" 101 | dependencies = [ 102 | "proc-macro2", 103 | "quote", 104 | "syn", 105 | ] 106 | 107 | [[package]] 108 | name = "cc" 109 | version = "1.2.23" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" 112 | dependencies = [ 113 | "shlex", 114 | ] 115 | 116 | [[package]] 117 | name = "cfg-if" 118 | version = "1.0.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 121 | 122 | [[package]] 123 | name = "chumsky" 124 | version = "1.0.0-alpha.7" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "c7b80276986f86789dc56ca6542d53bba9cda3c66091ebbe7bd96fc1bdf20f1f" 127 | dependencies = [ 128 | "hashbrown 0.14.5", 129 | "regex-automata 0.3.9", 130 | "serde", 131 | "stacker", 132 | "unicode-ident", 133 | ] 134 | 135 | [[package]] 136 | name = "crc32fast" 137 | version = "1.4.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 140 | dependencies = [ 141 | "cfg-if", 142 | ] 143 | 144 | [[package]] 145 | name = "crossbeam-channel" 146 | version = "0.5.15" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 149 | dependencies = [ 150 | "crossbeam-utils", 151 | ] 152 | 153 | [[package]] 154 | name = "crossbeam-deque" 155 | version = "0.8.6" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 158 | dependencies = [ 159 | "crossbeam-epoch", 160 | "crossbeam-utils", 161 | ] 162 | 163 | [[package]] 164 | name = "crossbeam-epoch" 165 | version = "0.9.18" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 168 | dependencies = [ 169 | "crossbeam-utils", 170 | ] 171 | 172 | [[package]] 173 | name = "crossbeam-utils" 174 | version = "0.8.21" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 177 | 178 | [[package]] 179 | name = "derive-where" 180 | version = "1.4.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "e73f2692d4bd3cac41dca28934a39894200c9fabf49586d77d0e5954af1d7902" 183 | dependencies = [ 184 | "proc-macro2", 185 | "quote", 186 | "syn", 187 | ] 188 | 189 | [[package]] 190 | name = "elsa" 191 | version = "1.11.2" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" 194 | dependencies = [ 195 | "indexmap", 196 | "stable_deref_trait", 197 | ] 198 | 199 | [[package]] 200 | name = "equivalent" 201 | version = "1.0.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 204 | 205 | [[package]] 206 | name = "fluent-uri" 207 | version = "0.1.4" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" 210 | dependencies = [ 211 | "bitflags 1.3.2", 212 | ] 213 | 214 | [[package]] 215 | name = "fluent-uri" 216 | version = "0.3.2" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" 219 | dependencies = [ 220 | "borrow-or-share", 221 | "ref-cast", 222 | ] 223 | 224 | [[package]] 225 | name = "foldhash" 226 | version = "0.1.5" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 229 | 230 | [[package]] 231 | name = "globset" 232 | version = "0.4.16" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 235 | dependencies = [ 236 | "aho-corasick", 237 | "bstr", 238 | "log", 239 | "regex-automata 0.4.9", 240 | "regex-syntax 0.8.5", 241 | ] 242 | 243 | [[package]] 244 | name = "hashbrown" 245 | version = "0.14.5" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 248 | dependencies = [ 249 | "ahash", 250 | "allocator-api2", 251 | ] 252 | 253 | [[package]] 254 | name = "hashbrown" 255 | version = "0.15.3" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 258 | dependencies = [ 259 | "allocator-api2", 260 | "equivalent", 261 | "foldhash", 262 | ] 263 | 264 | [[package]] 265 | name = "heck" 266 | version = "0.4.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 269 | 270 | [[package]] 271 | name = "identity-hash" 272 | version = "0.1.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" 275 | 276 | [[package]] 277 | name = "ignore" 278 | version = "0.4.23" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 281 | dependencies = [ 282 | "crossbeam-deque", 283 | "globset", 284 | "log", 285 | "memchr", 286 | "regex-automata 0.4.9", 287 | "same-file", 288 | "walkdir", 289 | "winapi-util", 290 | ] 291 | 292 | [[package]] 293 | name = "indexmap" 294 | version = "2.9.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 297 | dependencies = [ 298 | "equivalent", 299 | "hashbrown 0.15.3", 300 | ] 301 | 302 | [[package]] 303 | name = "itoa" 304 | version = "1.0.15" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 307 | 308 | [[package]] 309 | name = "libc" 310 | version = "0.2.172" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 313 | 314 | [[package]] 315 | name = "libmimalloc-sys" 316 | version = "0.1.42" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" 319 | dependencies = [ 320 | "cc", 321 | "libc", 322 | ] 323 | 324 | [[package]] 325 | name = "log" 326 | version = "0.4.27" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 329 | 330 | [[package]] 331 | name = "lsp-server" 332 | version = "0.7.8" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9" 335 | dependencies = [ 336 | "crossbeam-channel", 337 | "log", 338 | "serde", 339 | "serde_derive", 340 | "serde_json", 341 | ] 342 | 343 | [[package]] 344 | name = "lsp-types" 345 | version = "0.97.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" 348 | dependencies = [ 349 | "bitflags 1.3.2", 350 | "fluent-uri 0.1.4", 351 | "serde", 352 | "serde_json", 353 | "serde_repr", 354 | ] 355 | 356 | [[package]] 357 | name = "memchr" 358 | version = "2.7.4" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 361 | 362 | [[package]] 363 | name = "mimalloc" 364 | version = "0.1.46" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" 367 | dependencies = [ 368 | "libmimalloc-sys", 369 | ] 370 | 371 | [[package]] 372 | name = "once_cell" 373 | version = "1.21.3" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 376 | 377 | [[package]] 378 | name = "ouroboros" 379 | version = "0.18.5" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" 382 | dependencies = [ 383 | "aliasable", 384 | "ouroboros_macro", 385 | "static_assertions", 386 | ] 387 | 388 | [[package]] 389 | name = "ouroboros_macro" 390 | version = "0.18.5" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" 393 | dependencies = [ 394 | "heck", 395 | "proc-macro2", 396 | "proc-macro2-diagnostics", 397 | "quote", 398 | "syn", 399 | ] 400 | 401 | [[package]] 402 | name = "paste" 403 | version = "1.0.15" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 406 | 407 | [[package]] 408 | name = "pretty_dtoa" 409 | version = "0.3.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "a239bcdfda2c685fda1add3b4695c06225f50075e3cfb5b954e91545587edff2" 412 | dependencies = [ 413 | "ryu_floating_decimal", 414 | ] 415 | 416 | [[package]] 417 | name = "proc-macro2" 418 | version = "1.0.95" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 421 | dependencies = [ 422 | "unicode-ident", 423 | ] 424 | 425 | [[package]] 426 | name = "proc-macro2-diagnostics" 427 | version = "0.10.1" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 430 | dependencies = [ 431 | "proc-macro2", 432 | "quote", 433 | "syn", 434 | "version_check", 435 | "yansi", 436 | ] 437 | 438 | [[package]] 439 | name = "psm" 440 | version = "0.1.26" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" 443 | dependencies = [ 444 | "cc", 445 | ] 446 | 447 | [[package]] 448 | name = "quote" 449 | version = "1.0.40" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 452 | dependencies = [ 453 | "proc-macro2", 454 | ] 455 | 456 | [[package]] 457 | name = "redscript-ast" 458 | version = "1.0.0-preview.13" 459 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 460 | dependencies = [ 461 | "bitflags 2.9.1", 462 | "chumsky", 463 | "derive-where", 464 | "elsa", 465 | "ignore", 466 | ] 467 | 468 | [[package]] 469 | name = "redscript-compiler-api" 470 | version = "1.0.0-preview.13" 471 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 472 | dependencies = [ 473 | "log", 474 | "redscript-ast", 475 | "redscript-compiler-backend", 476 | "redscript-compiler-frontend", 477 | "redscript-io", 478 | "redscript-parser", 479 | "thiserror", 480 | ] 481 | 482 | [[package]] 483 | name = "redscript-compiler-backend" 484 | version = "1.0.0-preview.13" 485 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 486 | dependencies = [ 487 | "hashbrown 0.15.3", 488 | "identity-hash", 489 | "indexmap", 490 | "redscript-compiler-frontend", 491 | "redscript-io", 492 | "slab", 493 | "smallvec", 494 | "thiserror", 495 | ] 496 | 497 | [[package]] 498 | name = "redscript-compiler-frontend" 499 | version = "1.0.0-preview.13" 500 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 501 | dependencies = [ 502 | "bitfield-struct", 503 | "derive-where", 504 | "elsa", 505 | "hashbrown 0.15.3", 506 | "identity-hash", 507 | "indexmap", 508 | "paste", 509 | "redscript-ast", 510 | "redscript-parser", 511 | "sequence_trie", 512 | "slab", 513 | "smallvec", 514 | "thiserror", 515 | ] 516 | 517 | [[package]] 518 | name = "redscript-dotfile" 519 | version = "1.0.0-preview.13" 520 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 521 | dependencies = [ 522 | "anyhow", 523 | "serde", 524 | "toml", 525 | ] 526 | 527 | [[package]] 528 | name = "redscript-formatter" 529 | version = "1.0.0-preview.13" 530 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 531 | dependencies = [ 532 | "hashbrown 0.15.3", 533 | "pretty_dtoa", 534 | "redscript-ast", 535 | "redscript-parser", 536 | ] 537 | 538 | [[package]] 539 | name = "redscript-ide" 540 | version = "0.2.5" 541 | dependencies = [ 542 | "anyhow", 543 | "crossbeam-channel", 544 | "fluent-uri 0.3.2", 545 | "hashbrown 0.15.3", 546 | "lsp-server", 547 | "lsp-types", 548 | "mimalloc", 549 | "ouroboros", 550 | "redscript-compiler-api", 551 | "redscript-dotfile", 552 | "redscript-formatter", 553 | "redscript-parser", 554 | "ropey", 555 | "serde", 556 | "serde_json", 557 | "toml", 558 | ] 559 | 560 | [[package]] 561 | name = "redscript-io" 562 | version = "1.0.0-preview.13" 563 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 564 | dependencies = [ 565 | "bitfield-struct", 566 | "byte", 567 | "crc32fast", 568 | "foldhash", 569 | "identity-hash", 570 | "indexmap", 571 | "vmap", 572 | ] 573 | 574 | [[package]] 575 | name = "redscript-parser" 576 | version = "1.0.0-preview.13" 577 | source = "git+https://github.com/jac3km4/redscript?rev=v1.0.0-preview.13#9152298425525a153e62de6b2a10d94e98a80e8a" 578 | dependencies = [ 579 | "chumsky", 580 | "redscript-ast", 581 | ] 582 | 583 | [[package]] 584 | name = "ref-cast" 585 | version = "1.0.24" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" 588 | dependencies = [ 589 | "ref-cast-impl", 590 | ] 591 | 592 | [[package]] 593 | name = "ref-cast-impl" 594 | version = "1.0.24" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" 597 | dependencies = [ 598 | "proc-macro2", 599 | "quote", 600 | "syn", 601 | ] 602 | 603 | [[package]] 604 | name = "regex-automata" 605 | version = "0.3.9" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" 608 | dependencies = [ 609 | "aho-corasick", 610 | "memchr", 611 | "regex-syntax 0.7.5", 612 | ] 613 | 614 | [[package]] 615 | name = "regex-automata" 616 | version = "0.4.9" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 619 | dependencies = [ 620 | "aho-corasick", 621 | "memchr", 622 | "regex-syntax 0.8.5", 623 | ] 624 | 625 | [[package]] 626 | name = "regex-syntax" 627 | version = "0.7.5" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 630 | 631 | [[package]] 632 | name = "regex-syntax" 633 | version = "0.8.5" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 636 | 637 | [[package]] 638 | name = "ropey" 639 | version = "1.6.1" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" 642 | dependencies = [ 643 | "smallvec", 644 | "str_indices", 645 | ] 646 | 647 | [[package]] 648 | name = "ryu" 649 | version = "1.0.20" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 652 | 653 | [[package]] 654 | name = "ryu_floating_decimal" 655 | version = "0.1.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "700de91d5fd6091442d00fdd9ee790af6d4f0f480562b0f5a1e8f59e90aafe73" 658 | 659 | [[package]] 660 | name = "same-file" 661 | version = "1.0.6" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 664 | dependencies = [ 665 | "winapi-util", 666 | ] 667 | 668 | [[package]] 669 | name = "sequence_trie" 670 | version = "0.3.6" 671 | source = "git+https://github.com/jac3km4/rust_sequence_trie?rev=20c28c4#20c28c40789bb253c9c39b0287e5c5290539a3a6" 672 | dependencies = [ 673 | "hashbrown 0.15.3", 674 | ] 675 | 676 | [[package]] 677 | name = "serde" 678 | version = "1.0.219" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 681 | dependencies = [ 682 | "serde_derive", 683 | ] 684 | 685 | [[package]] 686 | name = "serde_derive" 687 | version = "1.0.219" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 690 | dependencies = [ 691 | "proc-macro2", 692 | "quote", 693 | "syn", 694 | ] 695 | 696 | [[package]] 697 | name = "serde_json" 698 | version = "1.0.140" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 701 | dependencies = [ 702 | "itoa", 703 | "memchr", 704 | "ryu", 705 | "serde", 706 | ] 707 | 708 | [[package]] 709 | name = "serde_repr" 710 | version = "0.1.20" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 713 | dependencies = [ 714 | "proc-macro2", 715 | "quote", 716 | "syn", 717 | ] 718 | 719 | [[package]] 720 | name = "serde_spanned" 721 | version = "0.6.8" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 724 | dependencies = [ 725 | "serde", 726 | ] 727 | 728 | [[package]] 729 | name = "shlex" 730 | version = "1.3.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 733 | 734 | [[package]] 735 | name = "slab" 736 | version = "0.4.9" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 739 | dependencies = [ 740 | "autocfg", 741 | ] 742 | 743 | [[package]] 744 | name = "smallvec" 745 | version = "1.15.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 748 | 749 | [[package]] 750 | name = "stable_deref_trait" 751 | version = "1.1.1" 752 | source = "git+https://github.com/Storyyeller/stable_deref_trait?rev=59a35e0#59a35e0a40041133c6b8744f2b62f922e7d18d5b" 753 | 754 | [[package]] 755 | name = "stacker" 756 | version = "0.1.21" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" 759 | dependencies = [ 760 | "cc", 761 | "cfg-if", 762 | "libc", 763 | "psm", 764 | "windows-sys", 765 | ] 766 | 767 | [[package]] 768 | name = "static_assertions" 769 | version = "1.1.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 772 | 773 | [[package]] 774 | name = "str_indices" 775 | version = "0.4.4" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" 778 | 779 | [[package]] 780 | name = "syn" 781 | version = "2.0.101" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 784 | dependencies = [ 785 | "proc-macro2", 786 | "quote", 787 | "unicode-ident", 788 | ] 789 | 790 | [[package]] 791 | name = "system_error" 792 | version = "0.2.0" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "b52ada790ecca61aa8e561eba96355deb41b9b1b796dd3c965e74a590e9734a3" 795 | 796 | [[package]] 797 | name = "thiserror" 798 | version = "2.0.12" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 801 | dependencies = [ 802 | "thiserror-impl", 803 | ] 804 | 805 | [[package]] 806 | name = "thiserror-impl" 807 | version = "2.0.12" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 810 | dependencies = [ 811 | "proc-macro2", 812 | "quote", 813 | "syn", 814 | ] 815 | 816 | [[package]] 817 | name = "toml" 818 | version = "0.8.22" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 821 | dependencies = [ 822 | "serde", 823 | "serde_spanned", 824 | "toml_datetime", 825 | "toml_edit", 826 | ] 827 | 828 | [[package]] 829 | name = "toml_datetime" 830 | version = "0.6.9" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 833 | dependencies = [ 834 | "serde", 835 | ] 836 | 837 | [[package]] 838 | name = "toml_edit" 839 | version = "0.22.26" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 842 | dependencies = [ 843 | "indexmap", 844 | "serde", 845 | "serde_spanned", 846 | "toml_datetime", 847 | "toml_write", 848 | "winnow", 849 | ] 850 | 851 | [[package]] 852 | name = "toml_write" 853 | version = "0.1.1" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 856 | 857 | [[package]] 858 | name = "unicode-ident" 859 | version = "1.0.18" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 862 | 863 | [[package]] 864 | name = "version_check" 865 | version = "0.9.5" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 868 | 869 | [[package]] 870 | name = "vmap" 871 | version = "0.6.3" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "58dc9bf6fc1f05e86dc98f03e3bf89dacfd5098734ee0aad1d8d6a45b38856a2" 874 | dependencies = [ 875 | "libc", 876 | "system_error", 877 | "winapi", 878 | ] 879 | 880 | [[package]] 881 | name = "walkdir" 882 | version = "2.5.0" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 885 | dependencies = [ 886 | "same-file", 887 | "winapi-util", 888 | ] 889 | 890 | [[package]] 891 | name = "winapi" 892 | version = "0.3.9" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 895 | dependencies = [ 896 | "winapi-i686-pc-windows-gnu", 897 | "winapi-x86_64-pc-windows-gnu", 898 | ] 899 | 900 | [[package]] 901 | name = "winapi-i686-pc-windows-gnu" 902 | version = "0.4.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 905 | 906 | [[package]] 907 | name = "winapi-util" 908 | version = "0.1.9" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 911 | dependencies = [ 912 | "windows-sys", 913 | ] 914 | 915 | [[package]] 916 | name = "winapi-x86_64-pc-windows-gnu" 917 | version = "0.4.0" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 920 | 921 | [[package]] 922 | name = "windows-sys" 923 | version = "0.59.0" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 926 | dependencies = [ 927 | "windows-targets", 928 | ] 929 | 930 | [[package]] 931 | name = "windows-targets" 932 | version = "0.52.6" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 935 | dependencies = [ 936 | "windows_aarch64_gnullvm", 937 | "windows_aarch64_msvc", 938 | "windows_i686_gnu", 939 | "windows_i686_gnullvm", 940 | "windows_i686_msvc", 941 | "windows_x86_64_gnu", 942 | "windows_x86_64_gnullvm", 943 | "windows_x86_64_msvc", 944 | ] 945 | 946 | [[package]] 947 | name = "windows_aarch64_gnullvm" 948 | version = "0.52.6" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 951 | 952 | [[package]] 953 | name = "windows_aarch64_msvc" 954 | version = "0.52.6" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 957 | 958 | [[package]] 959 | name = "windows_i686_gnu" 960 | version = "0.52.6" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 963 | 964 | [[package]] 965 | name = "windows_i686_gnullvm" 966 | version = "0.52.6" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 969 | 970 | [[package]] 971 | name = "windows_i686_msvc" 972 | version = "0.52.6" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 975 | 976 | [[package]] 977 | name = "windows_x86_64_gnu" 978 | version = "0.52.6" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 981 | 982 | [[package]] 983 | name = "windows_x86_64_gnullvm" 984 | version = "0.52.6" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 987 | 988 | [[package]] 989 | name = "windows_x86_64_msvc" 990 | version = "0.52.6" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 993 | 994 | [[package]] 995 | name = "winnow" 996 | version = "0.7.10" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 999 | dependencies = [ 1000 | "memchr", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "yansi" 1005 | version = "1.0.1" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1008 | 1009 | [[package]] 1010 | name = "zerocopy" 1011 | version = "0.8.25" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 1014 | dependencies = [ 1015 | "zerocopy-derive", 1016 | ] 1017 | 1018 | [[package]] 1019 | name = "zerocopy-derive" 1020 | version = "0.8.25" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 1023 | dependencies = [ 1024 | "proc-macro2", 1025 | "quote", 1026 | "syn", 1027 | ] 1028 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redscript-ide" 3 | version = "0.2.5" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "1" 8 | hashbrown = "0.15" 9 | serde = { version = "1", features = ["derive"] } 10 | serde_json = "1" 11 | toml = "0.8" 12 | ouroboros = "0.18" 13 | fluent-uri = "0.3" 14 | ropey = "1" 15 | crossbeam-channel = "0.5" 16 | mimalloc = "0.1" 17 | lsp-server = "0.7" 18 | lsp-types = "0.97" 19 | 20 | [dependencies.redscript-parser] 21 | git = "https://github.com/jac3km4/redscript" 22 | rev = "v1.0.0-preview.13" 23 | 24 | [dependencies.redscript-formatter] 25 | git = "https://github.com/jac3km4/redscript" 26 | rev = "v1.0.0-preview.13" 27 | 28 | [dependencies.redscript-compiler-api] 29 | git = "https://github.com/jac3km4/redscript" 30 | rev = "v1.0.0-preview.13" 31 | features = ["ignore"] 32 | 33 | [dependencies.redscript-dotfile] 34 | git = "https://github.com/jac3km4/redscript" 35 | rev = "v1.0.0-preview.13" 36 | 37 | [patch.crates-io] 38 | stable_deref_trait = { git = "https://github.com/Storyyeller/stable_deref_trait", rev = "59a35e0" } 39 | 40 | [lints.rust] 41 | warnings = "warn" 42 | future-incompatible = "warn" 43 | let-underscore = "warn" 44 | nonstandard-style = "warn" 45 | rust-2018-compatibility = "warn" 46 | rust-2018-idioms = "warn" 47 | rust-2021-compatibility = "warn" 48 | rust-2024-compatibility = "warn" 49 | 50 | [lints.clippy] 51 | all = { level = "warn", priority = -1 } 52 | match_same_arms = "warn" 53 | semicolon_if_nothing_returned = "warn" 54 | single_match_else = "warn" 55 | redundant_closure_for_method_calls = "warn" 56 | cloned_instead_of_copied = "warn" 57 | redundant_else = "warn" 58 | unnested_or_patterns = "warn" 59 | unreadable_literal = "warn" 60 | type_repetition_in_bounds = "warn" 61 | equatable_if_let = "warn" 62 | implicit_clone = "warn" 63 | default_trait_access = "warn" 64 | explicit_deref_methods = "warn" 65 | explicit_iter_loop = "warn" 66 | inefficient_to_string = "warn" 67 | match_bool = "warn" 68 | 69 | [workspace.metadata.release] 70 | pre-release-commit-message = "chore: release {{version}}" 71 | publish = false 72 | 73 | [profile.release] 74 | lto = true 75 | strip = true 76 | codegen-units = 1 77 | panic = "abort" 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 jekky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redscript-ide 2 | 3 | Language server for [redscript](https://github.com/jac3km4/redscript) 4 | 5 | ## features 6 | 7 | - error and warning diagnostics 8 | - autocompletion for methods and fields 9 | - hover for function definitions and types 10 | - go-to-definition 11 | - bonus: limited support for redmod (scripted functions) 12 | - workspace symbols 13 | - formatting (beta) 14 | - debugger (requires [redscript-dap](https://github.com/jac3km4/redscript-dap)) 15 | - hooks for external tools 16 | 17 | ![ide-gif](https://user-images.githubusercontent.com/11986158/135734766-b5423e2c-cf47-4836-97ba-5c771cef7cf2.gif) 18 | 19 | ## configuration 20 | 21 | The language server will attempt to load a TOML file named `.redscript` from every workspace folder. 22 | This file can contain the following configuration options: 23 | 24 | - `source_roots` allows you to specify source directories where the compiler should look for REDscript files, defaults to `["."]` 25 | - `format` block allows you to configure the formatter, you can find the available options [here](https://github.com/jac3km4/redscript/blob/c3d0ec6f12583eccc51b5a482583e8fb6641ce8d/crates/dotfile/src/lib.rs#L36-L43) 26 | 27 | Here's an example `.redscript` file: 28 | 29 | ```toml 30 | source_roots = [".", "../red4ext/plugins"] 31 | 32 | [format] 33 | indent = 2 34 | max_width = 80 35 | ``` 36 | 37 | ## usage 38 | 39 | - ### VS Code 40 | 41 | Use the [Redscript IDE extension from the marketplace](https://marketplace.visualstudio.com/items?itemName=jac3km4.redscript-ide-vscode). 42 | 43 | - ### Zed 44 | 45 | - Install the `REDscript` extension from the marketplace. 46 | - Configure the required `lsp.redscript-ide.initialization_options.game_dir` setting in Zed's `settings.json`: 47 | ```json 48 | "lsp": { 49 | "redscript-ide": { 50 | "initialization_options": { 51 | "game_dir": "D:\\Games\\Cyberpunk 2077" 52 | } 53 | } 54 | } 55 | ``` 56 | - Ensure that you downloaded the `redscript-ide` executable and it is in your PATH. 57 | - You can set it by searching for 'Edit the system environment variables' on Windows, clicking on 'Environment Variables', and adding the path to the `redscript-ide` executable to the `Path` variable. 58 | 59 | - ### IntelliJ 60 | 61 | Use the (work in progress) [Redscript-IntelliJ Plugin](https://github.com/pawrequest/redscript-intellij). 62 | 63 | - ### Neovim 64 | 65 | Add the snippet below to your `init.lua` and replace `/path/to/redscript-ide` with the path to 66 | a redscript-ide executable downloaded from [here](https://github.com/jac3km4/redscript-ide/releases/latest) 67 | and `/path/to/cyberpunk2077` with the path to the root of your Cyberpunk 2077 installation directory: 68 | 69 | ```lua 70 | local configs = require 'lspconfig.configs' 71 | local util = require 'lspconfig.util' 72 | local lsp = require 'lspconfig' 73 | 74 | -- define the redscript filetype 75 | vim.cmd [[ 76 | augroup RedscriptFile 77 | autocmd! 78 | autocmd BufNewFile,BufRead *.reds set filetype=reds | set syntax=swift 79 | augroup END 80 | ]] 81 | 82 | -- configure the redscript language server 83 | configs.redscript_ide = { 84 | default_config = { 85 | cmd = { '/path/to/redscript-ide' }, 86 | filetypes = 'reds', 87 | init_options = { 88 | game_dir = '/path/to/cyberpunk2077', 89 | }, 90 | root_dir = function(fname) 91 | return util.root_pattern('.git')(fname) or util.path.dirname(fname) 92 | end, 93 | single_file_support = true 94 | } 95 | } 96 | 97 | -- invoke the lsp-config setup 98 | lsp.redscript_ide.setup {} 99 | ``` 100 | 101 | - ### Helix 102 | 103 | Add the snippet below to your [`languages.toml`](https://docs.helix-editor.com/languages.html) 104 | and replace the paths, similarly to the Neovim setup: 105 | 106 | ```toml 107 | [language-server.redscript-ide] 108 | command = "/path/to/redscript-ide" 109 | config = { game_dir = "/path/to/cyberpunk2077" } 110 | 111 | [[language]] 112 | name = "redscript" 113 | scope = "source.reds" 114 | file-types = ["reds"] 115 | # you have to have a .redscript file for workspace to be detected (it can be empty) 116 | roots = [".redscript"] 117 | auto-format = true 118 | comment-tokens = "//" 119 | block-comment-tokens = { start = "/*", end = "*/" } 120 | indent = { tab-width = 2, unit = " " } 121 | language-servers = ["redscript-ide"] 122 | 123 | [language.auto-pairs] 124 | '(' = ')' 125 | '{' = '}' 126 | '[' = ']' 127 | '"' = '"' 128 | 129 | [[grammar]] 130 | name = "redscript" 131 | source = { git = "https://github.com/jac3km4/tree-sitter-redscript", rev = "master" } 132 | ``` 133 | 134 | To get the syntax highlighting to work, you also need to rebuild the grammars and populate the queries folder: 135 | 136 | ```powerhsell 137 | # build the grammars 138 | helix --grammar fetch 139 | helix --grammar build 140 | 141 | # populate the queries 142 | # on Linux run: 143 | # git clone -b queries --single-branch https://github.com/jac3km4/tree-sitter-redscript "$HOME/.config/helix/runtime/queries/redscript" 144 | # on Windows run: 145 | # git clone -b queries --single-branch https://github.com/jac3km4/tree-sitter-redscript "$env:USERPROFILE\AppData\Roaming\helix\runtime\queries\redscript" 146 | ``` 147 | -------------------------------------------------------------------------------- /src/buffers.rs: -------------------------------------------------------------------------------- 1 | use hashbrown::HashMap; 2 | use lsp_types as lsp; 3 | use ropey::Rope; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct Buffers { 7 | map: HashMap, 8 | } 9 | 10 | impl Buffers { 11 | pub fn add(&mut self, url: lsp::Uri, content: String) { 12 | self.map.insert(url, Buffer::new(content)); 13 | } 14 | 15 | pub fn remove(&mut self, url: &lsp::Uri) { 16 | self.map.remove(url); 17 | } 18 | 19 | pub fn update_range(&mut self, url: &lsp::Uri, range: lsp::Range, text: String) { 20 | let buf = self.map.get_mut(url).unwrap(); 21 | buf.update_range(range, text); 22 | } 23 | 24 | pub fn get(&self, url: &lsp::Uri) -> Option<&Buffer> { 25 | self.map.get(url) 26 | } 27 | } 28 | 29 | #[derive(Debug)] 30 | pub struct Buffer { 31 | contents: Rope, 32 | } 33 | 34 | impl Buffer { 35 | pub fn new(contents: String) -> Self { 36 | Self { 37 | contents: Rope::from_str(&contents), 38 | } 39 | } 40 | 41 | pub fn update_range(&mut self, range: lsp::Range, text: String) { 42 | let start = 43 | self.contents.line_to_char(range.start.line as usize) + range.start.character as usize; 44 | let end = 45 | self.contents.line_to_char(range.end.line as usize) + range.end.character as usize; 46 | self.contents.remove(start..end); 47 | self.contents.insert(start, &text); 48 | } 49 | 50 | pub fn get_pos(&self, line: u32, col: u32) -> Option { 51 | let line_start = self.contents.line_to_char(line as usize); 52 | let byte_index = self.contents.char_to_byte(line_start + col as usize); 53 | Some(byte_index as u32) 54 | } 55 | 56 | pub fn contents(&self) -> &Rope { 57 | &self.contents 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/completions.rs: -------------------------------------------------------------------------------- 1 | use lsp_types as lsp; 2 | use redscript_compiler_api::{Field, FunctionType}; 3 | 4 | use crate::display::{DocDisplay, FunctionTypeDisplay, SnippetDisplay}; 5 | 6 | pub fn method<'ctx>( 7 | name: &'ctx str, 8 | typ: &FunctionType<'ctx>, 9 | doc: &[&'ctx str], 10 | ) -> lsp_types::CompletionItem { 11 | let detail = FunctionTypeDisplay::new(typ).to_string(); 12 | let snippet_display = SnippetDisplay::new(name, typ).to_string(); 13 | lsp::CompletionItem { 14 | label: name.to_owned(), 15 | label_details: Some(lsp::CompletionItemLabelDetails { 16 | detail: Some(detail.clone()), 17 | description: None, 18 | }), 19 | detail: Some(detail), 20 | kind: Some(lsp::CompletionItemKind::METHOD), 21 | documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { 22 | kind: lsp::MarkupKind::Markdown, 23 | value: DocDisplay::new(doc).to_string(), 24 | })), 25 | insert_text: Some(snippet_display), 26 | insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), 27 | ..Default::default() 28 | } 29 | } 30 | 31 | pub fn field(name: &str, field: &Field<'_>) -> lsp_types::CompletionItem { 32 | let detail = format!(": {}", field.type_()); 33 | lsp::CompletionItem { 34 | label: name.to_owned(), 35 | label_details: Some(lsp::CompletionItemLabelDetails { 36 | detail: Some(detail.clone()), 37 | description: None, 38 | }), 39 | detail: Some(detail), 40 | kind: Some(lsp::CompletionItemKind::FIELD), 41 | documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { 42 | kind: lsp::MarkupKind::Markdown, 43 | value: DocDisplay::new(field.doc()).to_string(), 44 | })), 45 | ..Default::default() 46 | } 47 | } 48 | 49 | pub fn enum_member(name: &str) -> lsp_types::CompletionItem { 50 | lsp::CompletionItem { 51 | label: name.to_owned(), 52 | kind: Some(lsp::CompletionItemKind::ENUM_MEMBER), 53 | ..Default::default() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use redscript_compiler_api::{FunctionType, Symbols, ir}; 4 | 5 | use crate::query::{ExprAt, type_of}; 6 | 7 | pub struct ExprAtDisplay<'ctx, 'a> { 8 | inner: ExprAt<'ctx, 'a>, 9 | } 10 | 11 | impl<'ctx, 'a> ExprAtDisplay<'ctx, 'a> { 12 | pub fn new(expr_at: ExprAt<'ctx, 'a>) -> Self { 13 | Self { inner: expr_at } 14 | } 15 | } 16 | 17 | impl fmt::Display for ExprAtDisplay<'_, '_> { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | let inner = &self.inner; 20 | let has_header = if let (Some(expr), Some(func)) = (inner.expr(), inner.func()) { 21 | if let ir::Expr::Call { call, .. } = expr { 22 | writeln!(f, "### Signature")?; 23 | writeln!(f, "{}", CallDisplay::new(call, inner.symbols()))?; 24 | true 25 | } else if let Some(typ) = type_of(expr, func, inner.symbols()) { 26 | writeln!(f, "### Type")?; 27 | writeln!(f, "```\n{typ}\n```")?; 28 | true 29 | } else { 30 | false 31 | } 32 | } else { 33 | false 34 | }; 35 | if let Some(typ) = inner.type_() { 36 | if has_header { 37 | writeln!(f, "---")?; 38 | } 39 | writeln!(f, "### Symbol")?; 40 | writeln!(f, "```\n{typ}\n```")?; 41 | writeln!(f, "{}", DocDisplay::new(inner.symbols()[typ].doc()))?; 42 | }; 43 | Ok(()) 44 | } 45 | } 46 | 47 | #[derive(Debug)] 48 | pub struct CallDisplay<'ctx, 'a> { 49 | call: &'a ir::Call<'ctx>, 50 | symbols: &'a Symbols<'ctx>, 51 | } 52 | 53 | impl<'ctx, 'a> CallDisplay<'ctx, 'a> { 54 | pub fn new(call: &'a ir::Call<'ctx>, symbols: &'a Symbols<'ctx>) -> Self { 55 | Self { call, symbols } 56 | } 57 | } 58 | 59 | impl fmt::Display for CallDisplay<'_, '_> { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | writeln!(f, "```")?; 62 | let doc = match self.call { 63 | &ir::Call::FreeFunction { function, .. } => { 64 | let (name, func) = self.symbols.get_free_function(function).unwrap(); 65 | writeln!(f, "{name}{}", FunctionTypeDisplay::new(func.type_()))?; 66 | func.doc() 67 | } 68 | &ir::Call::Static { method, .. } => { 69 | let (name, func) = self.symbols.get_method(method).unwrap(); 70 | writeln!( 71 | f, 72 | "{}::{name}{}", 73 | method.parent(), 74 | FunctionTypeDisplay::new(func.type_()) 75 | )?; 76 | func.doc() 77 | } 78 | ir::Call::Instance { method, .. } => { 79 | let (name, func) = self.symbols.get_method(*method).unwrap(); 80 | writeln!( 81 | f, 82 | "{}::{name}{}", 83 | method.parent(), 84 | FunctionTypeDisplay::new(func.type_()) 85 | )?; 86 | func.doc() 87 | } 88 | ir::Call::Closure { closure_type, .. } => { 89 | if let Ok(ft) = closure_type.coalesced(self.symbols) { 90 | writeln!(f, "{ft}")?; 91 | } 92 | return Ok(()); 93 | } 94 | }; 95 | writeln!(f, "```")?; 96 | writeln!(f, "{}", DocDisplay::new(doc))?; 97 | Ok(()) 98 | } 99 | } 100 | 101 | #[derive(Debug)] 102 | pub struct FunctionTypeDisplay<'ctx, 'a> { 103 | func: &'a FunctionType<'ctx>, 104 | } 105 | 106 | impl<'ctx, 'a> FunctionTypeDisplay<'ctx, 'a> { 107 | pub fn new(func: &'a FunctionType<'ctx>) -> Self { 108 | Self { func } 109 | } 110 | } 111 | 112 | impl fmt::Display for FunctionTypeDisplay<'_, '_> { 113 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | write!(f, "(")?; 115 | for (i, param) in self.func.params().iter().enumerate() { 116 | if i > 0 { 117 | write!(f, ", ")?; 118 | } 119 | write!(f, "{}: {}", param.name(), param.type_())?; 120 | } 121 | write!(f, ") -> {}", self.func.return_type())?; 122 | Ok(()) 123 | } 124 | } 125 | 126 | #[derive(Debug)] 127 | pub struct SnippetDisplay<'ctx, 'a> { 128 | name: &'ctx str, 129 | func: &'a FunctionType<'ctx>, 130 | } 131 | 132 | impl<'ctx, 'a> SnippetDisplay<'ctx, 'a> { 133 | pub fn new(name: &'ctx str, func: &'a FunctionType<'ctx>) -> Self { 134 | Self { name, func } 135 | } 136 | } 137 | 138 | impl fmt::Display for SnippetDisplay<'_, '_> { 139 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 140 | write!(f, "{}(", self.name)?; 141 | for (i, param) in self.func.params().iter().enumerate() { 142 | if i > 0 { 143 | write!(f, ", ")?; 144 | } 145 | write!(f, "${{{}:{}}}", i + 1, param.name())?; 146 | } 147 | write!(f, ")")?; 148 | Ok(()) 149 | } 150 | } 151 | 152 | #[derive(Debug)] 153 | pub struct DocDisplay<'ctx, 'a> { 154 | doc: &'a [&'ctx str], 155 | } 156 | 157 | impl<'ctx, 'a> DocDisplay<'ctx, 'a> { 158 | pub fn new(doc: &'a [&'ctx str]) -> Self { 159 | Self { doc } 160 | } 161 | } 162 | 163 | impl fmt::Display for DocDisplay<'_, '_> { 164 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 165 | if self.doc.is_empty() { 166 | return Ok(()); 167 | } 168 | writeln!(f, "---")?; 169 | for l in self.doc { 170 | writeln!(f, "{}", l.strip_prefix("///").unwrap_or(l).trim())?; 171 | } 172 | Ok(()) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/ls.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::path::{Path, PathBuf}; 3 | use std::rc::Rc; 4 | use std::{fs, iter, mem}; 5 | 6 | use hashbrown::{HashMap, HashSet}; 7 | use lsp_types as lsp; 8 | use ouroboros::self_referencing; 9 | use redscript_compiler_api::types::Type; 10 | use redscript_compiler_api::{ 11 | CompilationInputs, CompileErrorReporter, Diagnostic, Evaluator, LoweredCompilationUnit, 12 | ScriptBundle, SourceMapExt, Symbols, TypeInterner, TypeSchema, ast, infer_from_sources, 13 | parse_file, parse_files, process_sources, 14 | }; 15 | use redscript_dotfile::Dotfile; 16 | use redscript_formatter::{FormatSettings, format_document}; 17 | 18 | use crate::completions; 19 | use crate::query::{AtContext, ExprAt}; 20 | use crate::server::{CodeLocation, Document, LanguageServer, LspContext}; 21 | 22 | pub struct RedscriptLanguageServer { 23 | workspaces: HashMap, 24 | cache: CompilationCache, 25 | cached_completions: RefCell>, 26 | last_diagnostics: RefCell>, 27 | } 28 | 29 | impl RedscriptLanguageServer { 30 | pub fn new( 31 | cache_path: &Path, 32 | workspace_folders: impl IntoIterator>, 33 | ) -> anyhow::Result { 34 | let cache_bytes = fs::read(cache_path)?; 35 | let workspaces = workspace_folders 36 | .into_iter() 37 | .map(|dir| { 38 | let dir = dir.into(); 39 | let workspace = WorkspaceDir::load(&dir)?; 40 | Ok((dir, workspace)) 41 | }) 42 | .collect::>>()?; 43 | let workspace_dirs = workspaces.iter().flat_map(|(_, w)| &w.roots); 44 | Ok(Self { 45 | cache: CompilationCache::make(cache_bytes, workspace_dirs, TypeInterner::default())?, 46 | workspaces, 47 | cached_completions: RefCell::new(None), 48 | last_diagnostics: RefCell::new(HashSet::new()), 49 | }) 50 | } 51 | 52 | fn resolve_file(&self, path: &Path) -> FileResolution<'_> { 53 | if let Some(workspace) = 54 | iter::successors(Some(path), |p| p.parent()).find_map(|p| self.workspaces.get(p)) 55 | { 56 | FileResolution::Workspace(workspace) 57 | } else { 58 | FileResolution::NonWorkspace(path.to_owned()) 59 | } 60 | } 61 | 62 | fn hover_at(&self, loc: CodeLocation<'_>, ctx: &LspContext) -> anyhow::Result { 63 | self.expr_at( 64 | loc, 65 | |at| { 66 | let range = at.expr().and_then(|e| { 67 | let span = e.span(); 68 | range(span, at.sources().get(span.file)?) 69 | }); 70 | Ok(lsp::Hover { 71 | contents: lsp::HoverContents::Markup(lsp::MarkupContent { 72 | kind: lsp::MarkupKind::Markdown, 73 | value: at.display().to_string(), 74 | }), 75 | range, 76 | }) 77 | }, 78 | ctx, 79 | ) 80 | } 81 | 82 | fn completion_at( 83 | &self, 84 | loc: CodeLocation<'_>, 85 | ctx: &LspContext, 86 | ) -> anyhow::Result> { 87 | let preceding_pos = loc.pos() - 1; 88 | let byte = loc.doc().buffer().contents().byte(preceding_pos as usize); 89 | 90 | if let Some(cached) = &mut *self.cached_completions.borrow_mut() { 91 | if cached.file == loc.doc().path() 92 | && loc.pos().saturating_sub(cached.pos) <= 1 93 | && (byte == b'_' || byte.is_ascii_alphanumeric()) 94 | { 95 | cached.pos = loc.pos(); 96 | return Ok(cached.completions.clone()); 97 | } 98 | }; 99 | 100 | if byte != b'.' { 101 | return Ok(Rc::new(lsp::CompletionResponse::Array(vec![]))); 102 | } 103 | 104 | let completions = self.patched_expr_at( 105 | loc.clone().with_pos(preceding_pos), 106 | generate_completions, 107 | ctx, 108 | )?; 109 | 110 | if !completions.is_empty() { 111 | let resp = Rc::new(lsp::CompletionResponse::Array(completions)); 112 | *self.cached_completions.borrow_mut() = Some(CachedCompletions::new( 113 | resp.clone(), 114 | loc.doc().path().to_owned(), 115 | loc.pos(), 116 | )); 117 | Ok(resp) 118 | } else { 119 | Ok(Rc::new(lsp::CompletionResponse::Array(vec![]))) 120 | } 121 | } 122 | 123 | fn definition_at( 124 | &self, 125 | loc: CodeLocation<'_>, 126 | ctx: &LspContext, 127 | ) -> anyhow::Result { 128 | self.expr_at( 129 | loc, 130 | |at| { 131 | let locations = at 132 | .definition_span() 133 | .and_then(|span| Some(vec![location(span, at.sources(), ctx)?])) 134 | .unwrap_or_default(); 135 | Ok(locations.into()) 136 | }, 137 | ctx, 138 | ) 139 | } 140 | 141 | fn workspace_symbols( 142 | &self, 143 | query: &str, 144 | ctx: &LspContext, 145 | ) -> anyhow::Result { 146 | self.check_workspace(|_, syms, _, sources| { 147 | let funcs = syms 148 | .free_functions() 149 | .filter_map(|e| Some((*e.name(), e.func().span()?))) 150 | .filter(|(name, _)| name.as_ref().last().is_some_and(|n| n.contains(query))) 151 | .filter_map(|(name, span)| { 152 | #[allow(deprecated)] 153 | Some(lsp::SymbolInformation { 154 | name: name.to_string(), 155 | kind: lsp::SymbolKind::FUNCTION, 156 | tags: None, 157 | deprecated: None, 158 | location: location(span, sources, ctx)?, 159 | container_name: None, 160 | }) 161 | }); 162 | let types = syms 163 | .types() 164 | .filter_map(|(id, def)| Some((id, def.span()?))) 165 | .filter(|(id, _)| id.as_str().contains(query)) 166 | .filter_map(|(id, span)| { 167 | #[allow(deprecated)] 168 | Some(lsp::SymbolInformation { 169 | name: id.to_string(), 170 | kind: lsp::SymbolKind::CLASS, 171 | tags: None, 172 | deprecated: None, 173 | location: location(span, sources, ctx)?, 174 | container_name: None, 175 | }) 176 | }); 177 | 178 | let results = types.chain(funcs).collect(); 179 | Ok(lsp::WorkspaceSymbolResponse::Flat(results)) 180 | }) 181 | } 182 | 183 | fn format_document( 184 | &self, 185 | doc: Document<'_>, 186 | _tab_size: u16, 187 | ) -> anyhow::Result> { 188 | let resolved = self.resolve_file(doc.path()); 189 | let settings = resolved 190 | .as_workspace() 191 | .map(|ws| &ws.format_settings) 192 | .unwrap_or(&FormatSettings::DEFAULT); 193 | 194 | let contents = doc.buffer().contents(); 195 | let map = ast::SourceMap::new(); 196 | let id = map.push_back(doc.path(), contents.to_string()); 197 | let file = map.get(id).unwrap(); 198 | 199 | let (module, errors) = format_document(file.source(), id, settings); 200 | if let (Some(module), []) = (module, &errors[..]) { 201 | let last_line = contents.len_lines() - 1; 202 | let edit = lsp::TextEdit::new( 203 | lsp::Range::new( 204 | lsp::Position::new(0, 0), 205 | lsp::Position::new( 206 | last_line as u32, 207 | contents.chars_at(contents.line_to_char(last_line)).count() as u32, 208 | ), 209 | ), 210 | module.to_string(), 211 | ); 212 | return Ok(vec![edit]); 213 | }; 214 | 215 | Ok(vec![]) 216 | } 217 | 218 | pub fn check_workspace_and_publish(&self, ctx: &LspContext) -> anyhow::Result<()> { 219 | self.check_workspace(|_, _, diags, sources| self.publish_diagnostics(diags, sources, ctx)) 220 | } 221 | 222 | fn check_workspace( 223 | &self, 224 | cb: impl Fn( 225 | &LoweredCompilationUnit<'_>, 226 | &Symbols<'_>, 227 | &[Diagnostic<'_>], 228 | &ast::SourceMap, 229 | ) -> anyhow::Result, 230 | ) -> anyhow::Result { 231 | self.cache.with(|cache| { 232 | let mut reporter = CompileErrorReporter::default(); 233 | let (unit, syms) = infer_from_sources( 234 | cache.sources, 235 | cache.symbols.clone(), 236 | &mut reporter, 237 | cache.interner, 238 | ); 239 | cb(&unit, &syms, &reporter.into_reported(), cache.sources) 240 | }) 241 | } 242 | 243 | fn publish_diagnostics( 244 | &self, 245 | diags: &[Diagnostic<'_>], 246 | sources: &ast::SourceMap, 247 | ctx: &LspContext, 248 | ) -> anyhow::Result<()> { 249 | let mut file_diags = HashMap::new(); 250 | for diag in diags { 251 | let file = diag.span().file; 252 | if sources 253 | .get(file) 254 | .is_some_and(|f| self.resolve_file(f.path()).as_workspace().is_some()) 255 | { 256 | file_diags.entry(file).or_insert_with(Vec::new).push(diag); 257 | } 258 | } 259 | 260 | let mut last_diagnostics = self.last_diagnostics.borrow_mut(); 261 | 262 | for path in last_diagnostics.drain() { 263 | ctx.notify::(lsp::PublishDiagnosticsParams { 264 | uri: ctx.uri(&path)?, 265 | diagnostics: vec![], 266 | version: None, 267 | }); 268 | } 269 | 270 | for (file, diags) in file_diags { 271 | let file = sources.get(file).unwrap(); 272 | 273 | if !last_diagnostics.contains(file.path()) { 274 | last_diagnostics.insert(file.path().to_owned()); 275 | } 276 | 277 | ctx.notify::(lsp::PublishDiagnosticsParams { 278 | uri: ctx.uri(file.path())?, 279 | diagnostics: diags 280 | .into_iter() 281 | .filter_map(|diag| { 282 | Some(lsp::Diagnostic { 283 | range: range(diag.span(), file)?, 284 | severity: Some(if diag.is_fatal() { 285 | lsp::DiagnosticSeverity::ERROR 286 | } else { 287 | lsp::DiagnosticSeverity::WARNING 288 | }), 289 | message: diag.to_string(), 290 | ..Default::default() 291 | }) 292 | }) 293 | .collect(), 294 | version: None, 295 | }); 296 | } 297 | Ok(()) 298 | } 299 | 300 | fn expr_at( 301 | &self, 302 | loc: CodeLocation<'_>, 303 | cb: impl Fn(ExprAt<'_, '_>) -> anyhow::Result, 304 | ctx: &LspContext, 305 | ) -> anyhow::Result { 306 | self.expr_at_with(loc, false, cb, ctx) 307 | } 308 | 309 | fn patched_expr_at( 310 | &self, 311 | loc: CodeLocation<'_>, 312 | cb: impl Fn(ExprAt<'_, '_>) -> anyhow::Result, 313 | ctx: &LspContext, 314 | ) -> anyhow::Result { 315 | self.expr_at_with(loc, true, cb, ctx) 316 | } 317 | 318 | fn expr_at_with( 319 | &self, 320 | loc: CodeLocation<'_>, 321 | patch: bool, 322 | cb: impl Fn(ExprAt<'_, '_>) -> anyhow::Result, 323 | _ctx: &LspContext, 324 | ) -> anyhow::Result { 325 | self.cache.with(|cache| { 326 | let mut contents = loc.doc().buffer().contents().to_string(); 327 | if patch { 328 | let pos = loc.pos() as usize; 329 | if let Some(c) = contents[pos..].chars().next() { 330 | contents.replace_range(pos..pos + c.len_utf8(), &" ".repeat(c.len_utf8())); 331 | } 332 | }; 333 | 334 | let id = cache.sources.push_back(loc.doc().path(), contents); 335 | let file = cache.sources.get(id).unwrap(); 336 | 337 | let previous_id = cache.file_ids.get(loc.doc().path()).copied(); 338 | 339 | let mut reporter = CompileErrorReporter::default(); 340 | let module = parse_file(id, file, &mut reporter); 341 | 342 | let match_ = module.as_ref().and_then(|m| m.find_at(loc.pos())); 343 | let (ctx, typ) = match match_ { 344 | Some(ast::QueryResult::Type(&ast::Type::Named { name, .. })) => (None, Some(name)), 345 | Some(ast::QueryResult::Expr(&ast::Expr::Ident(name))) => { 346 | (Some(AtContext::Expr), Some(name)) 347 | } 348 | _ => (None, None), 349 | }; 350 | 351 | let evaluator = Evaluator::from_modules(cache.modules.iter().chain(module.as_ref())); 352 | let mods = cache 353 | .modules 354 | .iter() 355 | .filter(|m| m.span().map(|s| s.file) != previous_id) 356 | .cloned() 357 | .chain(module); 358 | let (unit, syms) = process_sources( 359 | mods, 360 | cache.symbols.clone(), 361 | evaluator, 362 | &mut reporter, 363 | cache.interner, 364 | ); 365 | let func = unit 366 | .all_functions() 367 | .find(|f| f.span.file == id && f.span.contains(loc.pos())); 368 | let expr = func.and_then(|f| f.block.find_at(loc.pos())); 369 | 370 | let typ = typ 371 | .and_then(|t| unit.scopes.get(&id)?.get(t)?.id()) 372 | .or_else(|| cache.interner.get_index(cache.interner.get_index_of(typ?)?)); 373 | cb(ExprAt::new(expr, func, typ, &syms, cache.sources, ctx)) 374 | }) 375 | } 376 | } 377 | 378 | fn generate_completions( 379 | at: ExprAt<'_, '_>, 380 | ) -> Result, anyhow::Error> { 381 | let mut completions = vec![]; 382 | 383 | if let (Some(typ), Some(AtContext::Expr)) = (at.type_(), at.context()) { 384 | match at.symbols()[typ].schema() { 385 | TypeSchema::Aggregate(_) => { 386 | let methods = at 387 | .symbols() 388 | .query_methods(typ) 389 | .filter(|m| m.func().flags().is_static()) 390 | .map(|e| completions::method(e.name(), e.func().type_(), e.func().doc())); 391 | completions.extend(methods); 392 | } 393 | TypeSchema::Enum(enum_) => { 394 | let variants = enum_ 395 | .variants() 396 | .map(|(name, _)| completions::enum_member(name)); 397 | completions.extend(variants); 398 | } 399 | _ => {} 400 | }; 401 | } 402 | 403 | let typ = at.expr_type(); 404 | if let Some(typ) = typ 405 | .as_ref() 406 | .map(Type::unwrap_ref_or_self) 407 | .and_then(Type::upper_bound) 408 | { 409 | let methods = at 410 | .symbols() 411 | .query_methods(typ.id()) 412 | .filter(|m| !m.func().flags().is_static()) 413 | .map(|e| completions::method(e.name(), e.func().type_(), e.func().doc())); 414 | completions.extend(methods); 415 | 416 | let fields = at 417 | .symbols() 418 | .base_iter(typ.id()) 419 | .filter_map(|(_, def)| def.schema().as_aggregate()) 420 | .flat_map(|agg| agg.fields().iter()) 421 | .map(|e| completions::field(e.name(), e.field())); 422 | completions.extend(fields); 423 | }; 424 | 425 | Ok(completions) 426 | } 427 | 428 | impl LanguageServer for RedscriptLanguageServer { 429 | fn check(&mut self, path: PathBuf, ctx: &LspContext) -> anyhow::Result<()> { 430 | match self.resolve_file(&path) { 431 | FileResolution::Workspace(_) => { 432 | let workspace_dirs = self.workspaces.values().flat_map(|w| &w.roots); 433 | self.cache.remake(workspace_dirs)?; 434 | self.check_workspace_and_publish(ctx) 435 | } 436 | FileResolution::NonWorkspace(path) => { 437 | self.cache.remake([path])?; 438 | self.check_workspace_and_publish(ctx) 439 | } 440 | } 441 | } 442 | 443 | fn change_workspace_folders( 444 | &mut self, 445 | added: Vec, 446 | removed: Vec, 447 | _ctx: &LspContext, 448 | ) -> anyhow::Result<()> { 449 | for dir in added { 450 | let value = WorkspaceDir::load(&dir)?; 451 | self.workspaces.insert(dir, value); 452 | } 453 | for folder in removed { 454 | self.workspaces.remove(&folder); 455 | } 456 | Ok(()) 457 | } 458 | 459 | fn hover(&self, loc: CodeLocation<'_>, _ctx: &LspContext) -> anyhow::Result { 460 | self.hover_at(loc, _ctx) 461 | } 462 | 463 | fn completion( 464 | &self, 465 | loc: CodeLocation<'_>, 466 | _ctx: &LspContext, 467 | ) -> anyhow::Result> { 468 | self.completion_at(loc, _ctx) 469 | } 470 | 471 | fn goto_definition( 472 | &self, 473 | loc: CodeLocation<'_>, 474 | ctx: &LspContext, 475 | ) -> anyhow::Result { 476 | self.definition_at(loc, ctx) 477 | } 478 | 479 | fn workspace_symbol( 480 | &self, 481 | query: &str, 482 | ctx: &LspContext, 483 | ) -> anyhow::Result { 484 | self.workspace_symbols(query, ctx) 485 | } 486 | 487 | fn format( 488 | &self, 489 | doc: Document<'_>, 490 | tab_size: u16, 491 | _ctx: &LspContext, 492 | ) -> anyhow::Result> { 493 | self.format_document(doc, tab_size) 494 | } 495 | } 496 | 497 | fn location(span: ast::Span, sources: &ast::SourceMap, ctx: &LspContext) -> Option { 498 | let file = sources.get(span.file)?; 499 | Some(lsp::Location { 500 | uri: ctx.uri(file.path()).ok()?, 501 | range: range(span, file)?, 502 | }) 503 | } 504 | 505 | fn range(span: ast::Span, file: &ast::File) -> Option { 506 | let start = file.lookup(span.start); 507 | let end = file.lookup(span.end); 508 | Some(lsp::Range { 509 | start: lsp::Position::new(start.line as u32, start.col as u32), 510 | end: lsp::Position::new(end.line as u32, end.col as u32), 511 | }) 512 | } 513 | 514 | #[self_referencing] 515 | struct CompilationCache { 516 | cache_bytes: Vec, 517 | interner: TypeInterner, 518 | sources: ast::SourceMap, 519 | file_ids: HashMap, 520 | 521 | #[borrows(cache_bytes, interner, sources)] 522 | #[not_covariant] 523 | symbols: Symbols<'this>, 524 | 525 | #[borrows(sources)] 526 | #[not_covariant] 527 | modules: Vec>, 528 | } 529 | 530 | impl CompilationCache { 531 | pub fn make( 532 | cache_bytes: Vec, 533 | workspace_dirs: impl IntoIterator>, 534 | interner: TypeInterner, 535 | ) -> anyhow::Result { 536 | let sources = ast::SourceMap::from_paths_recursively(workspace_dirs)?; 537 | 538 | let file_ids = sources 539 | .files() 540 | .map(|(id, file)| (file.path().to_owned(), id)) 541 | .collect(); 542 | sources.populate_boot_lib(); 543 | 544 | Self::try_new( 545 | cache_bytes, 546 | interner, 547 | sources, 548 | file_ids, 549 | |bytes, interner, _| { 550 | let bundle = ScriptBundle::from_bytes(bytes)?; 551 | Ok(CompilationInputs::load_without_mapping(&bundle, interner)?) 552 | }, 553 | |sources| { 554 | let mut reporter = CompileErrorReporter::default(); 555 | Ok(parse_files(sources, &mut reporter)) 556 | }, 557 | ) 558 | } 559 | 560 | pub fn remake( 561 | &mut self, 562 | dirs: impl IntoIterator>, 563 | ) -> anyhow::Result<()> { 564 | let cache = mem::take(self).into_heads(); 565 | *self = CompilationCache::make(cache.cache_bytes, dirs, cache.interner)?; 566 | Ok(()) 567 | } 568 | } 569 | 570 | impl Default for CompilationCache { 571 | fn default() -> Self { 572 | Self::new( 573 | vec![], 574 | TypeInterner::default(), 575 | ast::SourceMap::default(), 576 | HashMap::default(), 577 | |_, _, _| Symbols::with_default_types(), 578 | |_| vec![], 579 | ) 580 | } 581 | } 582 | 583 | #[derive(Debug)] 584 | struct WorkspaceDir { 585 | roots: Vec, 586 | format_settings: FormatSettings, 587 | } 588 | 589 | impl WorkspaceDir { 590 | fn load(dir: &Path) -> anyhow::Result { 591 | let dotfile = Dotfile::load_or_default(dir)?; 592 | let roots = dotfile 593 | .source_roots 594 | .into_iter() 595 | .map(|p| dir.join(p)) 596 | .collect(); 597 | 598 | let mut settings = FormatSettings::default(); 599 | if let Some(indent) = dotfile.format.indent { 600 | settings.indent = indent; 601 | } 602 | if let Some(max_width) = dotfile.format.max_width { 603 | settings.max_width = max_width; 604 | } 605 | if let Some(max_chain_calls) = dotfile.format.max_chain_calls { 606 | settings.max_chain_calls = max_chain_calls; 607 | } 608 | if let Some(max_chain_fields) = dotfile.format.max_chain_fields { 609 | settings.max_chain_fields = max_chain_fields; 610 | } 611 | if let Some(max_chain_operators) = dotfile.format.max_chain_operators { 612 | settings.max_chain_operators = max_chain_operators; 613 | } 614 | if let Some(max_chain_total) = dotfile.format.max_chain_total { 615 | settings.max_chain_total = max_chain_total; 616 | } 617 | 618 | Ok(Self { 619 | roots, 620 | format_settings: settings, 621 | }) 622 | } 623 | } 624 | 625 | #[derive(Debug)] 626 | enum FileResolution<'a> { 627 | Workspace(&'a WorkspaceDir), 628 | NonWorkspace(PathBuf), 629 | } 630 | 631 | impl FileResolution<'_> { 632 | pub fn as_workspace(&self) -> Option<&WorkspaceDir> { 633 | match self { 634 | Self::Workspace(ws) => Some(ws), 635 | Self::NonWorkspace(_) => None, 636 | } 637 | } 638 | } 639 | 640 | #[derive(Debug)] 641 | struct CachedCompletions { 642 | completions: Rc, 643 | file: PathBuf, 644 | pos: u32, 645 | } 646 | 647 | impl CachedCompletions { 648 | fn new(completions: Rc, file: PathBuf, pos: u32) -> Self { 649 | Self { 650 | completions, 651 | file, 652 | pos, 653 | } 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use anyhow::bail; 4 | use ls::RedscriptLanguageServer; 5 | use mimalloc::MiMalloc; 6 | use serde::Deserialize; 7 | use server::LspServer; 8 | 9 | mod buffers; 10 | mod completions; 11 | mod display; 12 | mod ls; 13 | mod query; 14 | mod server; 15 | 16 | #[global_allocator] 17 | static GLOBAL: MiMalloc = MiMalloc; 18 | 19 | #[derive(Debug, Deserialize)] 20 | struct InitializationOptions { 21 | game_dir: PathBuf, 22 | } 23 | 24 | fn main() -> anyhow::Result<()> { 25 | LspServer::spawn( 26 | |opts, _ctx| { 27 | let game_dir = if let Some(opts) = opts 28 | .init_options 29 | .map(serde_json::from_value::) 30 | .transpose()? 31 | { 32 | opts.game_dir 33 | } else { 34 | bail!("game directory initialization options were not provided"); 35 | }; 36 | Ok((opts.workspace_dirs, game_dir)) 37 | }, 38 | |(dirs, game_dir), ctx| { 39 | let ls = RedscriptLanguageServer::new(&find_cache_file(game_dir)?, dirs)?; 40 | ls.check_workspace_and_publish(ctx)?; 41 | Ok(Box::new(ls)) 42 | }, 43 | ) 44 | } 45 | 46 | fn find_cache_file(game_dir: &Path) -> anyhow::Result { 47 | let default = game_dir 48 | .join("r6") 49 | .join("cache") 50 | .join("modded") 51 | .join("final.redscripts.bk"); 52 | if default.exists() { 53 | return Ok(default); 54 | } 55 | 56 | let fallback = game_dir 57 | .join("r6") 58 | .join("cache") 59 | .join("final.redscripts.bk"); 60 | if fallback.exists() { 61 | return Ok(fallback); 62 | } 63 | 64 | let fallback = game_dir.join("r6").join("cache").join("final.redscripts"); 65 | if fallback.exists() { 66 | return Ok(fallback); 67 | } 68 | 69 | bail!("cache file not found") 70 | } 71 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use redscript_compiler_api::ast::{SourceMap, Span}; 4 | use redscript_compiler_api::types::{Type, TypeApp}; 5 | use redscript_compiler_api::{FunctionType, LoweredFunction, PolyType, Symbols, TypeId, ir}; 6 | 7 | use crate::display::ExprAtDisplay; 8 | 9 | #[derive(Clone)] 10 | pub struct ExprAt<'ctx, 'a> { 11 | expr: Option<&'a ir::Expr<'ctx>>, 12 | func: Option<&'a LoweredFunction<'ctx>>, 13 | type_: Option>, 14 | symbols: &'a Symbols<'ctx>, 15 | sources: &'ctx SourceMap, 16 | context: Option, 17 | } 18 | 19 | impl<'ctx, 'a> ExprAt<'ctx, 'a> { 20 | pub fn new( 21 | expr: Option<&'a ir::Expr<'ctx>>, 22 | func: Option<&'a LoweredFunction<'ctx>>, 23 | type_: Option>, 24 | symbols: &'a Symbols<'ctx>, 25 | sources: &'ctx SourceMap, 26 | context: Option, 27 | ) -> Self { 28 | Self { 29 | expr, 30 | func, 31 | type_, 32 | symbols, 33 | sources, 34 | context, 35 | } 36 | } 37 | 38 | pub fn expr(&self) -> Option<&'a ir::Expr<'ctx>> { 39 | self.expr 40 | } 41 | 42 | pub fn func(&self) -> Option<&'a LoweredFunction<'ctx>> { 43 | self.func 44 | } 45 | 46 | pub fn type_(&self) -> Option> { 47 | self.type_ 48 | } 49 | 50 | pub fn symbols(&self) -> &'a Symbols<'ctx> { 51 | self.symbols 52 | } 53 | 54 | pub fn sources(&self) -> &'ctx SourceMap { 55 | self.sources 56 | } 57 | 58 | pub fn context(&self) -> Option { 59 | self.context 60 | } 61 | 62 | pub fn expr_type(&self) -> Option> { 63 | if let (Some(expr), Some(func)) = (self.expr, self.func) { 64 | type_of(expr, func, self.symbols) 65 | } else { 66 | None 67 | } 68 | } 69 | 70 | pub fn definition_span(&self) -> Option { 71 | if let Some(typ) = self.type_ { 72 | self.symbols[typ].span() 73 | } else if let Some(expr) = self.expr { 74 | match expr { 75 | ir::Expr::Call { call, .. } => match &**call { 76 | ir::Call::FreeFunction { function, .. } => self.symbols[*function].span(), 77 | ir::Call::Instance { method, .. } | ir::Call::Static { method, .. } => { 78 | self.symbols[*method].span() 79 | } 80 | _ => None, 81 | }, 82 | ir::Expr::Field { field, .. } => self.symbols[*field].span(), 83 | _ => None, 84 | } 85 | } else { 86 | None 87 | } 88 | } 89 | 90 | pub fn display(self) -> impl Display { 91 | ExprAtDisplay::new(self) 92 | } 93 | } 94 | 95 | #[derive(Debug, Clone, Copy)] 96 | pub enum AtContext { 97 | Expr, 98 | } 99 | 100 | pub fn type_of<'ctx>( 101 | expr: &ir::Expr<'ctx>, 102 | func: &LoweredFunction<'ctx>, 103 | symbols: &Symbols<'ctx>, 104 | ) -> Option> { 105 | let t = match expr { 106 | ir::Expr::NewClass { class_type, .. } => class_type, 107 | ir::Expr::NewStruct { struct_type, .. } => struct_type, 108 | ir::Expr::NewClosure { closure, .. } => &closure.typ, 109 | ir::Expr::Call { call, .. } => { 110 | let (fn_type, receiver_type_args, fn_type_args) = match &**call { 111 | ir::Call::FreeFunction { 112 | function, 113 | type_args, 114 | .. 115 | } => (symbols[*function].type_(), None, type_args), 116 | ir::Call::Instance { 117 | receiver_type, 118 | method, 119 | type_args, 120 | .. 121 | } => { 122 | let typ = receiver_type.coalesced(symbols).ok()?; 123 | ( 124 | symbols[*method].type_(), 125 | Some((typ.id(), typ.args().to_vec())), 126 | type_args, 127 | ) 128 | } 129 | ir::Call::Static { 130 | parent_id, 131 | parent_type_args, 132 | method, 133 | type_args, 134 | .. 135 | } => ( 136 | symbols[*method].type_(), 137 | Some(( 138 | *parent_id, 139 | parent_type_args 140 | .iter() 141 | .map(|t| t.coalesced(symbols).ok()) 142 | .collect::>>()?, 143 | )), 144 | type_args, 145 | ), 146 | ir::Call::Closure { closure_type, .. } => { 147 | return closure_type.coalesced(symbols).ok()?.args().last().cloned(); 148 | } 149 | }; 150 | let fn_type_args = fn_type_args 151 | .iter() 152 | .map(|t| t.coalesced(symbols).ok()) 153 | .collect::>>()?; 154 | return type_of_call( 155 | fn_type, 156 | receiver_type_args 157 | .as_ref() 158 | .map(|(id, args)| (*id, &args[..])), 159 | &fn_type_args, 160 | symbols, 161 | ); 162 | } 163 | ir::Expr::Assign { place, .. } => return type_of(place, func, symbols), 164 | ir::Expr::Field { 165 | receiver_type, 166 | field, 167 | .. 168 | } => { 169 | let env = TypeApp::from_type(&receiver_type.coalesced(symbols).ok()?).type_env(symbols); 170 | return PolyType::from_type_with_env(symbols[*field].type_(), &env) 171 | .ok()? 172 | .coalesce(symbols) 173 | .ok(); 174 | } 175 | ir::Expr::Index { array_type, .. } => match array_type.coalesced(symbols).ok()? { 176 | Type::Data(app) => return app.args().first().cloned(), 177 | _ => return None, 178 | }, 179 | ir::Expr::Conditional { then, .. } => return type_of(then, func, symbols), 180 | ir::Expr::DynCast { target_type, .. } => target_type, 181 | &ir::Expr::Local(local, _) => { 182 | return func.find_local(local)?.coalesce(symbols).ok(); 183 | } 184 | &ir::Expr::Capture(local, _) => { 185 | return func.find_local(local)?.coalesce(symbols).ok(); 186 | } 187 | ir::Expr::Const(const_, _) => return Some(Type::nullary(const_.type_id())), 188 | _ => return None, 189 | }; 190 | Some(t.coalesced(symbols).ok()?.into_type()) 191 | } 192 | 193 | fn type_of_call<'ctx>( 194 | ft: &FunctionType<'ctx>, 195 | receiver: Option<(TypeId<'ctx>, &[Type<'ctx>])>, 196 | type_args: &[Type<'ctx>], 197 | symbols: &Symbols<'ctx>, 198 | ) -> Option> { 199 | let env = receiver 200 | .iter() 201 | .flat_map(|&(id, args)| symbols[id].vars().zip(args)) 202 | .chain(ft.type_vars().zip(type_args)) 203 | .map(|(n, arg)| Some((n, PolyType::from_type(arg)))) 204 | .collect::>()?; 205 | 206 | PolyType::from_type_with_env(ft.return_type(), &env) 207 | .ok()? 208 | .coalesce(symbols) 209 | .ok() 210 | } 211 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::path::{Path, PathBuf}; 3 | use std::rc::Rc; 4 | use std::str::FromStr; 5 | 6 | use anyhow::{Context, anyhow, bail}; 7 | use crossbeam_channel::Sender; 8 | use lsp_server::{Connection, ErrorCode, Message, RequestId, Response, ResponseError}; 9 | use lsp_types::notification::Notification; 10 | use lsp_types::request::Request; 11 | use lsp_types::{self as lsp}; 12 | use serde::Serialize; 13 | use serde_json::{Value, json}; 14 | 15 | use crate::buffers::{Buffer, Buffers}; 16 | 17 | pub struct LspServer<'a> { 18 | connection: Connection, 19 | server: Box, 20 | buffers: Buffers, 21 | context: LspContext, 22 | } 23 | 24 | impl LspServer<'_> { 25 | pub fn spawn

( 26 | create_props: impl Fn(Options, &LspContext) -> anyhow::Result

, 27 | create_server: impl for<'a> Fn( 28 | &'a P, 29 | &LspContext, 30 | ) -> anyhow::Result>, 31 | ) -> anyhow::Result<()> { 32 | let (connection, _io) = Connection::stdio(); 33 | let context = LspContext::new(connection.sender.clone()); 34 | let (id, params) = connection.initialize_start()?; 35 | 36 | let init_params: lsp::InitializeParams = serde_json::from_value(params)?; 37 | let workspace_folders = init_params 38 | .workspace_folders 39 | .unwrap_or_default() 40 | .into_iter() 41 | .map(|folder| path_from_uri(&folder.uri)) 42 | .collect::, _>>()?; 43 | let options = Options::new(workspace_folders, init_params.initialization_options); 44 | let props = create_props(options, &context)?; 45 | 46 | let server_capabilities = capabilities(); 47 | let initialize_data = json!({ 48 | "capabilities": server_capabilities, 49 | "serverInfo": { 50 | "name": env!("CARGO_PKG_NAME"), 51 | "version": env!("CARGO_PKG_VERSION") 52 | } 53 | }); 54 | connection.initialize_finish(id, initialize_data)?; 55 | 56 | let server = create_server(&props, &context)?; 57 | let mut this = LspServer { 58 | connection, 59 | server, 60 | context: context.clone(), 61 | buffers: Buffers::default(), 62 | }; 63 | 64 | loop { 65 | match this.handle() { 66 | Ok(true) => break, 67 | Ok(false) => {} 68 | Err(err) => { 69 | context.logger().error(err); 70 | } 71 | } 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | fn handle(&mut self) -> anyhow::Result { 78 | match self.connection.receiver.recv()? { 79 | Message::Request(request) => match request.method.as_str() { 80 | lsp::request::HoverRequest::METHOD => { 81 | let lsp::HoverParams { 82 | text_document_position_params: params, 83 | .. 84 | } = serde_json::from_value(request.params)?; 85 | let doc = self.document_location(¶ms.text_document.uri, params.position)?; 86 | self.handle_response(request.id, self.server.hover(doc, &self.context))?; 87 | } 88 | lsp::request::Completion::METHOD => { 89 | let lsp::CompletionParams { 90 | text_document_position: params, 91 | .. 92 | } = serde_json::from_value(request.params)?; 93 | let doc = self.document_location(¶ms.text_document.uri, params.position)?; 94 | let completion = self.server.completion(doc, &self.context); 95 | self.handle_response( 96 | request.id, 97 | completion.as_deref().map_err(|err| anyhow!("{err}")), 98 | )?; 99 | } 100 | lsp::request::GotoDefinition::METHOD => { 101 | let lsp::GotoDefinitionParams { 102 | text_document_position_params: params, 103 | .. 104 | } = serde_json::from_value(request.params)?; 105 | let doc = self.document_location(¶ms.text_document.uri, params.position)?; 106 | self.handle_response( 107 | request.id, 108 | self.server.goto_definition(doc, &self.context), 109 | )?; 110 | } 111 | lsp::request::WorkspaceSymbolRequest::METHOD => { 112 | let lsp::WorkspaceSymbolParams { query, .. } = 113 | serde_json::from_value(request.params)?; 114 | self.handle_response( 115 | request.id, 116 | self.server.workspace_symbol(&query, &self.context), 117 | )?; 118 | } 119 | lsp::request::Formatting::METHOD => { 120 | let lsp::DocumentFormattingParams { 121 | text_document, 122 | options, 123 | .. 124 | } = serde_json::from_value(request.params)?; 125 | let doc = self.document(&text_document.uri)?; 126 | self.handle_response( 127 | request.id, 128 | self.server 129 | .format(doc, options.tab_size as u16, &self.context), 130 | )?; 131 | } 132 | lsp::request::Shutdown::METHOD => { 133 | self.handle_response(request.id, Ok(Value::Null))?; 134 | } 135 | _ => { 136 | self.context 137 | .logger() 138 | .error(format!("Unknown request: {}", request.method)); 139 | } 140 | }, 141 | 142 | Message::Notification(notif) => match notif.method.as_str() { 143 | lsp::notification::DidSaveTextDocument::METHOD => { 144 | let lsp::DidSaveTextDocumentParams { 145 | text_document: doc, .. 146 | } = serde_json::from_value(notif.params)?; 147 | self.server.check(path_from_uri(&doc.uri)?, &self.context)?; 148 | } 149 | lsp::notification::DidOpenTextDocument::METHOD => { 150 | let lsp::DidOpenTextDocumentParams { text_document: doc } = 151 | serde_json::from_value(notif.params)?; 152 | self.buffers.add(doc.uri, doc.text); 153 | } 154 | lsp::notification::DidCloseTextDocument::METHOD => { 155 | let lsp::DidCloseTextDocumentParams { text_document: doc } = 156 | serde_json::from_value(notif.params)?; 157 | self.buffers.remove(&doc.uri); 158 | } 159 | lsp::notification::DidChangeTextDocument::METHOD => { 160 | let lsp::DidChangeTextDocumentParams { 161 | text_document: doc, 162 | content_changes, 163 | } = serde_json::from_value(notif.params)?; 164 | for change in content_changes { 165 | match change.range { 166 | Some(range) => self.buffers.update_range(&doc.uri, range, change.text), 167 | None => self.buffers.add(doc.uri.clone(), change.text), 168 | } 169 | } 170 | } 171 | lsp::notification::DidChangeWorkspaceFolders::METHOD => { 172 | let lsp::DidChangeWorkspaceFoldersParams { event } = 173 | serde_json::from_value(notif.params)?; 174 | let added = event 175 | .added 176 | .into_iter() 177 | .map(|folder| path_from_uri(&folder.uri)) 178 | .collect::, _>>()?; 179 | let removed = event 180 | .removed 181 | .into_iter() 182 | .map(|uri| path_from_uri(&uri.uri)) 183 | .collect::, _>>()?; 184 | self.server 185 | .change_workspace_folders(added, removed, &self.context)?; 186 | } 187 | lsp::notification::Exit::METHOD => { 188 | return Ok(true); 189 | } 190 | _ => { 191 | self.context.logger().info(format!( 192 | "Received an unexpected notification: {}", 193 | notif.method 194 | )); 195 | } 196 | }, 197 | Message::Response(response) => { 198 | self.context 199 | .logger() 200 | .info(format!("Received an unexpected response: {}", response.id)); 201 | } 202 | }; 203 | Ok(false) 204 | } 205 | 206 | fn handle_response( 207 | &self, 208 | id: RequestId, 209 | res: anyhow::Result, 210 | ) -> anyhow::Result<()> { 211 | match res { 212 | Ok(resp) => self.connection.sender.send(Message::Response(Response { 213 | id, 214 | result: Some(serde_json::to_value(&resp)?), 215 | error: None, 216 | }))?, 217 | Err(err) => self.connection.sender.send(Message::Response(Response { 218 | id, 219 | result: None, 220 | error: Some(ResponseError { 221 | code: ErrorCode::InternalError as i32, 222 | message: err.to_string(), 223 | data: None, 224 | }), 225 | }))?, 226 | }; 227 | Ok(()) 228 | } 229 | 230 | fn document(&self, uri: &lsp::Uri) -> anyhow::Result> { 231 | let path = path_from_uri(uri)?; 232 | let content = self.buffers.get(uri).context("URI not found")?; 233 | Ok(Document::new(path, content)) 234 | } 235 | 236 | fn document_location( 237 | &self, 238 | uri: &lsp::Uri, 239 | pos: lsp::Position, 240 | ) -> anyhow::Result> { 241 | let document = self.document(uri)?; 242 | let pos = document 243 | .body 244 | .get_pos(pos.line, pos.character) 245 | .context("position not found")?; 246 | Ok(CodeLocation::new(document, pos)) 247 | } 248 | } 249 | 250 | pub trait LanguageServer { 251 | fn check(&mut self, path: PathBuf, ctx: &LspContext) -> anyhow::Result<()>; 252 | fn change_workspace_folders( 253 | &mut self, 254 | added: Vec, 255 | removed: Vec, 256 | ctx: &LspContext, 257 | ) -> anyhow::Result<()>; 258 | fn hover(&self, doc: CodeLocation<'_>, ctx: &LspContext) -> anyhow::Result; 259 | fn completion( 260 | &self, 261 | doc: CodeLocation<'_>, 262 | ctx: &LspContext, 263 | ) -> anyhow::Result>; 264 | fn goto_definition( 265 | &self, 266 | doc: CodeLocation<'_>, 267 | ctx: &LspContext, 268 | ) -> anyhow::Result; 269 | fn workspace_symbol( 270 | &self, 271 | query: &str, 272 | ctx: &LspContext, 273 | ) -> anyhow::Result; 274 | fn format( 275 | &self, 276 | doc: Document<'_>, 277 | tab_size: u16, 278 | ctx: &LspContext, 279 | ) -> anyhow::Result>; 280 | } 281 | 282 | #[derive(Debug, Clone)] 283 | pub struct LspContext { 284 | sender: Sender, 285 | } 286 | 287 | impl LspContext { 288 | fn new(sender: Sender) -> Self { 289 | Self { sender } 290 | } 291 | 292 | pub fn logger(&self) -> LspLog<'_> { 293 | LspLog::new(self) 294 | } 295 | 296 | pub fn notify(&self, params: N::Params) { 297 | let notification = lsp_server::Notification::new(N::METHOD.into(), params); 298 | self.sender.send(Message::Notification(notification)).ok(); 299 | } 300 | 301 | pub fn uri(&self, path: &Path) -> anyhow::Result { 302 | uri_from_path(path) 303 | } 304 | } 305 | 306 | #[derive(Debug)] 307 | pub struct LspLog<'a> { 308 | context: &'a LspContext, 309 | } 310 | 311 | impl<'a> LspLog<'a> { 312 | fn new(context: &'a LspContext) -> Self { 313 | Self { context } 314 | } 315 | 316 | pub fn info(&self, message: impl Display) { 317 | self.context 318 | .notify::(lsp::LogMessageParams { 319 | typ: lsp::MessageType::INFO, 320 | message: message.to_string(), 321 | }); 322 | } 323 | 324 | pub fn error(&self, message: impl Display) { 325 | self.context 326 | .notify::(lsp::LogMessageParams { 327 | typ: lsp::MessageType::ERROR, 328 | message: message.to_string(), 329 | }); 330 | } 331 | } 332 | 333 | #[derive(Debug, Clone)] 334 | pub struct Document<'a> { 335 | path: PathBuf, 336 | body: &'a Buffer, 337 | } 338 | 339 | impl<'a> Document<'a> { 340 | fn new(path: PathBuf, body: &'a Buffer) -> Self { 341 | Self { path, body } 342 | } 343 | 344 | pub fn path(&self) -> &Path { 345 | &self.path 346 | } 347 | 348 | pub fn buffer(&self) -> &Buffer { 349 | self.body 350 | } 351 | } 352 | 353 | #[derive(Debug, Clone)] 354 | pub struct CodeLocation<'a> { 355 | document: Document<'a>, 356 | pos: u32, 357 | } 358 | 359 | impl<'a> CodeLocation<'a> { 360 | fn new(document: Document<'a>, pos: u32) -> Self { 361 | Self { document, pos } 362 | } 363 | 364 | pub fn doc(&self) -> &Document<'a> { 365 | &self.document 366 | } 367 | 368 | pub fn pos(&self) -> u32 { 369 | self.pos 370 | } 371 | 372 | pub fn with_pos(self, pos: u32) -> Self { 373 | Self { 374 | document: self.document, 375 | pos, 376 | } 377 | } 378 | } 379 | 380 | #[derive(Debug)] 381 | pub struct Options { 382 | pub workspace_dirs: Vec, 383 | pub init_options: Option, 384 | } 385 | 386 | impl Options { 387 | fn new(workspace_dirs: Vec, init_options: Option) -> Self { 388 | Self { 389 | workspace_dirs, 390 | init_options, 391 | } 392 | } 393 | } 394 | 395 | fn path_from_uri(uri: &lsp::Uri) -> anyhow::Result { 396 | if !uri 397 | .scheme() 398 | .is_some_and(|scheme| scheme.eq_lowercase("file")) 399 | { 400 | bail!("Unsupported URI scheme"); 401 | } 402 | 403 | let stripped = uri.path().as_estr().decode().into_string()?; 404 | #[cfg(windows)] 405 | let path = PathBuf::from(stripped.trim_start_matches("/")); 406 | #[cfg(not(windows))] 407 | let path = PathBuf::from(&*stripped); 408 | Ok(path) 409 | } 410 | 411 | fn uri_from_path(p: &Path) -> anyhow::Result { 412 | let mut path = fluent_uri::encoding::EString::new(); 413 | path.encode::(p.as_os_str().as_encoded_bytes()); 414 | 415 | let uri = fluent_uri::Uri::builder() 416 | .scheme(fluent_uri::component::Scheme::new_or_panic("file")) 417 | .path(&path) 418 | .build()?; 419 | Ok(lsp::Uri::from_str(uri.as_str())?) 420 | } 421 | 422 | fn capabilities() -> lsp::ServerCapabilities { 423 | lsp::ServerCapabilities { 424 | text_document_sync: Some(lsp::TextDocumentSyncCapability::Kind( 425 | lsp::TextDocumentSyncKind::INCREMENTAL, 426 | )), 427 | completion_provider: Some(lsp::CompletionOptions { 428 | trigger_characters: Some(vec![".".to_owned()]), 429 | completion_item: Some(lsp::CompletionOptionsCompletionItem { 430 | label_details_support: Some(true), 431 | }), 432 | ..Default::default() 433 | }), 434 | hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), 435 | definition_provider: Some(lsp::OneOf::Left(true)), 436 | workspace: Some(lsp::WorkspaceServerCapabilities { 437 | workspace_folders: Some(lsp::WorkspaceFoldersServerCapabilities { 438 | supported: Some(true), 439 | change_notifications: Some(lsp::OneOf::Left(true)), 440 | }), 441 | ..Default::default() 442 | }), 443 | workspace_symbol_provider: Some(lsp::OneOf::Left(true)), 444 | document_formatting_provider: Some(lsp::OneOf::Left(true)), 445 | ..Default::default() 446 | } 447 | } 448 | --------------------------------------------------------------------------------