├── .github └── workflows │ ├── ci.yml │ └── upload-binaries.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md └── src ├── api ├── files.rs ├── mod.rs └── proxy.rs ├── lsp ├── error.rs ├── ext │ ├── mod.rs │ └── relative_uri.rs ├── framed │ ├── codec.rs │ ├── mod.rs │ └── parser.rs ├── mod.rs ├── notification.rs ├── request.rs ├── response.rs └── types.rs └── main.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | components: rustfmt, clippy 20 | - uses: Swatinem/rust-cache@v1 21 | - name: cargo check 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: check 25 | - name: cargo test 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: test 29 | - name: cargo clippy 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: clippy 33 | args: -- -D warnings 34 | - name: cargo fmt 35 | uses: actions-rs/cargo@v1 36 | with: 37 | command: fmt 38 | args: --all -- --check 39 | -------------------------------------------------------------------------------- /.github/workflows/upload-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Upload Binaries 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | upload: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | include: 14 | - name: linux 15 | os: ubuntu-latest 16 | bin_file: lsp-ws-proxy 17 | - name: macos 18 | os: macos-latest 19 | bin_file: lsp-ws-proxy 20 | - name: windows 21 | os: windows-latest 22 | bin_file: lsp-ws-proxy.exe 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | override: true 29 | toolchain: stable 30 | profile: minimal 31 | - uses: Swatinem/rust-cache@v1 32 | - uses: actions-rs/cargo@v1 33 | with: 34 | command: build 35 | args: --release --locked 36 | 37 | - run: cd target/release && tar czf ${{ matrix.bin_file }}.tar.gz ${{ matrix.bin_file }} 38 | 39 | - name: Upload Binary 40 | uses: actions/upload-release-asset@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | upload_url: ${{ github.event.release.upload_url }} 45 | asset_path: target/release/${{ matrix.bin_file }}.tar.gz 46 | asset_name: lsp-ws-proxy_${{ matrix.name }}.tar.gz 47 | asset_content_type: application/octet-stream 48 | 49 | upload_musl: 50 | runs-on: ubuntu-latest 51 | env: 52 | TARGET: x86_64-unknown-linux-musl 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | override: true 58 | toolchain: stable 59 | profile: minimal 60 | target: ${{ env.TARGET }} 61 | - uses: Swatinem/rust-cache@v1 62 | 63 | - uses: actions-rs/cargo@v1 64 | with: 65 | use-cross: true 66 | command: build 67 | args: --release --locked --target x86_64-unknown-linux-musl 68 | 69 | - run: cd target/${{ env.TARGET }}/release && tar czf lsp-ws-proxy.tar.gz lsp-ws-proxy 70 | 71 | - name: Upload Binary 72 | uses: actions/upload-release-asset@v1 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | with: 76 | upload_url: ${{ github.event.release.upload_url }} 77 | asset_path: target/${{ env.TARGET }}/release/lsp-ws-proxy.tar.gz 78 | asset_name: lsp-ws-proxy_linux-musl.tar.gz 79 | asset_content_type: application/octet-stream 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Using default 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "ansi_term" 13 | version = "0.12.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 16 | dependencies = [ 17 | "winapi", 18 | ] 19 | 20 | [[package]] 21 | name = "argh" 22 | version = "0.1.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "91792f088f87cdc7a2cfb1d617fa5ea18d7f1dc22ef0e1b5f82f3157cdc522be" 25 | dependencies = [ 26 | "argh_derive", 27 | "argh_shared", 28 | ] 29 | 30 | [[package]] 31 | name = "argh_derive" 32 | version = "0.1.4" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "c4eb0c0c120ad477412dc95a4ce31e38f2113e46bd13511253f79196ca68b067" 35 | dependencies = [ 36 | "argh_shared", 37 | "heck", 38 | "proc-macro2", 39 | "quote", 40 | "syn", 41 | ] 42 | 43 | [[package]] 44 | name = "argh_shared" 45 | version = "0.1.4" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "781f336cc9826dbaddb9754cb5db61e64cab4f69668bd19dcc4a0394a86f4cb1" 48 | 49 | [[package]] 50 | name = "autocfg" 51 | version = "1.0.1" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 54 | 55 | [[package]] 56 | name = "base64" 57 | version = "0.13.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 60 | 61 | [[package]] 62 | name = "bitflags" 63 | version = "1.2.1" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 66 | 67 | [[package]] 68 | name = "bitvec" 69 | version = "0.19.5" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" 72 | dependencies = [ 73 | "funty", 74 | "radium", 75 | "tap", 76 | "wyz", 77 | ] 78 | 79 | [[package]] 80 | name = "block-buffer" 81 | version = "0.9.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 84 | dependencies = [ 85 | "generic-array", 86 | ] 87 | 88 | [[package]] 89 | name = "byteorder" 90 | version = "1.4.3" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 93 | 94 | [[package]] 95 | name = "bytes" 96 | version = "1.0.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" 99 | 100 | [[package]] 101 | name = "cfg-if" 102 | version = "1.0.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 105 | 106 | [[package]] 107 | name = "chrono" 108 | version = "0.4.19" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 111 | dependencies = [ 112 | "libc", 113 | "num-integer", 114 | "num-traits", 115 | "winapi", 116 | ] 117 | 118 | [[package]] 119 | name = "cpufeatures" 120 | version = "0.1.4" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" 123 | dependencies = [ 124 | "libc", 125 | ] 126 | 127 | [[package]] 128 | name = "crc32fast" 129 | version = "1.2.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" 132 | dependencies = [ 133 | "cfg-if", 134 | ] 135 | 136 | [[package]] 137 | name = "digest" 138 | version = "0.9.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 141 | dependencies = [ 142 | "generic-array", 143 | ] 144 | 145 | [[package]] 146 | name = "flate2" 147 | version = "1.0.21" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "80edafed416a46fb378521624fab1cfa2eb514784fd8921adbe8a8d8321da811" 150 | dependencies = [ 151 | "cfg-if", 152 | "crc32fast", 153 | "libc", 154 | "miniz_oxide", 155 | ] 156 | 157 | [[package]] 158 | name = "fnv" 159 | version = "1.0.7" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 162 | 163 | [[package]] 164 | name = "form_urlencoded" 165 | version = "1.0.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 168 | dependencies = [ 169 | "matches", 170 | "percent-encoding", 171 | ] 172 | 173 | [[package]] 174 | name = "funty" 175 | version = "1.1.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" 178 | 179 | [[package]] 180 | name = "futures" 181 | version = "0.3.15" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" 184 | dependencies = [ 185 | "futures-channel", 186 | "futures-core", 187 | "futures-io", 188 | "futures-sink", 189 | "futures-task", 190 | "futures-util", 191 | ] 192 | 193 | [[package]] 194 | name = "futures-channel" 195 | version = "0.3.15" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" 198 | dependencies = [ 199 | "futures-core", 200 | "futures-sink", 201 | ] 202 | 203 | [[package]] 204 | name = "futures-core" 205 | version = "0.3.15" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" 208 | 209 | [[package]] 210 | name = "futures-io" 211 | version = "0.3.15" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" 214 | 215 | [[package]] 216 | name = "futures-macro" 217 | version = "0.3.15" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" 220 | dependencies = [ 221 | "autocfg", 222 | "proc-macro-hack", 223 | "proc-macro2", 224 | "quote", 225 | "syn", 226 | ] 227 | 228 | [[package]] 229 | name = "futures-sink" 230 | version = "0.3.15" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" 233 | 234 | [[package]] 235 | name = "futures-task" 236 | version = "0.3.15" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" 239 | 240 | [[package]] 241 | name = "futures-util" 242 | version = "0.3.15" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" 245 | dependencies = [ 246 | "autocfg", 247 | "futures-core", 248 | "futures-macro", 249 | "futures-sink", 250 | "futures-task", 251 | "pin-project-lite", 252 | "pin-utils", 253 | "proc-macro-hack", 254 | "proc-macro-nested", 255 | "slab", 256 | ] 257 | 258 | [[package]] 259 | name = "generic-array" 260 | version = "0.14.4" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" 263 | dependencies = [ 264 | "typenum", 265 | "version_check", 266 | ] 267 | 268 | [[package]] 269 | name = "getrandom" 270 | version = "0.2.3" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 273 | dependencies = [ 274 | "cfg-if", 275 | "libc", 276 | "wasi", 277 | ] 278 | 279 | [[package]] 280 | name = "headers" 281 | version = "0.3.4" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" 284 | dependencies = [ 285 | "base64", 286 | "bitflags", 287 | "bytes", 288 | "headers-core", 289 | "http", 290 | "mime", 291 | "sha-1", 292 | "time", 293 | ] 294 | 295 | [[package]] 296 | name = "headers-core" 297 | version = "0.2.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 300 | dependencies = [ 301 | "http", 302 | ] 303 | 304 | [[package]] 305 | name = "heck" 306 | version = "0.3.3" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 309 | dependencies = [ 310 | "unicode-segmentation", 311 | ] 312 | 313 | [[package]] 314 | name = "hermit-abi" 315 | version = "0.1.18" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 318 | dependencies = [ 319 | "libc", 320 | ] 321 | 322 | [[package]] 323 | name = "http" 324 | version = "0.2.4" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" 327 | dependencies = [ 328 | "bytes", 329 | "fnv", 330 | "itoa", 331 | ] 332 | 333 | [[package]] 334 | name = "http-body" 335 | version = "0.4.2" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" 338 | dependencies = [ 339 | "bytes", 340 | "http", 341 | "pin-project-lite", 342 | ] 343 | 344 | [[package]] 345 | name = "httparse" 346 | version = "1.4.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" 349 | 350 | [[package]] 351 | name = "httpdate" 352 | version = "1.0.1" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" 355 | 356 | [[package]] 357 | name = "hyper" 358 | version = "0.14.9" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" 361 | dependencies = [ 362 | "bytes", 363 | "futures-channel", 364 | "futures-core", 365 | "futures-util", 366 | "http", 367 | "http-body", 368 | "httparse", 369 | "httpdate", 370 | "itoa", 371 | "pin-project-lite", 372 | "socket2", 373 | "tokio", 374 | "tower-service", 375 | "tracing", 376 | "want", 377 | ] 378 | 379 | [[package]] 380 | name = "idna" 381 | version = "0.2.3" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 384 | dependencies = [ 385 | "matches", 386 | "unicode-bidi", 387 | "unicode-normalization", 388 | ] 389 | 390 | [[package]] 391 | name = "itoa" 392 | version = "0.4.7" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 395 | 396 | [[package]] 397 | name = "lazy_static" 398 | version = "1.4.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 401 | 402 | [[package]] 403 | name = "libc" 404 | version = "0.2.97" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" 407 | 408 | [[package]] 409 | name = "log" 410 | version = "0.4.14" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 413 | dependencies = [ 414 | "cfg-if", 415 | ] 416 | 417 | [[package]] 418 | name = "lsp-types" 419 | version = "0.89.2" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "852e0dedfd52cc32325598b2631e0eba31b7b708959676a9f837042f276b09a2" 422 | dependencies = [ 423 | "bitflags", 424 | "serde", 425 | "serde_json", 426 | "serde_repr", 427 | "url", 428 | ] 429 | 430 | [[package]] 431 | name = "lsp-ws-proxy" 432 | version = "0.9.0-rc.4" 433 | dependencies = [ 434 | "argh", 435 | "bytes", 436 | "futures-util", 437 | "lsp-types", 438 | "nom", 439 | "serde", 440 | "serde_json", 441 | "thiserror", 442 | "tokio", 443 | "tokio-util", 444 | "tracing", 445 | "tracing-subscriber", 446 | "url", 447 | "warp", 448 | ] 449 | 450 | [[package]] 451 | name = "matchers" 452 | version = "0.0.1" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" 455 | dependencies = [ 456 | "regex-automata", 457 | ] 458 | 459 | [[package]] 460 | name = "matches" 461 | version = "0.1.8" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 464 | 465 | [[package]] 466 | name = "memchr" 467 | version = "2.4.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" 470 | 471 | [[package]] 472 | name = "mime" 473 | version = "0.3.16" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 476 | 477 | [[package]] 478 | name = "mime_guess" 479 | version = "2.0.3" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" 482 | dependencies = [ 483 | "mime", 484 | "unicase", 485 | ] 486 | 487 | [[package]] 488 | name = "miniz_oxide" 489 | version = "0.4.4" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 492 | dependencies = [ 493 | "adler", 494 | "autocfg", 495 | ] 496 | 497 | [[package]] 498 | name = "mio" 499 | version = "0.7.13" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" 502 | dependencies = [ 503 | "libc", 504 | "log", 505 | "miow", 506 | "ntapi", 507 | "winapi", 508 | ] 509 | 510 | [[package]] 511 | name = "miow" 512 | version = "0.3.7" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 515 | dependencies = [ 516 | "winapi", 517 | ] 518 | 519 | [[package]] 520 | name = "nom" 521 | version = "6.1.2" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" 524 | dependencies = [ 525 | "bitvec", 526 | "funty", 527 | "memchr", 528 | "version_check", 529 | ] 530 | 531 | [[package]] 532 | name = "ntapi" 533 | version = "0.3.6" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 536 | dependencies = [ 537 | "winapi", 538 | ] 539 | 540 | [[package]] 541 | name = "num-integer" 542 | version = "0.1.44" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 545 | dependencies = [ 546 | "autocfg", 547 | "num-traits", 548 | ] 549 | 550 | [[package]] 551 | name = "num-traits" 552 | version = "0.2.14" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 555 | dependencies = [ 556 | "autocfg", 557 | ] 558 | 559 | [[package]] 560 | name = "num_cpus" 561 | version = "1.13.0" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 564 | dependencies = [ 565 | "hermit-abi", 566 | "libc", 567 | ] 568 | 569 | [[package]] 570 | name = "once_cell" 571 | version = "1.8.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 574 | 575 | [[package]] 576 | name = "opaque-debug" 577 | version = "0.3.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 580 | 581 | [[package]] 582 | name = "percent-encoding" 583 | version = "2.1.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 586 | 587 | [[package]] 588 | name = "pin-project" 589 | version = "1.0.7" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" 592 | dependencies = [ 593 | "pin-project-internal", 594 | ] 595 | 596 | [[package]] 597 | name = "pin-project-internal" 598 | version = "1.0.7" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" 601 | dependencies = [ 602 | "proc-macro2", 603 | "quote", 604 | "syn", 605 | ] 606 | 607 | [[package]] 608 | name = "pin-project-lite" 609 | version = "0.2.6" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 612 | 613 | [[package]] 614 | name = "pin-utils" 615 | version = "0.1.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 618 | 619 | [[package]] 620 | name = "ppv-lite86" 621 | version = "0.2.10" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 624 | 625 | [[package]] 626 | name = "proc-macro-hack" 627 | version = "0.5.19" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 630 | 631 | [[package]] 632 | name = "proc-macro-nested" 633 | version = "0.1.7" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 636 | 637 | [[package]] 638 | name = "proc-macro2" 639 | version = "1.0.27" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 642 | dependencies = [ 643 | "unicode-xid", 644 | ] 645 | 646 | [[package]] 647 | name = "quote" 648 | version = "1.0.9" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 651 | dependencies = [ 652 | "proc-macro2", 653 | ] 654 | 655 | [[package]] 656 | name = "radium" 657 | version = "0.5.3" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" 660 | 661 | [[package]] 662 | name = "rand" 663 | version = "0.8.3" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" 666 | dependencies = [ 667 | "libc", 668 | "rand_chacha", 669 | "rand_core", 670 | "rand_hc", 671 | ] 672 | 673 | [[package]] 674 | name = "rand_chacha" 675 | version = "0.3.1" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 678 | dependencies = [ 679 | "ppv-lite86", 680 | "rand_core", 681 | ] 682 | 683 | [[package]] 684 | name = "rand_core" 685 | version = "0.6.2" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" 688 | dependencies = [ 689 | "getrandom", 690 | ] 691 | 692 | [[package]] 693 | name = "rand_hc" 694 | version = "0.3.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" 697 | dependencies = [ 698 | "rand_core", 699 | ] 700 | 701 | [[package]] 702 | name = "regex" 703 | version = "1.5.4" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 706 | dependencies = [ 707 | "regex-syntax", 708 | ] 709 | 710 | [[package]] 711 | name = "regex-automata" 712 | version = "0.1.10" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 715 | dependencies = [ 716 | "regex-syntax", 717 | ] 718 | 719 | [[package]] 720 | name = "regex-syntax" 721 | version = "0.6.25" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 724 | 725 | [[package]] 726 | name = "ryu" 727 | version = "1.0.5" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 730 | 731 | [[package]] 732 | name = "scoped-tls" 733 | version = "1.0.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 736 | 737 | [[package]] 738 | name = "serde" 739 | version = "1.0.126" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" 742 | dependencies = [ 743 | "serde_derive", 744 | ] 745 | 746 | [[package]] 747 | name = "serde_derive" 748 | version = "1.0.126" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" 751 | dependencies = [ 752 | "proc-macro2", 753 | "quote", 754 | "syn", 755 | ] 756 | 757 | [[package]] 758 | name = "serde_json" 759 | version = "1.0.64" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" 762 | dependencies = [ 763 | "itoa", 764 | "ryu", 765 | "serde", 766 | ] 767 | 768 | [[package]] 769 | name = "serde_repr" 770 | version = "0.1.7" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" 773 | dependencies = [ 774 | "proc-macro2", 775 | "quote", 776 | "syn", 777 | ] 778 | 779 | [[package]] 780 | name = "serde_urlencoded" 781 | version = "0.7.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" 784 | dependencies = [ 785 | "form_urlencoded", 786 | "itoa", 787 | "ryu", 788 | "serde", 789 | ] 790 | 791 | [[package]] 792 | name = "sha-1" 793 | version = "0.9.6" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" 796 | dependencies = [ 797 | "block-buffer", 798 | "cfg-if", 799 | "cpufeatures", 800 | "digest", 801 | "opaque-debug", 802 | ] 803 | 804 | [[package]] 805 | name = "sharded-slab" 806 | version = "0.1.1" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" 809 | dependencies = [ 810 | "lazy_static", 811 | ] 812 | 813 | [[package]] 814 | name = "signal-hook-registry" 815 | version = "1.4.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 818 | dependencies = [ 819 | "libc", 820 | ] 821 | 822 | [[package]] 823 | name = "slab" 824 | version = "0.4.3" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" 827 | 828 | [[package]] 829 | name = "smallvec" 830 | version = "1.6.1" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 833 | 834 | [[package]] 835 | name = "socket2" 836 | version = "0.4.0" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" 839 | dependencies = [ 840 | "libc", 841 | "winapi", 842 | ] 843 | 844 | [[package]] 845 | name = "syn" 846 | version = "1.0.73" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" 849 | dependencies = [ 850 | "proc-macro2", 851 | "quote", 852 | "unicode-xid", 853 | ] 854 | 855 | [[package]] 856 | name = "tap" 857 | version = "1.0.1" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 860 | 861 | [[package]] 862 | name = "thiserror" 863 | version = "1.0.26" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" 866 | dependencies = [ 867 | "thiserror-impl", 868 | ] 869 | 870 | [[package]] 871 | name = "thiserror-impl" 872 | version = "1.0.26" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" 875 | dependencies = [ 876 | "proc-macro2", 877 | "quote", 878 | "syn", 879 | ] 880 | 881 | [[package]] 882 | name = "thread_local" 883 | version = "1.1.3" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" 886 | dependencies = [ 887 | "once_cell", 888 | ] 889 | 890 | [[package]] 891 | name = "time" 892 | version = "0.1.43" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 895 | dependencies = [ 896 | "libc", 897 | "winapi", 898 | ] 899 | 900 | [[package]] 901 | name = "tinyvec" 902 | version = "1.2.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 905 | dependencies = [ 906 | "tinyvec_macros", 907 | ] 908 | 909 | [[package]] 910 | name = "tinyvec_macros" 911 | version = "0.1.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 914 | 915 | [[package]] 916 | name = "tokio" 917 | version = "1.6.1" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975" 920 | dependencies = [ 921 | "autocfg", 922 | "bytes", 923 | "libc", 924 | "memchr", 925 | "mio", 926 | "num_cpus", 927 | "once_cell", 928 | "pin-project-lite", 929 | "signal-hook-registry", 930 | "tokio-macros", 931 | "winapi", 932 | ] 933 | 934 | [[package]] 935 | name = "tokio-macros" 936 | version = "1.2.0" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" 939 | dependencies = [ 940 | "proc-macro2", 941 | "quote", 942 | "syn", 943 | ] 944 | 945 | [[package]] 946 | name = "tokio-stream" 947 | version = "0.1.6" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" 950 | dependencies = [ 951 | "futures-core", 952 | "pin-project-lite", 953 | "tokio", 954 | ] 955 | 956 | [[package]] 957 | name = "tokio-tungstenite" 958 | version = "0.15.0" 959 | source = "git+https://github.com/kazk/tokio-tungstenite?branch=permessage-deflate#4133a28b800529ec920ebe052496785e1cf37b3a" 960 | dependencies = [ 961 | "futures-util", 962 | "log", 963 | "pin-project", 964 | "tokio", 965 | "tungstenite", 966 | ] 967 | 968 | [[package]] 969 | name = "tokio-util" 970 | version = "0.6.7" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" 973 | dependencies = [ 974 | "bytes", 975 | "futures-core", 976 | "futures-sink", 977 | "log", 978 | "pin-project-lite", 979 | "tokio", 980 | ] 981 | 982 | [[package]] 983 | name = "tower-service" 984 | version = "0.3.1" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 987 | 988 | [[package]] 989 | name = "tracing" 990 | version = "0.1.26" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" 993 | dependencies = [ 994 | "cfg-if", 995 | "pin-project-lite", 996 | "tracing-attributes", 997 | "tracing-core", 998 | ] 999 | 1000 | [[package]] 1001 | name = "tracing-attributes" 1002 | version = "0.1.15" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" 1005 | dependencies = [ 1006 | "proc-macro2", 1007 | "quote", 1008 | "syn", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tracing-core" 1013 | version = "0.1.18" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" 1016 | dependencies = [ 1017 | "lazy_static", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "tracing-log" 1022 | version = "0.1.2" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" 1025 | dependencies = [ 1026 | "lazy_static", 1027 | "log", 1028 | "tracing-core", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "tracing-serde" 1033 | version = "0.1.2" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" 1036 | dependencies = [ 1037 | "serde", 1038 | "tracing-core", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "tracing-subscriber" 1043 | version = "0.2.18" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "aa5553bf0883ba7c9cbe493b085c29926bd41b66afc31ff72cf17ff4fb60dcd5" 1046 | dependencies = [ 1047 | "ansi_term", 1048 | "chrono", 1049 | "lazy_static", 1050 | "matchers", 1051 | "regex", 1052 | "serde", 1053 | "serde_json", 1054 | "sharded-slab", 1055 | "smallvec", 1056 | "thread_local", 1057 | "tracing", 1058 | "tracing-core", 1059 | "tracing-log", 1060 | "tracing-serde", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "try-lock" 1065 | version = "0.2.3" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1068 | 1069 | [[package]] 1070 | name = "tungstenite" 1071 | version = "0.15.0" 1072 | source = "git+https://github.com/kazk/tungstenite-rs?branch=permessage-deflate#734a0b983070d7f965c984d5489c5be038e01530" 1073 | dependencies = [ 1074 | "base64", 1075 | "byteorder", 1076 | "bytes", 1077 | "flate2", 1078 | "http", 1079 | "httparse", 1080 | "log", 1081 | "rand", 1082 | "sha-1", 1083 | "thiserror", 1084 | "url", 1085 | "utf-8", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "typenum" 1090 | version = "1.13.0" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" 1093 | 1094 | [[package]] 1095 | name = "unicase" 1096 | version = "2.6.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1099 | dependencies = [ 1100 | "version_check", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "unicode-bidi" 1105 | version = "0.3.5" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 1108 | dependencies = [ 1109 | "matches", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "unicode-normalization" 1114 | version = "0.1.19" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1117 | dependencies = [ 1118 | "tinyvec", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "unicode-segmentation" 1123 | version = "1.7.1" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 1126 | 1127 | [[package]] 1128 | name = "unicode-xid" 1129 | version = "0.2.2" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1132 | 1133 | [[package]] 1134 | name = "url" 1135 | version = "2.2.2" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1138 | dependencies = [ 1139 | "form_urlencoded", 1140 | "idna", 1141 | "matches", 1142 | "percent-encoding", 1143 | "serde", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "utf-8" 1148 | version = "0.7.6" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1151 | 1152 | [[package]] 1153 | name = "version_check" 1154 | version = "0.9.3" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1157 | 1158 | [[package]] 1159 | name = "want" 1160 | version = "0.3.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1163 | dependencies = [ 1164 | "log", 1165 | "try-lock", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "warp" 1170 | version = "0.3.1" 1171 | source = "git+https://github.com/kazk/warp?branch=permessage-deflate#a7425b320916e6f25e4f77b09ddd24490ccf62a4" 1172 | dependencies = [ 1173 | "bytes", 1174 | "futures", 1175 | "headers", 1176 | "http", 1177 | "hyper", 1178 | "log", 1179 | "mime", 1180 | "mime_guess", 1181 | "percent-encoding", 1182 | "pin-project", 1183 | "scoped-tls", 1184 | "serde", 1185 | "serde_json", 1186 | "serde_urlencoded", 1187 | "tokio", 1188 | "tokio-stream", 1189 | "tokio-tungstenite", 1190 | "tokio-util", 1191 | "tower-service", 1192 | "tracing", 1193 | ] 1194 | 1195 | [[package]] 1196 | name = "wasi" 1197 | version = "0.10.2+wasi-snapshot-preview1" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1200 | 1201 | [[package]] 1202 | name = "winapi" 1203 | version = "0.3.9" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1206 | dependencies = [ 1207 | "winapi-i686-pc-windows-gnu", 1208 | "winapi-x86_64-pc-windows-gnu", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "winapi-i686-pc-windows-gnu" 1213 | version = "0.4.0" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1216 | 1217 | [[package]] 1218 | name = "winapi-x86_64-pc-windows-gnu" 1219 | version = "0.4.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1222 | 1223 | [[package]] 1224 | name = "wyz" 1225 | version = "0.2.0" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" 1228 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lsp-ws-proxy" 3 | version = "0.9.0-rc.4" 4 | authors = ["kazk "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "WebSocketify any Language Server" 8 | homepage = "https://github.com/qualified/lsp-ws-proxy" 9 | repository = "https://github.com/qualified/lsp-ws-proxy" 10 | readme = "README.md" 11 | keywords = ["lsp", "websocket"] 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | argh = "0.1.4" 16 | bytes = "1.0.1" 17 | futures-util = "0.3.15" 18 | lsp-types = "0.89.2" 19 | nom = { version = "6.1.2", default-features = false, features = ["std"] } 20 | serde = { version = "1.0.126", features = ["derive"] } 21 | serde_json = "1.0.64" 22 | url = "2.2.2" 23 | 24 | tokio = { version = "1.6.1", features = ["fs", "process", "macros", "rt", "rt-multi-thread", "time"] } 25 | tokio-util = { version = "0.6.7", features = ["codec"] } 26 | warp = { git = "https://github.com/kazk/warp", branch = "permessage-deflate", default-features = false, features = ["websocket"] } 27 | 28 | tracing = "0.1.26" 29 | tracing-subscriber = "0.2.18" 30 | thiserror = "1.0.26" 31 | 32 | [dev-dependencies] 33 | 34 | [profile.release] 35 | # Turn Link Time Optimization on to reduce the binary size. 36 | # See https://doc.rust-lang.org/cargo/reference/profiles.html#lto 37 | lto = true 38 | 39 | # Strip symbols from the final build (nightly) 40 | # See https://github.com/rust-lang/cargo/issues/3483#issuecomment-631395566 41 | # strip = "symbols" 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Qualified, Inc 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 | # lsp-ws-proxy 2 | 3 | WebSocket proxy for Language Servers. 4 | 5 | ## Usage 6 | 7 | ``` 8 | $ lsp-ws-proxy --help 9 | 10 | Usage: lsp-ws-proxy [-l ] [-s] [-r] [-v] 11 | 12 | Start WebSocket proxy for the LSP Server. 13 | Anything after the option delimiter is used to start the server. 14 | 15 | Multiple servers can be registered by separating each with an option delimiter, 16 | and using the query parameter `name` to specify the command name on connection. 17 | If no query parameter is present, the first one is started. 18 | 19 | Examples: 20 | lsp-ws-proxy -- rust-analyzer 21 | lsp-ws-proxy -- typescript-language-server --stdio 22 | lsp-ws-proxy --listen 8888 -- rust-analyzer 23 | lsp-ws-proxy --listen 0.0.0.0:8888 -- rust-analyzer 24 | # Register multiple servers. 25 | # Choose the server with query parameter `name` when connecting. 26 | lsp-ws-proxy --listen 9999 --sync --remap \ 27 | -- typescript-language-server --stdio \ 28 | -- css-languageserver --stdio \ 29 | -- html-languageserver --stdio 30 | 31 | Options: 32 | -l, --listen address or port to listen on (default: 0.0.0.0:9999) 33 | -s, --sync write text document to disk on save, and enable `/files` 34 | endpoint 35 | -r, --remap remap relative uri (source://) 36 | -v, --version show version and exit 37 | --help display usage information 38 | ``` 39 | 40 | ## Why? 41 | 42 | Remote Language Server is necessary when it's not possible to run the server next to the client. 43 | 44 | For example, this can be used to let in-browser editors like [CodeMirror][codemirror] and [Monaco][monaco] to use any Language Servers. 45 | See [qualified/lsps] for an example of using proxied [Rust Analyzer][rust-analyzer] with CodeMirror. 46 | 47 | ## Features 48 | 49 | - [x] Proxy messages 50 | - [x] Synchronize files 51 | - [x] Manipulate remote files with `POST /files` 52 | - [x] Remap relative `DocumentUri` (`source://`) 53 | 54 | [codemirror]: https://codemirror.net/ 55 | [monaco]: https://microsoft.github.io/monaco-editor/ 56 | [qualified/lsps]: https://github.com/qualified/lsps 57 | [rust-analyzer]: https://github.com/rust-analyzer/rust-analyzer 58 | -------------------------------------------------------------------------------- /src/api/files.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::Infallible, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use lsp_types::{FileChangeType, FileEvent}; 7 | use thiserror::Error; 8 | use tokio::fs; 9 | use url::Url; 10 | use warp::{http::StatusCode, Filter, Rejection, Reply}; 11 | 12 | use super::{json_body, json_response, with_context}; 13 | 14 | #[derive(Debug, Error)] 15 | enum Error { 16 | #[error("{0} is not under the project root")] 17 | NotProjectPath(String), 18 | 19 | #[error("failed to create dirs {path}: {source}")] 20 | CreateDirs { 21 | path: String, 22 | source: std::io::Error, 23 | }, 24 | 25 | #[error("failed to write {path}: {source}")] 26 | WriteFile { 27 | path: String, 28 | source: std::io::Error, 29 | }, 30 | 31 | #[error("failed to remove {path}: {source}")] 32 | RemoveFile { 33 | path: String, 34 | source: std::io::Error, 35 | }, 36 | 37 | #[error("failed to rename {from} to {to}: {source}")] 38 | RenameFile { 39 | from: String, 40 | to: String, 41 | source: std::io::Error, 42 | }, 43 | } 44 | 45 | #[derive(Debug, serde::Deserialize)] 46 | struct Payload { 47 | operations: Vec, 48 | } 49 | 50 | /// File operation. 51 | /// 52 | /// ```json 53 | /// {"op": "write", "path": "foo.js", "contents": "// foo"} 54 | /// {"op": "remove", "path": "bar.js"} 55 | /// {"op": "rename", "from": "foo.js", "to": "bar.js"} 56 | /// ``` 57 | #[derive(Debug, serde::Deserialize, serde::Serialize)] 58 | #[serde(tag = "op", rename_all = "camelCase")] 59 | enum Operation { 60 | /// Write `contents` to a file at relative `path`. 61 | /// 62 | /// This will create a file if it does not exist, and will replace its contents if it does. 63 | /// Any missing directories are also created. 64 | Write { path: String, contents: String }, 65 | 66 | /// Remove a file at relative `path`. 67 | /// 68 | /// Any empty parent directories under `cwd` are also removed. 69 | /// Errors if `path` doesn't exist, or is not a file. 70 | Remove { path: String }, 71 | 72 | /// Rename a file or directory at relative path `from` to `to`. 73 | /// 74 | /// Any missing directories are created. 75 | /// Any empty parent directories under `cwd` as a result of renaming are removed. 76 | Rename { from: String, to: String }, 77 | } 78 | 79 | impl Operation { 80 | /// Perform operation relative to `cwd`. 81 | async fn perform

(&self, cwd: P, remap: bool) -> Result, Error> 82 | where 83 | P: AsRef, 84 | { 85 | match self { 86 | Operation::Write { path, contents } => { 87 | let apath = get_path(&cwd, path)?; 88 | tracing::debug!("writing file {:?}", path); 89 | create_parent_dirs(&cwd, path).await?; 90 | let create = !apath.exists(); 91 | fs::write(&apath, contents.as_bytes()) 92 | .await 93 | .map_err(|source| Error::WriteFile { 94 | path: path.to_owned(), 95 | source, 96 | })?; 97 | 98 | Ok(vec![FileEvent::new( 99 | path_uri(&cwd, path, false, remap), 100 | if create { 101 | FileChangeType::Created 102 | } else { 103 | FileChangeType::Changed 104 | }, 105 | )]) 106 | } 107 | 108 | Operation::Remove { path } => { 109 | let apath = get_path(&cwd, path)?; 110 | tracing::debug!("removing file {:?}", path); 111 | fs::remove_file(&apath) 112 | .await 113 | .map_err(|source| Error::RemoveFile { 114 | path: path.to_owned(), 115 | source, 116 | })?; 117 | remove_empty_parents(&cwd, path).await; 118 | 119 | Ok(vec![FileEvent::new( 120 | path_uri(&cwd, path, false, remap), 121 | FileChangeType::Deleted, 122 | )]) 123 | } 124 | 125 | Operation::Rename { from, to } => { 126 | let src = get_path(&cwd, from)?; 127 | let dst = get_path(&cwd, to)?; 128 | 129 | tracing::debug!("renaming file {:?} to {:?}", from, to); 130 | create_parent_dirs(&cwd, to).await?; 131 | let create = !dst.exists(); 132 | fs::rename(&src, &dst) 133 | .await 134 | .map_err(|source| Error::RenameFile { 135 | from: from.to_owned(), 136 | to: to.to_owned(), 137 | source, 138 | })?; 139 | remove_empty_parents(&cwd, from).await; 140 | 141 | let is_dir = dst.is_dir(); 142 | Ok(vec![ 143 | FileEvent::new(path_uri(&cwd, from, is_dir, remap), FileChangeType::Deleted), 144 | FileEvent::new( 145 | path_uri(&cwd, to, is_dir, remap), 146 | if create { 147 | FileChangeType::Created 148 | } else { 149 | FileChangeType::Changed 150 | }, 151 | ), 152 | ]) 153 | } 154 | } 155 | } 156 | } 157 | 158 | fn get_path

(cwd: P, path: &str) -> Result 159 | where 160 | P: AsRef, 161 | { 162 | let apath = cwd.as_ref().join(path); 163 | if !apath.starts_with(&cwd) { 164 | return Err(Error::NotProjectPath(path.to_owned())); 165 | } 166 | Ok(apath) 167 | } 168 | 169 | async fn create_parent_dirs(cwd: P, path: Q) -> Result<(), Error> 170 | where 171 | P: AsRef, 172 | Q: AsRef, 173 | { 174 | if let Some(parent) = path.as_ref().parent() { 175 | tracing::debug!("creating directories for {:?}", path.as_ref()); 176 | fs::create_dir_all(cwd.as_ref().join(parent)) 177 | .await 178 | .map_err(|source| Error::CreateDirs { 179 | path: parent.to_str().expect("utf-8").to_owned(), 180 | source, 181 | })?; 182 | } 183 | Ok(()) 184 | } 185 | 186 | /// Remove empty parents of relative `path` after removing or renaming. 187 | async fn remove_empty_parents(cwd: P, path: Q) 188 | where 189 | P: AsRef, 190 | Q: AsRef, 191 | { 192 | let mut path = path.as_ref(); 193 | while let Some(parent) = path.parent() { 194 | // Fails if the directory isn't empty. 195 | if fs::remove_dir(cwd.as_ref().join(parent)).await.is_ok() { 196 | tracing::debug!("removed empty parent {:?}", parent); 197 | path = parent 198 | } else { 199 | break; 200 | } 201 | } 202 | } 203 | 204 | fn path_uri

(cwd: P, path: &str, is_dir: bool, remap: bool) -> Url 205 | where 206 | P: AsRef, 207 | { 208 | if remap { 209 | let uri = format!( 210 | "source://{}{}", 211 | path, 212 | if is_dir && !path.ends_with('/') { 213 | "/" 214 | } else { 215 | "" 216 | } 217 | ); 218 | Url::parse(&uri).expect("valid uri") 219 | } else { 220 | let path = cwd.as_ref().join(path); 221 | if is_dir { 222 | Url::from_directory_path(&path).expect("no error") 223 | } else { 224 | Url::from_file_path(&path).expect("no error") 225 | } 226 | } 227 | } 228 | 229 | #[test] 230 | fn test_path_uri() { 231 | let cwd = "/tmp"; 232 | let path = "foo"; 233 | let is_dir = true; 234 | let remap = true; 235 | assert_eq!( 236 | path_uri(cwd, path, is_dir, remap).to_string(), 237 | "source://foo/" 238 | ); 239 | assert_eq!( 240 | path_uri(cwd, path, !is_dir, remap).to_string(), 241 | "source://foo" 242 | ); 243 | assert_eq!( 244 | path_uri(cwd, path, is_dir, !remap).to_string(), 245 | "file:///tmp/foo/" 246 | ); 247 | assert_eq!( 248 | path_uri(cwd, path, !is_dir, !remap).to_string(), 249 | "file:///tmp/foo" 250 | ); 251 | } 252 | 253 | #[derive(Debug, serde::Serialize)] 254 | struct Response { 255 | /// `FileEvent`s for `workspace/didChangeWatchedFiles` notification. 256 | changes: Vec, 257 | /// Any errors that occured trying to perform operations. 258 | #[serde(skip_serializing_if = "Option::is_none")] 259 | errors: Option>, 260 | } 261 | 262 | #[derive(Debug, serde::Serialize)] 263 | struct OperationError { 264 | operation: Operation, 265 | reason: String, 266 | } 267 | 268 | #[derive(Debug, Clone)] 269 | pub struct Context { 270 | pub cwd: PathBuf, 271 | pub remap: bool, 272 | } 273 | 274 | /// Handler for `POST /files` 275 | pub fn handler(ctx: Context) -> impl Filter + Clone { 276 | warp::post() 277 | .and(warp::path("files")) 278 | .and(warp::path::end()) 279 | .and(with_context(ctx)) 280 | .and(json_body::()) 281 | .and_then(handle_operations) 282 | } 283 | 284 | #[tracing::instrument(level = "debug", skip(ctx, payload))] 285 | async fn handle_operations(ctx: Context, payload: Payload) -> Result { 286 | let mut errors = Vec::new(); 287 | let mut changes = Vec::new(); 288 | // Do them one by one in order 289 | for op in payload.operations { 290 | match op.perform(&ctx.cwd, ctx.remap).await { 291 | Ok(mut events) => { 292 | changes.append(&mut events); 293 | } 294 | Err(err) => { 295 | errors.push(OperationError { 296 | operation: op, 297 | reason: err.to_string(), 298 | }); 299 | } 300 | } 301 | } 302 | 303 | let (errors, status) = if errors.is_empty() { 304 | (None, StatusCode::OK) 305 | } else { 306 | (Some(errors), StatusCode::UNPROCESSABLE_ENTITY) 307 | }; 308 | Ok(json_response(&Response { changes, errors }, status)) 309 | } 310 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, error::Error}; 2 | 3 | use warp::{http::StatusCode, reply, Filter, Rejection, Reply}; 4 | 5 | pub mod files; 6 | pub mod proxy; 7 | 8 | fn with_context(ctx: T) -> impl Filter + Clone 9 | where 10 | T: Clone + Send, 11 | { 12 | warp::any().map(move || ctx.clone()) 13 | } 14 | 15 | fn json_body() -> impl Filter + Clone 16 | where 17 | T: serde::de::DeserializeOwned + Send, 18 | { 19 | warp::body::content_length_limit(2 * 1024 * 1024).and(warp::body::json()) 20 | } 21 | 22 | fn json_response(res: &T, status: StatusCode) -> reply::Response { 23 | reply::with_status(reply::json(res), status).into_response() 24 | } 25 | 26 | /// Convert rejections into a JSON response. 27 | #[allow(clippy::unused_async)] 28 | pub async fn recover(err: Rejection) -> Result { 29 | let (reason, status) = if err.is_not_found() { 30 | ("Not Found", StatusCode::NOT_FOUND) 31 | } else if let Some(e) = err.find::() { 32 | if let Some(cause) = e.source() { 33 | tracing::debug!("deserialize error: {:?}", cause); 34 | if let Some(err) = cause.downcast_ref::() { 35 | return Ok(json_error_response( 36 | err.to_string(), 37 | StatusCode::BAD_REQUEST, 38 | )); 39 | } 40 | } 41 | ("Bad Request", StatusCode::BAD_REQUEST) 42 | } else if err.find::().is_some() { 43 | ("Unsupported Media Type", StatusCode::UNSUPPORTED_MEDIA_TYPE) 44 | } else if err.find::().is_some() { 45 | ("Payload Too Large", StatusCode::PAYLOAD_TOO_LARGE) 46 | } else if err.find::().is_some() { 47 | ("Method Not Allowed", StatusCode::METHOD_NOT_ALLOWED) 48 | } else { 49 | tracing::warn!("unhandled rejection: {:?}", err); 50 | ("Internal Server Error", StatusCode::INTERNAL_SERVER_ERROR) 51 | }; 52 | 53 | Ok(json_error_response(reason, status)) 54 | } 55 | 56 | #[derive(serde::Serialize)] 57 | struct ErrorMessage { 58 | reason: String, 59 | } 60 | 61 | fn json_error_response>( 62 | reason: T, 63 | status: warp::http::StatusCode, 64 | ) -> reply::Response { 65 | reply::with_status( 66 | reply::json(&ErrorMessage { 67 | reason: reason.into(), 68 | }), 69 | status, 70 | ) 71 | .into_response() 72 | } 73 | -------------------------------------------------------------------------------- /src/api/proxy.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, process::Stdio, str::FromStr}; 2 | 3 | use futures_util::{ 4 | future::{select, Either}, 5 | stream, SinkExt, StreamExt, 6 | }; 7 | use tokio::{fs, process::Command}; 8 | use url::Url; 9 | use warp::{Filter, Rejection, Reply}; 10 | 11 | use crate::lsp; 12 | 13 | use super::with_context; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Context { 17 | /// One or more commands to start a Language Server. 18 | pub commands: Vec>, 19 | /// Write file on save. 20 | pub sync: bool, 21 | /// Remap relative `source://` to absolute `file://`. 22 | pub remap: bool, 23 | /// Project root. 24 | pub cwd: Url, 25 | } 26 | 27 | #[derive(Clone, Debug, serde::Deserialize)] 28 | struct Query { 29 | /// The command name of the Language Server to start. 30 | /// If not specified, the first one is started. 31 | name: String, 32 | } 33 | 34 | fn with_optional_query() -> impl Filter,), Error = Infallible> + Clone { 35 | warp::query::() 36 | .map(Some) 37 | .or_else(|_| async { Ok::<(Option,), Infallible>((None,)) }) 38 | } 39 | 40 | /// Handler for WebSocket connection. 41 | pub fn handler(ctx: Context) -> impl Filter + Clone { 42 | warp::path::end() 43 | .and(warp::ws()) 44 | .and(with_context(ctx)) 45 | .and(with_optional_query()) 46 | .map(|ws: warp::ws::Ws, ctx, query| { 47 | ws.with_compression() 48 | .on_upgrade(move |socket| on_upgrade(socket, ctx, query)) 49 | }) 50 | } 51 | 52 | #[tracing::instrument(level = "debug", err, skip(msg))] 53 | async fn maybe_write_text_document(msg: &lsp::Message) -> Result<(), std::io::Error> { 54 | if let lsp::Message::Notification(lsp::Notification::DidSave { params }) = msg { 55 | if let Some(text) = ¶ms.text { 56 | let uri = ¶ms.text_document.uri; 57 | if uri.scheme() == "file" { 58 | if let Ok(path) = uri.to_file_path() { 59 | if let Some(parent) = path.parent() { 60 | tracing::debug!("writing to {:?}", path); 61 | fs::create_dir_all(parent).await?; 62 | fs::write(&path, text.as_bytes()).await?; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | Ok(()) 69 | } 70 | 71 | async fn on_upgrade(socket: warp::ws::WebSocket, ctx: Context, query: Option) { 72 | tracing::info!("connected"); 73 | if let Err(err) = connected(socket, ctx, query).await { 74 | tracing::error!("connection error: {}", err); 75 | } 76 | tracing::info!("disconnected"); 77 | } 78 | 79 | #[tracing::instrument(level = "debug", skip(ws, ctx), fields(remap = %ctx.remap, sync = %ctx.sync))] 80 | async fn connected( 81 | ws: warp::ws::WebSocket, 82 | ctx: Context, 83 | query: Option, 84 | ) -> Result<(), Box> { 85 | let command = if let Some(query) = query { 86 | if let Some(command) = ctx.commands.iter().find(|v| v[0] == query.name) { 87 | command 88 | } else { 89 | // TODO Validate this earlier and reject, or close immediately. 90 | tracing::warn!( 91 | "Unknown Language Server '{}', falling back to the default", 92 | query.name 93 | ); 94 | &ctx.commands[0] 95 | } 96 | } else { 97 | &ctx.commands[0] 98 | }; 99 | tracing::info!("starting {} in {}", command[0], ctx.cwd); 100 | let mut server = Command::new(&command[0]) 101 | .args(&command[1..]) 102 | .stdin(Stdio::piped()) 103 | .stdout(Stdio::piped()) 104 | .kill_on_drop(true) 105 | .spawn()?; 106 | tracing::debug!("running {}", command[0]); 107 | 108 | let mut server_send = lsp::framed::writer(server.stdin.take().unwrap()); 109 | let mut server_recv = lsp::framed::reader(server.stdout.take().unwrap()); 110 | let (mut client_send, client_recv) = ws.split(); 111 | let client_recv = client_recv 112 | .filter_map(filter_map_warp_ws_message) 113 | // Chain this with `Done` so we know when the client disconnects 114 | .chain(stream::once(async { Ok(Message::Done) })); 115 | // Tick every 30s so we can ping the client to keep the connection alive 116 | let ticks = stream::unfold( 117 | tokio::time::interval(std::time::Duration::from_secs(30)), 118 | |mut interval| async move { 119 | interval.tick().await; 120 | Some((Ok(Message::Tick), interval)) 121 | }, 122 | ); 123 | let mut client_recv = stream::select(client_recv, ticks).boxed(); 124 | 125 | let mut client_msg = client_recv.next(); 126 | let mut server_msg = server_recv.next(); 127 | // Keeps track if `pong` was received since sending the last `ping`. 128 | let mut is_alive = true; 129 | 130 | loop { 131 | match select(client_msg, server_msg).await { 132 | // From Client 133 | Either::Left((from_client, p_server_msg)) => { 134 | match from_client { 135 | // Valid LSP message 136 | Some(Ok(Message::Message(mut msg))) => { 137 | if ctx.remap { 138 | lsp::ext::remap_relative_uri(&mut msg, &ctx.cwd)?; 139 | tracing::debug!("remapped relative URI from client"); 140 | } 141 | if ctx.sync { 142 | maybe_write_text_document(&msg).await?; 143 | } 144 | let text = serde_json::to_string(&msg)?; 145 | tracing::debug!("-> {}", text); 146 | server_send.send(text).await?; 147 | } 148 | 149 | // Invalid JSON body 150 | Some(Ok(Message::Invalid(text))) => { 151 | tracing::warn!("-> {}", text); 152 | // Just forward it to the server as is. 153 | server_send.send(text).await?; 154 | } 155 | 156 | // Close message 157 | Some(Ok(Message::Close)) => { 158 | // The connection will terminate when None is received. 159 | tracing::info!("received Close message"); 160 | } 161 | 162 | // Ping the client to keep the connection alive 163 | Some(Ok(Message::Tick)) => { 164 | // Terminate if we haven't heard back from the previous ping. 165 | if !is_alive { 166 | tracing::warn!("terminating unhealthy connection"); 167 | break; 168 | } 169 | 170 | is_alive = false; 171 | tracing::debug!("pinging the client"); 172 | client_send.send(warp::ws::Message::ping(vec![])).await?; 173 | } 174 | 175 | // Mark the connection as alive on any pong. 176 | Some(Ok(Message::Pong)) => { 177 | tracing::debug!("received pong"); 178 | is_alive = true; 179 | } 180 | 181 | // Connection closed 182 | Some(Ok(Message::Done)) => { 183 | tracing::info!("connection closed"); 184 | break; 185 | } 186 | 187 | // WebSocket Error 188 | Some(Err(err)) => { 189 | tracing::error!("websocket error: {}", err); 190 | } 191 | 192 | None => { 193 | // Unreachable because of the interval stream 194 | unreachable!("should never yield None"); 195 | } 196 | } 197 | 198 | client_msg = client_recv.next(); 199 | server_msg = p_server_msg; 200 | } 201 | 202 | // From Server 203 | Either::Right((from_server, p_client_msg)) => { 204 | match from_server { 205 | // Serialized LSP Message 206 | Some(Ok(text)) => { 207 | if ctx.remap { 208 | if let Ok(mut msg) = lsp::Message::from_str(&text) { 209 | lsp::ext::remap_relative_uri(&mut msg, &ctx.cwd)?; 210 | tracing::debug!("remapped relative URI from server"); 211 | let text = serde_json::to_string(&msg)?; 212 | tracing::debug!("<- {}", text); 213 | client_send.send(warp::ws::Message::text(text)).await?; 214 | } else { 215 | tracing::warn!("<- {}", text); 216 | client_send.send(warp::ws::Message::text(text)).await?; 217 | } 218 | } else { 219 | tracing::debug!("<- {}", text); 220 | client_send.send(warp::ws::Message::text(text)).await?; 221 | } 222 | } 223 | 224 | // Codec Error 225 | Some(Err(err)) => { 226 | tracing::error!("{}", err); 227 | } 228 | 229 | // Server exited 230 | None => { 231 | tracing::error!("server process exited unexpectedly"); 232 | client_send.send(warp::ws::Message::close()).await?; 233 | break; 234 | } 235 | } 236 | 237 | client_msg = p_client_msg; 238 | server_msg = server_recv.next(); 239 | } 240 | } 241 | } 242 | 243 | Ok(()) 244 | } 245 | 246 | // Type to describe a message from the client conveniently. 247 | #[allow(clippy::large_enum_variant)] 248 | #[allow(clippy::enum_variant_names)] 249 | enum Message { 250 | // Valid LSP message 251 | Message(lsp::Message), 252 | // Invalid JSON 253 | Invalid(String), 254 | // Close message 255 | Close, 256 | // Ping the client to keep the connection alive. 257 | // Note that this is from the interval stream and not actually from client. 258 | Tick, 259 | // Client disconnected. Necessary because the combined stream is infinite. 260 | Done, 261 | // A reply for ping or heartbeat from client. 262 | Pong, 263 | } 264 | 265 | // Parse the message and ignore anything we don't care. 266 | async fn filter_map_warp_ws_message( 267 | wsm: Result, 268 | ) -> Option> { 269 | match wsm { 270 | Ok(msg) => { 271 | if msg.is_close() { 272 | Some(Ok(Message::Close)) 273 | } else if msg.is_text() { 274 | let text = msg.to_str().expect("text"); 275 | match lsp::Message::from_str(text) { 276 | Ok(msg) => Some(Ok(Message::Message(msg))), 277 | Err(_) => Some(Ok(Message::Invalid(text.to_owned()))), 278 | } 279 | } else if msg.is_pong() { 280 | Some(Ok(Message::Pong)) 281 | } else { 282 | // Ignore any other message types 283 | None 284 | } 285 | } 286 | 287 | Err(err) => Some(Err(err)), 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/lsp/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types defined by the JSON-RPC specification. 2 | // Extracted from [tower-lsp](https://github.com/ebkalderon/tower-lsp). 3 | // Copyright (c) 2020 Eyal Kalderon. MIT License. 4 | // Changes: 5 | // - removed methods to create Error 6 | 7 | use std::fmt::{self, Display, Formatter}; 8 | 9 | use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; 10 | use serde_json::Value; 11 | 12 | /// A list of numeric error codes used in JSON-RPC responses. 13 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 14 | pub enum ErrorCode { 15 | /// Invalid JSON was received by the server. 16 | ParseError, 17 | /// The JSON sent is not a valid Request object. 18 | InvalidRequest, 19 | /// The method does not exist / is not available. 20 | MethodNotFound, 21 | /// Invalid method parameter(s). 22 | InvalidParams, 23 | /// Internal JSON-RPC error. 24 | InternalError, 25 | /// Reserved for implementation-defined server errors. 26 | ServerError(i64), 27 | 28 | /// The request was cancelled by the client. 29 | /// 30 | /// # Compatibility 31 | /// 32 | /// This error code is defined by the Language Server Protocol. 33 | RequestCancelled, 34 | /// The request was invalidated by another incoming request. 35 | /// 36 | /// # Compatibility 37 | /// 38 | /// This error code is specific to the Language Server Protocol. 39 | ContentModified, 40 | } 41 | 42 | impl ErrorCode { 43 | /// Returns the integer error code value. 44 | #[inline] 45 | pub fn code(&self) -> i64 { 46 | match *self { 47 | Self::ParseError => -32700, 48 | Self::InvalidRequest => -32600, 49 | Self::MethodNotFound => -32601, 50 | Self::InvalidParams => -32602, 51 | Self::InternalError => -32603, 52 | Self::RequestCancelled => -32800, 53 | Self::ContentModified => -32801, 54 | Self::ServerError(code) => code, 55 | } 56 | } 57 | 58 | /// Returns a human-readable description of the error. 59 | #[inline] 60 | pub fn description(&self) -> &'static str { 61 | match *self { 62 | Self::ParseError => "Parse error", 63 | Self::InvalidRequest => "Invalid request", 64 | Self::MethodNotFound => "Method not found", 65 | Self::InvalidParams => "Invalid params", 66 | Self::InternalError => "Internal error", 67 | Self::RequestCancelled => "Canceled", 68 | Self::ContentModified => "Content modified", 69 | Self::ServerError(_) => "Server error", 70 | } 71 | } 72 | } 73 | 74 | impl From for ErrorCode { 75 | #[inline] 76 | fn from(code: i64) -> Self { 77 | match code { 78 | -32700 => Self::ParseError, 79 | -32600 => Self::InvalidRequest, 80 | -32601 => Self::MethodNotFound, 81 | -32602 => Self::InvalidParams, 82 | -32603 => Self::InternalError, 83 | -32800 => Self::RequestCancelled, 84 | -32801 => Self::ContentModified, 85 | code => Self::ServerError(code), 86 | } 87 | } 88 | } 89 | 90 | impl Display for ErrorCode { 91 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 92 | Display::fmt(&self.code(), f) 93 | } 94 | } 95 | 96 | impl<'a> Deserialize<'a> for ErrorCode { 97 | fn deserialize(deserializer: D) -> std::result::Result 98 | where 99 | D: Deserializer<'a>, 100 | { 101 | let code: i64 = Deserialize::deserialize(deserializer)?; 102 | Ok(Self::from(code)) 103 | } 104 | } 105 | 106 | impl Serialize for ErrorCode { 107 | fn serialize(&self, serializer: S) -> std::result::Result 108 | where 109 | S: Serializer, 110 | { 111 | self.code().serialize(serializer) 112 | } 113 | } 114 | 115 | /// A JSON-RPC error object. 116 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 117 | #[serde(deny_unknown_fields)] 118 | pub struct Error { 119 | /// A number indicating the error type that occurred. 120 | pub code: ErrorCode, 121 | /// A short description of the error. 122 | pub message: String, 123 | /// Additional information about the error, if any. 124 | #[serde(skip_serializing_if = "Option::is_none")] 125 | pub data: Option, 126 | } 127 | 128 | impl Display for Error { 129 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 130 | write!(f, "{}: {}", self.code.description(), self.message) 131 | } 132 | } 133 | 134 | impl std::error::Error for Error {} 135 | -------------------------------------------------------------------------------- /src/lsp/ext/mod.rs: -------------------------------------------------------------------------------- 1 | //! Nonstandard LSP features. 2 | mod relative_uri; 3 | 4 | pub use relative_uri::remap_relative_uri; 5 | -------------------------------------------------------------------------------- /src/lsp/ext/relative_uri.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use url::Url; 4 | 5 | use crate::lsp::{Message, Notification, Request, Response, ResponseResult}; 6 | 7 | /// Remap URI relative to current directory (`source://`) to absolute URI (`file://`). 8 | /// `source://` was chosen because it's used by [Metals Remote Language Server]. 9 | /// 10 | /// [Metals Remote Language Server]: https://scalameta.org/metals/docs/contributors/remote-language-server.html 11 | pub fn remap_relative_uri(msg: &mut Message, cwd: &Url) -> Result<(), std::io::Error> { 12 | match msg { 13 | Message::Notification(notification) => remap_notification(notification, cwd)?, 14 | Message::Request(request) => remap_request(request, cwd)?, 15 | Message::Response(response) => remap_response(response, cwd)?, 16 | Message::Unknown(_) => {} 17 | } 18 | Ok(()) 19 | } 20 | 21 | fn remap_notification(notification: &mut Notification, cwd: &Url) -> Result<(), std::io::Error> { 22 | match notification { 23 | Notification::DidSave { params: p } => { 24 | remap_text_document_identifier(&mut p.text_document, cwd)?; 25 | } 26 | 27 | Notification::DidChangeWorkspaceFolders { params: p } => { 28 | for folder in &mut p.event.added { 29 | remap_workspace_folder(folder, cwd)?; 30 | } 31 | for folder in &mut p.event.removed { 32 | remap_workspace_folder(folder, cwd)?; 33 | } 34 | } 35 | 36 | Notification::DidChangeWatchedFiles { params: p } => { 37 | for event in &mut p.changes { 38 | if let Some(uri) = to_file(&event.uri, cwd)? { 39 | event.uri = uri; 40 | } 41 | } 42 | } 43 | 44 | Notification::DidOpen { params: p } => { 45 | if let Some(uri) = to_file(&p.text_document.uri, cwd)? { 46 | p.text_document.uri = uri; 47 | } 48 | } 49 | 50 | Notification::DidChange { params: p } => { 51 | if let Some(uri) = to_file(&p.text_document.uri, cwd)? { 52 | p.text_document.uri = uri; 53 | } 54 | } 55 | 56 | Notification::WillSave { params: p } => { 57 | remap_text_document_identifier(&mut p.text_document, cwd)?; 58 | } 59 | 60 | Notification::DidClose { params: p } => { 61 | remap_text_document_identifier(&mut p.text_document, cwd)?; 62 | } 63 | 64 | Notification::PublishDiagnostics { params: p } => { 65 | // `to_source` because this goes to client 66 | if let Some(uri) = to_source(&p.uri, cwd)? { 67 | p.uri = uri; 68 | } 69 | } 70 | 71 | Notification::DidChangeConfiguration { params: _ } 72 | | Notification::Initialized { params: _ } 73 | | Notification::Exit { params: _ } 74 | | Notification::LogMessage { params: _ } 75 | | Notification::ShowMessage { params: _ } 76 | | Notification::Progress { params: _ } 77 | | Notification::CancelRequest { params: _ } 78 | | Notification::TelemetryEvent { params: _ } => {} 79 | } 80 | 81 | Ok(()) 82 | } 83 | 84 | fn remap_request(request: &mut Request, cwd: &Url) -> Result<(), std::io::Error> { 85 | match request { 86 | Request::Initialize { id: _, params: p } => { 87 | if let Some(root_uri) = &p.root_uri { 88 | if let Some(root_uri) = to_file(root_uri, cwd)? { 89 | p.root_uri = Some(root_uri); 90 | } 91 | } 92 | if let Some(folders) = &mut p.workspace_folders { 93 | for folder in folders { 94 | remap_workspace_folder(folder, cwd)?; 95 | } 96 | } 97 | } 98 | 99 | Request::DocumentSymbol { id: _, params: p } => { 100 | remap_text_document_identifier(&mut p.text_document, cwd)?; 101 | } 102 | 103 | Request::WillSaveWaitUntil { id: _, params: p } => { 104 | remap_text_document_identifier(&mut p.text_document, cwd)?; 105 | } 106 | 107 | Request::Completion { id: _, params: p } => { 108 | remap_text_document_identifier(&mut p.text_document_position.text_document, cwd)?; 109 | } 110 | 111 | Request::Hover { id: _, params: p } => { 112 | remap_text_document_identifier( 113 | &mut p.text_document_position_params.text_document, 114 | cwd, 115 | )?; 116 | } 117 | 118 | Request::SignatureHelp { id: _, params: p } => { 119 | remap_text_document_identifier( 120 | &mut p.text_document_position_params.text_document, 121 | cwd, 122 | )?; 123 | } 124 | 125 | Request::GotoDeclaration { id: _, params: p } 126 | | Request::GotoDefinition { id: _, params: p } 127 | | Request::GotoTypeDefinition { id: _, params: p } 128 | | Request::GotoImplementation { id: _, params: p } => { 129 | remap_text_document_identifier( 130 | &mut p.text_document_position_params.text_document, 131 | cwd, 132 | )?; 133 | } 134 | 135 | Request::References { id: _, params: p } => { 136 | remap_text_document_identifier(&mut p.text_document_position.text_document, cwd)?; 137 | } 138 | 139 | Request::DocumentHighlight { id: _, params: p } => { 140 | remap_text_document_identifier( 141 | &mut p.text_document_position_params.text_document, 142 | cwd, 143 | )?; 144 | } 145 | 146 | Request::CodeAction { id: _, params: p } => { 147 | remap_text_document_identifier(&mut p.text_document, cwd)?; 148 | } 149 | 150 | Request::CodeLens { id: _, params: p } => { 151 | remap_text_document_identifier(&mut p.text_document, cwd)?; 152 | } 153 | 154 | Request::DocumentLink { id: _, params: p } => { 155 | remap_text_document_identifier(&mut p.text_document, cwd)?; 156 | } 157 | 158 | Request::DocumentLinkResolve { id: _, params: p } => { 159 | if let Some(target) = &p.target { 160 | if let Some(target) = to_file(target, cwd)? { 161 | p.target = Some(target); 162 | } 163 | } 164 | } 165 | 166 | Request::DocumentColor { id: _, params: p } => { 167 | remap_text_document_identifier(&mut p.text_document, cwd)?; 168 | } 169 | 170 | Request::ColorPresentation { id: _, params: p } => { 171 | remap_text_document_identifier(&mut p.text_document, cwd)?; 172 | } 173 | 174 | Request::Formatting { id: _, params: p } => { 175 | remap_text_document_identifier(&mut p.text_document, cwd)?; 176 | } 177 | 178 | Request::RangeFormatting { id: _, params: p } => { 179 | remap_text_document_identifier(&mut p.text_document, cwd)?; 180 | } 181 | 182 | Request::OnTypeFormatting { id: _, params: p } => { 183 | remap_text_document_identifier(&mut p.text_document_position.text_document, cwd)?; 184 | } 185 | 186 | Request::Rename { id: _, params: p } => { 187 | remap_text_document_identifier(&mut p.text_document_position.text_document, cwd)?; 188 | } 189 | 190 | Request::PrepareRename { id: _, params: p } => { 191 | remap_text_document_identifier(&mut p.text_document, cwd)?; 192 | } 193 | 194 | Request::FoldingRange { id: _, params: p } => { 195 | remap_text_document_identifier(&mut p.text_document, cwd)?; 196 | } 197 | 198 | Request::SelectionRange { id: _, params: p } => { 199 | remap_text_document_identifier(&mut p.text_document, cwd)?; 200 | } 201 | 202 | // To Client 203 | Request::ApplyEdit { id: _, params: p } => { 204 | remap_workspace_edit(&mut p.edit, cwd)?; 205 | } 206 | 207 | // To Client 208 | Request::Configuration { id: _, params: p } => { 209 | for item in &mut p.items { 210 | if let Some(scope_uri) = &item.scope_uri { 211 | if let Some(scope_uri) = to_source(scope_uri, cwd)? { 212 | item.scope_uri = Some(scope_uri); 213 | } 214 | } 215 | } 216 | } 217 | 218 | Request::WorkspaceFolders { id: _, params: _ } 219 | | Request::ShowMessage { id: _, params: _ } 220 | | Request::CompletionResolve { id: _, params: _ } 221 | | Request::CodeLensResolve { id: _, params: _ } 222 | | Request::RegisterCapability { id: _, params: _ } 223 | | Request::UnregisterCapability { id: _, params: _ } 224 | | Request::CreateWorkDoneProgress { id: _, params: _ } 225 | | Request::CancelWorkDoneProgress { id: _, params: _ } 226 | | Request::Symbol { id: _, params: _ } 227 | | Request::ExecuteCommand { id: _, params: _ } 228 | | Request::Shutdown { id: _, params: _ } => {} 229 | } 230 | 231 | Ok(()) 232 | } 233 | 234 | fn remap_response(response: &mut Response, cwd: &Url) -> Result<(), std::io::Error> { 235 | match response { 236 | Response::Success { id: _, result } => { 237 | match result { 238 | ResponseResult::DocumentLinkWithTarget(links) => { 239 | for link in links { 240 | if let Some(target) = to_source(&link.target, cwd)? { 241 | link.target = target; 242 | } 243 | } 244 | } 245 | 246 | ResponseResult::DocumentLinkWithTargetResolve(link) => { 247 | if let Some(target) = to_source(&link.target, cwd)? { 248 | link.target = target; 249 | } 250 | } 251 | 252 | ResponseResult::CodeAction(actions) => { 253 | for aoc in actions { 254 | match aoc { 255 | lsp_types::CodeActionOrCommand::Command(_) => {} 256 | lsp_types::CodeActionOrCommand::CodeAction(action) => { 257 | if let Some(workspace_edit) = &mut action.edit { 258 | remap_workspace_edit(workspace_edit, cwd)?; 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | ResponseResult::Location(location) => { 266 | remap_location(location, cwd)?; 267 | } 268 | 269 | ResponseResult::Locations(locations) => { 270 | for location in locations { 271 | remap_location(location, cwd)?; 272 | } 273 | } 274 | 275 | ResponseResult::LocationLinks(links) => { 276 | for link in links { 277 | if let Some(target_uri) = to_source(&link.target_uri, cwd)? { 278 | link.target_uri = target_uri; 279 | } 280 | } 281 | } 282 | 283 | ResponseResult::SymbolInfos(syms) => { 284 | for sym in syms { 285 | remap_location(&mut sym.location, cwd)?; 286 | } 287 | } 288 | 289 | ResponseResult::WorkspaceFolders(folders) => { 290 | for folder in folders { 291 | // `to_file` because this is a response from Client. 292 | if let Some(uri) = to_file(&folder.uri, cwd)? { 293 | folder.uri = uri; 294 | } 295 | } 296 | } 297 | 298 | ResponseResult::WorkspaceEditWithBoth(edit) => { 299 | remap_workspace_edit_changes(&mut edit.changes, cwd)?; 300 | remap_document_changes(&mut edit.document_changes, cwd)?; 301 | } 302 | 303 | ResponseResult::WorkspaceEditWithChanges(edit) => { 304 | remap_workspace_edit_changes(&mut edit.changes, cwd)?; 305 | } 306 | 307 | ResponseResult::WorkspaceEditWithDocumentChanges(edit) => { 308 | remap_document_changes(&mut edit.document_changes, cwd)?; 309 | } 310 | 311 | ResponseResult::Any(_) => {} 312 | } 313 | } 314 | 315 | Response::Failure { id: _, error: _ } => {} 316 | } 317 | 318 | Ok(()) 319 | } 320 | 321 | fn to_file(uri: &Url, cwd: &Url) -> Result, std::io::Error> { 322 | if uri.scheme() == "source" { 323 | cwd.join(uri.as_str().strip_prefix("source://").unwrap()) 324 | .map_err(map_parse_error) 325 | .map(Some) 326 | } else { 327 | Ok(None) 328 | } 329 | } 330 | 331 | fn to_source(uri: &Url, cwd: &Url) -> Result, std::io::Error> { 332 | if uri.scheme() == "file" { 333 | if let Some(rel) = uri.as_str().strip_prefix(cwd.as_str()) { 334 | let source_uri = format!("source://{}", rel); 335 | Url::parse(&source_uri).map_err(map_parse_error).map(Some) 336 | } else { 337 | Ok(None) 338 | } 339 | } else { 340 | Ok(None) 341 | } 342 | } 343 | 344 | fn map_parse_error(err: url::ParseError) -> std::io::Error { 345 | std::io::Error::new(std::io::ErrorKind::InvalidData, err) 346 | } 347 | 348 | /// Remap `DocumentUri` in `WorkspaceEdit` to use `source://` 349 | fn remap_workspace_edit( 350 | workspace_edit: &mut lsp_types::WorkspaceEdit, 351 | cwd: &Url, 352 | ) -> Result<(), std::io::Error> { 353 | if let Some(changes) = &mut workspace_edit.changes { 354 | remap_workspace_edit_changes(changes, cwd)?; 355 | } 356 | 357 | if let Some(doc_changes) = &mut workspace_edit.document_changes { 358 | remap_document_changes(doc_changes, cwd)?; 359 | } 360 | Ok(()) 361 | } 362 | 363 | /// Remap keys of `WorkspaceEdit.changes` 364 | fn remap_workspace_edit_changes( 365 | changes: &mut HashMap>, 366 | cwd: &Url, 367 | ) -> Result<(), std::io::Error> { 368 | let mut tmp = Vec::with_capacity(changes.len()); 369 | for (key, val) in changes.drain() { 370 | if let Some(rel) = to_source(&key, cwd)? { 371 | tmp.push((rel, val)); 372 | } else { 373 | tmp.push((key, val)); 374 | } 375 | } 376 | for (key, val) in tmp { 377 | changes.insert(key, val); 378 | } 379 | Ok(()) 380 | } 381 | 382 | fn remap_document_changes( 383 | document_changes: &mut lsp_types::DocumentChanges, 384 | cwd: &Url, 385 | ) -> Result<(), std::io::Error> { 386 | match document_changes { 387 | lsp_types::DocumentChanges::Edits(edits) => { 388 | for edit in edits { 389 | if let Some(uri) = to_source(&edit.text_document.uri, cwd)? { 390 | edit.text_document.uri = uri; 391 | } 392 | } 393 | } 394 | 395 | lsp_types::DocumentChanges::Operations(ops) => { 396 | for op in ops { 397 | match op { 398 | lsp_types::DocumentChangeOperation::Op(op) => match op { 399 | lsp_types::ResourceOp::Create(c) => { 400 | if let Some(uri) = to_source(&c.uri, cwd)? { 401 | c.uri = uri; 402 | } 403 | } 404 | lsp_types::ResourceOp::Rename(r) => { 405 | if let Some(uri) = to_source(&r.old_uri, cwd)? { 406 | r.old_uri = uri; 407 | } 408 | if let Some(uri) = to_source(&r.new_uri, cwd)? { 409 | r.new_uri = uri; 410 | } 411 | } 412 | lsp_types::ResourceOp::Delete(d) => { 413 | if let Some(uri) = to_source(&d.uri, cwd)? { 414 | d.uri = uri; 415 | } 416 | } 417 | }, 418 | 419 | lsp_types::DocumentChangeOperation::Edit(e) => { 420 | if let Some(uri) = to_source(&e.text_document.uri, cwd)? { 421 | e.text_document.uri = uri; 422 | } 423 | } 424 | } 425 | } 426 | } 427 | } 428 | 429 | Ok(()) 430 | } 431 | 432 | /// Remap `Location.uri` to use `source://` 433 | fn remap_location(location: &mut lsp_types::Location, cwd: &Url) -> Result<(), std::io::Error> { 434 | if let Some(uri) = to_source(&location.uri, cwd)? { 435 | location.uri = uri; 436 | } 437 | Ok(()) 438 | } 439 | 440 | /// Remap `TextDocumentIdentifier.uri` to use `file://` 441 | fn remap_text_document_identifier( 442 | text_document: &mut lsp_types::TextDocumentIdentifier, 443 | cwd: &Url, 444 | ) -> Result<(), std::io::Error> { 445 | if let Some(uri) = to_file(&text_document.uri, cwd)? { 446 | text_document.uri = uri; 447 | } 448 | Ok(()) 449 | } 450 | 451 | fn remap_workspace_folder( 452 | folder: &mut lsp_types::WorkspaceFolder, 453 | cwd: &Url, 454 | ) -> Result<(), std::io::Error> { 455 | if let Some(uri) = to_file(&folder.uri, cwd)? { 456 | folder.uri = uri; 457 | } 458 | Ok(()) 459 | } 460 | 461 | #[cfg(test)] 462 | mod tests { 463 | use super::*; 464 | use std::path::Path; 465 | use url::Url; 466 | 467 | #[test] 468 | fn test_to_file() { 469 | let cwd = Url::from_directory_path(Path::new("/workspace")).unwrap(); 470 | let uri = Url::parse("source://src/main.rs").unwrap(); 471 | let remapped = to_file(&uri, &cwd).unwrap().unwrap(); 472 | assert_eq!(remapped.as_str(), "file:///workspace/src/main.rs"); 473 | } 474 | 475 | #[test] 476 | fn test_to_source() { 477 | let cwd = Url::from_directory_path(Path::new("/workspace")).unwrap(); 478 | let uri = Url::from_file_path(Path::new("/workspace/src/main.rs")).unwrap(); 479 | let remapped = to_source(&uri, &cwd).unwrap().unwrap(); 480 | assert_eq!(remapped.as_str(), "source://src/main.rs"); 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/lsp/framed/codec.rs: -------------------------------------------------------------------------------- 1 | // Codec for LSP JSON RPC frame. 2 | // Based on LanguageServerCodec from [tower-lsp](https://github.com/ebkalderon/tower-lsp). 3 | // Copyright (c) 2020 Eyal Kalderon. MIT License. 4 | 5 | use std::{ 6 | error::Error, 7 | fmt::{self, Display, Formatter}, 8 | io::{Error as IoError, Write}, 9 | str::{self, Utf8Error}, 10 | }; 11 | 12 | use bytes::{Buf, BufMut, BytesMut}; 13 | use tokio::io::{AsyncRead, AsyncWrite}; 14 | use tokio_util::codec::{Decoder, Encoder, FramedRead, FramedWrite}; 15 | 16 | use super::parser; 17 | 18 | pub fn reader(inner: R) -> FramedRead { 19 | FramedRead::new(inner, LspFrameCodec::default()) 20 | } 21 | 22 | pub fn writer(inner: W) -> FramedWrite { 23 | FramedWrite::new(inner, LspFrameCodec::default()) 24 | } 25 | 26 | /// Errors from LspFrameCodec. 27 | #[derive(Debug)] 28 | pub enum CodecError { 29 | /// The frame lacks the required `Content-Length` header. 30 | MissingHeader, 31 | /// The length value in the `Content-Length` header is invalid. 32 | InvalidLength, 33 | /// The media type in the `Content-Type` header is invalid. 34 | InvalidType, 35 | /// Failed to encode the frame. 36 | Encode(IoError), 37 | /// The frame contains invalid UTF8. 38 | Utf8(Utf8Error), 39 | } 40 | 41 | impl Display for CodecError { 42 | fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { 43 | match *self { 44 | Self::MissingHeader => write!(fmt, "missing required `Content-Length` header"), 45 | Self::InvalidLength => write!(fmt, "unable to parse content length"), 46 | Self::InvalidType => write!(fmt, "unable to parse content type"), 47 | Self::Encode(ref e) => write!(fmt, "failed to encode frame: {}", e), 48 | Self::Utf8(ref e) => write!(fmt, "frame contains invalid UTF8: {}", e), 49 | } 50 | } 51 | } 52 | 53 | impl Error for CodecError { 54 | fn source(&self) -> Option<&(dyn Error + 'static)> { 55 | match *self { 56 | Self::Encode(ref e) => Some(e), 57 | Self::Utf8(ref e) => Some(e), 58 | _ => None, 59 | } 60 | } 61 | } 62 | 63 | impl From for CodecError { 64 | fn from(error: IoError) -> Self { 65 | Self::Encode(error) 66 | } 67 | } 68 | 69 | impl From for CodecError { 70 | fn from(error: Utf8Error) -> Self { 71 | Self::Utf8(error) 72 | } 73 | } 74 | 75 | #[derive(Clone, Debug, Default)] 76 | pub struct LspFrameCodec { 77 | remaining_bytes: usize, 78 | } 79 | 80 | impl Encoder for LspFrameCodec { 81 | type Error = CodecError; 82 | fn encode(&mut self, item: String, dst: &mut BytesMut) -> Result<(), Self::Error> { 83 | if !item.is_empty() { 84 | // `Content-Length: ` + `\r\n\r\n` = 20 85 | dst.reserve(item.len() + number_of_digits(item.len()) + 20); 86 | let mut writer = dst.writer(); 87 | write!(writer, "Content-Length: {}\r\n\r\n{}", item.len(), item)?; 88 | writer.flush()?; 89 | } 90 | Ok(()) 91 | } 92 | } 93 | 94 | impl Decoder for LspFrameCodec { 95 | type Item = String; 96 | type Error = CodecError; 97 | 98 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 99 | if self.remaining_bytes > src.len() { 100 | return Ok(None); 101 | } 102 | 103 | match parser::parse_message(src) { 104 | Ok((remaining, message)) => { 105 | let message = str::from_utf8(message)?.to_string(); 106 | let len = src.len() - remaining.len(); 107 | src.advance(len); 108 | self.remaining_bytes = 0; 109 | // Ignore empty frame 110 | if message.is_empty() { 111 | Ok(None) 112 | } else { 113 | Ok(Some(message)) 114 | } 115 | } 116 | 117 | Err(nom::Err::Incomplete(nom::Needed::Size(needed))) => { 118 | self.remaining_bytes = needed.get(); 119 | Ok(None) 120 | } 121 | 122 | Err(nom::Err::Incomplete(nom::Needed::Unknown)) => Ok(None), 123 | 124 | Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => { 125 | let code = err.code; 126 | let parsed_bytes = src.len() - err.input.len(); 127 | src.advance(parsed_bytes); 128 | match parser::find_next_message(src) { 129 | Ok((_, position)) => src.advance(position), 130 | Err(_) => src.advance(src.len()), 131 | } 132 | match code { 133 | nom::error::ErrorKind::Digit | nom::error::ErrorKind::MapRes => { 134 | Err(CodecError::InvalidLength) 135 | } 136 | nom::error::ErrorKind::Char | nom::error::ErrorKind::IsNot => { 137 | Err(CodecError::InvalidType) 138 | } 139 | _ => Err(CodecError::MissingHeader), 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | #[inline] 147 | fn number_of_digits(mut n: usize) -> usize { 148 | let mut num_digits = 0; 149 | while n > 0 { 150 | n /= 10; 151 | num_digits += 1; 152 | } 153 | num_digits 154 | } 155 | 156 | #[cfg(test)] 157 | mod tests { 158 | use bytes::BytesMut; 159 | 160 | use super::*; 161 | 162 | #[test] 163 | fn encode_and_decode() { 164 | let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#.to_string(); 165 | let encoded = format!("Content-Length: {}\r\n\r\n{}", decoded.len(), decoded); 166 | 167 | let mut codec = LspFrameCodec::default(); 168 | let mut buffer = BytesMut::new(); 169 | codec.encode(decoded.clone(), &mut buffer).unwrap(); 170 | assert_eq!(buffer, BytesMut::from(encoded.as_str())); 171 | 172 | let mut buffer = BytesMut::from(encoded.as_str()); 173 | let message = codec.decode(&mut buffer).unwrap(); 174 | assert_eq!(message, Some(decoded)); 175 | } 176 | 177 | #[test] 178 | fn skips_encoding_empty_message() { 179 | let mut codec = LspFrameCodec::default(); 180 | let mut buffer = BytesMut::new(); 181 | codec.encode("".to_string(), &mut buffer).unwrap(); 182 | assert_eq!(buffer, BytesMut::new()); 183 | } 184 | 185 | #[test] 186 | fn decodes_optional_content_type() { 187 | let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#.to_string(); 188 | let content_len = format!("Content-Length: {}", decoded.len()); 189 | let content_type = "Content-Type: application/vscode-jsonrpc; charset=utf-8".to_string(); 190 | let encoded = format!("{}\r\n{}\r\n\r\n{}", content_len, content_type, decoded); 191 | 192 | let mut codec = LspFrameCodec::default(); 193 | let mut buffer = BytesMut::from(encoded.as_str()); 194 | let message = codec.decode(&mut buffer).unwrap(); 195 | assert_eq!(message, Some(decoded)); 196 | } 197 | 198 | #[test] 199 | fn recovers_from_parse_error() { 200 | let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#.to_string(); 201 | let encoded = format!("Content-Length: {}\r\n\r\n{}", decoded.len(), decoded); 202 | let mixed = format!("1234567890abcdefgh{}", encoded); 203 | 204 | let mut codec = LspFrameCodec::default(); 205 | let mut buffer = BytesMut::from(mixed.as_str()); 206 | 207 | match codec.decode(&mut buffer) { 208 | Err(CodecError::MissingHeader) => {} 209 | other => panic!("expected `Err(ParseError::MissingHeader)`, got {:?}", other), 210 | } 211 | 212 | let message = codec.decode(&mut buffer).unwrap(); 213 | assert_eq!(message, Some(decoded)); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/lsp/framed/mod.rs: -------------------------------------------------------------------------------- 1 | // Provides reader and writer for framed LSP messages. 2 | mod codec; 3 | mod parser; 4 | 5 | pub use codec::{reader, writer}; 6 | -------------------------------------------------------------------------------- /src/lsp/framed/parser.rs: -------------------------------------------------------------------------------- 1 | // Extracted from [tower-lsp](https://github.com/ebkalderon/tower-lsp). 2 | // Copyright (c) 2020 Eyal Kalderon. MIT License. 3 | // See codec.rs. 4 | 5 | use std::str; 6 | 7 | use nom::{ 8 | branch::alt, 9 | bytes::streaming::{is_not, tag, take_until}, 10 | character::streaming::{char, crlf, digit1, space0}, 11 | combinator::{map, map_res, opt}, 12 | multi::length_data, 13 | sequence::{delimited, terminated, tuple}, 14 | IResult, 15 | }; 16 | 17 | // Get JSON message from input using the Content-Length header. 18 | pub fn parse_message(input: &[u8]) -> IResult<&[u8], &[u8]> { 19 | let content_len = delimited(tag("Content-Length: "), digit1, crlf); 20 | 21 | let utf8 = alt((tag("utf-8"), tag("utf8"))); 22 | let charset = tuple((char(';'), space0, tag("charset="), utf8)); 23 | let content_type = tuple((tag("Content-Type: "), is_not(";\r"), opt(charset), crlf)); 24 | 25 | let header = terminated(terminated(content_len, opt(content_type)), crlf); 26 | 27 | let header = map_res(header, str::from_utf8); 28 | let length = map_res(header, |s: &str| s.parse::()); 29 | let mut message = length_data(length); 30 | 31 | message(input) 32 | } 33 | 34 | pub fn find_next_message(input: &[u8]) -> IResult<&[u8], usize> { 35 | map(take_until("Content-Length"), |s: &[u8]| s.len())(input) 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | 42 | #[test] 43 | fn test_parse_exact() { 44 | let decoded = 45 | r#"{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"capabilities":{}}}"#; 46 | let sample = format!("Content-Length: {}\r\n\r\n{}", decoded.len(), decoded); 47 | assert_eq!( 48 | parse_message(sample.as_bytes()), 49 | Ok(("".as_bytes(), decoded.as_bytes())) 50 | ); 51 | } 52 | 53 | #[test] 54 | fn test_optional_content_type() { 55 | let decoded = 56 | r#"{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"capabilities":{}}}"#; 57 | let content_type = "Content-Type: application/vscode-jsonrpc; charset=utf-8".to_string(); 58 | 59 | let sample = format!( 60 | "Content-Length: {}\r\n{}\r\n\r\n{}", 61 | decoded.len(), 62 | content_type, 63 | decoded 64 | ); 65 | assert_eq!( 66 | parse_message(sample.as_bytes()), 67 | Ok(("".as_bytes(), decoded.as_bytes())) 68 | ); 69 | } 70 | 71 | #[test] 72 | fn test_incomplete_error_with_size() { 73 | let decoded = 74 | r#"{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"capabilities":{}}}"#; 75 | 76 | let sample = format!("Content-Length: {}\r\n\r\n", decoded.len()); 77 | assert_eq!( 78 | parse_message(sample.as_bytes()), 79 | Err(nom::Err::Incomplete(nom::Needed::new(decoded.len()))) 80 | ); 81 | 82 | assert_eq!( 83 | parse_message((sample + "{").as_bytes()), 84 | Err(nom::Err::Incomplete(nom::Needed::new(decoded.len() - 1))) 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/lsp/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod ext; 3 | pub mod framed; 4 | mod notification; 5 | mod request; 6 | mod response; 7 | pub mod types; 8 | 9 | use std::{convert::TryFrom, str::FromStr}; 10 | 11 | use serde::ser::Serializer; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | pub use notification::Notification; 15 | pub use request::Request; 16 | pub use response::{Response, ResponseResult}; 17 | use types::Unknown; 18 | 19 | #[derive(Clone, Debug, PartialEq, Deserialize)] 20 | #[serde(untagged)] 21 | #[allow(clippy::large_enum_variant)] 22 | pub enum Message { 23 | Request(Request), 24 | 25 | Notification(Notification), 26 | 27 | Response(Response), 28 | 29 | Unknown(Unknown), 30 | } 31 | 32 | impl From for Message { 33 | fn from(request: Request) -> Self { 34 | Self::Request(request) 35 | } 36 | } 37 | 38 | impl From for Message { 39 | fn from(notification: Notification) -> Self { 40 | Self::Notification(notification) 41 | } 42 | } 43 | 44 | impl From for Message { 45 | fn from(response: Response) -> Self { 46 | Self::Response(response) 47 | } 48 | } 49 | 50 | impl From for Message { 51 | fn from(unknown: Unknown) -> Self { 52 | Self::Unknown(unknown) 53 | } 54 | } 55 | 56 | impl FromStr for Message { 57 | type Err = serde_json::Error; 58 | 59 | fn from_str(s: &str) -> Result { 60 | serde_json::from_str(s) 61 | } 62 | } 63 | 64 | impl TryFrom for Message { 65 | type Error = serde_json::Error; 66 | 67 | fn try_from(value: serde_json::Value) -> Result { 68 | serde_json::from_value(value) 69 | } 70 | } 71 | 72 | // We assume that all messages have `jsonrpc: "2.0"`. 73 | impl Serialize for Message { 74 | fn serialize(&self, serializer: S) -> std::result::Result 75 | where 76 | S: Serializer, 77 | { 78 | #[derive(Serialize)] 79 | struct WithJsonRpc<'a, T: Serialize> { 80 | jsonrpc: &'static str, 81 | #[serde(flatten)] 82 | msg: &'a T, 83 | } 84 | 85 | match &self { 86 | Self::Request(request) => { 87 | let wrapped = WithJsonRpc { 88 | jsonrpc: "2.0", 89 | msg: &request, 90 | }; 91 | wrapped.serialize(serializer) 92 | } 93 | 94 | Self::Notification(notification) => { 95 | let wrapped = WithJsonRpc { 96 | jsonrpc: "2.0", 97 | msg: ¬ification, 98 | }; 99 | wrapped.serialize(serializer) 100 | } 101 | 102 | Self::Response(response) => { 103 | let wrapped = WithJsonRpc { 104 | jsonrpc: "2.0", 105 | msg: &response, 106 | }; 107 | wrapped.serialize(serializer) 108 | } 109 | 110 | Self::Unknown(unknown) => unknown.serialize(serializer), 111 | } 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use serde_json::json; 118 | 119 | use super::*; 120 | 121 | #[test] 122 | fn test_request_from_str_or_value() { 123 | let v = json!({"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":1}); 124 | let from_str: Message = serde_json::from_str(&v.to_string()).unwrap(); 125 | let from_value: Message = serde_json::from_value(v).unwrap(); 126 | assert_eq!(from_str, from_value); 127 | } 128 | 129 | #[test] 130 | fn test_notification_from_str_or_value() { 131 | let v = json!({"jsonrpc":"2.0","method":"initialized","params":{}}); 132 | let from_str: Message = serde_json::from_str(&v.to_string()).unwrap(); 133 | let from_value: Message = serde_json::from_value(v).unwrap(); 134 | assert_eq!(from_str, from_value); 135 | } 136 | 137 | #[test] 138 | fn test_response_from_str_or_value() { 139 | let v = json!({"jsonrpc":"2.0","result":{},"id":1}); 140 | let from_str: Message = serde_json::from_str(&v.to_string()).unwrap(); 141 | let from_value: Message = serde_json::from_value(v).unwrap(); 142 | assert_eq!(from_str, from_value); 143 | } 144 | 145 | #[test] 146 | fn test_deserialize_unknown() { 147 | let v = json!({"jsonrpc":"2.0","method":"xinitialize","params":{"capabilities":{}},"id":1}); 148 | let from_str: Message = serde_json::from_str(&v.to_string()).unwrap(); 149 | let from_value: Message = serde_json::from_value(v).unwrap(); 150 | assert_eq!(from_str, from_value); 151 | } 152 | 153 | #[test] 154 | fn test_serialize_unknown_notification() { 155 | let v = json!({"jsonrpc":"2.0","method":"language/status","params":{"message":""}}); 156 | let s = v.to_string(); 157 | let from_value: Message = serde_json::from_value(v).unwrap(); 158 | assert_eq!(serde_json::to_string(&from_value).unwrap(), s); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/lsp/notification.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | // NOTE Not using `lsp_types::lsp_notification!` because rust-analyzer 4 | // doesn't seem to understand it well at the moment and shows `{unknown}`. 5 | 6 | /// A [notification message]. 7 | /// 8 | /// [notification message]: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage 9 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 10 | #[serde(tag = "method")] 11 | pub enum Notification { 12 | // To Server 13 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialized 14 | #[serde(rename = "initialized")] 15 | Initialized { 16 | params: lsp_types::InitializedParams, 17 | }, 18 | 19 | // To Server 20 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#exit 21 | #[serde(rename = "exit")] 22 | Exit { params: () }, 23 | 24 | // To Server 25 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeWorkspaceFolders 26 | #[serde(rename = "workspace/didChangeWorkspaceFolders")] 27 | DidChangeWorkspaceFolders { 28 | params: lsp_types::DidChangeWorkspaceFoldersParams, 29 | }, 30 | 31 | // To Server 32 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeConfiguration 33 | #[serde(rename = "workspace/didChangeConfiguration")] 34 | DidChangeConfiguration { 35 | params: lsp_types::DidChangeConfigurationParams, 36 | }, 37 | 38 | // To Server 39 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeConfiguration 40 | #[serde(rename = "workspace/didChangeWatchedFiles")] 41 | DidChangeWatchedFiles { 42 | params: lsp_types::DidChangeWatchedFilesParams, 43 | }, 44 | 45 | // To Server 46 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didOpen 47 | #[serde(rename = "textDocument/didOpen")] 48 | DidOpen { 49 | params: lsp_types::DidOpenTextDocumentParams, 50 | }, 51 | 52 | // To Server 53 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didChange 54 | #[serde(rename = "textDocument/didChange")] 55 | DidChange { 56 | params: lsp_types::DidChangeTextDocumentParams, 57 | }, 58 | 59 | // To Server 60 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_willSave 61 | #[serde(rename = "textDocument/willSave")] 62 | WillSave { 63 | params: lsp_types::WillSaveTextDocumentParams, 64 | }, 65 | 66 | // To Server 67 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didSave 68 | #[serde(rename = "textDocument/didSave")] 69 | DidSave { 70 | params: lsp_types::DidSaveTextDocumentParams, 71 | }, 72 | 73 | // To Server 74 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didClose 75 | #[serde(rename = "textDocument/didClose")] 76 | DidClose { 77 | params: lsp_types::DidCloseTextDocumentParams, 78 | }, 79 | 80 | // To Client 81 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage 82 | #[serde(rename = "window/logMessage")] 83 | LogMessage { params: lsp_types::LogMessageParams }, 84 | 85 | // To Client 86 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage 87 | #[serde(rename = "window/showMessage")] 88 | ShowMessage { 89 | params: lsp_types::ShowMessageParams, 90 | }, 91 | 92 | // To Client 93 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#telemetry_event 94 | #[serde(rename = "telemetry/event")] 95 | TelemetryEvent { params: serde_json::Value }, 96 | 97 | // To Client 98 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_publishDiagnostics 99 | #[serde(rename = "textDocument/publishDiagnostics")] 100 | PublishDiagnostics { 101 | params: lsp_types::PublishDiagnosticsParams, 102 | }, 103 | 104 | // To Server/Client 105 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress 106 | #[serde(rename = "$/progress")] 107 | Progress { params: lsp_types::ProgressParams }, 108 | 109 | // To Server/Client 110 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#cancelRequest 111 | #[serde(rename = "$/cancelRequest")] 112 | CancelRequest { params: lsp_types::CancelParams }, 113 | } 114 | -------------------------------------------------------------------------------- /src/lsp/request.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::types::Id; 4 | 5 | // NOTE Not using `lsp_types::lsp_request!` because rust-analyzer 6 | // doesn't seem to understand it well at the moment and shows `{unknown}`. 7 | 8 | /// [Request message]. Includes both from the Client and from the Server. 9 | /// 10 | /// [Request message]: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#requestMessage 11 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 12 | #[serde(tag = "method")] 13 | #[allow(clippy::large_enum_variant)] 14 | pub enum Request { 15 | // To Server 16 | /// > The [initialize] request is sent as the first request from the client 17 | /// > to the server. 18 | /// 19 | /// [initialize]: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize 20 | #[serde(rename = "initialize")] 21 | Initialize { 22 | id: Id, 23 | params: lsp_types::InitializeParams, 24 | }, 25 | 26 | // To Server 27 | /// > The [shutdown] request is sent from the client to the server. 28 | /// > It asks the server to shut down, but to not exit (otherwise the 29 | /// > response might not be delivered correctly to the client). 30 | /// 31 | /// [shutdown]: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#shutdown 32 | #[serde(rename = "shutdown")] 33 | Shutdown { id: Id, params: () }, 34 | 35 | // To Server 36 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol 37 | #[serde(rename = "workspace/symbol")] 38 | Symbol { 39 | id: Id, 40 | params: lsp_types::WorkspaceSymbolParams, 41 | }, 42 | 43 | // To Server 44 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand 45 | #[serde(rename = "workspace/executeCommand")] 46 | ExecuteCommand { 47 | id: Id, 48 | params: lsp_types::ExecuteCommandParams, 49 | }, 50 | 51 | // To Server 52 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_willSaveWaitUntil 53 | WillSaveWaitUntil { 54 | id: Id, 55 | params: lsp_types::WillSaveTextDocumentParams, 56 | }, 57 | 58 | // To Server 59 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion 60 | #[serde(rename = "textDocument/completion")] 61 | Completion { 62 | id: Id, 63 | params: lsp_types::CompletionParams, 64 | }, 65 | 66 | // To Server 67 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#completionItem_resolve 68 | #[serde(rename = "completionItem/resolve")] 69 | CompletionResolve { 70 | id: Id, 71 | params: lsp_types::CompletionItem, 72 | }, 73 | 74 | // To Server 75 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover 76 | #[serde(rename = "textDocument/hover")] 77 | Hover { 78 | id: Id, 79 | params: lsp_types::HoverParams, 80 | }, 81 | 82 | // To Server 83 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp 84 | #[serde(rename = "textDocument/signatureHelp")] 85 | SignatureHelp { 86 | id: Id, 87 | params: lsp_types::SignatureHelpParams, 88 | }, 89 | 90 | // To Server 91 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration 92 | #[serde(rename = "textDocument/declaration")] 93 | GotoDeclaration { 94 | id: Id, 95 | params: lsp_types::request::GotoDeclarationParams, 96 | }, 97 | 98 | // To Server 99 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition 100 | #[serde(rename = "textDocument/definition")] 101 | GotoDefinition { 102 | id: Id, 103 | params: lsp_types::GotoDefinitionParams, 104 | }, 105 | 106 | // To Server 107 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition 108 | #[serde(rename = "textDocument/typeDefinition")] 109 | GotoTypeDefinition { 110 | id: Id, 111 | params: lsp_types::request::GotoTypeDefinitionParams, 112 | }, 113 | 114 | // To Server 115 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation 116 | #[serde(rename = "textDocument/implementation")] 117 | GotoImplementation { 118 | id: Id, 119 | params: lsp_types::request::GotoImplementationParams, 120 | }, 121 | 122 | // To Server 123 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references 124 | #[serde(rename = "textDocument/references")] 125 | References { 126 | id: Id, 127 | params: lsp_types::ReferenceParams, 128 | }, 129 | 130 | // To Server 131 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight 132 | #[serde(rename = "textDocument/documentHighlight")] 133 | DocumentHighlight { 134 | id: Id, 135 | params: lsp_types::DocumentHighlightParams, 136 | }, 137 | 138 | // To Server 139 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol 140 | #[serde(rename = "textDocument/documentSymbol")] 141 | DocumentSymbol { 142 | id: Id, 143 | params: lsp_types::DocumentSymbolParams, 144 | }, 145 | 146 | // To Server 147 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction 148 | #[serde(rename = "textDocument/codeAction")] 149 | CodeAction { 150 | id: Id, 151 | params: lsp_types::CodeActionParams, 152 | }, 153 | 154 | // To Server 155 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeLens 156 | #[serde(rename = "textDocument/codeLens")] 157 | CodeLens { 158 | id: Id, 159 | params: lsp_types::CodeLensParams, 160 | }, 161 | 162 | // To Server 163 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#codeLens_resolve 164 | #[serde(rename = "codeLens/resolve")] 165 | CodeLensResolve { id: Id, params: lsp_types::CodeLens }, 166 | 167 | // To Server 168 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentLink 169 | #[serde(rename = "textDocument/documentLink")] 170 | DocumentLink { 171 | id: Id, 172 | params: lsp_types::DocumentLinkParams, 173 | }, 174 | 175 | // To Server 176 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#documentLink_resolve 177 | #[serde(rename = "documentLink/resolve")] 178 | DocumentLinkResolve { 179 | id: Id, 180 | params: lsp_types::DocumentLink, 181 | }, 182 | 183 | // To Server 184 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentColor 185 | #[serde(rename = "textDocument/documentColor")] 186 | DocumentColor { 187 | id: Id, 188 | params: lsp_types::DocumentColorParams, 189 | }, 190 | 191 | // To Server 192 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_colorPresentation 193 | #[serde(rename = "textDocument/colorPresentation")] 194 | ColorPresentation { 195 | id: Id, 196 | params: lsp_types::ColorPresentationParams, 197 | }, 198 | 199 | // To Server 200 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting 201 | #[serde(rename = "textDocument/formatting")] 202 | Formatting { 203 | id: Id, 204 | params: lsp_types::DocumentFormattingParams, 205 | }, 206 | 207 | // To Server 208 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting 209 | #[serde(rename = "textDocument/rangeFormatting")] 210 | RangeFormatting { 211 | id: Id, 212 | params: lsp_types::DocumentRangeFormattingParams, 213 | }, 214 | 215 | // To Server 216 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_onTypeFormatting 217 | #[serde(rename = "textDocument/onTypeFormatting")] 218 | OnTypeFormatting { 219 | id: Id, 220 | params: lsp_types::DocumentOnTypeFormattingParams, 221 | }, 222 | 223 | // To Server 224 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename 225 | #[serde(rename = "textDocument/rename")] 226 | Rename { 227 | id: Id, 228 | params: lsp_types::RenameParams, 229 | }, 230 | 231 | // To Server 232 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_prepareRename 233 | #[serde(rename = "textDocument/prepareRename")] 234 | PrepareRename { 235 | id: Id, 236 | params: lsp_types::TextDocumentPositionParams, 237 | }, 238 | 239 | // To Server 240 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_foldingRange 241 | #[serde(rename = "textDocument/foldingRange")] 242 | FoldingRange { 243 | id: Id, 244 | params: lsp_types::FoldingRangeParams, 245 | }, 246 | 247 | // To Server 248 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_selectionRange 249 | #[serde(rename = "textDocument/selectionRange")] 250 | SelectionRange { 251 | id: Id, 252 | params: lsp_types::SelectionRangeParams, 253 | }, 254 | 255 | // To Server 256 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_cancel 257 | #[serde(rename = "window/workDoneProgress/cancel")] 258 | CancelWorkDoneProgress { 259 | id: Id, 260 | params: lsp_types::WorkDoneProgressCancelParams, 261 | }, 262 | 263 | // To Client 264 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest 265 | #[serde(rename = "window/showMessageRequest")] 266 | ShowMessage { 267 | id: Id, 268 | params: lsp_types::ShowMessageRequestParams, 269 | }, 270 | 271 | // To Client 272 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability 273 | #[serde(rename = "client/registerCapability")] 274 | RegisterCapability { 275 | id: Id, 276 | params: lsp_types::RegistrationParams, 277 | }, 278 | 279 | // To Client 280 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability 281 | #[serde(rename = "client/unregisterCapability")] 282 | UnregisterCapability { 283 | id: Id, 284 | params: lsp_types::UnregistrationParams, 285 | }, 286 | 287 | // To Client 288 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders 289 | #[serde(rename = "workspace/workspaceFolders")] 290 | WorkspaceFolders { id: Id, params: () }, 291 | 292 | // To Client 293 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration 294 | #[serde(rename = "workspace/configuration")] 295 | Configuration { 296 | id: Id, 297 | params: lsp_types::ConfigurationParams, 298 | }, 299 | 300 | // To Client 301 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit 302 | #[serde(rename = "workspace/applyEdit")] 303 | ApplyEdit { 304 | id: Id, 305 | params: lsp_types::ApplyWorkspaceEditParams, 306 | }, 307 | 308 | // To Client 309 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create 310 | #[serde(rename = "window/workDoneProgress/create")] 311 | CreateWorkDoneProgress { 312 | id: Id, 313 | params: lsp_types::WorkDoneProgressCreateParams, 314 | }, 315 | } 316 | -------------------------------------------------------------------------------- /src/lsp/response.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::error::Error; 6 | use super::types::Id; 7 | 8 | /// [Response message]. Either Success or Failure response. 9 | /// 10 | /// [Response message]: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage 11 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 12 | #[serde(untagged)] 13 | pub enum Response { 14 | Success { id: Id, result: ResponseResult }, 15 | 16 | Failure { id: Option, error: Error }, 17 | } 18 | 19 | // Typed results so we can remap relative URI. 20 | // Note that the order is significant because it's deserialized to the first variant that works. 21 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 22 | #[serde(untagged)] 23 | #[serde(deny_unknown_fields)] 24 | pub enum ResponseResult { 25 | // remap location 26 | // {name,kind,location, tags?,deprecated?,containerName?}[] 27 | SymbolInfos(Vec), 28 | // remap targetUri 29 | // {targetUri,targetRange,targetSelectionRange,originSelectionRange?}[] 30 | LocationLinks(Vec), 31 | // remap uri 32 | // {uri,range}[] 33 | Locations(Vec), 34 | // remap uri 35 | // {uri,range} 36 | Location(lsp_types::Location), 37 | // remap uri 38 | // {uri,name}[] 39 | WorkspaceFolders(Vec), 40 | // remap target 41 | // {range,target, tooltip?,data?}[] 42 | DocumentLinkWithTarget(Vec), 43 | // remap target 44 | // {range,target, tooltip?,data?} 45 | DocumentLinkWithTargetResolve(DocumentLinkWithTarget), 46 | // remap if action.edit is present 47 | // ({title,command, arguments?} | {title, kind?,diagnostics?,edit?,command?,isPreferred?})[] 48 | CodeAction(lsp_types::CodeActionResponse), 49 | // remap changes and documentChanges 50 | // {changes, documentChanges} 51 | WorkspaceEditWithBoth(WorkspaceEditWithBoth), 52 | // remap changes 53 | // {changes} 54 | WorkspaceEditWithChanges(WorkspaceEditWithChanges), 55 | // remap documentChanges 56 | // {documentChanges} 57 | WorkspaceEditWithDocumentChanges(WorkspaceEditWithDocumentChanges), 58 | 59 | // noremap 60 | // {name,kind,range,selectionRange, detail?,tags?,deprecated?,children?}[] 61 | // DocumentSymbols(Vec), 62 | 63 | // noremap 64 | // {capabilities: {}, serverInfo?: {}} 65 | // Initialize(lsp_types::InitializeResult), 66 | 67 | // noremap 68 | // {contents, range?} 69 | // Hover(lsp_types::Hover), 70 | 71 | // noremap 72 | // {signatures, activeSignature?,activeParameter?} 73 | // SignatureHelp(lsp_types::SignatureHelp), 74 | 75 | // noremap 76 | // {range,newText}[] 77 | // Formatting(Vec), 78 | 79 | // noremap 80 | // {range,color}[] 81 | // DocumentColor(Vec), 82 | 83 | // noremap 84 | // {label, kind?,detail?,documentation?,deprecated?,preselect?,sortText?, 85 | // filterText?,insertText?,insertTextFormat?,tetEdit?,additionalTextEdits?, 86 | // command?,data?,tags?}[] 87 | // | {isComplete, items} 88 | // Must be before `ColorPresentation` 89 | // Completion(lsp_types::CompletionResponse), 90 | 91 | // noremap 92 | // {label,textEdit?,additionalTextEdits?}[] 93 | // ColorPresentation(Vec), 94 | 95 | // noremap 96 | // {applied} 97 | // ApplyWorkspaceEdit(lsp_types::ApplyWorkspaceEditResponse), 98 | 99 | // noremap 100 | // {label, kind?,detail?,documentation?,deprecated?,preselect?,sortText?, 101 | // filterText?,insertText?,insertTextFormat?,tetEdit?,additionalTextEdits?, 102 | // command?,data?,tags?}[] 103 | // ResolveCompletionItem(lsp_types::CompletionItem), 104 | 105 | // noremap 106 | // {title} 107 | // ShowMessage(lsp_types::MessageActionItem), 108 | 109 | // noremap 110 | // {start, end} | {range, placeholder} 111 | // PrepareRename(lsp_types::PrepareRenameResponse), 112 | 113 | // noremap 114 | // {startLine,endLine, startCharacter?,endCharacter?,kind?} 115 | // FoldingRange(Vec), 116 | 117 | // noremap 118 | // {range, command?,data?}[] 119 | // CodeLens(Vec), 120 | 121 | // noremap 122 | // {range, parent?}[] 123 | // SelectionRange(Vec), 124 | 125 | // noremap 126 | // {range, kind?}[] 127 | // DocumentHighlight(Vec), 128 | 129 | // noremap 130 | // {range, command?,data?} 131 | // CodeLensResolve(lsp_types::CodeLens), 132 | 133 | // noremap 134 | Any(serde_json::Value), 135 | // Proposed 136 | // SemanticTokensFull(lsp_types::SemanticTokensResult), 137 | // SemanticTokensFullDelta(lsp_types::SemanticTokensFullDeltaResult), 138 | // SemanticTokensRange(lsp_types::SemanticTokensRangeResult), 139 | // 140 | // CallHierarchyPrepare(Vec), 141 | // CallHierarchyOutgoingCalls(Vec), 142 | // CallHierarchyIncomingCalls(Vec), 143 | } 144 | 145 | // Some custom types to make untagged enum work. 146 | // 147 | // `DocumentLink` (`{range, target?,tooltip?,data?}`) needs to be remapped when `target` is present. 148 | // But using it in untagged enum will deserialize any objects with `range` as `DocumentLink`. 149 | // We define `DocumentLinkWithTarget` (`{range,target, tooltip?,data?}`) to workaround this. 150 | // 151 | // `lsp_types::DocumentLink` with `target` set. 152 | #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] 153 | pub struct DocumentLinkWithTarget { 154 | pub range: lsp_types::Range, 155 | pub target: url::Url, 156 | #[serde(skip_serializing_if = "Option::is_none")] 157 | pub tooltip: Option, 158 | #[serde(skip_serializing_if = "Option::is_none")] 159 | pub data: Option, 160 | } 161 | 162 | // `WorkspaceEdit` (`{changes?, documentChanges?}`) needs to be remapped. But we can't 163 | // match it using untagged enum because both fields are optional making it match any objects. 164 | // We define the following custom types to workaround it. 165 | // - With both: `{changes, documentChanges}` 166 | // - With `changes`: `{changes}` 167 | // - With `documentChanges`: `{documentChanges}` 168 | // 169 | // `lsp_types::WorkspaceEdit` both `changes` and `document_changes` 170 | #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] 171 | #[serde(rename_all = "camelCase")] 172 | pub struct WorkspaceEditWithBoth { 173 | pub changes: HashMap>, 174 | pub document_changes: lsp_types::DocumentChanges, 175 | } 176 | 177 | // `lsp_types::WorkspaceEdit` with `changes` 178 | #[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] 179 | #[serde(rename_all = "camelCase")] 180 | pub struct WorkspaceEditWithChanges { 181 | pub changes: HashMap>, 182 | } 183 | 184 | // `lsp_types::WorkspaceEdit` with `document_changes` 185 | #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] 186 | #[serde(rename_all = "camelCase")] 187 | pub struct WorkspaceEditWithDocumentChanges { 188 | pub document_changes: lsp_types::DocumentChanges, 189 | } 190 | -------------------------------------------------------------------------------- /src/lsp/types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Request ID 6 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 7 | #[serde(untagged)] 8 | pub enum Id { 9 | /// Numeric ID. 10 | Number(u64), 11 | /// String ID. 12 | String(String), 13 | } 14 | 15 | impl Display for Id { 16 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 17 | match self { 18 | Self::Number(id) => Display::fmt(id, f), 19 | Self::String(id) => fmt::Debug::fmt(id, f), 20 | } 21 | } 22 | } 23 | 24 | /// Parameters for Request and Notification. 25 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 26 | #[serde(deny_unknown_fields)] 27 | #[serde(untagged)] 28 | pub enum Params { 29 | Array(Vec), 30 | Object(serde_json::Map), 31 | } 32 | 33 | /// Unknown message type. 34 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 35 | pub struct Unknown(serde_json::Value); 36 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use argh::FromArgs; 4 | use url::Url; 5 | use warp::{http, Filter}; 6 | 7 | mod api; 8 | mod lsp; 9 | 10 | #[derive(FromArgs)] 11 | // Using block doc comments so that `argh` preserves newlines in help output. 12 | // We need to also write block doc comments without leading space. 13 | /** 14 | Start WebSocket proxy for the LSP Server. 15 | Anything after the option delimiter is used to start the server. 16 | 17 | Multiple servers can be registered by separating each with an option delimiter, 18 | and using the query parameter `name` to specify the command name on connection. 19 | If no query parameter is present, the first one is started. 20 | 21 | Examples: 22 | lsp-ws-proxy -- rust-analyzer 23 | lsp-ws-proxy -- typescript-language-server --stdio 24 | lsp-ws-proxy --listen 8888 -- rust-analyzer 25 | lsp-ws-proxy --listen 0.0.0.0:8888 -- rust-analyzer 26 | # Register multiple servers. 27 | # Choose the server with query parameter `name` when connecting. 28 | lsp-ws-proxy --listen 9999 --sync --remap \ 29 | -- typescript-language-server --stdio \ 30 | -- css-languageserver --stdio \ 31 | -- html-languageserver --stdio 32 | */ 33 | struct Options { 34 | /// address or port to listen on (default: 0.0.0.0:9999) 35 | #[argh( 36 | option, 37 | short = 'l', 38 | default = "String::from(\"0.0.0.0:9999\")", 39 | from_str_fn(parse_listen) 40 | )] 41 | listen: String, 42 | /// write text document to disk on save, and enable `/files` endpoint 43 | #[argh(switch, short = 's')] 44 | sync: bool, 45 | /// remap relative uri (source://) 46 | #[argh(switch, short = 'r')] 47 | remap: bool, 48 | /// show version and exit 49 | #[argh(switch, short = 'v')] 50 | version: bool, 51 | } 52 | 53 | #[tokio::main] 54 | async fn main() -> Result<(), Box> { 55 | tracing_subscriber::fmt() 56 | .with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned())) 57 | .init(); 58 | 59 | let (opts, commands) = get_opts_and_commands(); 60 | 61 | let cwd = std::env::current_dir()?; 62 | // TODO Move these to `api` module. 63 | let cors = warp::cors() 64 | .allow_any_origin() 65 | .allow_headers(&[http::header::CONTENT_TYPE]) 66 | .allow_methods(&[http::Method::GET, http::Method::OPTIONS, http::Method::POST]); 67 | // TODO Limit concurrent connection. Can get messy when `sync` is used. 68 | // TODO? Keep track of added files and remove them on disconnect? 69 | let proxy = api::proxy::handler(api::proxy::Context { 70 | commands, 71 | sync: opts.sync, 72 | remap: opts.remap, 73 | cwd: Url::from_directory_path(&cwd).expect("valid url from current dir"), 74 | }); 75 | let healthz = warp::path::end().and(warp::get()).map(|| "OK"); 76 | let addr = opts.listen.parse::().expect("valid addr"); 77 | // Enable `/files` endpoint if sync 78 | if opts.sync { 79 | let files = api::files::handler(api::files::Context { 80 | cwd, 81 | remap: opts.remap, 82 | }); 83 | warp::serve(proxy.or(healthz).or(files).recover(api::recover).with(cors)) 84 | .run(addr) 85 | .await; 86 | } else { 87 | warp::serve(proxy.or(healthz).recover(api::recover).with(cors)) 88 | .run(addr) 89 | .await; 90 | } 91 | Ok(()) 92 | } 93 | 94 | fn get_opts_and_commands() -> (Options, Vec>) { 95 | let args: Vec = std::env::args().collect(); 96 | let splitted: Vec> = args.split(|s| *s == "--").map(|s| s.to_vec()).collect(); 97 | let strs: Vec<&str> = splitted[0].iter().map(|s| s.as_str()).collect(); 98 | 99 | // Parse options or show help and exit. 100 | let opts = Options::from_args(&[strs[0]], &strs[1..]).unwrap_or_else(|early_exit| { 101 | // show generated help message 102 | println!("{}", early_exit.output); 103 | std::process::exit(match early_exit.status { 104 | Ok(()) => 0, 105 | Err(()) => 1, 106 | }) 107 | }); 108 | 109 | if opts.version { 110 | println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 111 | std::process::exit(0); 112 | } 113 | 114 | if splitted.len() < 2 { 115 | panic!("Command to start the server is required. See --help for examples."); 116 | } 117 | 118 | let commands = splitted[1..].iter().map(|s| s.to_owned()).collect(); 119 | (opts, commands) 120 | } 121 | 122 | fn parse_listen(value: &str) -> Result { 123 | // Allow specifying only a port number. 124 | if value.chars().all(|c| c.is_ascii_digit()) { 125 | return Ok(format!("0.0.0.0:{}", value)); 126 | } 127 | 128 | match value.parse::() { 129 | Ok(_) => Ok(String::from(value)), 130 | Err(_) => Err(format!("{} cannot be parsed as SocketAddr", value)), 131 | } 132 | } 133 | --------------------------------------------------------------------------------