├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ci └── cargo-out-dir ├── plan9 ├── Cargo.toml └── src │ ├── acme.rs │ ├── conn.rs │ ├── dial.rs │ ├── fid.rs │ ├── fsys.rs │ ├── lib.rs │ └── plumb.rs ├── rustfmt.toml └── src ├── lsp.rs └── main.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml. 2 | 3 | name: release 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | jobs: 9 | create-release: 10 | name: create-release 11 | runs-on: ubuntu-latest 12 | outputs: 13 | upload_url: ${{ steps.release.outputs.upload_url }} 14 | rg_version: ${{ env.RG_VERSION }} 15 | steps: 16 | - name: Get the release version from the tag 17 | shell: bash 18 | if: env.ACRE_VERSION == '' 19 | run: | 20 | # Apparently, this is the right way to get a tag name. Really? 21 | # 22 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 23 | echo "ACRE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 24 | echo "version is: ${{ env.ACRE_VERSION }}" 25 | - name: Create GitHub release 26 | id: release 27 | uses: actions/create-release@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | with: 31 | tag_name: ${{ env.ACRE_VERSION }} 32 | release_name: ${{ env.ACRE_VERSION }} 33 | 34 | build-release: 35 | name: build-release 36 | needs: ['create-release'] 37 | runs-on: ${{ matrix.os }} 38 | env: 39 | # For some builds, we use cross to test on 32-bit and big-endian 40 | # systems. 41 | CARGO: cargo 42 | # When CARGO is set to CROSS, this is set to `--target matrix.target`. 43 | TARGET_FLAGS: 44 | # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. 45 | TARGET_DIR: ./target 46 | # Emit backtraces on panics. 47 | RUST_BACKTRACE: 1 48 | strategy: 49 | matrix: 50 | build: [linux, macos] 51 | include: 52 | - build: linux 53 | os: ubuntu-18.04 54 | target: x86_64-unknown-linux-musl 55 | - build: macos 56 | os: macos-latest 57 | target: x86_64-apple-darwin 58 | 59 | steps: 60 | - name: Checkout repository 61 | uses: actions/checkout@v2 62 | with: 63 | fetch-depth: 1 64 | 65 | - name: Install Rust 66 | uses: actions-rs/toolchain@v1 67 | with: 68 | toolchain: stable 69 | profile: minimal 70 | override: true 71 | target: ${{ matrix.target }} 72 | 73 | - name: Use Cross 74 | shell: bash 75 | run: | 76 | cargo install cross 77 | echo "CARGO=cross" >> $GITHUB_ENV 78 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 79 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 80 | 81 | - name: Show command used for Cargo 82 | run: | 83 | echo "cargo command is: ${{ env.CARGO }}" 84 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 85 | echo "target dir is: ${{ env.TARGET_DIR }}" 86 | 87 | - name: Strip release binary (linux and macos) 88 | if: matrix.build == 'linux' || matrix.build == 'macos' 89 | run: strip "target/${{ matrix.target }}/release/acre" 90 | 91 | - name: Upload release binary 92 | uses: actions/upload-release-asset@v1 93 | env: 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | with: 96 | upload_url: ${{ needs.create-release.outputs.upload_url }} 97 | asset_path: target/${{ matrix.target }}/release/acre 98 | asset_name: acre-${{ matrix.build }} 99 | asset_content_type: application/octet-stream 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /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 = "acre" 7 | version = "0.5.5" 8 | dependencies = [ 9 | "anyhow", 10 | "crossbeam-channel", 11 | "diff", 12 | "lazy_static", 13 | "lsp-types", 14 | "nine", 15 | "plan9", 16 | "regex", 17 | "serde", 18 | "serde_json", 19 | "toml", 20 | "xdg", 21 | ] 22 | 23 | [[package]] 24 | name = "addr2line" 25 | version = "0.17.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" 28 | dependencies = [ 29 | "gimli", 30 | ] 31 | 32 | [[package]] 33 | name = "adler" 34 | version = "1.0.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 37 | 38 | [[package]] 39 | name = "aho-corasick" 40 | version = "0.7.19" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" 43 | dependencies = [ 44 | "memchr", 45 | ] 46 | 47 | [[package]] 48 | name = "anyhow" 49 | version = "1.0.65" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" 52 | 53 | [[package]] 54 | name = "autocfg" 55 | version = "1.1.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 58 | 59 | [[package]] 60 | name = "backtrace" 61 | version = "0.3.66" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" 64 | dependencies = [ 65 | "addr2line", 66 | "cc", 67 | "cfg-if 1.0.0", 68 | "libc", 69 | "miniz_oxide", 70 | "object", 71 | "rustc-demangle", 72 | ] 73 | 74 | [[package]] 75 | name = "bitflags" 76 | version = "1.3.2" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 79 | 80 | [[package]] 81 | name = "byteorder" 82 | version = "1.4.3" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 85 | 86 | [[package]] 87 | name = "cc" 88 | version = "1.0.73" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 91 | 92 | [[package]] 93 | name = "cfg-if" 94 | version = "0.1.10" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 97 | 98 | [[package]] 99 | name = "cfg-if" 100 | version = "1.0.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 103 | 104 | [[package]] 105 | name = "crossbeam-channel" 106 | version = "0.4.4" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" 109 | dependencies = [ 110 | "crossbeam-utils", 111 | "maybe-uninit", 112 | ] 113 | 114 | [[package]] 115 | name = "crossbeam-utils" 116 | version = "0.7.2" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 119 | dependencies = [ 120 | "autocfg", 121 | "cfg-if 0.1.10", 122 | "lazy_static", 123 | ] 124 | 125 | [[package]] 126 | name = "diff" 127 | version = "0.1.13" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 130 | 131 | [[package]] 132 | name = "dirs" 133 | version = "4.0.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 136 | dependencies = [ 137 | "dirs-sys", 138 | ] 139 | 140 | [[package]] 141 | name = "dirs-sys" 142 | version = "0.3.7" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 145 | dependencies = [ 146 | "libc", 147 | "redox_users", 148 | "winapi", 149 | ] 150 | 151 | [[package]] 152 | name = "failure" 153 | version = "0.1.8" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" 156 | dependencies = [ 157 | "backtrace", 158 | "failure_derive", 159 | ] 160 | 161 | [[package]] 162 | name = "failure_derive" 163 | version = "0.1.8" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" 166 | dependencies = [ 167 | "proc-macro2", 168 | "quote", 169 | "syn", 170 | "synstructure", 171 | ] 172 | 173 | [[package]] 174 | name = "form_urlencoded" 175 | version = "1.1.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 178 | dependencies = [ 179 | "percent-encoding", 180 | ] 181 | 182 | [[package]] 183 | name = "getrandom" 184 | version = "0.2.7" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 187 | dependencies = [ 188 | "cfg-if 1.0.0", 189 | "libc", 190 | "wasi", 191 | ] 192 | 193 | [[package]] 194 | name = "gimli" 195 | version = "0.26.2" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" 198 | 199 | [[package]] 200 | name = "idna" 201 | version = "0.3.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 204 | dependencies = [ 205 | "unicode-bidi", 206 | "unicode-normalization", 207 | ] 208 | 209 | [[package]] 210 | name = "itoa" 211 | version = "1.0.4" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" 214 | 215 | [[package]] 216 | name = "lazy_static" 217 | version = "1.4.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 220 | 221 | [[package]] 222 | name = "libc" 223 | version = "0.2.135" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" 226 | 227 | [[package]] 228 | name = "lsp-types" 229 | version = "0.93.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "a3bcfee315dde785ba887edb540b08765fd7df75a7d948844be6bf5712246734" 232 | dependencies = [ 233 | "bitflags", 234 | "serde", 235 | "serde_json", 236 | "serde_repr", 237 | "url", 238 | ] 239 | 240 | [[package]] 241 | name = "maybe-uninit" 242 | version = "2.0.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 245 | 246 | [[package]] 247 | name = "memchr" 248 | version = "2.5.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 251 | 252 | [[package]] 253 | name = "miniz_oxide" 254 | version = "0.5.4" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" 257 | dependencies = [ 258 | "adler", 259 | ] 260 | 261 | [[package]] 262 | name = "nine" 263 | version = "0.5.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "4277137b8bc0f73381bc060fefdf84922d6f5762b102ec33af712c971990bfd7" 266 | dependencies = [ 267 | "bitflags", 268 | "byteorder", 269 | "failure", 270 | "serde", 271 | "serde_derive", 272 | ] 273 | 274 | [[package]] 275 | name = "object" 276 | version = "0.29.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" 279 | dependencies = [ 280 | "memchr", 281 | ] 282 | 283 | [[package]] 284 | name = "percent-encoding" 285 | version = "2.2.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 288 | 289 | [[package]] 290 | name = "plan9" 291 | version = "0.1.1" 292 | dependencies = [ 293 | "anyhow", 294 | "byteorder", 295 | "crossbeam-channel", 296 | "lazy_static", 297 | "nine", 298 | "regex", 299 | ] 300 | 301 | [[package]] 302 | name = "proc-macro2" 303 | version = "1.0.47" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 306 | dependencies = [ 307 | "unicode-ident", 308 | ] 309 | 310 | [[package]] 311 | name = "quote" 312 | version = "1.0.21" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 315 | dependencies = [ 316 | "proc-macro2", 317 | ] 318 | 319 | [[package]] 320 | name = "redox_syscall" 321 | version = "0.2.16" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 324 | dependencies = [ 325 | "bitflags", 326 | ] 327 | 328 | [[package]] 329 | name = "redox_users" 330 | version = "0.4.3" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 333 | dependencies = [ 334 | "getrandom", 335 | "redox_syscall", 336 | "thiserror", 337 | ] 338 | 339 | [[package]] 340 | name = "regex" 341 | version = "1.6.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 344 | dependencies = [ 345 | "aho-corasick", 346 | "memchr", 347 | "regex-syntax", 348 | ] 349 | 350 | [[package]] 351 | name = "regex-syntax" 352 | version = "0.6.27" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 355 | 356 | [[package]] 357 | name = "rustc-demangle" 358 | version = "0.1.21" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" 361 | 362 | [[package]] 363 | name = "ryu" 364 | version = "1.0.11" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 367 | 368 | [[package]] 369 | name = "serde" 370 | version = "1.0.145" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" 373 | dependencies = [ 374 | "serde_derive", 375 | ] 376 | 377 | [[package]] 378 | name = "serde_derive" 379 | version = "1.0.145" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" 382 | dependencies = [ 383 | "proc-macro2", 384 | "quote", 385 | "syn", 386 | ] 387 | 388 | [[package]] 389 | name = "serde_json" 390 | version = "1.0.86" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" 393 | dependencies = [ 394 | "itoa", 395 | "ryu", 396 | "serde", 397 | ] 398 | 399 | [[package]] 400 | name = "serde_repr" 401 | version = "0.1.9" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" 404 | dependencies = [ 405 | "proc-macro2", 406 | "quote", 407 | "syn", 408 | ] 409 | 410 | [[package]] 411 | name = "syn" 412 | version = "1.0.102" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" 415 | dependencies = [ 416 | "proc-macro2", 417 | "quote", 418 | "unicode-ident", 419 | ] 420 | 421 | [[package]] 422 | name = "synstructure" 423 | version = "0.12.6" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" 426 | dependencies = [ 427 | "proc-macro2", 428 | "quote", 429 | "syn", 430 | "unicode-xid", 431 | ] 432 | 433 | [[package]] 434 | name = "thiserror" 435 | version = "1.0.37" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 438 | dependencies = [ 439 | "thiserror-impl", 440 | ] 441 | 442 | [[package]] 443 | name = "thiserror-impl" 444 | version = "1.0.37" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 447 | dependencies = [ 448 | "proc-macro2", 449 | "quote", 450 | "syn", 451 | ] 452 | 453 | [[package]] 454 | name = "tinyvec" 455 | version = "1.6.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 458 | dependencies = [ 459 | "tinyvec_macros", 460 | ] 461 | 462 | [[package]] 463 | name = "tinyvec_macros" 464 | version = "0.1.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 467 | 468 | [[package]] 469 | name = "toml" 470 | version = "0.5.9" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 473 | dependencies = [ 474 | "serde", 475 | ] 476 | 477 | [[package]] 478 | name = "unicode-bidi" 479 | version = "0.3.8" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 482 | 483 | [[package]] 484 | name = "unicode-ident" 485 | version = "1.0.5" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 488 | 489 | [[package]] 490 | name = "unicode-normalization" 491 | version = "0.1.22" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 494 | dependencies = [ 495 | "tinyvec", 496 | ] 497 | 498 | [[package]] 499 | name = "unicode-xid" 500 | version = "0.2.4" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" 503 | 504 | [[package]] 505 | name = "url" 506 | version = "2.3.1" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 509 | dependencies = [ 510 | "form_urlencoded", 511 | "idna", 512 | "percent-encoding", 513 | "serde", 514 | ] 515 | 516 | [[package]] 517 | name = "wasi" 518 | version = "0.11.0+wasi-snapshot-preview1" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 521 | 522 | [[package]] 523 | name = "winapi" 524 | version = "0.3.9" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 527 | dependencies = [ 528 | "winapi-i686-pc-windows-gnu", 529 | "winapi-x86_64-pc-windows-gnu", 530 | ] 531 | 532 | [[package]] 533 | name = "winapi-i686-pc-windows-gnu" 534 | version = "0.4.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 537 | 538 | [[package]] 539 | name = "winapi-x86_64-pc-windows-gnu" 540 | version = "0.4.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 543 | 544 | [[package]] 545 | name = "xdg" 546 | version = "2.4.1" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" 549 | dependencies = [ 550 | "dirs", 551 | ] 552 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "acre" 3 | version = "0.5.5" 4 | authors = ["Matt Jibson "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | repository = "https://github.com/mjibson/acre" 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | crossbeam-channel = "0.4" 12 | diff = "0.1" 13 | lazy_static = "1" 14 | lsp-types = "0.93" 15 | nine = "0.5" 16 | plan9 = { path = "./plan9" } 17 | regex = "1" 18 | serde_json = { version = "1", features = ["raw_value"] } 19 | serde = { version = "1.0", features = ["derive"] } 20 | toml = "0.5" 21 | xdg = "2" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acre 2 | 3 | acre is a [langserver](https://langserver.org/) client for [acme](https://www.youtube.com/watch?v=dP1xVpMPn8M) in [Rust](https://www.rust-lang.org/). 4 | 5 | This is very much in **beta** and purposefully crashes on most errors. If a crash occurs, please file a bug so the feature can be added. Lenses and some other features are not yet supported. Config files may change. 6 | 7 | It functions by creating a new window in acme. The window lists all open supported files and commands. The commands can be run by right clicking on them. The currently focused window is prefixed by a `*`. Run the `Get` command in the acre window to clear the current output. 8 | 9 | Note: while the open file list contains all supported file types, those files may or may not be supported by the server if, say, the project they are in has not been configured in acre.toml. 10 | 11 | # Demo 12 | 13 | ![demo](https://user-images.githubusercontent.com/41181/79060721-afaa9080-7c45-11ea-92be-12846b108cf7.gif) 14 | 15 | # Installation 16 | 17 | The [latest release](https://github.com/mjibson/acre/releases/latest) is available for Linux and OSX. 18 | 19 | # Configuration 20 | 21 | Configuration (which servers to run) is handled by a file at `~/.config/acre.toml` (note: I'm not sure if this is true on OSX, but the location will be printed in an error if it does not exist). The file should contain a `servers` object with where names are LSP servers and values are an object: 22 | 23 | - `executable` (optional): the name of the binary to invoke. If not present, uses the name. 24 | - `files`: regex matching files that should be associated with this server. 25 | - `root_uri` (optional): Root URI of the workspace. 26 | - `workspace_folders` (optional): array of workspace folder URIs. 27 | - `options` (optional): list of options to be sent to the server. 28 | - `format_on_put` (optional): boolean (defaults to true) to run formatting on Put. 29 | - `actions_on_put` (optional): array of actions (strings) to run on Put. Only useful if `format_on_put` is not false. 30 | - `env` (optional): table of `key = "value"` pairs to add to the environment for `executable`. 31 | 32 | URIs should look something like `file:///home/user/project`. 33 | 34 | Here's an example file for `rust-analyzer` and `gopls`: 35 | 36 | ``` 37 | [servers.rust-analyzer] 38 | files = "\\.rs$" 39 | workspace_folders = [ 40 | "file:///home/username/some-project", 41 | "file:///home/username/other-project", 42 | ] 43 | [servers.rust-analyzer.env] 44 | CARGO_TARGET_DIR = "target-ra" 45 | RUSTFLAGS = "" 46 | 47 | [servers.gopls] 48 | files = '\.go$' 49 | root_uri = "file:///home/username/go-project" 50 | actions_on_put = ["source.organizeImports"] 51 | ``` 52 | 53 | This will execute the `rust-analyzer-linux` binary and associate it with all files ending in `.rs`. Two workspaces are configured. `gopls` will run on a single root for `.go` files. `gopls` will run the `organizeImports` command (i.e., `goimports`) on Put. 54 | 55 | Options to pass to each server can be added: 56 | 57 | ``` 58 | [servers.name] 59 | files = '\.ext$' 60 | root_uri = "file:///home/username/project" 61 | [servers.name.options] 62 | enableSomething = true 63 | hoverMode = "OneLine" 64 | 65 | # Disable checkOnSave in rust-analyzer: 66 | [servers.rust-analyzer.options] 67 | checkOnSave.enable = false 68 | ``` 69 | 70 | # Tested servers 71 | 72 | The following is a list of servers that have been tested with acre and are expected to work. 73 | 74 | - [rust-analyzer](https://rust-analyzer.github.io/) 75 | - [gopls](https://github.com/golang/tools/blob/master/gopls/README.md) 76 | 77 | # Other clients 78 | 79 | - [acme-lsp](https://github.com/fhs/acme-lsp) is another client for acme. It functions using Win commands instead of the new window method that acre uses. 80 | -------------------------------------------------------------------------------- /ci/cargo-out-dir: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Finds Cargo's `OUT_DIR` directory from the most recent build. 4 | # 5 | # This requires one parameter corresponding to the target directory 6 | # to search for the build output. 7 | 8 | if [ $# != 1 ]; then 9 | echo "Usage: $(basename "$0") " >&2 10 | exit 2 11 | fi 12 | 13 | # This works by finding the most recent stamp file, which is produced by 14 | # every ripgrep build. 15 | target_dir="$1" 16 | find "$target_dir" -name ripgrep-stamp -print0 \ 17 | | xargs -0 ls -t \ 18 | | head -n1 \ 19 | | xargs dirname 20 | -------------------------------------------------------------------------------- /plan9/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plan9" 3 | description = "Plan9 interaction, based on github.com/9fans/go" 4 | version = "0.1.1" 5 | authors = ["Matt Jibson "] 6 | edition = "2018" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/mjibson/acre" 9 | documentation = "https://docs.rs/plan9" 10 | keywords = ["plan9port", "acme"] 11 | 12 | [dependencies] 13 | anyhow = "1" 14 | byteorder = "1" 15 | crossbeam-channel = "0.4" 16 | lazy_static = "1" 17 | nine = "0.5" 18 | regex = "1" 19 | -------------------------------------------------------------------------------- /plan9/src/acme.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; 2 | use std::sync::Mutex; 3 | 4 | use anyhow::{bail, Result}; 5 | use lazy_static::lazy_static; 6 | use nine::p2000::OpenMode; 7 | 8 | use crate::dial; 9 | use crate::{fid::Fid, fsys::Fsys}; 10 | lazy_static! { 11 | pub static ref FSYS: Mutex = Mutex::new(dial::mount_service("acme").unwrap()); 12 | } 13 | 14 | #[derive(Debug)] 15 | pub struct WinInfo { 16 | pub id: usize, 17 | pub name: String, 18 | } 19 | 20 | impl WinInfo { 21 | pub fn windows() -> Result> { 22 | let index = FSYS.lock().unwrap().open("index", OpenMode::READ)?; 23 | let r = BufReader::new(index); 24 | let mut ws = Vec::new(); 25 | for line in r.lines() { 26 | if let Ok(line) = line { 27 | let sp: Vec<&str> = line.split_whitespace().collect(); 28 | if sp.len() < 6 { 29 | continue; 30 | } 31 | ws.push(WinInfo { 32 | id: sp[0].parse()?, 33 | name: sp[5].to_string(), 34 | }); 35 | } 36 | } 37 | Ok(ws) 38 | } 39 | } 40 | 41 | pub struct LogReader { 42 | f: Fid, 43 | buf: [u8; 8192], 44 | } 45 | 46 | #[derive(Debug)] 47 | pub struct LogEvent { 48 | pub id: usize, 49 | pub op: String, 50 | pub name: String, 51 | } 52 | 53 | impl LogReader { 54 | pub fn new() -> Result { 55 | let log = FSYS.lock().unwrap().open("log", OpenMode::READ)?; 56 | Ok(LogReader { 57 | f: log, 58 | buf: [0; 8192], 59 | }) 60 | } 61 | pub fn read(&mut self) -> Result { 62 | let sz = self.f.read(&mut self.buf)?; 63 | let data = String::from_utf8(self.buf[0..sz].to_vec())?; 64 | let sp: Vec = data.splitn(3, " ").map(|x| x.to_string()).collect(); 65 | if sp.len() != 3 { 66 | bail!("malformed log event"); 67 | } 68 | let id = sp[0].parse()?; 69 | let op = sp[1].to_string(); 70 | let name = sp[2].trim().to_string(); 71 | Ok(LogEvent { id, op, name }) 72 | } 73 | } 74 | 75 | pub struct Win { 76 | id: usize, 77 | ctl: Fid, 78 | body: Fid, 79 | addr: Fid, 80 | data: Fid, 81 | tag: Fid, 82 | } 83 | 84 | pub enum File { 85 | Ctl, 86 | Body, 87 | Addr, 88 | Data, 89 | Tag, 90 | } 91 | 92 | pub struct WinEvents { 93 | event: Fid, 94 | } 95 | 96 | impl WinEvents { 97 | pub fn read_event(&mut self) -> Result { 98 | let mut e = self.get_event()?; 99 | 100 | // expansion 101 | if e.flag & 2 != 0 { 102 | let mut e2 = self.get_event()?; 103 | if e.q0 == e.q1 { 104 | e2.orig_q0 = e.q0; 105 | e2.orig_q1 = e.q1; 106 | e2.flag = e.flag; 107 | e = e2; 108 | } 109 | } 110 | 111 | // chorded argument 112 | if e.flag & 8 != 0 { 113 | let e3 = self.get_event()?; 114 | let e4 = self.get_event()?; 115 | e.arg = e3.text; 116 | e.loc = e4.text; 117 | } 118 | 119 | Ok(e) 120 | } 121 | fn get_ch(&mut self) -> Result { 122 | let mut buf = [0; 1]; 123 | // TODO: figure out how to use a BufReader here to avoid a bunch of single reads. 124 | self.event.read_exact(&mut buf)?; 125 | Ok(buf[0] as char) 126 | } 127 | fn get_en(&mut self) -> Result { 128 | let mut c: char; 129 | let mut n: u32 = 0; 130 | loop { 131 | c = self.get_ch()?; 132 | if c < '0' || c > '9' { 133 | break; 134 | } 135 | n = n * 10 + c.to_digit(10).unwrap(); 136 | } 137 | if c != ' ' { 138 | bail!("event number syntax"); 139 | } 140 | Ok(n) 141 | } 142 | fn get_event(&mut self) -> Result { 143 | let c1 = self.get_ch()?; 144 | let c2 = self.get_ch()?; 145 | let q0 = self.get_en()?; 146 | let q1 = self.get_en()?; 147 | let flag = self.get_en()?; 148 | let nr = self.get_en()? as usize; 149 | if nr > EVENT_SIZE { 150 | bail!("event size too long"); 151 | } 152 | let mut text = vec![]; 153 | while text.len() < nr { 154 | text.push(self.get_ch()?); 155 | } 156 | let text: String = text.into_iter().collect(); 157 | if self.get_ch()? != '\n' { 158 | bail!("phase error"); 159 | } 160 | 161 | Ok(Event { 162 | c1, 163 | c2, 164 | q0, 165 | q1, 166 | flag, 167 | nr: nr as u32, 168 | text, 169 | orig_q0: q0, 170 | orig_q1: q1, 171 | arg: "".to_string(), 172 | loc: "".to_string(), 173 | }) 174 | } 175 | pub fn write_event(&mut self, ev: Event) -> Result<()> { 176 | let s = format!("{}{}{} {} \n", ev.c1, ev.c2, ev.q0, ev.q1); 177 | self.event.write(s.as_bytes())?; 178 | Ok(()) 179 | } 180 | } 181 | 182 | impl Win { 183 | pub fn new() -> Result { 184 | let mut fsys = FSYS.lock().unwrap(); 185 | let mut fid = fsys.open("new/ctl", OpenMode::RDWR)?; 186 | let mut buf = [0; 100]; 187 | let sz = fid.read(&mut buf)?; 188 | let data = String::from_utf8(buf[0..sz].to_vec())?; 189 | let sp: Vec<&str> = data.split_whitespace().collect(); 190 | if sp.len() == 0 { 191 | bail!("short read from acme/new/ctl"); 192 | } 193 | let id = sp[0].parse()?; 194 | Win::open(&mut fsys, id, fid) 195 | } 196 | // open connects to the existing window with the given id. 197 | pub fn open(fsys: &mut Fsys, id: usize, ctl: Fid) -> Result { 198 | let body = fsys.open(format!("{}/body", id).as_str(), OpenMode::RDWR)?; 199 | let addr = fsys.open(format!("{}/addr", id).as_str(), OpenMode::RDWR)?; 200 | let data = fsys.open(format!("{}/data", id).as_str(), OpenMode::RDWR)?; 201 | let tag = fsys.open(format!("{}/tag", id).as_str(), OpenMode::RDWR)?; 202 | Ok(Win { 203 | id, 204 | ctl, 205 | body, 206 | addr, 207 | data, 208 | tag, 209 | }) 210 | } 211 | pub fn events(&mut self) -> Result { 212 | let event = FSYS 213 | .lock() 214 | .unwrap() 215 | .open(format!("{}/event", self.id).as_str(), OpenMode::RDWR)?; 216 | Ok(WinEvents { event }) 217 | } 218 | pub fn id(&self) -> usize { 219 | self.id 220 | } 221 | pub fn write(&mut self, file: File, data: &str) -> Result<()> { 222 | let f = self.fid(file); 223 | f.write(data.as_bytes())?; 224 | Ok(()) 225 | } 226 | fn fid(&mut self, file: File) -> &mut Fid { 227 | match file { 228 | File::Ctl => &mut self.ctl, 229 | File::Body => &mut self.body, 230 | File::Addr => &mut self.addr, 231 | File::Data => &mut self.data, 232 | File::Tag => &mut self.tag, 233 | } 234 | } 235 | pub fn ctl(&mut self, data: &str) -> Result<()> { 236 | self.write(File::Ctl, &format!("{}\n", data)) 237 | } 238 | pub fn addr(&mut self, data: &str) -> Result<()> { 239 | self.write(File::Addr, &format!("{}", data)) 240 | } 241 | pub fn clear(&mut self) -> Result<()> { 242 | self.write(File::Addr, &format!(","))?; 243 | self.write(File::Data, &format!(""))?; 244 | Ok(()) 245 | } 246 | pub fn name(&mut self, name: &str) -> Result<()> { 247 | self.ctl(&format!("name {}", name)) 248 | } 249 | pub fn del(&mut self, sure: bool) -> Result<()> { 250 | let cmd = if sure { "delete" } else { "del" }; 251 | self.ctl(cmd) 252 | } 253 | pub fn read_addr(&mut self) -> Result<(u32, u32)> { 254 | let mut buf: [u8; 40] = [0; 40]; 255 | let f = self.fid(File::Addr); 256 | f.seek(SeekFrom::Start(0))?; 257 | let sz = f.read(&mut buf)?; 258 | let addr = std::str::from_utf8(&buf[0..sz])?; 259 | let a: Vec<&str> = addr.split_whitespace().collect(); 260 | if a.len() < 2 { 261 | bail!("short read from acme addr"); 262 | } 263 | Ok((a[0].parse()?, a[1].parse()?)) 264 | } 265 | pub fn read(&mut self, file: File) -> Result<&mut Fid> { 266 | let f = self.fid(file); 267 | f.seek(SeekFrom::Start(0))?; 268 | Ok(f) 269 | } 270 | pub fn seek(&mut self, file: File, pos: SeekFrom) -> Result { 271 | let f = self.fid(file); 272 | Ok(f.seek(pos)?) 273 | } 274 | } 275 | 276 | const EVENT_SIZE: usize = 256; 277 | 278 | #[derive(Debug)] 279 | pub struct Event { 280 | pub c1: char, 281 | pub c2: char, 282 | pub q0: u32, 283 | pub q1: u32, 284 | pub orig_q0: u32, 285 | pub orig_q1: u32, 286 | pub flag: u32, 287 | pub nr: u32, 288 | pub text: String, 289 | pub arg: String, 290 | pub loc: String, 291 | } 292 | 293 | impl Event { 294 | pub fn load_text(&mut self) { 295 | if self.text.len() == 0 && self.q0 < self.q1 { 296 | /* 297 | w.Addr("#%d,#%d", e.Q0, e.Q1) 298 | data, err := w.ReadAll("xdata") 299 | if err != nil { 300 | w.Err(err.Error()) 301 | } 302 | e.Text = data 303 | */ 304 | panic!("unimplemented"); 305 | } 306 | } 307 | } 308 | 309 | #[derive(Debug)] 310 | pub struct NlOffsets { 311 | nl: Vec, 312 | leftover: u32, 313 | } 314 | 315 | impl NlOffsets { 316 | pub fn new(r: R) -> Result { 317 | let mut r = BufReader::new(r); 318 | let mut nl = vec![0]; 319 | let mut o = 0; 320 | let mut line = vec![]; 321 | let mut leftover = 0; 322 | loop { 323 | line.clear(); 324 | let sz = r.read_until('\n' as u8, &mut line)?; 325 | if sz == 0 { 326 | break; 327 | } 328 | let n = std::str::from_utf8(&line)?.chars().count() as u32; 329 | let last: u8 = *line.last().unwrap(); 330 | if last != '\n' as u8 { 331 | leftover = n; 332 | break; 333 | } 334 | o += n; 335 | nl.push(o); 336 | } 337 | Ok(NlOffsets { nl, leftover }) 338 | } 339 | // returns line, col 340 | pub fn offset_to_line(&self, offset: u32) -> (u32, u32) { 341 | for (i, o) in self.nl.iter().enumerate() { 342 | if *o > offset { 343 | return (i as u32 - 1, offset - self.nl[i - 1]); 344 | } 345 | } 346 | let i = self.nl.len() - 1; 347 | if offset >= self.nl[i] { 348 | return (i as u32, offset - self.nl[i]); 349 | } 350 | panic!("unreachable"); 351 | } 352 | pub fn line_to_offset(&self, line: u32, col: u32) -> u32 { 353 | let line = line as usize; 354 | let eof = self.nl[self.nl.len() - 1] + self.leftover; 355 | if line >= self.nl.len() { 356 | // beyond EOF, so just return the highest offset. 357 | return eof; 358 | } 359 | let mut o = self.nl[line] + col; 360 | if o > eof { 361 | o = eof; 362 | } 363 | o 364 | } 365 | // returns the position of the last character in the file. 366 | pub fn last(&self) -> (u32, u32) { 367 | if self.nl.is_empty() { 368 | (0, self.leftover) 369 | } else { 370 | (self.nl.len() as u32 - 1, self.leftover) 371 | } 372 | } 373 | } 374 | 375 | #[cfg(test)] 376 | mod tests { 377 | use crate::acme::*; 378 | 379 | #[test] 380 | fn nloffsets() { 381 | let s = "12345\n678\n90"; 382 | let c = std::io::Cursor::new(s); 383 | let n = NlOffsets::new(c).unwrap(); 384 | assert_eq!(n.offset_to_line(0), (0, 0)); 385 | assert_eq!(n.offset_to_line(3), (0, 3)); 386 | assert_eq!(n.offset_to_line(5), (0, 5)); 387 | assert_eq!(n.offset_to_line(6), (1, 0)); 388 | assert_eq!(n.offset_to_line(7), (1, 1)); 389 | assert_eq!(n.offset_to_line(9), (1, 3)); 390 | assert_eq!(n.offset_to_line(10), (2, 0)); 391 | assert_eq!(n.offset_to_line(11), (2, 1)); 392 | assert_eq!(n.last(), (2, 2)); 393 | } 394 | 395 | #[test] 396 | fn windows() { 397 | let ws = WinInfo::windows().unwrap(); 398 | assert_ne!(ws.len(), 0); 399 | } 400 | 401 | #[test] 402 | fn log() { 403 | let mut log = LogReader::new().unwrap(); 404 | log.read().unwrap(); 405 | } 406 | 407 | #[test] 408 | #[ignore] 409 | fn new() { 410 | let mut w = Win::new().unwrap(); 411 | let mut wev = w.events().unwrap(); 412 | w.name("testing").unwrap(); 413 | w.write(File::Body, "blah hello done hello").unwrap(); 414 | loop { 415 | let mut ev = wev.read_event().unwrap(); 416 | match ev.c2 { 417 | 'x' | 'X' => { 418 | let text = ev.text.trim(); 419 | if text == "done" { 420 | break; 421 | } 422 | wev.write_event(ev).unwrap(); 423 | } 424 | 'l' | 'L' => { 425 | ev.load_text(); 426 | wev.write_event(ev).unwrap(); 427 | } 428 | _ => {} 429 | } 430 | } 431 | w.del(true).unwrap(); 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /plan9/src/conn.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Debug; 3 | use std::io::{Cursor, Read, Write}; 4 | use std::os::unix::net::UnixStream; 5 | use std::sync::{Arc, Mutex}; 6 | use std::thread; 7 | 8 | use anyhow::{bail, Result}; 9 | use byteorder::{LittleEndian, WriteBytesExt}; 10 | use crossbeam_channel::{bounded, Receiver, Sender}; 11 | use nine::{de::*, p2000::*, ser::*}; 12 | 13 | use crate::{fid, fsys}; 14 | 15 | #[derive(Clone)] 16 | pub struct Conn { 17 | writer: Arc>, 18 | pub msize: u32, 19 | tag_map: Arc>>>>, 20 | } 21 | 22 | struct ConnWriter { 23 | msg_buf: Vec, 24 | stream: UnixStream, 25 | nextfid: u32, 26 | next_tag: u16, 27 | free_tags: Vec, 28 | } 29 | 30 | impl Conn { 31 | pub fn new(stream: UnixStream) -> Result { 32 | let mut reader = stream.try_clone()?; 33 | let mut c = Conn { 34 | writer: Arc::new(Mutex::new(ConnWriter { 35 | msg_buf: Vec::new(), 36 | stream, 37 | nextfid: 1, 38 | next_tag: 0, 39 | free_tags: vec![], 40 | })), 41 | msize: 131072, 42 | tag_map: Arc::new(Mutex::new(HashMap::new())), 43 | }; 44 | let tm = Arc::clone(&c.tag_map); 45 | let cw = Arc::clone(&c.writer); 46 | 47 | thread::spawn(move || loop { 48 | let mut size: u32 = Conn::read_a(&reader).unwrap(); 49 | let mtype: u8 = Conn::read_a(&reader).unwrap(); 50 | size -= 5; 51 | let mut data = vec![0u8; size as usize]; 52 | reader.read_exact(&mut data).unwrap(); 53 | // Prepend the size back. The read_msg function needs 54 | // it incase an error type is returned. 55 | // TODO: is there a way to do this that doesn't involve 56 | // shifting everything to the right? 57 | data.insert(0, mtype); 58 | let tag: u16 = Conn::read_a(&data[1..3]).unwrap(); 59 | let s = tm 60 | .lock() 61 | .unwrap() 62 | .remove(&tag) 63 | .expect(format!("expected receiver with tag {:?}", tag).as_str()); 64 | cw.lock().unwrap().free_tags.push(tag); 65 | s.send(data).unwrap(); 66 | }); 67 | 68 | let (tag, r) = c.new_tag()?; 69 | let tx = Tversion { 70 | tag: tag, 71 | msize: c.msize, 72 | version: "9P2000".into(), 73 | }; 74 | let rx = c.rpc::(&tx, r)?; 75 | if rx.msize > c.msize { 76 | bail!("invalid msize {}", rx.msize); 77 | } 78 | c.msize = rx.msize; 79 | if rx.version != "9P2000" { 80 | bail!("invalid version {}", rx.version); 81 | } 82 | 83 | Ok(c) 84 | } 85 | 86 | fn new_tag(&mut self) -> Result<(u16, Receiver>)> { 87 | let mut cw = self.writer.lock().unwrap(); 88 | let tag: u16; 89 | if cw.free_tags.len() > 0 { 90 | tag = cw.free_tags.remove(0); 91 | } else if cw.next_tag == NOTAG { 92 | bail!("out of tags"); 93 | } else { 94 | tag = cw.next_tag; 95 | cw.next_tag += 1; 96 | } 97 | let (s, r) = bounded(0); 98 | self.tag_map.lock().unwrap().insert(tag, s); 99 | Ok((tag, r)) 100 | } 101 | 102 | fn rpc< 103 | 'de, 104 | S: Serialize + MessageTypeId + Debug, 105 | D: Deserialize<'de> + MessageTypeId + Debug, 106 | >( 107 | &mut self, 108 | s: &S, 109 | r: Receiver>, 110 | ) -> Result { 111 | self.send_msg(s)?; 112 | self.read_msg::(r) 113 | } 114 | 115 | fn send_msg(&mut self, t: &T) -> Result<()> { 116 | let mut cw = self.writer.lock().unwrap(); 117 | cw.msg_buf.truncate(0); 118 | let amt = into_vec(&t, &mut cw.msg_buf)?; 119 | 120 | assert!(self.msize >= amt); 121 | cw.stream.write_u32::(amt + 5)?; 122 | cw.stream.write_u8(::MSG_TYPE_ID)?; 123 | // Avoid a reference immutable/mutable borrowing problem. 124 | let mut stream = &cw.stream; 125 | Ok(stream.write_all(&cw.msg_buf[0..amt as usize])?) 126 | } 127 | 128 | fn read_msg<'de, T: Deserialize<'de> + MessageTypeId + Debug>( 129 | &mut self, 130 | r: Receiver>, 131 | ) -> Result { 132 | let v = r.recv()?; 133 | let mut rv = Cursor::new(v); 134 | let mtype: u8 = Conn::read_a(&mut rv)?; 135 | let want = ::MSG_TYPE_ID; 136 | if mtype == want { 137 | return Conn::read_a(&mut rv); 138 | } 139 | if mtype == 107 { 140 | let rerror: Rerror = Conn::read_a(&mut rv)?; 141 | bail!(rerror.ename); 142 | } 143 | bail!("unknown type: {}, expected: {}", mtype, want) 144 | } 145 | 146 | fn read_a<'de, R: Read, T: Deserialize<'de> + Debug>(r: R) -> Result { 147 | Ok(from_reader(r)?) 148 | } 149 | 150 | pub fn newfid(&mut self) -> u32 { 151 | let mut cw = self.writer.lock().unwrap(); 152 | cw.nextfid += 1; 153 | cw.nextfid 154 | } 155 | } 156 | 157 | const NOFID: u32 = !0; 158 | 159 | impl Conn { 160 | pub fn walk(&mut self, fid: u32, newfid: u32, wname: Vec) -> Result> { 161 | let (tag, r) = self.new_tag()?; 162 | let walk = Twalk { 163 | tag: tag, 164 | fid, 165 | newfid, 166 | wname, 167 | }; 168 | let rwalk = self.rpc::(&walk, r)?; 169 | Ok(rwalk.wqid) 170 | } 171 | pub fn open(&mut self, fid: u32, mode: OpenMode) -> Result<()> { 172 | let (tag, r) = self.new_tag()?; 173 | let open = Topen { 174 | tag: tag, 175 | fid, 176 | mode, 177 | }; 178 | self.rpc::(&open, r)?; 179 | Ok(()) 180 | } 181 | pub fn read(&mut self, fid: u32, offset: u64, count: u32) -> Result> { 182 | let (tag, r) = self.new_tag()?; 183 | let read = Tread { 184 | tag: tag, 185 | fid, 186 | offset, 187 | count, 188 | }; 189 | let rread = self.rpc::(&read, r)?; 190 | Ok(rread.data) 191 | } 192 | pub fn write(&mut self, fid: u32, offset: u64, data: Vec) -> Result { 193 | let (tag, r) = self.new_tag()?; 194 | let write = Twrite { 195 | tag: tag, 196 | fid, 197 | offset, 198 | data, 199 | }; 200 | let rwrite = self.rpc::(&write, r)?; 201 | Ok(rwrite.count) 202 | } 203 | pub fn clunk(&mut self, fid: u32) -> Result<()> { 204 | let (tag, r) = self.new_tag()?; 205 | let clunk = Tclunk { tag: tag, fid }; 206 | self.rpc::(&clunk, r)?; 207 | Ok(()) 208 | } 209 | pub fn attach(&mut self, user: String, aname: String) -> Result { 210 | let newfid = self.newfid(); 211 | let (tag, r) = self.new_tag()?; 212 | let attach = Tattach { 213 | tag: tag, 214 | fid: newfid, 215 | afid: NOFID, 216 | uname: user.into(), 217 | aname: aname.into(), 218 | }; 219 | let r = self.rpc::(&attach, r)?; 220 | Ok(fsys::Fsys { 221 | fid: fid::Fid::new(self.clone(), newfid, r.qid), 222 | }) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /plan9/src/dial.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::os::unix::net::UnixStream; 3 | 4 | use anyhow::{Result, Context}; 5 | use lazy_static::lazy_static; 6 | use regex::Regex; 7 | 8 | use crate::{conn::Conn, fid, fsys}; 9 | 10 | pub fn dial(addr: &str) -> Result { 11 | let stream = UnixStream::connect(addr).with_context(|| format!("dial addr={}", addr))?; 12 | Conn::new(stream) 13 | } 14 | 15 | pub fn dial_service(service: &str) -> Result { 16 | let ns = namespace(); 17 | dial((ns + "/" + service).as_str()) 18 | } 19 | 20 | pub fn mount_service(service: &str) -> Result { 21 | let mut conn = dial_service(service)?; 22 | let fsys = conn.attach(fid::get_user(), "".to_string())?; 23 | Ok(fsys) 24 | } 25 | 26 | // namespace returns the path to the name space directory. 27 | pub fn namespace() -> String { 28 | if let Ok(val) = env::var("NAMESPACE") { 29 | return val; 30 | } 31 | let mut disp = if let Ok(val) = env::var("DISPLAY") { 32 | val 33 | } else { 34 | // No $DISPLAY? Use :0.0 for non-X11 GUI (OS X). 35 | String::from(":0.0") 36 | }; 37 | 38 | lazy_static! { 39 | static ref DOT_ZERO: Regex = Regex::new(r"\A(.*:\d+)\.0\z").unwrap(); 40 | } 41 | // Canonicalize: xxx:0.0 => xxx:0. 42 | if let Some(m) = DOT_ZERO.captures(disp.as_str()) { 43 | disp = m.get(1).unwrap().as_str().to_string(); 44 | } 45 | // Turn /tmp/launch/:0 into _tmp_launch_:0 (OS X 10.5). 46 | disp = disp.replace("/", "_"); 47 | 48 | format!("/tmp/ns.{}.{}", env::var("USER").unwrap(), disp) 49 | } 50 | -------------------------------------------------------------------------------- /plan9/src/fid.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::env; 3 | use std::io; 4 | 5 | use anyhow::Result; 6 | use nine::p2000::{OpenMode, Qid}; 7 | 8 | use crate::conn::Conn; 9 | 10 | pub fn get_user() -> String { 11 | env::var("USER").unwrap() 12 | } 13 | 14 | pub struct Fid { 15 | pub c: Conn, 16 | 17 | pub qid: Qid, 18 | pub fid: u32, 19 | pub mode: OpenMode, 20 | offset: u64, 21 | } 22 | 23 | impl Fid { 24 | pub fn new(c: Conn, fid: u32, qid: Qid) -> Fid { 25 | Fid { 26 | c: c.clone(), 27 | qid, 28 | fid, 29 | mode: OpenMode::READ, 30 | offset: 0, 31 | } 32 | } 33 | pub fn walk(&mut self, name: &str) -> Result { 34 | let wfid = self.c.newfid(); 35 | let mut fid = self.fid; 36 | 37 | let name = String::from(name); 38 | let mut elem: Vec = name 39 | .split("/") 40 | .filter(|&x| x != "" && x != ".") 41 | .map(|x| x.to_string()) 42 | .collect(); 43 | let mut qid: Qid; 44 | 45 | const MAXWELEM: usize = 16; 46 | loop { 47 | let n = cmp::min(elem.len(), MAXWELEM); 48 | let wname = elem[0..n].to_vec(); 49 | elem.drain(0..n); 50 | let qids = self.c.walk(fid, wfid, wname)?; 51 | qid = if n == 0 { 52 | self.qid.clone() 53 | } else { 54 | qids[n - 1].clone() 55 | }; 56 | if elem.len() == 0 { 57 | break; 58 | } 59 | fid = wfid; 60 | } 61 | Ok(Fid::new(self.c.clone(), wfid, qid)) 62 | } 63 | 64 | pub fn open(&mut self, mode: OpenMode) -> Result<()> { 65 | self.c.open(self.fid, mode)?; 66 | self.mode = mode; 67 | Ok(()) 68 | } 69 | } 70 | 71 | const IOHDRSZ: u32 = 24; 72 | 73 | impl io::Read for Fid { 74 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 75 | let msize = self.c.msize - IOHDRSZ; 76 | let n: u32 = cmp::min(buf.len() as u32, msize); 77 | let data = match self.c.read(self.fid, self.offset, n) { 78 | Ok(r) => r, 79 | Err(e) => return Err(io::Error::new(io::ErrorKind::Other, format!("{}", e))), 80 | }; 81 | for (i, x) in data.iter().enumerate() { 82 | buf[i] = *x 83 | } 84 | self.offset += data.len() as u64; 85 | Ok(data.len()) 86 | } 87 | } 88 | 89 | impl io::Seek for Fid { 90 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 91 | match pos { 92 | io::SeekFrom::Start(n) => self.offset = n, 93 | io::SeekFrom::Current(n) => { 94 | if n >= 0 { 95 | self.offset += n as u64; 96 | } else { 97 | self.offset -= n as u64; 98 | } 99 | } 100 | io::SeekFrom::End(_) => { 101 | return Err(io::Error::new( 102 | io::ErrorKind::Other, 103 | format!("seeking to end unsupported"), 104 | )) 105 | } 106 | } 107 | Ok(self.offset) 108 | } 109 | } 110 | 111 | impl io::Write for Fid { 112 | fn write(&mut self, buf: &[u8]) -> io::Result { 113 | let msize = (self.c.msize - IOHDRSZ) as usize; 114 | let mut tot: usize = 0; 115 | let n = buf.len(); 116 | let mut first = true; 117 | while tot < n || first { 118 | let want: usize = cmp::min(n - tot, msize); 119 | let got = match self 120 | .c 121 | .write(self.fid, self.offset, buf[tot..tot + want].to_vec()) 122 | { 123 | Ok(r) => r as usize, 124 | Err(e) => return Err(io::Error::new(io::ErrorKind::Other, format!("{}", e))), 125 | }; 126 | tot += got; 127 | self.offset += got as u64; 128 | first = false; 129 | } 130 | Ok(tot) 131 | } 132 | fn flush(&mut self) -> io::Result<()> { 133 | Ok(()) 134 | } 135 | } 136 | 137 | impl Drop for Fid { 138 | fn drop(&mut self) { 139 | let _ = self.c.clunk(self.fid); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /plan9/src/fsys.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use nine::p2000::OpenMode; 3 | 4 | use crate::fid::Fid; 5 | 6 | pub struct Fsys { 7 | pub fid: Fid, 8 | } 9 | 10 | impl Fsys { 11 | pub fn open(&mut self, name: &str, mode: OpenMode) -> Result { 12 | let mut fid = self.fid.walk(name)?; 13 | fid.open(mode)?; 14 | Ok(fid) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /plan9/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod acme; 2 | pub mod conn; 3 | pub mod dial; 4 | pub mod fid; 5 | pub mod fsys; 6 | pub mod plumb; 7 | -------------------------------------------------------------------------------- /plan9/src/plumb.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::sync::Mutex; 3 | 4 | use anyhow::Result; 5 | use lazy_static::lazy_static; 6 | use nine::p2000::OpenMode; 7 | 8 | use crate::dial; 9 | use crate::{fid::Fid, fsys::Fsys}; 10 | 11 | lazy_static! { 12 | pub static ref FSYS: Mutex = Mutex::new(dial::mount_service("plumb").unwrap()); 13 | } 14 | 15 | pub fn open(name: &str, mode: OpenMode) -> Result { 16 | FSYS.lock().unwrap().open(&name, mode) 17 | } 18 | 19 | pub struct Message { 20 | pub dst: String, 21 | pub typ: String, 22 | pub data: Vec, 23 | } 24 | 25 | impl Message { 26 | pub fn send(self, mut f: Fid) -> Result<()> { 27 | let mut s: Vec = vec![]; 28 | write!(&mut s, "\n")?; // src 29 | write!(&mut s, "{}\n", self.dst)?; 30 | write!(&mut s, "\n")?; // dir 31 | write!(&mut s, "{}\n", self.typ)?; 32 | write!(&mut s, "\n")?; // attr 33 | write!(&mut s, "{}\n", self.data.len())?; 34 | s.extend(&self.data); 35 | f.write(&s)?; 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /src/lsp.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::{BufRead, BufReader, Read, Write}; 3 | use std::process::{Child, ChildStdin, Command, Stdio}; 4 | use std::thread; 5 | 6 | use anyhow::Result; 7 | use crossbeam_channel::{unbounded, Receiver}; 8 | use lsp_types::{notification::*, request::*, *}; 9 | use regex; 10 | use serde_json; 11 | 12 | pub struct Client { 13 | pub name: String, 14 | proc: Child, 15 | pub files: regex::Regex, 16 | stdin: ChildStdin, 17 | next_id: usize, 18 | 19 | pub msg_r: Receiver>, 20 | } 21 | 22 | impl Client { 23 | #![allow(deprecated)] 24 | pub fn new( 25 | name: String, 26 | files: String, 27 | program: S, 28 | args: I, 29 | envs: HashMap, 30 | root_uri: Option, 31 | workspace_folders: Option>, 32 | options: Option, 33 | ) -> Result<(Client, usize)> 34 | where 35 | I: IntoIterator, 36 | S: AsRef + std::fmt::Display + Clone, 37 | { 38 | let mut proc = Command::new(program.clone()) 39 | .args(args) 40 | .stdin(Stdio::piped()) 41 | .stdout(Stdio::piped()) 42 | .envs(envs) 43 | .spawn() 44 | .expect(&format!("could not execute: {}", program)); 45 | let mut stdout = BufReader::new(proc.stdout.take().unwrap()); 46 | let stdin = proc.stdin.take().unwrap(); 47 | let (msg_s, msg_r) = unbounded(); 48 | let mut c = Client { 49 | name, 50 | files: regex::Regex::new(&files)?, 51 | proc, 52 | stdin, 53 | next_id: 1, 54 | msg_r, 55 | }; 56 | thread::spawn(move || loop { 57 | let mut line = String::new(); 58 | let mut content_len: usize = 0; 59 | loop { 60 | line.clear(); 61 | stdout.read_line(&mut line).unwrap(); 62 | if line.trim().len() == 0 { 63 | break; 64 | } 65 | let sp: Vec<&str> = line.trim().split(": ").collect(); 66 | if sp.len() < 2 { 67 | panic!("bad line: {}", line); 68 | } 69 | match sp[0] { 70 | "Content-Length" => { 71 | content_len = sp[1].parse().unwrap(); 72 | } 73 | "Content-Type" => { 74 | if sp[1] != "application/vscode-jsonrpc; charset=utf-8" { 75 | panic!("unexpected content-type: {}", sp[1]); 76 | } 77 | } 78 | _ => { 79 | panic!("unrecognized header: {}", sp[0]); 80 | } 81 | } 82 | } 83 | if content_len == 0 { 84 | panic!("expected content-length"); 85 | } 86 | let mut v = vec![0u8; content_len]; 87 | stdout.read_exact(&mut v).unwrap(); 88 | msg_s.send(v).unwrap(); 89 | }); 90 | // TODO: remove the unwrap here. Unsure how to bubble up errors 91 | // from a closure. 92 | let workspace_folders: Option> = match workspace_folders { 93 | Some(f) => Some( 94 | f.iter() 95 | .map(|x| WorkspaceFolder { 96 | uri: Url::parse(x).unwrap(), 97 | name: "".to_string(), 98 | }) 99 | .collect(), 100 | ), 101 | None => None, 102 | }; 103 | let root_uri = match root_uri { 104 | Some(u) => Some(Url::parse(&u)?), 105 | None => None, 106 | }; 107 | let id = c.send::(InitializeParams { 108 | process_id: Some(1), 109 | root_path: None, 110 | root_uri, 111 | initialization_options: options, 112 | capabilities: ClientCapabilities { 113 | text_document: Some(TextDocumentClientCapabilities { 114 | code_action: Some(CodeActionClientCapabilities { 115 | resolve_support: Some(CodeActionCapabilityResolveSupport { 116 | properties: vec!["edit".to_string()], 117 | }), 118 | code_action_literal_support: Some(CodeActionLiteralSupport { 119 | code_action_kind: CodeActionKindLiteralSupport { 120 | value_set: vec![ 121 | "".to_string(), 122 | "quickfix".to_string(), 123 | "refactor".to_string(), 124 | "refactor.extract".to_string(), 125 | "refactor.inline".to_string(), 126 | "refactor.rewrite".to_string(), 127 | "source".to_string(), 128 | "source.organizeImports".to_string(), 129 | ], 130 | }, 131 | }), 132 | ..Default::default() 133 | }), 134 | ..Default::default() 135 | }), 136 | ..Default::default() 137 | }, 138 | trace: None, 139 | workspace_folders, 140 | client_info: None, 141 | locale: None, 142 | })?; 143 | Ok((c, id)) 144 | } 145 | pub fn send(&mut self, params: R::Params) -> Result { 146 | let id = self.new_id()?; 147 | let msg = RequestMessage { 148 | jsonrpc: "2.0", 149 | id, 150 | method: R::METHOD, 151 | params, 152 | }; 153 | let s = serde_json::to_string(&msg)?; 154 | let s = format!("Content-Length: {}\r\n\r\n{}", s.len(), s); 155 | write!(self.stdin, "{}", s)?; 156 | Ok(id) 157 | } 158 | pub fn notify(&mut self, params: N::Params) -> Result<()> { 159 | let msg = NotificationMessage { 160 | jsonrpc: "2.0", 161 | method: N::METHOD, 162 | params, 163 | }; 164 | let s = serde_json::to_string(&msg)?; 165 | let s = format!("Content-Length: {}\r\n\r\n{}", s.len(), s); 166 | write!(self.stdin, "{}", s)?; 167 | Ok(()) 168 | } 169 | fn new_id(&mut self) -> Result { 170 | let id = self.next_id; 171 | self.next_id += 1; 172 | Ok(id) 173 | } 174 | } 175 | 176 | impl Drop for Client { 177 | fn drop(&mut self) { 178 | let _ = self.proc.kill(); 179 | } 180 | } 181 | 182 | #[derive(serde::Serialize)] 183 | struct RequestMessage

{ 184 | jsonrpc: &'static str, 185 | id: usize, 186 | method: &'static str, 187 | params: P, 188 | } 189 | 190 | #[derive(serde::Serialize)] 191 | struct NotificationMessage

{ 192 | jsonrpc: &'static str, 193 | method: &'static str, 194 | params: P, 195 | } 196 | 197 | #[derive(Debug, serde::Deserialize)] 198 | pub struct DeMessage { 199 | pub id: Option, 200 | pub method: Option, 201 | pub params: Option>, 202 | pub result: Option>, 203 | pub error: Option, 204 | } 205 | 206 | #[derive(Debug, serde::Deserialize)] 207 | pub struct ResponseError { 208 | pub code: i64, 209 | pub message: String, 210 | pub data: Option, 211 | } 212 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::collections::{BTreeMap, HashMap, HashSet}; 3 | use std::convert::TryInto; 4 | use std::fmt::Write; 5 | use std::fs::{metadata, read_to_string}; 6 | use std::io::Read; 7 | use std::thread; 8 | 9 | use anyhow::{bail, Error, Result}; 10 | use crossbeam_channel::{bounded, Receiver, Select}; 11 | use diff; 12 | use lazy_static::lazy_static; 13 | use lsp_types::{notification::*, request::*, *}; 14 | use nine::p2000::OpenMode; 15 | use regex::Regex; 16 | use serde::Deserialize; 17 | use serde_json::Value; 18 | 19 | use plan9::{acme::*, plumb}; 20 | 21 | mod lsp; 22 | 23 | #[derive(Deserialize)] 24 | struct TomlConfig { 25 | servers: HashMap, 26 | } 27 | 28 | #[derive(Clone, Deserialize)] 29 | struct ConfigServer { 30 | executable: Option, 31 | args: Option>, 32 | files: String, 33 | root_uri: Option, 34 | workspace_folders: Option>, 35 | options: Option, 36 | actions_on_put: Option>, 37 | format_on_put: Option, 38 | env: Option>, 39 | } 40 | 41 | fn main() -> Result<()> { 42 | let dir = xdg::BaseDirectories::new()?; 43 | const ACRE_TOML: &str = "acre.toml"; 44 | let config = match dir.find_config_file(ACRE_TOML) { 45 | Some(c) => c, 46 | None => { 47 | let mut path = dir.get_config_home(); 48 | path.push(ACRE_TOML); 49 | eprintln!("could not find {}", path.to_str().unwrap()); 50 | std::process::exit(1); 51 | } 52 | }; 53 | let config = read_to_string(config)?; 54 | let config: TomlConfig = toml::from_str(&config)?; 55 | if config.servers.is_empty() { 56 | eprintln!("empty servers in configuration file"); 57 | std::process::exit(1); 58 | } 59 | let mut s = Server::new(config)?; 60 | s.wait() 61 | } 62 | 63 | struct WDProgress { 64 | name: String, 65 | percentage: Option, 66 | message: Option, 67 | title: String, 68 | } 69 | 70 | impl WDProgress { 71 | fn new( 72 | name: String, 73 | percentage: Option, 74 | message: Option, 75 | title: Option, 76 | ) -> Self { 77 | Self { 78 | name, 79 | percentage, 80 | message, 81 | title: title.unwrap_or("".to_string()), 82 | } 83 | } 84 | } 85 | 86 | impl std::fmt::Display for WDProgress { 87 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 88 | write!( 89 | f, 90 | "[{}%] {}:{} ({})", 91 | format_pct(self.percentage), 92 | self.name, 93 | if let Some(msg) = &self.message { 94 | format!(" {}", msg) 95 | } else { 96 | "".to_string() 97 | }, 98 | self.title, 99 | ) 100 | } 101 | } 102 | 103 | #[derive(Debug, Clone)] 104 | enum Action { 105 | Command(CodeActionOrCommand), 106 | Completion(CompletionItem), 107 | CodeLens(CodeLens), 108 | } 109 | 110 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] 111 | struct ClientId { 112 | client_name: String, 113 | msg_id: usize, 114 | } 115 | 116 | impl ClientId { 117 | fn new>(client_name: S, msg_id: usize) -> Self { 118 | ClientId { 119 | client_name: client_name.into(), 120 | msg_id, 121 | } 122 | } 123 | } 124 | 125 | struct Server { 126 | config: TomlConfig, 127 | w: Win, 128 | /// Filename -> win id -> server win. 129 | ws: HashMap>, 130 | /// Sorted Vec of ordered filenames for printing. 131 | names: Vec, 132 | /// Vec of (position, win id) to map Look locations to windows. 133 | addr: Vec<(usize, Option)>, 134 | /// Holds the last winid of the focus'd filename. 135 | focus_id: HashMap, 136 | body: String, 137 | output: String, 138 | focus: String, 139 | progress: HashMap, 140 | /// file name -> list of diagnostics 141 | diags: BTreeMap>, 142 | /// request (client_name, id) -> (method, file Url) 143 | requests: HashMap, 144 | 145 | /// current window info 146 | current_hover: Option, 147 | 148 | log_r: Receiver, 149 | ev_r: Receiver, 150 | err_r: Receiver, 151 | 152 | /// client name -> client 153 | clients: HashMap, 154 | /// client name -> capabilities 155 | capabilities: HashMap, 156 | /// file name -> client name 157 | files: HashMap, 158 | /// list of LSP message IDs to auto-run actions 159 | autorun: HashMap, 160 | } 161 | 162 | #[derive(Debug)] 163 | struct WindowHover { 164 | client_name: String, 165 | url: Url, 166 | /// line text of the hover. 167 | line: String, 168 | /// token (word at the cursor) of the hover. 169 | token: Option, 170 | /// on hover response from lsp 171 | hover: Option, 172 | /// result of signature request 173 | signature: Option, 174 | lens: Vec, 175 | /// completion response. we need to cache this because we also need the token 176 | /// response to come, and we don't know which will come first. 177 | completion: Vec, 178 | code_actions: Vec, 179 | 180 | /// merged actions from the code action and completion requests 181 | actions: Vec, 182 | /// Vec of (position, index) into the vec of actions. The Option is None for 183 | /// the last element. 184 | action_addrs: Vec<(usize, Option)>, 185 | /// cached output result of hover and actions 186 | body: String, 187 | } 188 | 189 | struct ServerWin { 190 | w: Win, 191 | url: Url, 192 | version: i32, 193 | client: String, 194 | } 195 | 196 | impl ServerWin { 197 | fn new(name: String, w: Win, client: String) -> Result { 198 | let url = Url::parse(&format!("file://{}", name))?; 199 | let version = 1; 200 | Ok(ServerWin { 201 | w, 202 | url, 203 | version, 204 | client, 205 | }) 206 | } 207 | fn pos(&mut self) -> Result<(u32, u32)> { 208 | self.w.ctl("addr=dot")?; 209 | // TODO: convert these character (rune) offsets to byte offsets. 210 | self.w.read_addr() 211 | } 212 | fn nl(&mut self) -> Result { 213 | NlOffsets::new(self.w.read(File::Body)?) 214 | } 215 | fn range(&mut self) -> Result { 216 | let pos = self.pos()?; 217 | let nl = self.nl()?; 218 | let (line, col) = nl.offset_to_line(pos.0); 219 | let start = Position::new(line, col); 220 | let (line, col) = nl.offset_to_line(pos.1); 221 | let end = Position::new(line, col); 222 | Ok(Range::new(start, end)) 223 | } 224 | fn text(&mut self) -> Result<(i32, String)> { 225 | let mut buf = String::new(); 226 | self.w.read(File::Body)?.read_to_string(&mut buf)?; 227 | self.version += 1; 228 | Ok((self.version, buf)) 229 | } 230 | fn change_params(&mut self) -> Result { 231 | let (version, text) = self.text()?; 232 | Ok(DidChangeTextDocumentParams { 233 | text_document: VersionedTextDocumentIdentifier::new(self.url.clone(), version), 234 | content_changes: vec![TextDocumentContentChangeEvent { 235 | range: None, 236 | range_length: None, 237 | text, 238 | }], 239 | }) 240 | } 241 | fn doc_ident(&self) -> TextDocumentIdentifier { 242 | TextDocumentIdentifier::new(self.url.clone()) 243 | } 244 | fn text_doc_pos(&mut self) -> Result { 245 | let range = self.range()?; 246 | Ok(TextDocumentPositionParams::new( 247 | self.doc_ident(), 248 | range.start, 249 | )) 250 | } 251 | /// Returns the current line's text. 252 | fn line(&mut self) -> Result { 253 | let mut buf = String::new(); 254 | self.w.read(File::Body)?.read_to_string(&mut buf)?; 255 | let pos = self.pos()?; 256 | let nl = NlOffsets::new(buf.as_bytes())?; 257 | let (line, _col) = nl.offset_to_line(pos.0); 258 | let line = buf 259 | .lines() 260 | .nth(line as usize) 261 | .ok_or(anyhow::anyhow!("no such line"))? 262 | .to_string(); 263 | Ok(line) 264 | } 265 | } 266 | 267 | impl Server { 268 | fn new(config: TomlConfig) -> Result { 269 | let mut clients = vec![]; 270 | let mut requests = HashMap::new(); 271 | for (name, server) in config.servers.clone() { 272 | let (client, msg_id) = lsp::Client::new( 273 | name.clone(), 274 | server.files, 275 | server.executable.unwrap_or(name.clone()), 276 | server.args.unwrap_or(vec![]), 277 | server.env.unwrap_or(HashMap::new()), 278 | server.root_uri, 279 | server.workspace_folders, 280 | server.options, 281 | )?; 282 | requests.insert( 283 | ClientId::new(name, msg_id), 284 | (Initialize::METHOD.into(), Url::parse("file:///").unwrap()), 285 | ); 286 | clients.push(client); 287 | } 288 | 289 | let (log_s, log_r) = bounded(0); 290 | let (ev_s, ev_r) = bounded(0); 291 | let (err_s, err_r) = bounded(0); 292 | let mut w = Win::new()?; 293 | w.name("acre")?; 294 | let mut wev = w.events()?; 295 | let mut cls = HashMap::new(); 296 | for c in clients { 297 | let name = c.name.clone(); 298 | cls.insert(name, c); 299 | } 300 | let s = Server { 301 | w, 302 | ws: HashMap::new(), 303 | names: vec![], 304 | focus_id: HashMap::new(), 305 | addr: vec![], 306 | output: "".to_string(), 307 | body: "".to_string(), 308 | focus: "".to_string(), 309 | progress: HashMap::new(), 310 | requests, 311 | diags: BTreeMap::new(), 312 | current_hover: None, 313 | log_r, 314 | ev_r, 315 | err_r, 316 | clients: cls, 317 | capabilities: HashMap::new(), 318 | files: HashMap::new(), 319 | config, 320 | autorun: HashMap::new(), 321 | }; 322 | let err_s1 = err_s.clone(); 323 | thread::Builder::new() 324 | .name("LogReader".to_string()) 325 | .spawn(move || { 326 | let mut log = LogReader::new().unwrap(); 327 | loop { 328 | match log.read() { 329 | Ok(ev) => match ev.op.as_str() { 330 | "new" | "del" | "focus" | "put" => match log_s.send(ev) { 331 | Ok(_) => {} 332 | Err(_err) => { 333 | //eprintln!("log_s send err {}", _err); 334 | return; 335 | } 336 | }, 337 | _ => {} 338 | }, 339 | Err(err) => { 340 | err_s1.send(err).unwrap(); 341 | return; 342 | } 343 | }; 344 | } 345 | }) 346 | .unwrap(); 347 | thread::Builder::new() 348 | .name("WindowEvents".to_string()) 349 | .spawn(move || loop { 350 | let mut ev = match wev.read_event() { 351 | Ok(ev) => ev, 352 | Err(err) => { 353 | eprintln!("read event err: {}", err); 354 | return; 355 | } 356 | }; 357 | match ev.c2 { 358 | 'x' | 'X' => match ev.text.as_str() { 359 | "Del" => { 360 | return; 361 | } 362 | "Get" => { 363 | ev_s.send(ev).unwrap(); 364 | } 365 | _ => { 366 | wev.write_event(ev).unwrap(); 367 | } 368 | }, 369 | 'L' => { 370 | ev.load_text(); 371 | ev_s.send(ev).unwrap(); 372 | } 373 | _ => {} 374 | } 375 | }) 376 | .unwrap(); 377 | Ok(s) 378 | } 379 | /// Runs f if self.current_hover is Some and matches the Url, and updates the hover action addrs 380 | /// and body. 381 | fn set_hover(&mut self, url: &Url, f: F) { 382 | if let Some(hover) = self.current_hover.as_mut() { 383 | if &hover.url == url { 384 | f(hover); 385 | 386 | hover.actions.clear(); 387 | hover.actions.extend(hover.code_actions.clone()); 388 | if let Some(token) = &hover.token { 389 | let mut v = vec![]; 390 | for a in &hover.completion { 391 | let filter = if let Some(ref filter) = a.filter_text { 392 | filter.clone() 393 | } else { 394 | a.label.clone() 395 | }; 396 | if filter.contains(token) { 397 | v.push(Action::Completion(a.clone())); 398 | if v.len() == 10 { 399 | break; 400 | } 401 | } 402 | } 403 | hover.actions.extend(v); 404 | } 405 | 406 | // Until lenses work, forcibly clear them. 407 | hover.lens.clear(); 408 | hover 409 | .actions 410 | .extend(hover.lens.iter().map(|lens| Action::CodeLens(lens.clone()))); 411 | 412 | hover.body.clear(); 413 | 414 | hover.action_addrs.clear(); 415 | for (idx, action) in hover.actions.iter().take(10).enumerate() { 416 | hover.action_addrs.push((hover.body.len(), Some(idx))); 417 | let newline = if hover.body.is_empty() { "" } else { "\n" }; 418 | match action { 419 | Action::Command(CodeActionOrCommand::Command(cmd)) => { 420 | write!(&mut hover.body, "{}[{}]", newline, cmd.title).unwrap(); 421 | } 422 | Action::Command(CodeActionOrCommand::CodeAction(action)) => { 423 | write!(&mut hover.body, "{}[{}]", newline, action.title).unwrap(); 424 | } 425 | Action::Completion(item) => { 426 | write!(&mut hover.body, "{}[insert] {}:", newline, item.label).unwrap(); 427 | if item.deprecated.unwrap_or(false) { 428 | write!(&mut hover.body, " DEPRECATED").unwrap(); 429 | } 430 | if let Some(k) = item.kind { 431 | write!(&mut hover.body, " ({:?})", k).unwrap(); 432 | } 433 | if let Some(d) = &item.detail { 434 | write!(&mut hover.body, " {}", d).unwrap(); 435 | } 436 | } 437 | // TODO: extract out the range text and append it to the command title to 438 | // distinguish between lenses. 439 | Action::CodeLens(lens) => { 440 | write!( 441 | &mut hover.body, 442 | "{}[{}]", 443 | newline, 444 | lens.command 445 | .as_ref() 446 | .map(|c| c.title.clone()) 447 | .unwrap_or("unknown command".into()) 448 | ) 449 | .unwrap(); 450 | } 451 | } 452 | } 453 | hover.action_addrs.push((hover.body.len(), None)); 454 | if !hover.body.is_empty() { 455 | hover.body.push_str("\n"); 456 | } 457 | 458 | if let Some(text) = &hover.hover { 459 | if !hover.body.is_empty() { 460 | hover.body.push_str("\n"); 461 | } 462 | for line in text.trim().lines().take(10) { 463 | hover.body.push_str(line); 464 | hover.body.push_str("\n"); 465 | } 466 | } 467 | if let Some(text) = &hover.signature { 468 | if !hover.body.is_empty() { 469 | hover.body.push_str("\n"); 470 | } 471 | for line in text.trim().lines().take(10) { 472 | hover.body.push_str(line); 473 | hover.body.push_str("\n"); 474 | } 475 | } 476 | } 477 | } 478 | } 479 | /// Returns the most recently focused win id for a filename. 480 | fn winid_by_name(&self, filename: &str) -> Option { 481 | self.focus_id.get(filename).cloned() 482 | } 483 | fn get_sw_by_name_id(&mut self, filename: &str, id: &usize) -> Option<&mut ServerWin> { 484 | self.ws.get_mut(filename).and_then(|ids| ids.get_mut(id)) 485 | } 486 | fn get_sw_by_name(&mut self, filename: &str) -> Option<(usize, &mut ServerWin)> { 487 | let wid = self.winid_by_name(filename)?; 488 | let sw = self.get_sw_by_name_id(filename, &wid)?; 489 | Some((wid, sw)) 490 | } 491 | fn get_sw_by_url(&mut self, url: &Url) -> Option<(usize, &mut ServerWin)> { 492 | let filename = url.path(); 493 | self.get_sw_by_name(filename) 494 | } 495 | fn sync(&mut self) -> Result<()> { 496 | let mut body = String::new(); 497 | if let Some(hover) = &self.current_hover { 498 | write!(&mut body, "{}----\n", hover.body)?; 499 | } 500 | self.addr.clear(); 501 | // Loop through by sorted file name. 502 | for file_name in &self.names { 503 | self.addr.push((body.len(), Some(file_name.to_string()))); 504 | write!( 505 | &mut body, 506 | "{}{}\n\t", 507 | if *file_name == self.focus { "*" } else { " " }, 508 | file_name 509 | )?; 510 | let client_name = self.files.get(file_name).unwrap(); 511 | let caps = match self.capabilities.get(client_name) { 512 | Some(v) => v, 513 | None => continue, 514 | }; 515 | if caps.definition_provider.is_some() { 516 | body.push_str("[definition] "); 517 | } 518 | if caps.implementation_provider.is_some() { 519 | body.push_str("[impl] "); 520 | } 521 | if caps.references_provider.is_some() { 522 | body.push_str("[references] "); 523 | } 524 | if caps.document_symbol_provider.is_some() { 525 | body.push_str("[symbols] "); 526 | } 527 | if caps.type_definition_provider.is_some() { 528 | body.push_str("[typedef] "); 529 | } 530 | body.push('\n'); 531 | } 532 | self.addr.push((body.len(), None)); 533 | write!(&mut body, "-----\n")?; 534 | if !self.output.is_empty() { 535 | // Only take the first 50 lines. 536 | let output = self 537 | .output 538 | .trim() 539 | .lines() 540 | .take(50) 541 | .collect::>() 542 | .join("\n"); 543 | write!(&mut body, "\n{}\n", output)?; 544 | } 545 | if self.progress.len() > 0 { 546 | body.push('\n'); 547 | } 548 | for (_, p) in &self.progress { 549 | write!(&mut body, "{}\n", p)?; 550 | } 551 | if self.requests.len() > 0 { 552 | body.push('\n'); 553 | } 554 | for (client_id, (method, url)) in &self.requests { 555 | write!( 556 | &mut body, 557 | "{}: {}: {}...\n", 558 | client_id.client_name, 559 | url.path(), 560 | method 561 | )?; 562 | } 563 | if !self.diags.is_empty() { 564 | write!(&mut body, "-----\n")?; 565 | for (_, ds) in self.diags.iter().take(5) { 566 | for d in ds.iter().take(3) { 567 | write!(&mut body, "{}\n", d)?; 568 | } 569 | } 570 | } 571 | if self.body != body { 572 | self.body = body.clone(); 573 | self.w.write(File::Addr, &format!(","))?; 574 | self.w.write(File::Data, &body)?; 575 | self.w.ctl("cleartag\nclean")?; 576 | self.w.write(File::Tag, " Get")?; 577 | } 578 | Ok(()) 579 | } 580 | fn init_win(&mut self, filename: String, winid: usize) -> Result { 581 | let client_name = match self.lookup_client(filename.clone()) { 582 | Ok(n) => n, 583 | Err(_) => bail!("no client for {}", filename), 584 | }; 585 | let need_open = !self.ws.contains_key(&filename); 586 | if need_open { 587 | self.ws.insert(filename.clone(), HashMap::new()); 588 | } 589 | let ids = self.ws.get_mut(&filename).unwrap(); 590 | let mut fsys = FSYS.lock().unwrap(); 591 | let ctl = fsys.open(format!("{}/ctl", winid).as_str(), OpenMode::RDWR)?; 592 | let w = Win::open(&mut fsys, winid, ctl)?; 593 | let sw = ServerWin::new(filename, w, client_name)?; 594 | ids.insert(winid, sw); 595 | Ok(need_open) 596 | } 597 | fn lookup_client(&mut self, filename: String) -> Result { 598 | for (_, c) in &self.clients { 599 | if c.files.is_match(&filename) { 600 | // Don't open windows for a client that hasn't initialized yet. 601 | if !self.capabilities.contains_key(&c.name) { 602 | continue; 603 | } 604 | self.files.insert(filename, c.name.clone()); 605 | return Ok(c.name.clone()); 606 | } 607 | } 608 | bail!("could not find client for {}", filename) 609 | } 610 | fn sync_windows(&mut self) -> Result<()> { 611 | let mut wins = WinInfo::windows()?; 612 | self.names.clear(); 613 | wins.sort_by(|a, b| { 614 | if a.name != b.name { 615 | return a.name.cmp(&b.name); 616 | } else { 617 | a.id.cmp(&b.id) 618 | } 619 | }); 620 | let mut to_close: HashSet = self.ws.keys().cloned().collect(); 621 | // wins appears to have one entry per filename, even if it's Zerox'd. It 622 | // uses the highest id of a zerox'd win (i.e., it will use the id of the new 623 | // window). If the newer window is Del'd, the next highest id is used. 624 | for wi in wins { 625 | to_close.remove(&wi.name); 626 | let need_open = match self.init_win(wi.name.clone(), wi.id) { 627 | Ok(n) => n, 628 | Err(_) => continue, 629 | }; 630 | self.names.push(wi.name.clone()); 631 | if need_open { 632 | let sw = match self.get_sw_by_name_id(&wi.name, &wi.id) { 633 | Some(sw) => sw, 634 | None => continue, 635 | }; 636 | let (version, text) = sw.text()?; 637 | let url = sw.url.clone(); 638 | let client_name = sw.client.clone(); 639 | drop(sw); 640 | self.send_notification::( 641 | &client_name, 642 | DidOpenTextDocumentParams { 643 | text_document: TextDocumentItem::new( 644 | url, 645 | "".to_string(), // lang id 646 | version, 647 | text, 648 | ), 649 | }, 650 | )?; 651 | } 652 | } 653 | 654 | // close remaining files 655 | for filename in to_close { 656 | self.ws.remove(&filename); 657 | self.files.remove(&filename); 658 | let url = Url::parse(&format!("file://{}", filename))?; 659 | let client_name = self.lookup_client(filename)?; 660 | self.send_notification::( 661 | &client_name, 662 | DidCloseTextDocumentParams { 663 | text_document: TextDocumentIdentifier::new(url), 664 | }, 665 | )?; 666 | } 667 | Ok(()) 668 | } 669 | fn lsp_msg(&mut self, client_name: String, orig_msg: Vec) -> Result<()> { 670 | let msg: lsp::DeMessage = serde_json::from_slice(&orig_msg)?; 671 | if msg.id.is_some() && msg.error.is_some() { 672 | self.lsp_error( 673 | ClientId::new(client_name, msg.id.unwrap()), 674 | msg.error.unwrap(), 675 | ) 676 | } else if msg.id.is_some() && msg.method.is_some() { 677 | self.lsp_request(msg) 678 | } else if msg.id.is_some() { 679 | self.lsp_response(ClientId::new(client_name, msg.id.unwrap()), msg, &orig_msg) 680 | } else if msg.method.is_some() { 681 | self.lsp_notification(client_name, msg.method.unwrap(), msg.params) 682 | } else { 683 | panic!( 684 | "unknown message {}", 685 | std::str::from_utf8(&orig_msg).unwrap() 686 | ); 687 | } 688 | } 689 | fn lsp_error(&mut self, client_id: ClientId, err: lsp::ResponseError) -> Result<()> { 690 | self.requests.remove(&client_id); 691 | self.output = format!("lsp error: {}", err.message); 692 | Ok(()) 693 | } 694 | fn lsp_response( 695 | &mut self, 696 | client_id: ClientId, 697 | msg: lsp::DeMessage, 698 | _orig_msg: &[u8], 699 | ) -> Result<()> { 700 | let (typ, url) = self 701 | .requests 702 | .remove(&client_id) 703 | .expect(&format!("expected client id {:?}", client_id)); 704 | let result = match msg.result { 705 | Some(v) => v, 706 | None => { 707 | // Ignore empty results. Unsure if/how we should report this to a user. 708 | return Ok(()); 709 | } 710 | }; 711 | match typ.as_str() { 712 | Initialize::METHOD => { 713 | let msg = serde_json::from_str::(result.get())?; 714 | self.send_notification::( 715 | &client_id.client_name, 716 | InitializedParams {}, 717 | )?; 718 | self.capabilities 719 | .insert(client_id.client_name, msg.capabilities); 720 | self.sync_windows()?; 721 | } 722 | GotoDefinition::METHOD => { 723 | let msg = serde_json::from_str::>(result.get())?; 724 | if let Some(msg) = msg { 725 | goto_definition(&msg)?; 726 | } 727 | } 728 | HoverRequest::METHOD => { 729 | let msg = serde_json::from_str::>(result.get())?; 730 | if let Some(msg) = msg { 731 | match &msg.contents { 732 | HoverContents::Array(mss) => { 733 | let mut o: Vec = vec![]; 734 | for ms in mss { 735 | match ms { 736 | MarkedString::String(s) => o.push(s.clone()), 737 | MarkedString::LanguageString(s) => o.push(s.value.clone()), 738 | }; 739 | } 740 | self.set_hover(&url, |hover| { 741 | hover.hover = Some(o.join("\n").trim().to_string()); 742 | }); 743 | } 744 | HoverContents::Markup(mc) => { 745 | self.set_hover(&url, |hover| { 746 | hover.hover = Some(mc.value.trim().to_string()); 747 | }); 748 | } 749 | _ => panic!("unknown hover response: {:?}", msg), 750 | }; 751 | } 752 | } 753 | References::METHOD => { 754 | let msg = serde_json::from_str::>>(result.get())?; 755 | if let Some(mut msg) = msg { 756 | msg.sort_by(cmp_location); 757 | let mut o = Vec::new(); 758 | let mut files: HashMap = HashMap::new(); 759 | for x in msg { 760 | o.push(location_to_plumb(&x)); 761 | let text = files.entry(x.uri.clone()).or_insert_with(|| { 762 | match self.get_sw_by_url(&x.uri) { 763 | Some((_, win)) => win.text().unwrap_or((0, "".into())).1, 764 | None => read_to_string(x.uri.path()).unwrap_or("".into()), 765 | } 766 | }); 767 | if let Some(line) = text.lines().nth(x.range.start.line.try_into().unwrap()) 768 | { 769 | o.push(format!("\t{}", line.trim())); 770 | } 771 | } 772 | if o.len() > 0 { 773 | self.output = o.join("\n"); 774 | } 775 | } 776 | } 777 | DocumentSymbolRequest::METHOD => { 778 | let msg = serde_json::from_str::>(result.get())?; 779 | if let Some(msg) = msg { 780 | let mut o: Vec = vec![]; 781 | fn add_symbol( 782 | o: &mut Vec, 783 | container: &Vec, 784 | name: &String, 785 | kind: SymbolKind, 786 | loc: &Location, 787 | ) { 788 | o.push(format!( 789 | "{}{} ({:?}): {}", 790 | container 791 | .iter() 792 | .map(|c| format!("{}::", c)) 793 | .collect::>() 794 | .join(""), 795 | name, 796 | kind, 797 | location_to_plumb(loc), 798 | )); 799 | } 800 | match msg.clone() { 801 | DocumentSymbolResponse::Flat(sis) => { 802 | for si in sis { 803 | // Ignore variables in methods. 804 | if si.container_name.as_ref().unwrap_or(&"".to_string()).len() == 0 805 | && si.kind == SymbolKind::VARIABLE 806 | { 807 | continue; 808 | } 809 | let cn = match si.container_name.clone() { 810 | Some(c) => vec![c], 811 | None => vec![], 812 | }; 813 | add_symbol(&mut o, &cn, &si.name, si.kind, &si.location); 814 | } 815 | } 816 | DocumentSymbolResponse::Nested(mut dss) => { 817 | fn process( 818 | url: &Url, 819 | mut o: &mut Vec, 820 | parents: &Vec, 821 | dss: &mut Vec, 822 | ) { 823 | dss.sort_by(|a, b| a.range.start.line.cmp(&b.range.start.line)); 824 | for ds in dss { 825 | add_symbol( 826 | &mut o, 827 | parents, 828 | &ds.name, 829 | ds.kind, 830 | &Location::new(url.clone(), ds.range), 831 | ); 832 | if let Some(mut children) = ds.children.clone() { 833 | let mut parents = parents.clone(); 834 | parents.push(ds.name.clone()); 835 | process(url, o, &parents, &mut children); 836 | } 837 | } 838 | } 839 | process(&url, &mut o, &vec![], &mut dss); 840 | } 841 | } 842 | if o.len() > 0 { 843 | self.output = o.join("\n"); 844 | } 845 | } 846 | } 847 | SignatureHelpRequest::METHOD => { 848 | let msg = serde_json::from_str::>(result.get())?; 849 | if let Some(msg) = msg { 850 | let sig = match msg.active_signature { 851 | Some(i) => i, 852 | None => 0, 853 | }; 854 | self.set_hover(&url, |hover| { 855 | hover.signature = msg.signatures.get(sig as usize).map(|sig| { 856 | let mut s: String = sig.label.clone(); 857 | if let Some(doc) = &sig.documentation { 858 | s.push_str("\n"); 859 | s.push_str(extract_doc(doc)); 860 | } 861 | s 862 | }); 863 | }); 864 | } 865 | } 866 | CodeLensRequest::METHOD => { 867 | let msg = serde_json::from_str::>>(result.get())?; 868 | if let Some(msg) = msg { 869 | self.set_hover(&url, |hover| { 870 | hover.lens = msg; 871 | }); 872 | } 873 | } 874 | CodeLensResolve::METHOD => { 875 | let msg = serde_json::from_str::>>(result.get())?; 876 | if let Some(msg) = msg { 877 | eprintln!("code resolve msg: {:?}", msg); 878 | } 879 | } 880 | CodeActionRequest::METHOD => { 881 | let msg = serde_json::from_str::>(result.get())?; 882 | if let Some(msg) = msg { 883 | if self.autorun.remove_entry(&client_id.msg_id).is_some() { 884 | for m in msg.iter().cloned() { 885 | self.run_action( 886 | &client_id.client_name, 887 | url.clone(), 888 | Action::Command(m), 889 | )?; 890 | } 891 | } else { 892 | self.set_hover(&url, |hover| { 893 | for m in msg.iter().cloned() { 894 | hover.code_actions.push(Action::Command(m)); 895 | } 896 | }); 897 | } 898 | } 899 | } 900 | CodeActionResolveRequest::METHOD => { 901 | let msg = serde_json::from_str::>(result.get())?; 902 | if let Some(msg) = msg { 903 | if let Some(edit) = msg.edit { 904 | self.apply_workspace_edit(&edit)?; 905 | } else { 906 | eprintln!("unexpected CodeActionResolveRequest response: {:#?}", msg); 907 | } 908 | } 909 | } 910 | Completion::METHOD => { 911 | let msg = serde_json::from_str::>(result.get())?; 912 | if let Some(msg) = msg { 913 | self.set_hover(&url, move |hover| { 914 | hover.completion = match msg { 915 | CompletionResponse::Array(cis) => cis, 916 | CompletionResponse::List(cls) => cls.items, 917 | }; 918 | }); 919 | } 920 | } 921 | Formatting::METHOD => { 922 | let msg = serde_json::from_str::>>(result.get())?; 923 | if let Some(msg) = msg { 924 | self.apply_text_edits(&url, InsertTextFormat::PLAIN_TEXT, &msg)?; 925 | // Run any on put actions. 926 | let actions = self 927 | .config 928 | .servers 929 | .get(&client_id.client_name) 930 | .unwrap() 931 | .actions_on_put 932 | .clone() 933 | .unwrap_or(vec![]); 934 | if !actions.is_empty() { 935 | let id = self.send_request::( 936 | &client_id.client_name, 937 | url.clone(), 938 | CodeActionParams { 939 | text_document: TextDocumentIdentifier { uri: url }, 940 | range: Range::new(Position::new(0, 0), Position::new(0, 0)), 941 | context: CodeActionContext { 942 | diagnostics: vec![], 943 | only: Some(actions), 944 | }, 945 | work_done_progress_params: WorkDoneProgressParams { 946 | work_done_token: None, 947 | }, 948 | partial_result_params: PartialResultParams { 949 | partial_result_token: None, 950 | }, 951 | }, 952 | )?; 953 | self.autorun.insert(id, ()); 954 | } 955 | } 956 | } 957 | GotoImplementation::METHOD => { 958 | let msg = serde_json::from_str::>(result.get())?; 959 | if let Some(msg) = msg { 960 | goto_definition(&msg)?; 961 | } 962 | } 963 | GotoTypeDefinition::METHOD => { 964 | let msg = serde_json::from_str::>(result.get())?; 965 | if let Some(msg) = msg { 966 | goto_definition(&msg)?; 967 | } 968 | } 969 | SemanticTokensRangeRequest::METHOD => { 970 | let msg = serde_json::from_str::>(result.get())?; 971 | if let Some(msg) = msg { 972 | match msg { 973 | SemanticTokensRangeResult::Tokens(tokens) => { 974 | // TODO: use the result_id and probably verify it with the send message? 975 | // Not sure why there would be more than 1 result, but we only need to care 976 | // about a single one anyway. 977 | if let Some(token) = tokens.data.into_iter().next() { 978 | self.set_hover(&url, |hover| { 979 | hover.token = Some( 980 | hover 981 | .line 982 | .chars() 983 | .skip(token.delta_start as usize) 984 | .take(token.length as usize) 985 | .collect(), 986 | ); 987 | }); 988 | } 989 | } 990 | _ => eprintln!("unsupported: {:#?}", msg), 991 | } 992 | } 993 | } 994 | _ => panic!("unrecognized type: {}", typ), 995 | } 996 | Ok(()) 997 | } 998 | fn lsp_notification( 999 | &mut self, 1000 | client_name: String, 1001 | method: String, 1002 | params: Option>, 1003 | ) -> Result<()> { 1004 | match method.as_str() { 1005 | LogMessage::METHOD => { 1006 | let msg: LogMessageParams = serde_json::from_str(params.unwrap().get())?; 1007 | self.output = format!("[{:?}] {}", msg.typ, msg.message); 1008 | } 1009 | PublishDiagnostics::METHOD => { 1010 | let msg: PublishDiagnosticsParams = serde_json::from_str(params.unwrap().get())?; 1011 | let mut v = vec![]; 1012 | let path = msg.uri.path(); 1013 | // Cap diagnostic length. 1014 | for p in msg.diagnostics.iter().take(5) { 1015 | let msg = p.message.lines().next().unwrap_or(""); 1016 | v.push(format!( 1017 | "{}:{}: [{:?}] {}", 1018 | path, 1019 | p.range.start.line + 1, 1020 | p.severity.unwrap_or(lsp_types::DiagnosticSeverity::ERROR), 1021 | msg, 1022 | )); 1023 | } 1024 | self.diags.insert(path.to_string(), v); 1025 | } 1026 | ShowMessage::METHOD => { 1027 | let msg: ShowMessageParams = serde_json::from_str(params.unwrap().get())?; 1028 | self.output = format!("[{:?}] {}", msg.typ, msg.message); 1029 | } 1030 | Progress::METHOD => { 1031 | let msg: ProgressParams = serde_json::from_str(params.unwrap().get())?; 1032 | let name = format!("{}-{:?}", client_name, msg.token); 1033 | match &msg.value { 1034 | ProgressParamsValue::WorkDone(value) => match value { 1035 | WorkDoneProgress::Begin(value) => { 1036 | self.progress.insert( 1037 | name.clone(), 1038 | WDProgress::new( 1039 | name, 1040 | value.percentage, 1041 | value.message.clone(), 1042 | Some(value.title.clone()), 1043 | ), 1044 | ); 1045 | } 1046 | WorkDoneProgress::Report(value) => { 1047 | let p = self.progress.get_mut(&name).unwrap(); 1048 | p.percentage = value.percentage; 1049 | p.message = value.message.clone(); 1050 | } 1051 | WorkDoneProgress::End(_) => { 1052 | self.progress.remove(&name); 1053 | } 1054 | }, 1055 | } 1056 | } 1057 | _ => { 1058 | eprintln!("unrecognized method: {}", method); 1059 | } 1060 | } 1061 | Ok(()) 1062 | } 1063 | fn lsp_request(&mut self, msg: lsp::DeMessage) -> Result<()> { 1064 | eprintln!("unknown request {:?}", msg); 1065 | Ok(()) 1066 | } 1067 | fn apply_workspace_edit(&mut self, edit: &WorkspaceEdit) -> Result<()> { 1068 | if let Some(ref doc_changes) = edit.document_changes { 1069 | match doc_changes { 1070 | DocumentChanges::Edits(edits) => { 1071 | for edit in edits { 1072 | let text_edits = edit 1073 | .edits 1074 | .iter() 1075 | .filter_map(|e| { 1076 | match e { 1077 | // A TextEdit, keep it. 1078 | OneOf::Left(e) => Some(e), 1079 | // A AnnotatedTextEdit, discard until we support it. 1080 | _ => None, 1081 | } 1082 | }) 1083 | .cloned() 1084 | .collect(); 1085 | self.apply_text_edits( 1086 | &edit.text_document.uri, 1087 | InsertTextFormat::PLAIN_TEXT, 1088 | &text_edits, 1089 | )?; 1090 | } 1091 | } 1092 | _ => panic!("unsupported document_changes {:?}", doc_changes), 1093 | } 1094 | } 1095 | if let Some(ref changes) = edit.changes { 1096 | for (url, edits) in changes { 1097 | self.apply_text_edits(&url, InsertTextFormat::PLAIN_TEXT, &edits)?; 1098 | } 1099 | } 1100 | Ok(()) 1101 | } 1102 | fn apply_text_edits( 1103 | &mut self, 1104 | url: &Url, 1105 | format: InsertTextFormat, 1106 | edits: &Vec, 1107 | ) -> Result<()> { 1108 | if edits.is_empty() { 1109 | return Ok(()); 1110 | } 1111 | let (_id, sw) = match self.get_sw_by_url(url) { 1112 | Some(v) => v, 1113 | None => return Ok(()), 1114 | }; 1115 | let mut body = String::new(); 1116 | sw.w.read(File::Body)?.read_to_string(&mut body)?; 1117 | let offsets = NlOffsets::new(std::io::Cursor::new(body.clone()))?; 1118 | if edits.len() == 1 { 1119 | if body == edits[0].new_text { 1120 | return Ok(()); 1121 | } 1122 | // Check if this is a full file replacement. If so, use a diff algorithm so acme doesn't scroll to the bottom. 1123 | let edit = edits[0].clone(); 1124 | let last = offsets.last(); 1125 | if edit.range.start == Position::new(0, 0) 1126 | && edit.range.end == Position::new(last.0, last.1) 1127 | { 1128 | let lines = diff::lines(&body, &edit.new_text); 1129 | let mut i = 0; 1130 | for line in lines.iter() { 1131 | i += 1; 1132 | match line { 1133 | diff::Result::Left(_) => { 1134 | sw.w.addr(&format!("{},{}", i, i))?; 1135 | sw.w.write(File::Data, "")?; 1136 | i -= 1; 1137 | } 1138 | diff::Result::Right(s) => { 1139 | sw.w.addr(&format!("{}+#0", i - 1))?; 1140 | sw.w.write(File::Data, &format!("{}\n", s))?; 1141 | } 1142 | diff::Result::Both(_, _) => {} 1143 | } 1144 | } 1145 | return Ok(()); 1146 | } 1147 | } 1148 | sw.w.seek(File::Body, std::io::SeekFrom::Start(0))?; 1149 | sw.w.ctl("nomark")?; 1150 | sw.w.ctl("mark")?; 1151 | for edit in edits.iter().rev() { 1152 | let soff = offsets.line_to_offset(edit.range.start.line, edit.range.start.character); 1153 | let eoff = offsets.line_to_offset(edit.range.end.line, edit.range.end.character); 1154 | let addr = format!("#{},#{}", soff, eoff); 1155 | sw.w.addr(&addr)?; 1156 | match format { 1157 | InsertTextFormat::SNIPPET => { 1158 | lazy_static! { 1159 | static ref SNIPPET: Regex = 1160 | Regex::new(r"(\$\{\d+:[[:alpha:]]+\})|(\$0)").unwrap(); 1161 | } 1162 | let text = &SNIPPET.replace_all(&edit.new_text, ""); 1163 | sw.w.write(File::Data, text)?; 1164 | text.len() 1165 | } 1166 | InsertTextFormat::PLAIN_TEXT => { 1167 | sw.w.write(File::Data, &edit.new_text)?; 1168 | edit.new_text.len() 1169 | } 1170 | _ => panic!("unexpected {:?}", format), 1171 | }; 1172 | } 1173 | Ok(()) 1174 | } 1175 | fn did_change(&mut self, name: String, wid: usize) -> Result<()> { 1176 | // Sometimes we are sending a DidChange before a DidOpen. Maybe this is because 1177 | // acme's event log sometimes misses events. Sync the windows just to be sure. 1178 | self.sync_windows()?; 1179 | // Because zerox windows don't appear in the windows call, make sure that 1180 | // whatever wid we are given is initialized. 1181 | if self.init_win(name.clone(), wid).is_err() { 1182 | return Ok(()); 1183 | } 1184 | let sw = match self.get_sw_by_name_id(&name, &wid) { 1185 | Some(sw) => sw, 1186 | None => return Ok(()), 1187 | }; 1188 | let client = sw.client.clone(); 1189 | let params = sw.change_params()?; 1190 | self.send_notification::(&client, params) 1191 | } 1192 | fn set_focus(&mut self, ev: LogEvent) -> Result<()> { 1193 | self.focus = ev.name.clone(); 1194 | self.focus_id.insert(ev.name.clone(), ev.id); 1195 | 1196 | self.did_change(ev.name.clone(), ev.id)?; 1197 | let sw = match self.get_sw_by_name_id(&ev.name, &ev.id) { 1198 | Some(sw) => sw, 1199 | None => return Ok(()), 1200 | }; 1201 | let client_name = &sw.client.clone(); 1202 | let url = sw.url.clone(); 1203 | let range = sw.range()?; 1204 | let text_document_position_params = 1205 | TextDocumentPositionParams::new(sw.doc_ident(), range.start); 1206 | let text_document = TextDocumentIdentifier::new(url.clone()); 1207 | let line = sw.line()?; 1208 | drop(sw); 1209 | 1210 | self.current_hover = Some(WindowHover { 1211 | client_name: client_name.into(), 1212 | url: url.clone(), 1213 | line, 1214 | token: None, 1215 | signature: None, 1216 | lens: vec![], 1217 | completion: vec![], 1218 | code_actions: vec![], 1219 | actions: vec![], 1220 | action_addrs: vec![], 1221 | hover: None, 1222 | body: "".into(), 1223 | }); 1224 | self.send_request::( 1225 | &client_name, 1226 | url.clone(), 1227 | HoverParams { 1228 | text_document_position_params: text_document_position_params.clone(), 1229 | work_done_progress_params, 1230 | }, 1231 | )?; 1232 | self.send_request::( 1233 | client_name, 1234 | url.clone(), 1235 | CodeActionParams { 1236 | text_document: text_document.clone(), 1237 | range, 1238 | context: CodeActionContext { 1239 | diagnostics: vec![], 1240 | only: None, 1241 | }, 1242 | work_done_progress_params, 1243 | partial_result_params, 1244 | }, 1245 | )?; 1246 | self.send_request::( 1247 | &client_name, 1248 | url.clone(), 1249 | CompletionParams { 1250 | text_document_position: text_document_position_params.clone(), 1251 | work_done_progress_params, 1252 | partial_result_params, 1253 | context: Some(CompletionContext { 1254 | trigger_kind: CompletionTriggerKind::INVOKED, 1255 | trigger_character: None, 1256 | }), 1257 | }, 1258 | )?; 1259 | self.send_request::( 1260 | &client_name, 1261 | url.clone(), 1262 | SemanticTokensRangeParams { 1263 | work_done_progress_params, 1264 | partial_result_params, 1265 | text_document: text_document.clone(), 1266 | range, 1267 | }, 1268 | )?; 1269 | self.send_request::( 1270 | client_name, 1271 | url.clone(), 1272 | SignatureHelpParams { 1273 | context: None, 1274 | text_document_position_params: text_document_position_params.clone(), 1275 | work_done_progress_params, 1276 | }, 1277 | )?; 1278 | self.send_request::( 1279 | client_name, 1280 | url.clone(), 1281 | CodeLensParams { 1282 | text_document, 1283 | work_done_progress_params, 1284 | partial_result_params, 1285 | }, 1286 | )?; 1287 | Ok(()) 1288 | } 1289 | fn run_event(&mut self, ev: Event, filename: &str) -> Result<()> { 1290 | let (id, sw) = match self.get_sw_by_name(filename) { 1291 | Some(v) => v, 1292 | None => return Ok(()), 1293 | }; 1294 | let client_name = &sw.client.clone(); 1295 | let url = sw.url.clone(); 1296 | let text_document_position_params = sw.text_doc_pos()?; 1297 | let text_document_position = text_document_position_params.clone(); 1298 | let text_document = TextDocumentIdentifier::new(url.clone()); 1299 | drop(sw); 1300 | self.did_change(filename.to_string(), id)?; 1301 | match ev.text.as_str() { 1302 | "definition" => { 1303 | self.send_request::( 1304 | client_name, 1305 | url, 1306 | GotoDefinitionParams { 1307 | text_document_position_params, 1308 | work_done_progress_params, 1309 | partial_result_params, 1310 | }, 1311 | )?; 1312 | } 1313 | "references" => { 1314 | self.send_request::( 1315 | client_name, 1316 | url, 1317 | ReferenceParams { 1318 | text_document_position, 1319 | work_done_progress_params, 1320 | partial_result_params, 1321 | context: ReferenceContext { 1322 | include_declaration: true, 1323 | }, 1324 | }, 1325 | )?; 1326 | } 1327 | "symbols" => { 1328 | self.send_request::( 1329 | client_name, 1330 | url, 1331 | DocumentSymbolParams { 1332 | text_document, 1333 | work_done_progress_params, 1334 | partial_result_params, 1335 | }, 1336 | )?; 1337 | } 1338 | "impl" => { 1339 | self.send_request::( 1340 | client_name, 1341 | url, 1342 | GotoImplementationParams { 1343 | text_document_position_params, 1344 | work_done_progress_params, 1345 | partial_result_params, 1346 | }, 1347 | )?; 1348 | } 1349 | "typedef" => { 1350 | self.send_request::( 1351 | client_name, 1352 | url, 1353 | GotoDefinitionParams { 1354 | text_document_position_params, 1355 | work_done_progress_params, 1356 | partial_result_params, 1357 | }, 1358 | )?; 1359 | } 1360 | _ => {} 1361 | } 1362 | Ok(()) 1363 | } 1364 | fn send_request( 1365 | &mut self, 1366 | client_name: &str, 1367 | url: Url, 1368 | params: R::Params, 1369 | ) -> Result { 1370 | let client = self.clients.get_mut(client_name).unwrap(); 1371 | let msg_id = client.send::(params)?; 1372 | self.requests 1373 | .insert(ClientId::new(client_name, msg_id), (R::METHOD.into(), url)); 1374 | Ok(msg_id) 1375 | } 1376 | fn send_notification( 1377 | &mut self, 1378 | client_name: &String, 1379 | params: N::Params, 1380 | ) -> Result<()> { 1381 | let client = self.clients.get_mut(client_name).unwrap(); 1382 | client.notify::(params) 1383 | } 1384 | fn run_action(&mut self, client_name: &str, url: Url, action: Action) -> Result<()> { 1385 | match action { 1386 | Action::Command(CodeActionOrCommand::Command(cmd)) => { 1387 | if let Some(args) = cmd.arguments { 1388 | for arg in args { 1389 | #[derive(Deserialize)] 1390 | #[serde(rename_all = "camelCase")] 1391 | struct ArgWorkspaceEdit { 1392 | workspace_edit: WorkspaceEdit, 1393 | } 1394 | match serde_json::from_value::(arg) { 1395 | Ok(v) => self.apply_workspace_edit(&v.workspace_edit)?, 1396 | Err(err) => { 1397 | eprintln!("json err {}", err); 1398 | continue; 1399 | } 1400 | } 1401 | } 1402 | } 1403 | } 1404 | Action::Command(CodeActionOrCommand::CodeAction(action)) => { 1405 | if let Some(edit) = action.edit { 1406 | self.apply_workspace_edit(&edit)?; 1407 | } else { 1408 | let _id = self.send_request::( 1409 | client_name.into(), 1410 | url, 1411 | action, 1412 | )?; 1413 | } 1414 | } 1415 | Action::Completion(item) => { 1416 | let format = item 1417 | .insert_text_format 1418 | .unwrap_or(InsertTextFormat::PLAIN_TEXT); 1419 | if let Some(edit) = item.text_edit.clone() { 1420 | match edit { 1421 | CompletionTextEdit::Edit(edit) => { 1422 | return self.apply_text_edits(&url, format, &vec![edit]) 1423 | } 1424 | CompletionTextEdit::InsertAndReplace(_) => { 1425 | eprintln!("InsertAndReplace not supported"); 1426 | return Ok(()); 1427 | } 1428 | } 1429 | } 1430 | panic!("unsupported"); 1431 | } 1432 | Action::CodeLens(lens) => { 1433 | // TODO: rust-analyzer complains about "code lens without data" here. If I set 1434 | // lens.data to Value::Null, it complains with some resolution error. 1435 | let _id = self.send_request::(client_name.into(), url, lens)?; 1436 | } 1437 | } 1438 | Ok(()) 1439 | } 1440 | fn run_cmd(&mut self, ev: Event) -> Result<()> { 1441 | match ev.c2 { 1442 | 'x' | 'X' => match ev.text.as_str() { 1443 | "Get" => { 1444 | //self.actions.clear(); 1445 | self.output.clear(); 1446 | self.sync_windows()?; 1447 | self.diags.clear(); 1448 | self.current_hover = None; 1449 | } 1450 | _ => { 1451 | panic!("unexpected"); 1452 | } 1453 | }, 1454 | 'L' => { 1455 | { 1456 | let mut name = None; 1457 | for (pos, n) in self.addr.iter().rev() { 1458 | if (*pos as u32) < ev.q0 { 1459 | name = n.clone(); 1460 | break; 1461 | } 1462 | } 1463 | if let Some(name) = name { 1464 | return self.run_event(ev, &name); 1465 | } 1466 | } 1467 | { 1468 | let mut action: Option<(String, Url, Action)> = None; 1469 | if let Some(hover) = self.current_hover.as_mut() { 1470 | let mut action_idx: Option = None; 1471 | for (pos, idx) in hover.action_addrs.iter().rev() { 1472 | if (*pos as u32) < ev.q0 { 1473 | action_idx = *idx; 1474 | break; 1475 | } 1476 | } 1477 | if let Some(idx) = action_idx { 1478 | action = Some(( 1479 | hover.client_name.to_string(), 1480 | hover.url.clone(), 1481 | hover.actions.remove(idx), 1482 | )); 1483 | } 1484 | } 1485 | if let Some((client_name, url, action)) = action { 1486 | self.set_hover(&url, |hover| { 1487 | hover.code_actions.clear(); 1488 | hover.completion.clear(); 1489 | }); 1490 | return self.run_action(&client_name, url, action); 1491 | } 1492 | } 1493 | return plumb_location(ev.text); 1494 | } 1495 | _ => {} 1496 | } 1497 | Ok(()) 1498 | } 1499 | fn cmd_put(&mut self, ev: LogEvent) -> Result<()> { 1500 | self.did_change(ev.name.clone(), ev.id)?; 1501 | let sw = match self.get_sw_by_name_id(&ev.name, &ev.id) { 1502 | Some(sw) => sw, 1503 | None => return Ok(()), 1504 | }; 1505 | let client_name = &sw.client.clone(); 1506 | let text_document = sw.doc_ident(); 1507 | let url = sw.url.clone(); 1508 | drop(sw); 1509 | self.send_notification::( 1510 | client_name, 1511 | DidSaveTextDocumentParams { 1512 | text_document: text_document.clone(), 1513 | text: None, 1514 | }, 1515 | )?; 1516 | let capabilities = self.capabilities.get(client_name).unwrap(); 1517 | if self 1518 | .config 1519 | .servers 1520 | .get(client_name) 1521 | .unwrap() 1522 | .format_on_put 1523 | .unwrap_or(true) 1524 | && capabilities.document_formatting_provider.is_some() 1525 | { 1526 | self.send_request::( 1527 | client_name, 1528 | url, 1529 | DocumentFormattingParams { 1530 | text_document, 1531 | options: FormattingOptions { 1532 | tab_size: 4, 1533 | insert_spaces: false, 1534 | properties: HashMap::new(), 1535 | trim_trailing_whitespace: Some(true), 1536 | insert_final_newline: Some(true), 1537 | trim_final_newlines: Some(true), 1538 | }, 1539 | work_done_progress_params: WorkDoneProgressParams { 1540 | work_done_token: None, 1541 | }, 1542 | }, 1543 | )?; 1544 | } 1545 | Ok(()) 1546 | } 1547 | fn wait(&mut self) -> Result<()> { 1548 | let (sync_s, sync_r) = bounded(1); 1549 | 1550 | self.sync_windows()?; 1551 | // chan index -> (recv chan, self.clients index) 1552 | 1553 | // one-time index setup 1554 | let mut sel = Select::new(); 1555 | let sel_log_r = sel.recv(&self.log_r); 1556 | let sel_ev_r = sel.recv(&self.ev_r); 1557 | let sel_err_r = sel.recv(&self.err_r); 1558 | let sel_sync_r = sel.recv(&sync_r); 1559 | let mut clients = HashMap::new(); 1560 | 1561 | for (name, c) in &self.clients { 1562 | clients.insert(sel.recv(&c.msg_r), (c.msg_r.clone(), name.to_string())); 1563 | } 1564 | drop(sel); 1565 | 1566 | loop { 1567 | let mut no_sync = false; 1568 | 1569 | let mut sel = Select::new(); 1570 | sel.recv(&self.log_r); 1571 | sel.recv(&self.ev_r); 1572 | sel.recv(&self.err_r); 1573 | sel.recv(&sync_r); 1574 | for (_, c) in &self.clients { 1575 | sel.recv(&c.msg_r); 1576 | } 1577 | let index = sel.ready(); 1578 | 1579 | match index { 1580 | _ if index == sel_log_r => { 1581 | let msg = self.log_r.recv(); 1582 | match msg { 1583 | Ok(ev) => match ev.op.as_str() { 1584 | "focus" => { 1585 | let _ = self.set_focus(ev); 1586 | } 1587 | "put" => { 1588 | self.cmd_put(ev)?; 1589 | no_sync = true; 1590 | } 1591 | "new" | "del" => { 1592 | self.sync_windows()?; 1593 | } 1594 | _ => { 1595 | eprintln!("unknown event op {:?}", ev); 1596 | } 1597 | }, 1598 | Err(_) => { 1599 | break; 1600 | } 1601 | } 1602 | } 1603 | _ if index == sel_ev_r => { 1604 | let msg = self.ev_r.recv(); 1605 | match msg { 1606 | Ok(ev) => { 1607 | self.run_cmd(ev)?; 1608 | } 1609 | Err(_) => { 1610 | break; 1611 | } 1612 | } 1613 | } 1614 | _ if index == sel_err_r => { 1615 | let msg = self.err_r.recv(); 1616 | eprintln!("err {:?}", msg); 1617 | match msg { 1618 | Ok(_) => { 1619 | break; 1620 | } 1621 | Err(_) => { 1622 | break; 1623 | } 1624 | } 1625 | } 1626 | _ if index == sel_sync_r => { 1627 | no_sync = true; 1628 | let _ = sync_r.recv(); 1629 | self.sync()?; 1630 | } 1631 | _ => { 1632 | let (ch, name) = clients.get(&index).unwrap(); 1633 | let msg = ch.recv()?; 1634 | self.lsp_msg(name.to_string(), msg)?; 1635 | } 1636 | }; 1637 | 1638 | // Only send a sync message if the channel is empty. If a bunch of LSP messages 1639 | // arrive (like window progress updatets), they don't each have to wait for a 1640 | // full sync before beingc processed. 1641 | if !no_sync && sync_s.is_empty() { 1642 | sync_s.send(())?; 1643 | } 1644 | } 1645 | Ok(()) 1646 | } 1647 | } 1648 | 1649 | impl Drop for Server { 1650 | fn drop(&mut self) { 1651 | let _ = self.w.del(true); 1652 | } 1653 | } 1654 | 1655 | fn goto_definition(goto: &GotoDefinitionResponse) -> Result<()> { 1656 | let loc: &Location = match goto { 1657 | GotoDefinitionResponse::Array(locs) => match locs.len() { 1658 | 0 => return Ok(()), 1659 | _ => &locs[0], 1660 | }, 1661 | GotoDefinitionResponse::Scalar(loc) => &loc, 1662 | _ => panic!("unknown definition response: {:?}", goto), 1663 | }; 1664 | let plumb = location_to_plumb(loc); 1665 | plumb_location(plumb)?; 1666 | Ok(()) 1667 | } 1668 | 1669 | fn location_to_plumb(l: &Location) -> String { 1670 | // Including the character here apparently isn't useful because the right click 1671 | // event from acme doesn't include it, only the line. Why is this? 1672 | format!("{}:{}", l.uri.path(), l.range.start.line + 1) 1673 | } 1674 | 1675 | fn plumb_location(loc: String) -> Result<()> { 1676 | let path = loc.split(":").next().unwrap(); 1677 | // Verify path exists. If not, do nothing. 1678 | if metadata(path).is_err() { 1679 | return Ok(()); 1680 | } 1681 | let f = plumb::open("send", OpenMode::WRITE)?; 1682 | let msg = plumb::Message { 1683 | dst: "edit".to_string(), 1684 | typ: "text".to_string(), 1685 | data: loc.into(), 1686 | }; 1687 | return msg.send(f); 1688 | } 1689 | 1690 | fn format_pct(pct: Option) -> String { 1691 | match pct { 1692 | Some(v) => format!("{}", v), 1693 | None => "?".to_string(), 1694 | } 1695 | } 1696 | 1697 | fn cmp_location(a: &Location, b: &Location) -> Ordering { 1698 | if a.uri != b.uri { 1699 | return a.uri.as_str().cmp(b.uri.as_str()); 1700 | } 1701 | return cmp_range(&a.range, &b.range); 1702 | } 1703 | 1704 | fn cmp_range(a: &Range, b: &Range) -> Ordering { 1705 | if a.start != b.start { 1706 | return cmp_position(&a.start, &b.start); 1707 | } 1708 | return cmp_position(&a.end, &b.end); 1709 | } 1710 | 1711 | fn cmp_position(a: &Position, b: &Position) -> Ordering { 1712 | if a.line != b.line { 1713 | return a.line.cmp(&b.line); 1714 | } 1715 | return a.character.cmp(&b.character); 1716 | } 1717 | 1718 | fn extract_doc(d: &Documentation) -> &str { 1719 | match d { 1720 | Documentation::String(s) => s, 1721 | Documentation::MarkupContent(c) => &c.value, 1722 | } 1723 | } 1724 | 1725 | #[allow(non_upper_case_globals)] 1726 | const work_done_progress_params: WorkDoneProgressParams = WorkDoneProgressParams { 1727 | work_done_token: None, 1728 | }; 1729 | #[allow(non_upper_case_globals)] 1730 | const partial_result_params: PartialResultParams = PartialResultParams { 1731 | partial_result_token: None, 1732 | }; 1733 | --------------------------------------------------------------------------------