├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── rust-toolchain.toml └── src ├── backend ├── language_server.rs └── mod.rs ├── deserialize.rs ├── error.rs ├── main.rs └── nu.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_BACKTRACE: 1 12 | 13 | jobs: 14 | build: 15 | container: rust:1-slim 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: current 23 | - name: dependencies 24 | run: | 25 | npm install --global nushell 26 | - name: Build 27 | run: cargo build --all-features --all-targets --workspace 28 | - name: Run formatter 29 | run: cargo fmt -- --check 30 | - name: Run linter 31 | run: cargo clippy --all-features --all-targets --workspace 32 | - name: Run tests 33 | run: cargo test --all-features --all-targets --workspace 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.73" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn 2.0.37", 29 | ] 30 | 31 | [[package]] 32 | name = "auto_impl" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" 36 | dependencies = [ 37 | "proc-macro-error", 38 | "proc-macro2", 39 | "quote", 40 | "syn 1.0.109", 41 | ] 42 | 43 | [[package]] 44 | name = "autocfg" 45 | version = "1.1.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 48 | 49 | [[package]] 50 | name = "backtrace" 51 | version = "0.3.69" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 54 | dependencies = [ 55 | "addr2line", 56 | "cc", 57 | "cfg-if", 58 | "libc", 59 | "miniz_oxide", 60 | "object", 61 | "rustc-demangle", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "1.3.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 69 | 70 | [[package]] 71 | name = "bytes" 72 | version = "1.5.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 75 | 76 | [[package]] 77 | name = "cc" 78 | version = "1.0.83" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 81 | dependencies = [ 82 | "libc", 83 | ] 84 | 85 | [[package]] 86 | name = "cfg-if" 87 | version = "1.0.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 90 | 91 | [[package]] 92 | name = "dashmap" 93 | version = "5.5.3" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" 96 | dependencies = [ 97 | "cfg-if", 98 | "hashbrown", 99 | "lock_api", 100 | "once_cell", 101 | "parking_lot_core", 102 | ] 103 | 104 | [[package]] 105 | name = "form_urlencoded" 106 | version = "1.2.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" 109 | dependencies = [ 110 | "percent-encoding", 111 | ] 112 | 113 | [[package]] 114 | name = "futures" 115 | version = "0.3.28" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 118 | dependencies = [ 119 | "futures-channel", 120 | "futures-core", 121 | "futures-io", 122 | "futures-sink", 123 | "futures-task", 124 | "futures-util", 125 | ] 126 | 127 | [[package]] 128 | name = "futures-channel" 129 | version = "0.3.28" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 132 | dependencies = [ 133 | "futures-core", 134 | "futures-sink", 135 | ] 136 | 137 | [[package]] 138 | name = "futures-core" 139 | version = "0.3.28" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 142 | 143 | [[package]] 144 | name = "futures-io" 145 | version = "0.3.28" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 148 | 149 | [[package]] 150 | name = "futures-macro" 151 | version = "0.3.28" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 154 | dependencies = [ 155 | "proc-macro2", 156 | "quote", 157 | "syn 2.0.37", 158 | ] 159 | 160 | [[package]] 161 | name = "futures-sink" 162 | version = "0.3.28" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 165 | 166 | [[package]] 167 | name = "futures-task" 168 | version = "0.3.28" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 171 | 172 | [[package]] 173 | name = "futures-util" 174 | version = "0.3.28" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 177 | dependencies = [ 178 | "futures-channel", 179 | "futures-core", 180 | "futures-io", 181 | "futures-macro", 182 | "futures-sink", 183 | "futures-task", 184 | "memchr", 185 | "pin-project-lite", 186 | "pin-utils", 187 | "slab", 188 | ] 189 | 190 | [[package]] 191 | name = "getrandom" 192 | version = "0.2.10" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 195 | dependencies = [ 196 | "cfg-if", 197 | "libc", 198 | "wasi", 199 | ] 200 | 201 | [[package]] 202 | name = "gimli" 203 | version = "0.28.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 206 | 207 | [[package]] 208 | name = "hashbrown" 209 | version = "0.14.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 212 | 213 | [[package]] 214 | name = "hermit-abi" 215 | version = "0.3.3" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 218 | 219 | [[package]] 220 | name = "httparse" 221 | version = "1.8.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 224 | 225 | [[package]] 226 | name = "idna" 227 | version = "0.4.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" 230 | dependencies = [ 231 | "unicode-bidi", 232 | "unicode-normalization", 233 | ] 234 | 235 | [[package]] 236 | name = "itoa" 237 | version = "1.0.9" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 240 | 241 | [[package]] 242 | name = "libc" 243 | version = "0.2.148" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 246 | 247 | [[package]] 248 | name = "lock_api" 249 | version = "0.4.10" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 252 | dependencies = [ 253 | "autocfg", 254 | "scopeguard", 255 | ] 256 | 257 | [[package]] 258 | name = "lsp-textdocument" 259 | version = "0.3.1" 260 | source = "git+https://github.com/GiveMe-A-Name/lsp-textdocument.git?rev=ad5525b#ad5525b47deeed014328bcc6bc1bc331ba94753e" 261 | dependencies = [ 262 | "lsp-types", 263 | "serde_json", 264 | ] 265 | 266 | [[package]] 267 | name = "lsp-types" 268 | version = "0.94.1" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" 271 | dependencies = [ 272 | "bitflags", 273 | "serde", 274 | "serde_json", 275 | "serde_repr", 276 | "url", 277 | ] 278 | 279 | [[package]] 280 | name = "memchr" 281 | version = "2.6.3" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 284 | 285 | [[package]] 286 | name = "miniz_oxide" 287 | version = "0.7.1" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 290 | dependencies = [ 291 | "adler", 292 | ] 293 | 294 | [[package]] 295 | name = "mio" 296 | version = "0.8.8" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 299 | dependencies = [ 300 | "libc", 301 | "wasi", 302 | "windows-sys", 303 | ] 304 | 305 | [[package]] 306 | name = "mktemp" 307 | version = "0.5.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "69fed8fbcd01affec44ac226784c6476a6006d98d13e33bc0ca7977aaf046bd8" 310 | dependencies = [ 311 | "uuid", 312 | ] 313 | 314 | [[package]] 315 | name = "nuls" 316 | version = "0.1.0" 317 | dependencies = [ 318 | "lsp-textdocument", 319 | "mktemp", 320 | "serde", 321 | "serde_json", 322 | "tokio", 323 | "tower-lsp", 324 | ] 325 | 326 | [[package]] 327 | name = "num_cpus" 328 | version = "1.16.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 331 | dependencies = [ 332 | "hermit-abi", 333 | "libc", 334 | ] 335 | 336 | [[package]] 337 | name = "object" 338 | version = "0.32.1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 341 | dependencies = [ 342 | "memchr", 343 | ] 344 | 345 | [[package]] 346 | name = "once_cell" 347 | version = "1.18.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 350 | 351 | [[package]] 352 | name = "parking_lot_core" 353 | version = "0.9.8" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 356 | dependencies = [ 357 | "cfg-if", 358 | "libc", 359 | "redox_syscall", 360 | "smallvec", 361 | "windows-targets", 362 | ] 363 | 364 | [[package]] 365 | name = "percent-encoding" 366 | version = "2.3.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 369 | 370 | [[package]] 371 | name = "pin-project" 372 | version = "1.1.3" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 375 | dependencies = [ 376 | "pin-project-internal", 377 | ] 378 | 379 | [[package]] 380 | name = "pin-project-internal" 381 | version = "1.1.3" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 384 | dependencies = [ 385 | "proc-macro2", 386 | "quote", 387 | "syn 2.0.37", 388 | ] 389 | 390 | [[package]] 391 | name = "pin-project-lite" 392 | version = "0.2.13" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 395 | 396 | [[package]] 397 | name = "pin-utils" 398 | version = "0.1.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 401 | 402 | [[package]] 403 | name = "proc-macro-error" 404 | version = "1.0.4" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 407 | dependencies = [ 408 | "proc-macro-error-attr", 409 | "proc-macro2", 410 | "quote", 411 | "syn 1.0.109", 412 | "version_check", 413 | ] 414 | 415 | [[package]] 416 | name = "proc-macro-error-attr" 417 | version = "1.0.4" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 420 | dependencies = [ 421 | "proc-macro2", 422 | "quote", 423 | "version_check", 424 | ] 425 | 426 | [[package]] 427 | name = "proc-macro2" 428 | version = "1.0.67" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 431 | dependencies = [ 432 | "unicode-ident", 433 | ] 434 | 435 | [[package]] 436 | name = "quote" 437 | version = "1.0.33" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 440 | dependencies = [ 441 | "proc-macro2", 442 | ] 443 | 444 | [[package]] 445 | name = "redox_syscall" 446 | version = "0.3.5" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 449 | dependencies = [ 450 | "bitflags", 451 | ] 452 | 453 | [[package]] 454 | name = "rustc-demangle" 455 | version = "0.1.23" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 458 | 459 | [[package]] 460 | name = "ryu" 461 | version = "1.0.15" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 464 | 465 | [[package]] 466 | name = "scopeguard" 467 | version = "1.2.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 470 | 471 | [[package]] 472 | name = "serde" 473 | version = "1.0.188" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 476 | dependencies = [ 477 | "serde_derive", 478 | ] 479 | 480 | [[package]] 481 | name = "serde_derive" 482 | version = "1.0.188" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 485 | dependencies = [ 486 | "proc-macro2", 487 | "quote", 488 | "syn 2.0.37", 489 | ] 490 | 491 | [[package]] 492 | name = "serde_json" 493 | version = "1.0.107" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 496 | dependencies = [ 497 | "itoa", 498 | "ryu", 499 | "serde", 500 | ] 501 | 502 | [[package]] 503 | name = "serde_repr" 504 | version = "0.1.16" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" 507 | dependencies = [ 508 | "proc-macro2", 509 | "quote", 510 | "syn 2.0.37", 511 | ] 512 | 513 | [[package]] 514 | name = "signal-hook-registry" 515 | version = "1.4.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 518 | dependencies = [ 519 | "libc", 520 | ] 521 | 522 | [[package]] 523 | name = "slab" 524 | version = "0.4.9" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 527 | dependencies = [ 528 | "autocfg", 529 | ] 530 | 531 | [[package]] 532 | name = "smallvec" 533 | version = "1.11.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 536 | 537 | [[package]] 538 | name = "syn" 539 | version = "1.0.109" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 542 | dependencies = [ 543 | "proc-macro2", 544 | "quote", 545 | "unicode-ident", 546 | ] 547 | 548 | [[package]] 549 | name = "syn" 550 | version = "2.0.37" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 553 | dependencies = [ 554 | "proc-macro2", 555 | "quote", 556 | "unicode-ident", 557 | ] 558 | 559 | [[package]] 560 | name = "tinyvec" 561 | version = "1.6.0" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 564 | dependencies = [ 565 | "tinyvec_macros", 566 | ] 567 | 568 | [[package]] 569 | name = "tinyvec_macros" 570 | version = "0.1.1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 573 | 574 | [[package]] 575 | name = "tokio" 576 | version = "1.32.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" 579 | dependencies = [ 580 | "backtrace", 581 | "bytes", 582 | "libc", 583 | "mio", 584 | "num_cpus", 585 | "pin-project-lite", 586 | "signal-hook-registry", 587 | "tokio-macros", 588 | "windows-sys", 589 | ] 590 | 591 | [[package]] 592 | name = "tokio-macros" 593 | version = "2.1.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" 596 | dependencies = [ 597 | "proc-macro2", 598 | "quote", 599 | "syn 2.0.37", 600 | ] 601 | 602 | [[package]] 603 | name = "tokio-util" 604 | version = "0.7.9" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" 607 | dependencies = [ 608 | "bytes", 609 | "futures-core", 610 | "futures-sink", 611 | "pin-project-lite", 612 | "tokio", 613 | "tracing", 614 | ] 615 | 616 | [[package]] 617 | name = "tower" 618 | version = "0.4.13" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 621 | dependencies = [ 622 | "futures-core", 623 | "futures-util", 624 | "pin-project", 625 | "pin-project-lite", 626 | "tower-layer", 627 | "tower-service", 628 | ] 629 | 630 | [[package]] 631 | name = "tower-layer" 632 | version = "0.3.2" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 635 | 636 | [[package]] 637 | name = "tower-lsp" 638 | version = "0.20.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" 641 | dependencies = [ 642 | "async-trait", 643 | "auto_impl", 644 | "bytes", 645 | "dashmap", 646 | "futures", 647 | "httparse", 648 | "lsp-types", 649 | "memchr", 650 | "serde", 651 | "serde_json", 652 | "tokio", 653 | "tokio-util", 654 | "tower", 655 | "tower-lsp-macros", 656 | "tracing", 657 | ] 658 | 659 | [[package]] 660 | name = "tower-lsp-macros" 661 | version = "0.9.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" 664 | dependencies = [ 665 | "proc-macro2", 666 | "quote", 667 | "syn 2.0.37", 668 | ] 669 | 670 | [[package]] 671 | name = "tower-service" 672 | version = "0.3.2" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 675 | 676 | [[package]] 677 | name = "tracing" 678 | version = "0.1.37" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 681 | dependencies = [ 682 | "cfg-if", 683 | "pin-project-lite", 684 | "tracing-attributes", 685 | "tracing-core", 686 | ] 687 | 688 | [[package]] 689 | name = "tracing-attributes" 690 | version = "0.1.26" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" 693 | dependencies = [ 694 | "proc-macro2", 695 | "quote", 696 | "syn 2.0.37", 697 | ] 698 | 699 | [[package]] 700 | name = "tracing-core" 701 | version = "0.1.31" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 704 | dependencies = [ 705 | "once_cell", 706 | ] 707 | 708 | [[package]] 709 | name = "unicode-bidi" 710 | version = "0.3.13" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 713 | 714 | [[package]] 715 | name = "unicode-ident" 716 | version = "1.0.12" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 719 | 720 | [[package]] 721 | name = "unicode-normalization" 722 | version = "0.1.22" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 725 | dependencies = [ 726 | "tinyvec", 727 | ] 728 | 729 | [[package]] 730 | name = "url" 731 | version = "2.4.1" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" 734 | dependencies = [ 735 | "form_urlencoded", 736 | "idna", 737 | "percent-encoding", 738 | "serde", 739 | ] 740 | 741 | [[package]] 742 | name = "uuid" 743 | version = "1.4.1" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" 746 | dependencies = [ 747 | "getrandom", 748 | ] 749 | 750 | [[package]] 751 | name = "version_check" 752 | version = "0.9.4" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 755 | 756 | [[package]] 757 | name = "wasi" 758 | version = "0.11.0+wasi-snapshot-preview1" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 761 | 762 | [[package]] 763 | name = "windows-sys" 764 | version = "0.48.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 767 | dependencies = [ 768 | "windows-targets", 769 | ] 770 | 771 | [[package]] 772 | name = "windows-targets" 773 | version = "0.48.5" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 776 | dependencies = [ 777 | "windows_aarch64_gnullvm", 778 | "windows_aarch64_msvc", 779 | "windows_i686_gnu", 780 | "windows_i686_msvc", 781 | "windows_x86_64_gnu", 782 | "windows_x86_64_gnullvm", 783 | "windows_x86_64_msvc", 784 | ] 785 | 786 | [[package]] 787 | name = "windows_aarch64_gnullvm" 788 | version = "0.48.5" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 791 | 792 | [[package]] 793 | name = "windows_aarch64_msvc" 794 | version = "0.48.5" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 797 | 798 | [[package]] 799 | name = "windows_i686_gnu" 800 | version = "0.48.5" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 803 | 804 | [[package]] 805 | name = "windows_i686_msvc" 806 | version = "0.48.5" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 809 | 810 | [[package]] 811 | name = "windows_x86_64_gnu" 812 | version = "0.48.5" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 815 | 816 | [[package]] 817 | name = "windows_x86_64_gnullvm" 818 | version = "0.48.5" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 821 | 822 | [[package]] 823 | name = "windows_x86_64_msvc" 824 | version = "0.48.5" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 827 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nuls" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | lsp-textdocument = { git = "https://github.com/GiveMe-A-Name/lsp-textdocument.git", rev = "ad5525b" } 11 | mktemp = "0.5" 12 | serde = { version = "1", features = ["derive"] } 13 | serde_json = "1" 14 | tokio = { version = "1.32.0", features = ["fs", "io-std", "macros", "process", "rt-multi-thread", "time"] } 15 | tower-lsp = "0.20.0" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ron Waldon-Howe 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 | # nuls 2 | 3 | Language Server Protocol implementation for nushell 4 | 5 | ## status 6 | 7 | - the official [nushell](http://www.nushell.sh/) project 8 | (from version [0.79](https://www.nushell.sh/blog/2023-04-25-nushell_0_79.html), onwards) 9 | is where the language-specific smarts are implemented, 10 | e.g. `nu --ide-hover` 11 | 12 | - the official [extension for Visual Studio Code](https://github.com/nushell/vscode-nushell-lang) 13 | is an IDE-specific wrapper around `nu --ide-hover`, etc 14 | 15 | - similarly, `nuls` (this project) is a wrapper around the `nu --ide-hover`, etc, 16 | but implements the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) 17 | 18 | ## project scope 19 | 20 | - `nuls` aims to have all the same LSP-powered features as the Visual Studio Code extension, 21 | but also working in any other IDE/editor that can connect to a language server, 22 | e.g. [`helix`](https://helix-editor.com/), [`lapce`](https://lapce.dev/), [`neovim`](https://neovim.io/), [`zed`](https://zed.dev/), etc 23 | 24 | - for now, please keep feature requests and bug reports focused on this goal 25 | 26 | - functionality that is not supported by upstream `nu --ide-...` is out-of-scope 27 | 28 | - functionality in `vscode-nushell-lang` that goes beyond LSP is out-of-scope 29 | 30 | ## roadmap 31 | 32 | (in no particular order, and open to suggestions) 33 | 34 | ### parity with [extension for Visual Studio Code](https://github.com/nushell/vscode-nushell-lang) 35 | 36 | - [x] [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) -> `nu --ide-hover` 37 | - [x] [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) -> `nu --ide-complete` 38 | - [x] [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) -> `nu --ide-goto-def` 39 | - [x] [textDocument/didChange](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange), 40 | [textDocument/didClose](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose), 41 | and [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen) 42 | - [x] [textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint) -> `nu --ide-check` 43 | - [x] [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics) -> `nu --ide-check` 44 | - [x] [workspace/configuration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration) 45 | - [x] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration) 46 | - [ ] raise a PR for `vscode-nushell-lang` to replace its wrapper/glue code with `nuls` 47 | 48 | ### stretch goals 49 | 50 | - [ ] [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) -> `nu --ide-check` 51 | - [ ] [textDocument/formatting](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting) -> [`nufmt`](https://github.com/nushell/nufmt) 52 | - [ ] [window/workDoneProgress/create](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration) and [window/workDoneProgress/cancel](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_workDoneProgress_cancel) 53 | 54 | ## getting started 55 | 56 | - it's still an open question as to how this project will be distributed and in what form 57 | 58 | - we'd like to tackle those specific in close collaboration with the nushell maintainers ([#3](https://github.com/jokeyrhyme/nuls/issues/3)), 59 | perhaps once we're a little closer to integation with the Visual Studio Code extension ([#9](https://github.com/jokeyrhyme/nuls/issues/9)) 60 | 61 | ### installation 62 | 63 | 1. you'll need a [stable Rust toolchain](https://www.rust-lang.org/) 64 | 65 | 2. `cargo install --git https://github.com/jokeyrhyme/nuls.git --locked` 66 | 67 | ### `helix` (23.05) 68 | 69 | - (optional) follow https://github.com/nushell/tree-sitter-nu/blob/main/installation/helix.md for the treesitter grammar 70 | 71 | - add the following to your languages.toml: 72 | 73 | ```toml 74 | [[language]] 75 | name = "nu" 76 | auto-format = false 77 | comment-token = "#" 78 | file-types = [ "nu" ] 79 | language-server = { command = "path/to/nuls" } 80 | roots = [] 81 | scope = "source.nu" 82 | shebangs = ["nu"] 83 | ``` 84 | 85 | ### `helix` with [multiple language servers per language](https://github.com/helix-editor/helix/pull/2507) 86 | 87 | recent-enough commits of `helix` now include the nushell grammar and language definition out-of-the-box, 88 | so all we need to do here tell it to use `nuls` 89 | 90 | - add the following to your languages.toml: 91 | 92 | ```toml 93 | [language-server.nuls] 94 | command = "nuls" # or "some/path/to/nuls" 95 | 96 | [[language]] 97 | name = "nu" 98 | language-servers = [ "nuls" ] 99 | ``` 100 | 101 | ## see also 102 | 103 | - http://www.nushell.sh/ 104 | - https://github.com/nushell/vscode-nushell-lang 105 | - https://github.com/nushell/vscode-nushell-lang/issues/117 106 | - https://github.com/nushell/tree-sitter-nu 107 | - https://github.com/tree-sitter/tree-sitter 108 | - https://microsoft.github.io/language-server-protocol/ 109 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/rust-lang/rustup/blob/master/doc/src/overrides.md#the-toolchain-file 2 | 3 | [toolchain] 4 | channel = "stable" 5 | components = ["clippy", "rustfmt"] 6 | -------------------------------------------------------------------------------- /src/backend/language_server.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::OsStr}; 2 | 3 | use crate::{ 4 | backend::Backend, 5 | error::map_err_to_parse_error, 6 | nu::{run_compiler, IdeComplete, IdeGotoDef, IdeHover}, 7 | }; 8 | 9 | #[allow(clippy::wildcard_imports)] 10 | use tower_lsp::lsp_types::*; 11 | use tower_lsp::{jsonrpc::Result, lsp_types::notification::DidChangeConfiguration}; 12 | use tower_lsp::{lsp_types::notification::Notification, LanguageServer}; 13 | 14 | #[tower_lsp::async_trait] 15 | impl LanguageServer for Backend { 16 | async fn did_change(&self, params: DidChangeTextDocumentParams) { 17 | let uri = params.text_document.uri.clone(); 18 | if let Err(e) = self.try_did_change(params) { 19 | self.client 20 | .log_message(MessageType::ERROR, format!("{e:?}")) 21 | .await; 22 | } 23 | if let Err(e) = self.throttled_validate_document(&uri).await { 24 | self.client 25 | .log_message(MessageType::ERROR, format!("{e:?}")) 26 | .await; 27 | }; 28 | } 29 | 30 | async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { 31 | if let Err(e) = self.try_did_change_configuration(params).await { 32 | self.client 33 | .log_message(MessageType::ERROR, format!("{e:?}")) 34 | .await; 35 | } 36 | } 37 | 38 | async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { 39 | self.client 40 | .log_message( 41 | MessageType::INFO, 42 | format!( 43 | "workspace folders: added={:?}; removed={:?}", 44 | params.event.added, params.event.removed 45 | ), 46 | ) 47 | .await; 48 | } 49 | 50 | async fn did_close(&self, params: DidCloseTextDocumentParams) { 51 | if let Err(e) = self.try_did_close(params) { 52 | self.client 53 | .log_message(MessageType::ERROR, format!("{e:?}")) 54 | .await; 55 | } 56 | } 57 | 58 | async fn did_open(&self, params: DidOpenTextDocumentParams) { 59 | let uri = params.text_document.uri.clone(); 60 | if let Err(e) = self.try_did_open(params) { 61 | self.client 62 | .log_message(MessageType::ERROR, format!("{e:?}")) 63 | .await; 64 | } 65 | if let Err(e) = self.validate_document(&uri).await { 66 | self.client 67 | .log_message(MessageType::ERROR, format!("{e:?}")) 68 | .await; 69 | }; 70 | } 71 | 72 | async fn initialize(&self, params: InitializeParams) -> Result { 73 | // panic: this is the only place we `OnceLock::set`, 74 | // so we've entered strange territory if something else writes to them first 75 | 76 | self.can_change_configuration 77 | .set(matches!( 78 | params.capabilities.workspace, 79 | Some(WorkspaceClientCapabilities { 80 | did_change_configuration: Some(_), 81 | .. 82 | }) 83 | )) 84 | .expect("server value initialized out of sequence"); 85 | 86 | self.can_lookup_configuration 87 | .set(matches!( 88 | params.capabilities.workspace, 89 | Some(WorkspaceClientCapabilities { 90 | configuration: Some(_), 91 | .. 92 | }) 93 | )) 94 | .expect("server value initialized out of sequence"); 95 | 96 | self.can_publish_diagnostics 97 | .set(matches!( 98 | params.capabilities.text_document, 99 | Some(TextDocumentClientCapabilities { 100 | publish_diagnostics: Some(_), 101 | .. 102 | }) 103 | )) 104 | .expect("server value initialized out of sequence"); 105 | 106 | Ok(InitializeResult { 107 | capabilities: ServerCapabilities { 108 | completion_provider: Some(CompletionOptions::default()), 109 | definition_provider: Some(OneOf::Left(true)), 110 | hover_provider: Some(HoverProviderCapability::Simple(true)), 111 | inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options( 112 | InlayHintOptions { 113 | resolve_provider: Some(false), 114 | ..Default::default() 115 | }, 116 | ))), 117 | // TODO: what do we do when the client doesn't support UTF-16 ? 118 | // lsp-textdocument crate requires UTF-16 119 | position_encoding: Some(PositionEncodingKind::UTF16), 120 | text_document_sync: Some(TextDocumentSyncCapability::Kind( 121 | TextDocumentSyncKind::INCREMENTAL, 122 | )), 123 | workspace: Some(WorkspaceServerCapabilities { 124 | workspace_folders: Some(WorkspaceFoldersServerCapabilities { 125 | supported: Some(true), 126 | change_notifications: Some(OneOf::Left(true)), 127 | }), 128 | ..Default::default() 129 | }), 130 | ..Default::default() 131 | }, 132 | server_info: Some(ServerInfo { 133 | name: String::from(env!("CARGO_PKG_NAME")), 134 | version: Some(String::from(env!("CARGO_PKG_VERSION"))), 135 | }), 136 | }) 137 | } 138 | 139 | async fn initialized(&self, _params: InitializedParams) { 140 | if *self.can_change_configuration.get().unwrap_or(&false) { 141 | let method = String::from(DidChangeConfiguration::METHOD); 142 | if let Err(e) = self 143 | .client 144 | .register_capability(vec![Registration { 145 | id: method.clone(), 146 | method, 147 | register_options: None, 148 | }]) 149 | .await 150 | { 151 | self.client 152 | .log_message( 153 | MessageType::INFO, 154 | format!("unable to register capability: {e:?}"), 155 | ) 156 | .await; 157 | }; 158 | } 159 | 160 | self.client 161 | .log_message(MessageType::INFO, "server initialized!") 162 | .await; 163 | } 164 | 165 | async fn shutdown(&self) -> Result<()> { 166 | self.client 167 | .log_message(MessageType::INFO, "server shutdown...!") 168 | .await; 169 | Ok(()) 170 | } 171 | 172 | async fn completion(&self, params: CompletionParams) -> Result> { 173 | let uri = params.text_document_position.text_document.uri; 174 | let (text, offset) = self.for_document(&uri, &|doc| { 175 | ( 176 | String::from(doc.get_content(None)), 177 | doc.offset_at(params.text_document_position.position), 178 | ) 179 | })?; 180 | 181 | let ide_settings = self.get_document_settings(&uri).await?; 182 | let output = run_compiler( 183 | &text, 184 | vec![ 185 | OsStr::new("--ide-complete"), 186 | OsStr::new(&format!("{offset}")), 187 | ], 188 | ide_settings, 189 | &uri, 190 | ) 191 | .await?; 192 | 193 | let complete = IdeComplete::try_from(output)?; 194 | 195 | Ok(Some(CompletionResponse::from(complete))) 196 | } 197 | 198 | async fn goto_definition( 199 | &self, 200 | params: GotoDefinitionParams, 201 | ) -> Result> { 202 | let uri = params.text_document_position_params.text_document.uri; 203 | let (text, offset) = self.for_document(&uri, &|doc| { 204 | ( 205 | String::from(doc.get_content(None)), 206 | doc.offset_at(params.text_document_position_params.position), 207 | ) 208 | })?; 209 | 210 | let ide_settings = self.get_document_settings(&uri).await?; 211 | let output = run_compiler( 212 | &text, 213 | vec![ 214 | OsStr::new("--ide-goto-def"), 215 | OsStr::new(&format!("{offset}")), 216 | ], 217 | ide_settings, 218 | &uri, 219 | ) 220 | .await?; 221 | 222 | let goto_def: IdeGotoDef = 223 | serde_json::from_slice(output.stdout.as_bytes()).map_err(|e| { 224 | map_err_to_parse_error(e, format!("cannot parse response from {}", output.cmdline)) 225 | })?; 226 | 227 | if matches!(goto_def.file.to_str(), None | Some("" | "__prelude__")) { 228 | return Ok(None); 229 | } 230 | 231 | if !goto_def.file.exists() { 232 | self.client 233 | .log_message( 234 | MessageType::ERROR, 235 | format!("File {} does not exist", goto_def.file.display()), 236 | ) 237 | .await; 238 | return Ok(None); 239 | } 240 | 241 | let range = self.for_document(&uri, &|doc| Range { 242 | start: doc.position_at(goto_def.start), 243 | end: doc.position_at(goto_def.end), 244 | })?; 245 | 246 | Ok(Some(GotoDefinitionResponse::Scalar(Location { 247 | uri: Url::from_file_path(goto_def.file).map_err(|()| { 248 | let mut err = tower_lsp::jsonrpc::Error::parse_error(); 249 | err.message = Cow::from( 250 | "failed to parse filesystem path in response from `nu --ide-goto-def`", 251 | ); 252 | err 253 | })?, 254 | range, 255 | }))) 256 | } 257 | 258 | async fn hover(&self, params: HoverParams) -> Result> { 259 | let uri = params.text_document_position_params.text_document.uri; 260 | let (text, offset) = self.for_document(&uri, &|doc| { 261 | ( 262 | String::from(doc.get_content(None)), 263 | doc.offset_at(params.text_document_position_params.position), 264 | ) 265 | })?; 266 | 267 | let ide_settings = self.get_document_settings(&uri).await?; 268 | let output = run_compiler( 269 | &text, 270 | vec![OsStr::new("--ide-hover"), OsStr::new(&format!("{offset}"))], 271 | ide_settings, 272 | &uri, 273 | ) 274 | .await?; 275 | 276 | let hover: IdeHover = serde_json::from_slice(output.stdout.as_bytes()).map_err(|e| { 277 | map_err_to_parse_error(e, format!("cannot parse response from {}", output.cmdline)) 278 | })?; 279 | 280 | let range = self.for_document(&uri, &|doc| { 281 | hover.span.as_ref().map(|span| Range { 282 | start: doc.position_at(span.start), 283 | end: doc.position_at(span.end), 284 | }) 285 | })?; 286 | 287 | Ok(Some(Hover { 288 | contents: HoverContents::Scalar(MarkedString::String(hover.hover)), 289 | range, 290 | })) 291 | } 292 | 293 | async fn inlay_hint(&self, params: InlayHintParams) -> Result>> { 294 | let document_inlay_hints = self.document_inlay_hints.read().map_err(|e| { 295 | tower_lsp::jsonrpc::Error::invalid_params(format!( 296 | "cannot read from inlay hints cache: {e:?}" 297 | )) 298 | })?; 299 | Ok(document_inlay_hints.get(¶ms.text_document.uri).cloned()) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::OnceLock; 3 | use std::time::{Duration, Instant}; 4 | use std::{ffi::OsStr, sync::RwLock}; 5 | 6 | pub(crate) mod language_server; 7 | use crate::nu::{IdeCheckHint, IdeCheckResponse}; 8 | use crate::{ 9 | error::map_err_to_internal_error, 10 | nu::{run_compiler, IdeCheckDiagnostic, IdeSettings}, 11 | }; 12 | use lsp_textdocument::{FullTextDocument, TextDocuments}; 13 | 14 | use serde::Deserialize; 15 | use tower_lsp::lsp_types::notification::{ 16 | DidChangeTextDocument, DidCloseTextDocument, Notification, 17 | }; 18 | #[allow(clippy::wildcard_imports)] 19 | use tower_lsp::lsp_types::*; 20 | use tower_lsp::Client; 21 | use tower_lsp::{jsonrpc::Result, lsp_types::notification::DidOpenTextDocument}; 22 | 23 | pub(crate) struct Backend { 24 | can_change_configuration: OnceLock, 25 | can_lookup_configuration: OnceLock, 26 | can_publish_diagnostics: OnceLock, 27 | client: Client, 28 | documents: RwLock, 29 | document_inlay_hints: RwLock>>, 30 | document_settings: RwLock>, 31 | global_settings: RwLock, 32 | last_validated: RwLock, 33 | } 34 | 35 | impl Backend { 36 | fn for_document(&self, uri: &Url, f: &dyn Fn(&FullTextDocument) -> T) -> Result { 37 | let documents = self.documents.read().map_err(|e| { 38 | tower_lsp::jsonrpc::Error::invalid_params(format!( 39 | "cannot read from document cache: {e:?}" 40 | )) 41 | })?; 42 | let doc = documents 43 | .get_document(uri) 44 | .ok_or(tower_lsp::jsonrpc::Error::invalid_params(format!( 45 | "{uri} not found in document cache" 46 | )))?; 47 | 48 | Ok(f(doc)) 49 | } 50 | 51 | async fn get_document_settings(&self, uri: &Url) -> Result { 52 | if !self.can_lookup_configuration.get().unwrap_or(&false) { 53 | self.client 54 | .log_message( 55 | MessageType::INFO, 56 | "no per-document settings lookup capability, returning global settings ...", 57 | ) 58 | .await; 59 | let global_settings = self.global_settings.read().map_err(|e| { 60 | tower_lsp::jsonrpc::Error::invalid_params(format!( 61 | "cannot read global settings: {e:?}" 62 | )) 63 | })?; 64 | return Ok(global_settings.clone()); 65 | } 66 | 67 | { 68 | self.client 69 | .log_message( 70 | MessageType::INFO, 71 | "checking per-document settings cache ...", 72 | ) 73 | .await; 74 | let document_settings = self.document_settings.read().map_err(|e| { 75 | map_err_to_internal_error(&e, format!("cannot read per-document settings: {e:?}")) 76 | })?; 77 | if let Some(settings) = document_settings.get(uri) { 78 | return Ok(settings.clone()); 79 | } 80 | } 81 | 82 | self.client 83 | .log_message( 84 | MessageType::INFO, 85 | "fetching per-document settings for cache ...", 86 | ) 87 | .await; 88 | let values = self 89 | .client 90 | .configuration(vec![ConfigurationItem { 91 | scope_uri: Some(uri.clone()), 92 | section: Some(String::from("nushellLanguageServer")), 93 | }]) 94 | .await?; 95 | if let Some(value) = values.into_iter().next() { 96 | let settings: IdeSettings = serde_json::from_value(value).unwrap_or_default(); 97 | let mut document_settings = self.document_settings.write().map_err(|e| { 98 | map_err_to_internal_error(&e, format!("cannot write per-document settings: {e:?}")) 99 | })?; 100 | document_settings.insert(uri.clone(), settings.clone()); 101 | return Ok(settings); 102 | } 103 | 104 | self.client 105 | .log_message(MessageType::INFO, "fallback, returning default settings") 106 | .await; 107 | Ok(IdeSettings::default()) 108 | } 109 | 110 | pub fn new(client: Client) -> Self { 111 | Self { 112 | can_change_configuration: OnceLock::new(), 113 | can_lookup_configuration: OnceLock::new(), 114 | can_publish_diagnostics: OnceLock::new(), 115 | client, 116 | documents: RwLock::new(TextDocuments::new()), 117 | document_inlay_hints: RwLock::new(HashMap::new()), 118 | document_settings: RwLock::new(HashMap::new()), 119 | global_settings: RwLock::new(IdeSettings::default()), 120 | last_validated: RwLock::new(Instant::now()), 121 | } 122 | } 123 | 124 | async fn throttled_validate_document(&self, uri: &Url) -> Result<()> { 125 | // TODO: this is a quick imperfect hack, but eventually we probably want a thorough solution using threads/channels? 126 | // TODO: ensure that we validate at least once after the most recent throttling (i.e. debounce instead of throttle) 127 | let then = { 128 | *self.last_validated.read().map_err(|e| { 129 | map_err_to_internal_error(&e, format!("cannot read throttling marker: {e:?}")) 130 | })? 131 | }; 132 | if then.elapsed() < Duration::from_millis(500) { 133 | return Ok(()); 134 | } 135 | 136 | self.validate_document(uri).await?; 137 | 138 | let mut then = self.last_validated.write().map_err(|e| { 139 | map_err_to_internal_error(&e, format!("cannot write throttling marker: {e:?}")) 140 | })?; 141 | *then = Instant::now(); 142 | Ok(()) 143 | } 144 | 145 | fn try_did_change(&self, params: DidChangeTextDocumentParams) -> Result<()> { 146 | let mut documents = self.documents.write().map_err(|e| { 147 | map_err_to_internal_error(&e, format!("cannot write to document cache: {e:?}")) 148 | })?; 149 | let params = serde_json::to_value(params).map_err(|e| { 150 | tower_lsp::jsonrpc::Error::invalid_params(format!( 151 | "cannot convert client parameters: {e:?}" 152 | )) 153 | })?; 154 | documents.listen(::METHOD, ¶ms); 155 | Ok(()) 156 | } 157 | 158 | async fn try_did_change_configuration( 159 | &self, 160 | params: DidChangeConfigurationParams, 161 | ) -> Result<()> { 162 | if *self.can_lookup_configuration.get().unwrap_or(&false) { 163 | let mut document_settings = self.document_settings.write().map_err(|e| { 164 | map_err_to_internal_error(&e, format!("cannot write per-document settings: {e:?}")) 165 | })?; 166 | document_settings.clear(); 167 | } else { 168 | let settings: ClientSettingsPayload = 169 | serde_json::from_value(params.settings).unwrap_or_default(); 170 | let mut global_settings = self.global_settings.write().map_err(|e| { 171 | map_err_to_internal_error(&e, format!("cannot write global settings: {e:?}")) 172 | })?; 173 | *global_settings = settings.nushell_language_server; 174 | } 175 | 176 | // Revalidate all open text documents 177 | let uris: Vec = { 178 | let documents = self.documents.read().map_err(|e| { 179 | tower_lsp::jsonrpc::Error::invalid_params(format!( 180 | "cannot read from document cache: {e:?}" 181 | )) 182 | })?; 183 | documents.documents().keys().cloned().collect() 184 | }; 185 | for uri in uris { 186 | self.validate_document(&uri).await?; 187 | } 188 | 189 | Ok(()) 190 | } 191 | 192 | fn try_did_close(&self, params: DidCloseTextDocumentParams) -> Result<()> { 193 | let mut documents = self.documents.write().map_err(|e| { 194 | map_err_to_internal_error(&e, format!("cannot write to document cache: {e:?}")) 195 | })?; 196 | let params = serde_json::to_value(params).map_err(|e| { 197 | tower_lsp::jsonrpc::Error::invalid_params(format!( 198 | "cannot convert client parameters: {e:?}" 199 | )) 200 | })?; 201 | documents.listen(::METHOD, ¶ms); 202 | Ok(()) 203 | } 204 | 205 | fn try_did_open(&self, params: DidOpenTextDocumentParams) -> Result<()> { 206 | let mut documents = self.documents.write().map_err(|e| { 207 | map_err_to_internal_error(&e, format!("cannot write to document cache: {e:?}")) 208 | })?; 209 | let params = serde_json::to_value(params).map_err(|e| { 210 | tower_lsp::jsonrpc::Error::invalid_params(format!( 211 | "cannot convert client parameters: {e:?}" 212 | )) 213 | })?; 214 | documents.listen(::METHOD, ¶ms); 215 | Ok(()) 216 | } 217 | 218 | async fn validate_document(&self, uri: &Url) -> Result<()> { 219 | let can_publish_diagnostics = self.can_publish_diagnostics.get().unwrap_or(&false); 220 | if !can_publish_diagnostics { 221 | self.client 222 | .log_message( 223 | MessageType::INFO, 224 | String::from("client did not report diagnostic capability"), 225 | ) 226 | .await; 227 | return Ok(()); 228 | } 229 | 230 | let text = self.for_document(uri, &|doc| String::from(doc.get_content(None)))?; 231 | 232 | let ide_settings = self.get_document_settings(uri).await?; 233 | let show_inferred_types = ide_settings.hints.show_inferred_types; 234 | let output = 235 | run_compiler(&text, vec![OsStr::new("--ide-check")], ide_settings, uri).await?; 236 | 237 | let ide_checks = IdeCheckResponse::from_compiler_response(&output); 238 | 239 | let (diagnostics, version) = self.for_document(uri, &|doc| { 240 | ( 241 | ide_checks 242 | .diagnostics 243 | .iter() 244 | .map(|d| IdeCheckDiagnostic::to_diagnostic(d, doc, uri)) 245 | .collect::>(), 246 | doc.version(), 247 | ) 248 | })?; 249 | 250 | self.client 251 | .publish_diagnostics(uri.clone(), diagnostics, Some(version)) 252 | .await; 253 | 254 | if show_inferred_types { 255 | let inlay_hints = self.for_document(uri, &|doc| { 256 | ide_checks 257 | .inlay_hints 258 | .iter() 259 | .map(|d| IdeCheckHint::to_inlay_hint(d, doc)) 260 | .collect::>() 261 | })?; 262 | 263 | let mut documents = self.document_inlay_hints.write().map_err(|e| { 264 | map_err_to_internal_error(&e, format!("cannot write inlay hints cache: {e:?}")) 265 | })?; 266 | documents.insert(uri.clone(), inlay_hints); 267 | } 268 | 269 | Ok(()) 270 | } 271 | } 272 | 273 | #[derive(Default, Deserialize)] 274 | #[serde(default, rename_all = "camelCase")] 275 | struct ClientSettingsPayload { 276 | nushell_language_server: IdeSettings, 277 | } 278 | -------------------------------------------------------------------------------- /src/deserialize.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use serde::{de::Error, Deserialize, Deserializer}; 4 | 5 | pub(crate) fn into_duration_ms<'de, D>(deserializer: D) -> std::result::Result 6 | where 7 | D: Deserializer<'de>, 8 | { 9 | let value = serde_json::Number::deserialize(deserializer)?; 10 | match value.as_u64() { 11 | Some(i) => Ok(Duration::from_millis(i)), 12 | None => Err(Error::custom("cannot convert value to u64")), 13 | } 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use serde_json::json; 19 | 20 | use super::*; 21 | 22 | #[test] 23 | fn into_duration_ms_ok() { 24 | let input = json!(123); 25 | 26 | let got = into_duration_ms(input).expect("value should be deserialized"); 27 | 28 | assert_eq!(got, Duration::from_millis(123)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, error::Error}; 2 | 3 | use serde_json::Value; 4 | 5 | pub(crate) fn map_err_to_internal_error(e: impl Error, msg: String) -> tower_lsp::jsonrpc::Error { 6 | let mut err = tower_lsp::jsonrpc::Error::internal_error(); 7 | err.data = Some(Value::String(format!("{e:?}"))); 8 | err.message = Cow::from(msg); 9 | err 10 | } 11 | 12 | pub(crate) fn map_err_to_parse_error(e: impl Error, msg: String) -> tower_lsp::jsonrpc::Error { 13 | let mut err = tower_lsp::jsonrpc::Error::parse_error(); 14 | err.data = Some(Value::String(format!("{e:?}"))); 15 | err.message = Cow::from(msg); 16 | err 17 | } 18 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all, clippy::pedantic, unsafe_code)] 2 | 3 | mod backend; 4 | mod deserialize; 5 | mod error; 6 | mod nu; 7 | use backend::Backend; 8 | 9 | use tower_lsp::{LspService, Server}; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | let stdin = tokio::io::stdin(); 14 | let stdout = tokio::io::stdout(); 15 | 16 | let (service, socket) = LspService::new(Backend::new); 17 | Server::new(stdin, stdout, socket).serve(service).await; 18 | } 19 | -------------------------------------------------------------------------------- /src/nu.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsStr, path::PathBuf, time::Duration}; 2 | 3 | use lsp_textdocument::FullTextDocument; 4 | use serde::Deserialize; 5 | use tokio::{fs, time::timeout}; 6 | use tower_lsp::lsp_types::{ 7 | CompletionItem, CompletionItemKind, CompletionResponse, DiagnosticSeverity, InlayHint, 8 | InlayHintKind, Range, Url, 9 | }; 10 | use tower_lsp::{jsonrpc::Result, lsp_types::Diagnostic}; 11 | 12 | use crate::error::{map_err_to_internal_error, map_err_to_parse_error}; 13 | 14 | #[derive(Debug, Deserialize, PartialEq)] 15 | #[serde(rename_all = "lowercase", tag = "type")] 16 | pub(crate) enum IdeCheck { 17 | Diagnostic(IdeCheckDiagnostic), 18 | Hint(IdeCheckHint), 19 | } 20 | 21 | #[derive(Clone, Debug, Deserialize, PartialEq)] 22 | pub(crate) struct IdeCheckDiagnostic { 23 | pub message: String, 24 | pub severity: IdeDiagnosticSeverity, 25 | pub span: IdeSpan, 26 | } 27 | impl IdeCheckDiagnostic { 28 | pub fn to_diagnostic(&self, doc: &FullTextDocument, uri: &Url) -> Diagnostic { 29 | Diagnostic { 30 | message: self.message.clone(), 31 | range: Range { 32 | end: doc.position_at(self.span.end), 33 | start: doc.position_at(self.span.start), 34 | }, 35 | severity: Some(DiagnosticSeverity::from(&self.severity)), 36 | source: Some(String::from(uri.clone())), 37 | ..Diagnostic::default() 38 | } 39 | } 40 | } 41 | 42 | #[derive(Clone, Debug, Deserialize, PartialEq)] 43 | pub(crate) struct IdeCheckHint { 44 | pub position: IdeSpan, 45 | pub typename: String, 46 | } 47 | impl IdeCheckHint { 48 | pub fn to_inlay_hint(&self, doc: &FullTextDocument) -> InlayHint { 49 | InlayHint { 50 | position: doc.position_at(self.position.end), 51 | label: tower_lsp::lsp_types::InlayHintLabel::String(format!(": {}", &self.typename)), 52 | kind: Some(InlayHintKind::TYPE), 53 | text_edits: None, 54 | tooltip: None, 55 | padding_left: None, 56 | padding_right: None, 57 | data: None, 58 | } 59 | } 60 | } 61 | 62 | #[derive(Debug, PartialEq)] 63 | pub(crate) struct IdeCheckResponse { 64 | pub diagnostics: Vec, 65 | pub inlay_hints: Vec, 66 | } 67 | impl IdeCheckResponse { 68 | pub fn from_compiler_response(value: &CompilerResponse) -> Self { 69 | let ide_checks: Vec = value 70 | .stdout 71 | .lines() 72 | .filter_map(|l| serde_json::from_slice(l.as_bytes()).ok()) 73 | .collect(); 74 | 75 | let diagnostics = ide_checks 76 | .iter() 77 | .filter_map(|c| match c { 78 | IdeCheck::Diagnostic(d) => Some(d), 79 | IdeCheck::Hint(_) => None, 80 | }) 81 | .cloned() 82 | .collect::>(); 83 | 84 | let inlay_hints = ide_checks 85 | .iter() 86 | .filter_map(|c| match c { 87 | IdeCheck::Diagnostic(_) => None, 88 | IdeCheck::Hint(h) => Some(h), 89 | }) 90 | .cloned() 91 | .collect::>(); 92 | 93 | Self { 94 | diagnostics, 95 | inlay_hints, 96 | } 97 | } 98 | } 99 | 100 | #[derive(Deserialize)] 101 | pub(crate) struct IdeComplete { 102 | pub completions: Vec, 103 | } 104 | impl TryFrom for IdeComplete { 105 | type Error = tower_lsp::jsonrpc::Error; 106 | 107 | fn try_from(value: CompilerResponse) -> std::result::Result { 108 | serde_json::from_slice(value.stdout.as_bytes()).map_err(|e| { 109 | map_err_to_parse_error(e, format!("cannot parse response from {}", value.cmdline)) 110 | }) 111 | } 112 | } 113 | impl From for CompletionResponse { 114 | fn from(value: IdeComplete) -> Self { 115 | CompletionResponse::Array( 116 | value 117 | .completions 118 | .into_iter() 119 | .enumerate() 120 | .map(|(i, c)| { 121 | let kind = if c.contains('(') { 122 | CompletionItemKind::FUNCTION 123 | } else { 124 | CompletionItemKind::FIELD 125 | }; 126 | CompletionItem { 127 | data: Some(serde_json::Value::from(i + 1)), 128 | kind: Some(kind), 129 | label: c, 130 | ..Default::default() 131 | } 132 | }) 133 | .collect(), 134 | ) 135 | } 136 | } 137 | 138 | #[derive(Clone, Debug, Deserialize, PartialEq)] 139 | pub(crate) enum IdeDiagnosticSeverity { 140 | Error, 141 | Warning, 142 | Information, 143 | Hint, 144 | } 145 | impl From<&IdeDiagnosticSeverity> for DiagnosticSeverity { 146 | fn from(value: &IdeDiagnosticSeverity) -> Self { 147 | match value { 148 | IdeDiagnosticSeverity::Error => Self::ERROR, 149 | IdeDiagnosticSeverity::Warning => Self::WARNING, 150 | IdeDiagnosticSeverity::Information => Self::INFORMATION, 151 | IdeDiagnosticSeverity::Hint => Self::HINT, 152 | } 153 | } 154 | } 155 | 156 | #[derive(Default, Deserialize)] 157 | #[serde(default)] 158 | pub(crate) struct IdeGotoDef { 159 | pub end: u32, 160 | pub file: PathBuf, 161 | pub start: u32, 162 | } 163 | 164 | #[derive(Deserialize)] 165 | pub(crate) struct IdeHover { 166 | pub hover: String, 167 | pub span: Option, 168 | } 169 | #[derive(Clone, Debug, Deserialize, PartialEq)] 170 | pub(crate) struct IdeSpan { 171 | pub end: u32, 172 | pub start: u32, 173 | } 174 | 175 | #[derive(Clone, Debug, Deserialize)] 176 | #[serde(default, rename_all = "camelCase")] 177 | pub(crate) struct IdeSettings { 178 | pub hints: IdeSettingsHints, 179 | pub include_dirs: Vec, 180 | pub max_number_of_problems: u32, 181 | #[serde(deserialize_with = "crate::deserialize::into_duration_ms")] 182 | pub max_nushell_invocation_time: Duration, 183 | pub nushell_executable_path: PathBuf, 184 | } 185 | impl Default for IdeSettings { 186 | fn default() -> Self { 187 | Self { 188 | hints: IdeSettingsHints::default(), 189 | include_dirs: vec![], 190 | max_number_of_problems: 1000, 191 | max_nushell_invocation_time: Duration::from_secs(10), 192 | nushell_executable_path: PathBuf::from("nu"), 193 | } 194 | } 195 | } 196 | 197 | #[derive(Clone, Debug, Deserialize)] 198 | #[serde(default, rename_all = "camelCase")] 199 | pub(crate) struct IdeSettingsHints { 200 | pub show_inferred_types: bool, 201 | } 202 | impl Default for IdeSettingsHints { 203 | fn default() -> Self { 204 | Self { 205 | show_inferred_types: true, 206 | } 207 | } 208 | } 209 | 210 | #[derive(Debug)] 211 | pub(crate) struct CompilerResponse { 212 | pub cmdline: String, 213 | pub stdout: String, 214 | } 215 | 216 | // ported from https://github.com/nushell/vscode-nushell-lang 217 | pub(crate) async fn run_compiler( 218 | text: &str, 219 | mut flags: Vec<&OsStr>, 220 | settings: IdeSettings, 221 | uri: &Url, 222 | ) -> Result { 223 | let max_number_of_problems = format!("{}", settings.max_number_of_problems); 224 | let max_number_of_problems_flag = OsStr::new(&max_number_of_problems); 225 | if flags.contains(&OsStr::new("--ide-check")) { 226 | flags.push(max_number_of_problems_flag); 227 | } 228 | 229 | // record separator character (a character that is unlikely to appear in a path) 230 | let record_separator: &OsStr = OsStr::new("\x1e"); 231 | let mut include_paths: Vec = vec![]; 232 | if uri.scheme() == "file" { 233 | let file_path = uri.to_file_path().map_err(|e| { 234 | tower_lsp::jsonrpc::Error::invalid_params(format!( 235 | "cannot convert URI to filesystem path: {e:?}", 236 | )) 237 | })?; 238 | if let Some(p) = file_path.parent() { 239 | include_paths.push(p.to_path_buf()); 240 | } 241 | } 242 | if !settings.include_dirs.is_empty() { 243 | include_paths.extend(settings.include_dirs); 244 | } 245 | let include_paths: Vec<&OsStr> = include_paths.iter().map(OsStr::new).collect(); 246 | let include_paths_flag = include_paths.join(record_separator); 247 | if !include_paths.is_empty() { 248 | flags.push(OsStr::new("--include-path")); 249 | flags.push(&include_paths_flag); 250 | } 251 | 252 | // vscode-nushell-lang creates this once per single-threaded server process, 253 | // but we create this here to ensure the temporary file is used once-per-request 254 | let temp_file = mktemp::Temp::new_file().map_err(|e| { 255 | map_err_to_internal_error(e, String::from("unable to create temporary file")) 256 | })?; 257 | fs::write(&temp_file, text).await.map_err(|e| { 258 | map_err_to_internal_error(e, String::from("unable to write to temporary file")) 259 | })?; 260 | flags.push(temp_file.as_os_str()); 261 | 262 | let cmdline = format!("nu {flags:?}"); 263 | 264 | // TODO: call nushell Rust code directly instead of via separate process, 265 | // https://github.com/jokeyrhyme/nuls/issues/7 266 | let output = timeout( 267 | settings.max_nushell_invocation_time, 268 | tokio::process::Command::new(settings.nushell_executable_path) 269 | .args(flags) 270 | .output(), 271 | ) 272 | .await 273 | .map_err(|e| { 274 | map_err_to_internal_error( 275 | e, 276 | format!( 277 | "`{cmdline}` timeout, {:?} elapsed", 278 | &settings.max_nushell_invocation_time 279 | ), 280 | ) 281 | })? 282 | .map_err(|e| map_err_to_internal_error(e, format!("`{cmdline}` failed")))?; 283 | // intentionally skip checking the ExitStatus, we always want stdout regardless 284 | 285 | let stdout = String::from_utf8(output.stdout).map_err(|e| { 286 | map_err_to_parse_error(e, format!("`{cmdline}` did not return valid UTF-8")) 287 | })?; 288 | Ok(CompilerResponse { cmdline, stdout }) 289 | } 290 | 291 | #[cfg(test)] 292 | mod tests { 293 | use tower_lsp::lsp_types::{DiagnosticSeverity, Position}; 294 | 295 | use super::*; 296 | 297 | #[test] 298 | fn deserialize_ide_check_diagnostic() { 299 | let input = r#"{"message":"Missing required positional argument.","severity":"Error","span":{"end":1026,"start":1026},"type":"diagnostic"}"#; 300 | 301 | let got: IdeCheck = serde_json::from_str(input).expect("cannot deserialize"); 302 | 303 | assert_eq!( 304 | got, 305 | IdeCheck::Diagnostic(IdeCheckDiagnostic { 306 | message: String::from("Missing required positional argument."), 307 | severity: IdeDiagnosticSeverity::Error, 308 | span: IdeSpan { 309 | end: 1026, 310 | start: 1026 311 | } 312 | }) 313 | ); 314 | } 315 | 316 | #[test] 317 | fn ide_check_diagnostic_to_diagnostic() { 318 | let input = IdeCheckDiagnostic { 319 | message: String::from("Missing required positional argument."), 320 | severity: IdeDiagnosticSeverity::Error, 321 | span: IdeSpan { end: 0, start: 0 }, 322 | }; 323 | let doc = FullTextDocument::new(String::new(), 0, String::from("foo")); 324 | let uri = Url::parse("file:///foo").expect("cannot parse URL"); 325 | 326 | let got = input.to_diagnostic(&doc, &uri); 327 | 328 | assert_eq!( 329 | got, 330 | Diagnostic { 331 | message: String::from("Missing required positional argument."), 332 | range: Range { 333 | end: Position { 334 | line: 0, 335 | character: 0 336 | }, 337 | start: Position { 338 | line: 0, 339 | character: 0 340 | }, 341 | }, 342 | severity: Some(DiagnosticSeverity::ERROR), 343 | source: Some(uri.to_string()), 344 | ..Diagnostic::default() 345 | } 346 | ); 347 | } 348 | 349 | #[tokio::test] 350 | async fn run_compiler_for_completion_ok() { 351 | let output = run_compiler( 352 | "wh", 353 | vec![OsStr::new("--ide-complete"), OsStr::new(&format!("{}", 2))], 354 | IdeSettings::default(), 355 | &Url::parse("file:///foo.nu").expect("unable to parse test URL"), 356 | ) 357 | .await 358 | .expect("unable to run `nu --ide-complete ...`"); 359 | 360 | let complete = IdeComplete::try_from(output) 361 | .expect("unable to convert output from `nu --ide-complete ...`"); 362 | let got = CompletionResponse::from(complete); 363 | 364 | if let CompletionResponse::Array(v) = &got { 365 | // sequence is non-deterministic, 366 | // so this is more reliable than using an assert_eq!() for the whole collection 367 | v.iter() 368 | .find(|c| c.label == *"where" || c.kind == Some(CompletionItemKind::FIELD)) 369 | .expect("'where' not in list"); 370 | v.iter() 371 | .find(|c| c.label == *"which" || c.kind == Some(CompletionItemKind::FIELD)) 372 | .expect("'which' not in list"); 373 | v.iter() 374 | .find(|c| c.label == *"while" || c.kind == Some(CompletionItemKind::FIELD)) 375 | .expect("'while' not in list"); 376 | } else { 377 | unreachable!(); 378 | } 379 | } 380 | 381 | #[tokio::test] 382 | async fn run_compiler_for_diagnostic_ok() { 383 | let doc = FullTextDocument::new( 384 | String::from("nushell"), 385 | 1, 386 | String::from( 387 | " 388 | let foo = ['one', 'two', 'three'] 389 | ls || 390 | ", 391 | ), 392 | ); 393 | let uri = Url::parse("file:///foo.nu").expect("unable to parse test URL"); 394 | let output = run_compiler( 395 | doc.get_content(None), 396 | vec![OsStr::new("--ide-check")], 397 | IdeSettings::default(), 398 | &uri, 399 | ) 400 | .await 401 | .expect("unable to run `nu --ide-check ...`"); 402 | 403 | let got = IdeCheckResponse::from_compiler_response(&output); 404 | 405 | assert_eq!( 406 | got, 407 | IdeCheckResponse { 408 | diagnostics: vec![IdeCheckDiagnostic { 409 | message: String::from("The '||' operator is not supported in Nushell"), 410 | severity: IdeDiagnosticSeverity::Error, 411 | span: IdeSpan { end: 72, start: 70 } 412 | }], 413 | inlay_hints: vec![IdeCheckHint { 414 | position: IdeSpan { end: 24, start: 21 }, 415 | typename: String::from("list") 416 | }], 417 | } 418 | ); 419 | } 420 | } 421 | --------------------------------------------------------------------------------