├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── TODO.md ├── build.rs ├── src ├── analysis.rs ├── browser_rustdoc.rs ├── browser_trait.rs ├── lib.rs ├── main.rs ├── scroll_pad.rs └── ui.rs └── tests ├── browser_test.rs ├── externcrate ├── Cargo.toml └── src │ └── lib.rs └── testcrate ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src ├── lib.rs └── main.rs /.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 = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 10 | dependencies = [ 11 | "cfg-if", 12 | "getrandom 0.2.15", 13 | "once_cell", 14 | "version_check", 15 | "zerocopy", 16 | ] 17 | 18 | [[package]] 19 | name = "anstream" 20 | version = "0.6.18" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 23 | dependencies = [ 24 | "anstyle", 25 | "anstyle-parse", 26 | "anstyle-query", 27 | "anstyle-wincon", 28 | "colorchoice", 29 | "is_terminal_polyfill", 30 | "utf8parse", 31 | ] 32 | 33 | [[package]] 34 | name = "anstyle" 35 | version = "1.0.10" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 38 | 39 | [[package]] 40 | name = "anstyle-parse" 41 | version = "0.2.6" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 44 | dependencies = [ 45 | "utf8parse", 46 | ] 47 | 48 | [[package]] 49 | name = "anstyle-query" 50 | version = "1.1.2" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 53 | dependencies = [ 54 | "windows-sys 0.59.0", 55 | ] 56 | 57 | [[package]] 58 | name = "anstyle-wincon" 59 | version = "3.0.7" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 62 | dependencies = [ 63 | "anstyle", 64 | "once_cell", 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anyhow" 70 | version = "1.0.98" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 73 | 74 | [[package]] 75 | name = "autocfg" 76 | version = "1.4.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 79 | 80 | [[package]] 81 | name = "bitflags" 82 | version = "2.9.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 85 | 86 | [[package]] 87 | name = "bumpalo" 88 | version = "3.17.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 91 | 92 | [[package]] 93 | name = "castaway" 94 | version = "0.2.3" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 97 | dependencies = [ 98 | "rustversion", 99 | ] 100 | 101 | [[package]] 102 | name = "cfg-if" 103 | version = "1.0.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 106 | 107 | [[package]] 108 | name = "clap" 109 | version = "4.5.37" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 112 | dependencies = [ 113 | "clap_builder", 114 | "clap_derive", 115 | ] 116 | 117 | [[package]] 118 | name = "clap_builder" 119 | version = "4.5.37" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 122 | dependencies = [ 123 | "anstream", 124 | "anstyle", 125 | "clap_lex", 126 | "strsim", 127 | ] 128 | 129 | [[package]] 130 | name = "clap_derive" 131 | version = "4.5.32" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 134 | dependencies = [ 135 | "heck", 136 | "proc-macro2", 137 | "quote", 138 | "syn", 139 | ] 140 | 141 | [[package]] 142 | name = "clap_lex" 143 | version = "0.7.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 146 | 147 | [[package]] 148 | name = "colorchoice" 149 | version = "1.0.3" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 152 | 153 | [[package]] 154 | name = "compact_str" 155 | version = "0.8.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 158 | dependencies = [ 159 | "castaway", 160 | "cfg-if", 161 | "itoa", 162 | "rustversion", 163 | "ryu", 164 | "static_assertions", 165 | ] 166 | 167 | [[package]] 168 | name = "console" 169 | version = "0.15.11" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 172 | dependencies = [ 173 | "encode_unicode", 174 | "libc", 175 | "once_cell", 176 | "unicode-width 0.2.0", 177 | "windows-sys 0.59.0", 178 | ] 179 | 180 | [[package]] 181 | name = "crossbeam-channel" 182 | version = "0.5.15" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 185 | dependencies = [ 186 | "crossbeam-utils", 187 | ] 188 | 189 | [[package]] 190 | name = "crossbeam-deque" 191 | version = "0.8.6" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 194 | dependencies = [ 195 | "crossbeam-epoch", 196 | "crossbeam-utils", 197 | ] 198 | 199 | [[package]] 200 | name = "crossbeam-epoch" 201 | version = "0.9.18" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 204 | dependencies = [ 205 | "crossbeam-utils", 206 | ] 207 | 208 | [[package]] 209 | name = "crossbeam-utils" 210 | version = "0.8.21" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 213 | 214 | [[package]] 215 | name = "crossterm" 216 | version = "0.28.1" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 219 | dependencies = [ 220 | "bitflags", 221 | "crossterm_winapi", 222 | "mio", 223 | "parking_lot", 224 | "rustix 0.38.44", 225 | "signal-hook", 226 | "signal-hook-mio", 227 | "winapi", 228 | ] 229 | 230 | [[package]] 231 | name = "crossterm_winapi" 232 | version = "0.9.1" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 235 | dependencies = [ 236 | "winapi", 237 | ] 238 | 239 | [[package]] 240 | name = "cursive" 241 | version = "0.21.1" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "386d5a36020bb856e9a34ecb8a4e6c9bd6b0262d1857bae4db7bc7e2fdaa532e" 244 | dependencies = [ 245 | "ahash", 246 | "cfg-if", 247 | "crossbeam-channel", 248 | "crossterm", 249 | "cursive_core", 250 | "lazy_static", 251 | "libc", 252 | "log", 253 | "signal-hook", 254 | "unicode-segmentation", 255 | "unicode-width 0.1.14", 256 | ] 257 | 258 | [[package]] 259 | name = "cursive-macros" 260 | version = "0.1.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "ac7ac0eb0cede3dfdfebf4d5f22354e05a730b79c25fd03481fc69fcfba0a73e" 263 | dependencies = [ 264 | "proc-macro2", 265 | ] 266 | 267 | [[package]] 268 | name = "cursive_core" 269 | version = "0.4.6" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "321ec774d27fafc66e812034d0025f8858bd7d9095304ff8fc200e0b9f9cc257" 272 | dependencies = [ 273 | "ahash", 274 | "compact_str", 275 | "crossbeam-channel", 276 | "cursive-macros", 277 | "enum-map", 278 | "enumset", 279 | "lazy_static", 280 | "log", 281 | "num", 282 | "parking_lot", 283 | "serde_json", 284 | "time", 285 | "unicode-segmentation", 286 | "unicode-width 0.1.14", 287 | "xi-unicode", 288 | ] 289 | 290 | [[package]] 291 | name = "darling" 292 | version = "0.20.11" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 295 | dependencies = [ 296 | "darling_core", 297 | "darling_macro", 298 | ] 299 | 300 | [[package]] 301 | name = "darling_core" 302 | version = "0.20.11" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 305 | dependencies = [ 306 | "fnv", 307 | "ident_case", 308 | "proc-macro2", 309 | "quote", 310 | "syn", 311 | ] 312 | 313 | [[package]] 314 | name = "darling_macro" 315 | version = "0.20.11" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 318 | dependencies = [ 319 | "darling_core", 320 | "quote", 321 | "syn", 322 | ] 323 | 324 | [[package]] 325 | name = "deranged" 326 | version = "0.4.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 329 | dependencies = [ 330 | "powerfmt", 331 | ] 332 | 333 | [[package]] 334 | name = "either" 335 | version = "1.15.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 338 | 339 | [[package]] 340 | name = "encode_unicode" 341 | version = "1.0.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 344 | 345 | [[package]] 346 | name = "enum-map" 347 | version = "2.7.3" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" 350 | dependencies = [ 351 | "enum-map-derive", 352 | ] 353 | 354 | [[package]] 355 | name = "enum-map-derive" 356 | version = "0.17.0" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" 359 | dependencies = [ 360 | "proc-macro2", 361 | "quote", 362 | "syn", 363 | ] 364 | 365 | [[package]] 366 | name = "enumset" 367 | version = "1.1.5" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293" 370 | dependencies = [ 371 | "enumset_derive", 372 | ] 373 | 374 | [[package]] 375 | name = "enumset_derive" 376 | version = "0.10.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" 379 | dependencies = [ 380 | "darling", 381 | "proc-macro2", 382 | "quote", 383 | "syn", 384 | ] 385 | 386 | [[package]] 387 | name = "errno" 388 | version = "0.3.11" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 391 | dependencies = [ 392 | "libc", 393 | "windows-sys 0.59.0", 394 | ] 395 | 396 | [[package]] 397 | name = "fastrand" 398 | version = "2.3.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 401 | 402 | [[package]] 403 | name = "fnv" 404 | version = "1.0.7" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 407 | 408 | [[package]] 409 | name = "getrandom" 410 | version = "0.2.15" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 413 | dependencies = [ 414 | "cfg-if", 415 | "libc", 416 | "wasi 0.11.0+wasi-snapshot-preview1", 417 | ] 418 | 419 | [[package]] 420 | name = "getrandom" 421 | version = "0.3.2" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 424 | dependencies = [ 425 | "cfg-if", 426 | "libc", 427 | "r-efi", 428 | "wasi 0.14.2+wasi-0.2.4", 429 | ] 430 | 431 | [[package]] 432 | name = "heck" 433 | version = "0.5.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 436 | 437 | [[package]] 438 | name = "ident_case" 439 | version = "1.0.1" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 442 | 443 | [[package]] 444 | name = "indicatif" 445 | version = "0.17.11" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 448 | dependencies = [ 449 | "console", 450 | "number_prefix", 451 | "portable-atomic", 452 | "unicode-width 0.2.0", 453 | "web-time", 454 | ] 455 | 456 | [[package]] 457 | name = "is_terminal_polyfill" 458 | version = "1.70.1" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 461 | 462 | [[package]] 463 | name = "itoa" 464 | version = "1.0.15" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 467 | 468 | [[package]] 469 | name = "js-sys" 470 | version = "0.3.77" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 473 | dependencies = [ 474 | "once_cell", 475 | "wasm-bindgen", 476 | ] 477 | 478 | [[package]] 479 | name = "lazy_static" 480 | version = "1.5.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 483 | 484 | [[package]] 485 | name = "libc" 486 | version = "0.2.172" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 489 | 490 | [[package]] 491 | name = "linux-raw-sys" 492 | version = "0.4.15" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 495 | 496 | [[package]] 497 | name = "linux-raw-sys" 498 | version = "0.9.4" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 501 | 502 | [[package]] 503 | name = "lock_api" 504 | version = "0.4.12" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 507 | dependencies = [ 508 | "autocfg", 509 | "scopeguard", 510 | ] 511 | 512 | [[package]] 513 | name = "log" 514 | version = "0.4.27" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 517 | 518 | [[package]] 519 | name = "memchr" 520 | version = "2.7.4" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 523 | 524 | [[package]] 525 | name = "mio" 526 | version = "1.0.3" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 529 | dependencies = [ 530 | "libc", 531 | "log", 532 | "wasi 0.11.0+wasi-snapshot-preview1", 533 | "windows-sys 0.52.0", 534 | ] 535 | 536 | [[package]] 537 | name = "num" 538 | version = "0.4.3" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 541 | dependencies = [ 542 | "num-complex", 543 | "num-integer", 544 | "num-iter", 545 | "num-rational", 546 | "num-traits", 547 | ] 548 | 549 | [[package]] 550 | name = "num-complex" 551 | version = "0.4.6" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 554 | dependencies = [ 555 | "num-traits", 556 | ] 557 | 558 | [[package]] 559 | name = "num-conv" 560 | version = "0.1.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 563 | 564 | [[package]] 565 | name = "num-integer" 566 | version = "0.1.46" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 569 | dependencies = [ 570 | "num-traits", 571 | ] 572 | 573 | [[package]] 574 | name = "num-iter" 575 | version = "0.1.45" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 578 | dependencies = [ 579 | "autocfg", 580 | "num-integer", 581 | "num-traits", 582 | ] 583 | 584 | [[package]] 585 | name = "num-rational" 586 | version = "0.4.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 589 | dependencies = [ 590 | "num-integer", 591 | "num-traits", 592 | ] 593 | 594 | [[package]] 595 | name = "num-traits" 596 | version = "0.2.19" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 599 | dependencies = [ 600 | "autocfg", 601 | ] 602 | 603 | [[package]] 604 | name = "num_threads" 605 | version = "0.1.7" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 608 | dependencies = [ 609 | "libc", 610 | ] 611 | 612 | [[package]] 613 | name = "number_prefix" 614 | version = "0.4.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 617 | 618 | [[package]] 619 | name = "once_cell" 620 | version = "1.21.3" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 623 | 624 | [[package]] 625 | name = "parking_lot" 626 | version = "0.12.3" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 629 | dependencies = [ 630 | "lock_api", 631 | "parking_lot_core", 632 | ] 633 | 634 | [[package]] 635 | name = "parking_lot_core" 636 | version = "0.9.10" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 639 | dependencies = [ 640 | "cfg-if", 641 | "libc", 642 | "redox_syscall", 643 | "smallvec", 644 | "windows-targets", 645 | ] 646 | 647 | [[package]] 648 | name = "portable-atomic" 649 | version = "1.11.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 652 | 653 | [[package]] 654 | name = "powerfmt" 655 | version = "0.2.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 658 | 659 | [[package]] 660 | name = "proc-macro2" 661 | version = "1.0.95" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 664 | dependencies = [ 665 | "unicode-ident", 666 | ] 667 | 668 | [[package]] 669 | name = "quote" 670 | version = "1.0.40" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 673 | dependencies = [ 674 | "proc-macro2", 675 | ] 676 | 677 | [[package]] 678 | name = "r-efi" 679 | version = "5.2.0" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 682 | 683 | [[package]] 684 | name = "rayon" 685 | version = "1.10.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 688 | dependencies = [ 689 | "either", 690 | "rayon-core", 691 | ] 692 | 693 | [[package]] 694 | name = "rayon-core" 695 | version = "1.12.1" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 698 | dependencies = [ 699 | "crossbeam-deque", 700 | "crossbeam-utils", 701 | ] 702 | 703 | [[package]] 704 | name = "redox_syscall" 705 | version = "0.5.11" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" 708 | dependencies = [ 709 | "bitflags", 710 | ] 711 | 712 | [[package]] 713 | name = "rsbrowse" 714 | version = "0.2.10" 715 | dependencies = [ 716 | "anyhow", 717 | "clap", 718 | "cursive", 719 | "indicatif", 720 | "lazy_static", 721 | "log", 722 | "rayon", 723 | "rustdoc-types", 724 | "serde_json", 725 | "tempfile", 726 | ] 727 | 728 | [[package]] 729 | name = "rustdoc-types" 730 | version = "0.40.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "39c891e8dfc3b35c614707145e1483d74a9c66f85f709f8cfa67b208d355016e" 733 | dependencies = [ 734 | "serde", 735 | ] 736 | 737 | [[package]] 738 | name = "rustix" 739 | version = "0.38.44" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 742 | dependencies = [ 743 | "bitflags", 744 | "errno", 745 | "libc", 746 | "linux-raw-sys 0.4.15", 747 | "windows-sys 0.59.0", 748 | ] 749 | 750 | [[package]] 751 | name = "rustix" 752 | version = "1.0.5" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 755 | dependencies = [ 756 | "bitflags", 757 | "errno", 758 | "libc", 759 | "linux-raw-sys 0.9.4", 760 | "windows-sys 0.59.0", 761 | ] 762 | 763 | [[package]] 764 | name = "rustversion" 765 | version = "1.0.20" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 768 | 769 | [[package]] 770 | name = "ryu" 771 | version = "1.0.20" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 774 | 775 | [[package]] 776 | name = "scopeguard" 777 | version = "1.2.0" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 780 | 781 | [[package]] 782 | name = "serde" 783 | version = "1.0.219" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 786 | dependencies = [ 787 | "serde_derive", 788 | ] 789 | 790 | [[package]] 791 | name = "serde_derive" 792 | version = "1.0.219" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 795 | dependencies = [ 796 | "proc-macro2", 797 | "quote", 798 | "syn", 799 | ] 800 | 801 | [[package]] 802 | name = "serde_json" 803 | version = "1.0.140" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 806 | dependencies = [ 807 | "itoa", 808 | "memchr", 809 | "ryu", 810 | "serde", 811 | ] 812 | 813 | [[package]] 814 | name = "signal-hook" 815 | version = "0.3.17" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 818 | dependencies = [ 819 | "libc", 820 | "signal-hook-registry", 821 | ] 822 | 823 | [[package]] 824 | name = "signal-hook-mio" 825 | version = "0.2.4" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 828 | dependencies = [ 829 | "libc", 830 | "mio", 831 | "signal-hook", 832 | ] 833 | 834 | [[package]] 835 | name = "signal-hook-registry" 836 | version = "1.4.4" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "a1ee1aca2bc74ef9589efa7ccaa0f3752751399940356209b3fd80c078149b5e" 839 | dependencies = [ 840 | "libc", 841 | ] 842 | 843 | [[package]] 844 | name = "smallvec" 845 | version = "1.15.0" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 848 | 849 | [[package]] 850 | name = "static_assertions" 851 | version = "1.1.0" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 854 | 855 | [[package]] 856 | name = "strsim" 857 | version = "0.11.1" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 860 | 861 | [[package]] 862 | name = "syn" 863 | version = "2.0.100" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 866 | dependencies = [ 867 | "proc-macro2", 868 | "quote", 869 | "unicode-ident", 870 | ] 871 | 872 | [[package]] 873 | name = "tempfile" 874 | version = "3.19.1" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 877 | dependencies = [ 878 | "fastrand", 879 | "getrandom 0.3.2", 880 | "once_cell", 881 | "rustix 1.0.5", 882 | "windows-sys 0.59.0", 883 | ] 884 | 885 | [[package]] 886 | name = "time" 887 | version = "0.3.41" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 890 | dependencies = [ 891 | "deranged", 892 | "itoa", 893 | "libc", 894 | "num-conv", 895 | "num_threads", 896 | "powerfmt", 897 | "serde", 898 | "time-core", 899 | "time-macros", 900 | ] 901 | 902 | [[package]] 903 | name = "time-core" 904 | version = "0.1.4" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 907 | 908 | [[package]] 909 | name = "time-macros" 910 | version = "0.2.22" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 913 | dependencies = [ 914 | "num-conv", 915 | "time-core", 916 | ] 917 | 918 | [[package]] 919 | name = "unicode-ident" 920 | version = "1.0.18" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 923 | 924 | [[package]] 925 | name = "unicode-segmentation" 926 | version = "1.12.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 929 | 930 | [[package]] 931 | name = "unicode-width" 932 | version = "0.1.14" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 935 | 936 | [[package]] 937 | name = "unicode-width" 938 | version = "0.2.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 941 | 942 | [[package]] 943 | name = "utf8parse" 944 | version = "0.2.2" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 947 | 948 | [[package]] 949 | name = "version_check" 950 | version = "0.9.5" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 953 | 954 | [[package]] 955 | name = "wasi" 956 | version = "0.11.0+wasi-snapshot-preview1" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 959 | 960 | [[package]] 961 | name = "wasi" 962 | version = "0.14.2+wasi-0.2.4" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 965 | dependencies = [ 966 | "wit-bindgen-rt", 967 | ] 968 | 969 | [[package]] 970 | name = "wasm-bindgen" 971 | version = "0.2.100" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 974 | dependencies = [ 975 | "cfg-if", 976 | "once_cell", 977 | "wasm-bindgen-macro", 978 | ] 979 | 980 | [[package]] 981 | name = "wasm-bindgen-backend" 982 | version = "0.2.100" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 985 | dependencies = [ 986 | "bumpalo", 987 | "log", 988 | "proc-macro2", 989 | "quote", 990 | "syn", 991 | "wasm-bindgen-shared", 992 | ] 993 | 994 | [[package]] 995 | name = "wasm-bindgen-macro" 996 | version = "0.2.100" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 999 | dependencies = [ 1000 | "quote", 1001 | "wasm-bindgen-macro-support", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "wasm-bindgen-macro-support" 1006 | version = "0.2.100" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1009 | dependencies = [ 1010 | "proc-macro2", 1011 | "quote", 1012 | "syn", 1013 | "wasm-bindgen-backend", 1014 | "wasm-bindgen-shared", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "wasm-bindgen-shared" 1019 | version = "0.2.100" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1022 | dependencies = [ 1023 | "unicode-ident", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "web-time" 1028 | version = "1.1.0" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1031 | dependencies = [ 1032 | "js-sys", 1033 | "wasm-bindgen", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "winapi" 1038 | version = "0.3.9" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1041 | dependencies = [ 1042 | "winapi-i686-pc-windows-gnu", 1043 | "winapi-x86_64-pc-windows-gnu", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "winapi-i686-pc-windows-gnu" 1048 | version = "0.4.0" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1051 | 1052 | [[package]] 1053 | name = "winapi-x86_64-pc-windows-gnu" 1054 | version = "0.4.0" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1057 | 1058 | [[package]] 1059 | name = "windows-sys" 1060 | version = "0.52.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1063 | dependencies = [ 1064 | "windows-targets", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "windows-sys" 1069 | version = "0.59.0" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1072 | dependencies = [ 1073 | "windows-targets", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "windows-targets" 1078 | version = "0.52.6" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1081 | dependencies = [ 1082 | "windows_aarch64_gnullvm", 1083 | "windows_aarch64_msvc", 1084 | "windows_i686_gnu", 1085 | "windows_i686_gnullvm", 1086 | "windows_i686_msvc", 1087 | "windows_x86_64_gnu", 1088 | "windows_x86_64_gnullvm", 1089 | "windows_x86_64_msvc", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "windows_aarch64_gnullvm" 1094 | version = "0.52.6" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1097 | 1098 | [[package]] 1099 | name = "windows_aarch64_msvc" 1100 | version = "0.52.6" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1103 | 1104 | [[package]] 1105 | name = "windows_i686_gnu" 1106 | version = "0.52.6" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1109 | 1110 | [[package]] 1111 | name = "windows_i686_gnullvm" 1112 | version = "0.52.6" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1115 | 1116 | [[package]] 1117 | name = "windows_i686_msvc" 1118 | version = "0.52.6" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1121 | 1122 | [[package]] 1123 | name = "windows_x86_64_gnu" 1124 | version = "0.52.6" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1127 | 1128 | [[package]] 1129 | name = "windows_x86_64_gnullvm" 1130 | version = "0.52.6" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1133 | 1134 | [[package]] 1135 | name = "windows_x86_64_msvc" 1136 | version = "0.52.6" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1139 | 1140 | [[package]] 1141 | name = "wit-bindgen-rt" 1142 | version = "0.39.0" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1145 | dependencies = [ 1146 | "bitflags", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "xi-unicode" 1151 | version = "0.3.0" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" 1154 | 1155 | [[package]] 1156 | name = "zerocopy" 1157 | version = "0.7.35" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1160 | dependencies = [ 1161 | "zerocopy-derive", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "zerocopy-derive" 1166 | version = "0.7.35" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1169 | dependencies = [ 1170 | "proc-macro2", 1171 | "quote", 1172 | "syn", 1173 | ] 1174 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsbrowse" 3 | version = "0.2.10" 4 | authors = ["Bill Fraser "] 5 | description = "an interactive browser for the contents of Rust crates" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | anyhow = "1" 10 | clap = { version = "4.4.8", features = ["derive"] } 11 | cursive = "0.21.1" 12 | indicatif = "0.17.7" 13 | lazy_static = "1" 14 | log = "0.4" 15 | rayon = "1.8.0" 16 | rustdoc-types = "0.40.0" 17 | serde_json = "1.0" 18 | tempfile = "3.8.1" 19 | 20 | [dev-dependencies] 21 | lazy_static = "1.4" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rsbrowse 2 | ======== 3 | 4 | Browse Rust code from the compiler's perspective. 5 | 6 | [![demo!](https://asciinema.org/a/9BeP2h7n0taVtQHrhbGhuIe2E.svg)](https://asciinema.org/a/9BeP2h7n0taVtQHrhbGhuIe2E) 7 | (the demo shows browsing the excellent [cursive](https://github.com/gyscos/cursive) crate upon which this program's UI is built ❤️) 8 | 9 | rsbrowse runs `rustdoc` on your code and tells it to save the type info for everything it sees. It then presents it in an interactive text-mode viewer. This lets you browse the structure of the program from the compiler's view 10 | 11 | # Requirements 12 | 13 | * cargo 14 | * nightly rust toolchain (this is needed to use the currently unstable `--output-format=json` flag in rustdoc) 15 | * optional but *highly recommended*: the rustdoc JSON for the standard library 16 | * you can install this using `rustup component add rust-docs-json --toolchain nightly` 17 | 18 | # Usage 19 | 20 | ``` 21 | $ rsbrowse 22 | ``` 23 | 24 | rsbrowse will start up with the left pane listing all the workspace's crates as well as its dependencies. 25 | 26 | Use the up and down keys to move within a column, and left and right to jump between columns. As you move within a column, columns to the right of it will be updated to show things inside of whatever you have selected. 27 | 28 | At any time, you can press ENTER to bring up a dialog with info about whatever you have highlighted, including its source code. In this dialog, press TAB to switch to the buttons. The Debug button gives a dump of the raw rust-analysis data. 29 | 30 | To exit, press ESC to activate the menu bar, and right arrow to select Quit. 31 | 32 | # Help 33 | 34 | rsbrowse is still pretty new and may have bugs. Unfortunately, as a curses application, text written to stderr gets lost, so log messages are redirected to a file. If you observe any problems or panics, please file an issue and attach the log file :) (Also set `RUST_BACKTRACE=1` while you're at it.) 35 | 36 | To see a list of TODOs and ideas for future enhancements, see [`TODO.md`](TODO.md). 37 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Limitations 2 | * Types get shown with their canonical name (after resolving all aliases and re-exports) regardless of what the code called them. So you'll see lots of `impl core::...` when the code really wrote `impl std::...` because `std` re-exports lots of things from `core`. 3 | * Probably not possible to fix this without parsing the source code. 4 | * It would be nice to show crate versions, but versions are a Cargo thing, not a rustdoc thing, and so it isn't present in the JSON data anywhere. 5 | * Related, it's not currenlty possible to show crate types; having a binary and lib crate with the same name won't work. 6 | 7 | # Enhancements 8 | * implement some form of live search, where you can start typing and rsbrowse selects the thing 9 | * initially, within the current pane would be a nice start 10 | * eventually, within the current crate is probably good enough 11 | * globally is probably a bad idea 12 | * let users hit F3 or something to continue to the next match 13 | * add child items for generic parameters. Things like Arc<`Foo`> should have `Foo`'s children visible. 14 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::process::Command; 3 | 4 | fn main() { 5 | if let Ok(git) = Command::new("git").arg("rev-parse").arg("HEAD").output() { 6 | if git.status.success() { 7 | print!("cargo:rustc-env=GIT_COMMIT_HASH="); 8 | std::io::stdout().write_all(&git.stdout).unwrap(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/analysis.rs: -------------------------------------------------------------------------------- 1 | use std::backtrace::{Backtrace, BacktraceStatus}; 2 | use std::collections::HashMap; 3 | use std::fs::{self, File}; 4 | use std::io::BufReader; 5 | use std::path::{Path, PathBuf}; 6 | use std::process::Command; 7 | use std::sync::Mutex; 8 | 9 | use anyhow::{anyhow, Context}; 10 | use indicatif::{ProgressBar, ProgressStyle}; 11 | use rayon::prelude::*; 12 | 13 | /// Write the analysis data to a subdirectory under target/ with this name. 14 | const SUBDIR: &str = "rsbrowse"; 15 | 16 | const EMPTY_ID: &rustdoc_types::Id = &rustdoc_types::Id(u32::MAX); 17 | static EMPTY_STRING: String = String::new(); 18 | 19 | // An ItemId which silently won't resolve to anything. 20 | pub static EMPTY_ITEM_ID: ItemId<'static> = ItemId( 21 | CrateId { 22 | name: &EMPTY_STRING, 23 | }, 24 | EMPTY_ID, 25 | ); 26 | 27 | pub struct Analysis { 28 | pub crates: HashMap, 29 | } 30 | 31 | impl Analysis { 32 | pub fn generate( 33 | workspace_path: impl AsRef, 34 | toolchain: Option<&str>, 35 | ) -> anyhow::Result<()> { 36 | let mut cmd = Command::new("cargo"); 37 | if let Some(toolchain) = toolchain { 38 | cmd.arg(format!("+{toolchain}")); 39 | } 40 | 41 | let workspace_path = workspace_path.as_ref(); 42 | 43 | let cargo_status = cmd 44 | .arg("doc") 45 | .arg("--target-dir") 46 | .arg(Path::new("target").join(SUBDIR)) 47 | .arg("--workspace") 48 | .env( 49 | "RUSTDOCFLAGS", 50 | "-Zunstable-options \ 51 | --output-format=json \ 52 | --document-private-items \ 53 | --document-hidden-items \ 54 | ", 55 | ) 56 | .current_dir(workspace_path) 57 | .status() 58 | .context("failed to run 'cargo rustdoc'")?; 59 | 60 | if !cargo_status.success() { 61 | if let Some(code) = cargo_status.code() { 62 | anyhow::bail!("'cargo build' failed with exit code {code}"); 63 | } else { 64 | anyhow::bail!("'cargo build' killed by signal"); 65 | } 66 | } 67 | 68 | if let Err(e) = copy_stdlib_json(workspace_path, toolchain) { 69 | error!("Error copying stdlib analysis: {e}"); 70 | error!("Standard library types will not be inspectable."); 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | pub fn load(workspace_path: impl Into) -> anyhow::Result { 77 | let root = json_root(&workspace_path.into()); 78 | let mut paths = vec![]; 79 | for res in fs::read_dir(root)? { 80 | let entry = res?; 81 | if entry.file_name().as_encoded_bytes().ends_with(b".json") { 82 | let path = entry.path(); 83 | let crate_name = path 84 | .file_stem() 85 | .unwrap() 86 | .to_str() 87 | .ok_or_else(|| anyhow::anyhow!("{path:?} isn't utf-8"))? 88 | .to_owned(); 89 | paths.push((crate_name, path)); 90 | } 91 | } 92 | 93 | let progress = ProgressBar::new(paths.len() as u64) 94 | .with_style( 95 | ProgressStyle::with_template( 96 | "{prefix:>12.cyan.bold} [{bar:25}]: {pos}/{len} {wide_msg}", 97 | )? 98 | .progress_chars("=> "), 99 | ) 100 | .with_prefix("Loading"); 101 | let active = Mutex::new(vec![]); 102 | 103 | let crates = paths 104 | .into_par_iter() 105 | .map(|(crate_name, path)| { 106 | let filename = path.file_stem().unwrap().to_string_lossy().into_owned(); 107 | { 108 | let mut active = active.lock().unwrap(); 109 | active.push(filename.clone()); 110 | progress.println(format!("reading {path:?}")); 111 | progress.set_message(active.join(", ")); 112 | } 113 | let data = parse_json(&path).with_context(|| path.display().to_string())?; 114 | { 115 | let mut active = active.lock().unwrap(); 116 | active.retain(|f| f != &filename); 117 | progress.set_message(active.join(", ")); 118 | progress.inc(1); 119 | } 120 | Ok((crate_name, data)) 121 | }) 122 | .collect::>>()?; 123 | 124 | Ok(Self { crates }) 125 | } 126 | 127 | pub fn crate_ids(&self) -> impl Iterator + '_ { 128 | self.crates 129 | .values() 130 | .flat_map(|crate_| &crate_.index) 131 | .filter_map(|(_id, item)| match &item.inner { 132 | rustdoc_types::ItemEnum::Module(m) if m.is_crate && item.crate_id == 0 => { 133 | let name = item.name.as_ref().expect("crate module should have a name"); 134 | Some(ItemId::crate_root(CrateId { name })) 135 | } 136 | _ => None, 137 | }) 138 | } 139 | 140 | pub fn items<'a, 'b>( 141 | &'a self, 142 | parent_id: &'b ItemId<'a>, 143 | ) -> impl Iterator, Item<'a>)> + 'b 144 | where 145 | 'a: 'b, 146 | { 147 | // Look up the parent item, including possibly resolving it to an item in a different crate 148 | // (i.e. parent_id may change). 149 | let (parent_id, parent) = if parent_id == &EMPTY_ITEM_ID { 150 | (parent_id.clone(), None) 151 | } else { 152 | match self.get_item(parent_id.clone()) { 153 | Some((resolved_id, item)) => match item { 154 | Item::Item(i) => (resolved_id, Some(i)), 155 | Item::Root => panic!("unexpected Item::Root from get_item()"), 156 | }, 157 | None => (parent_id.clone(), None), 158 | } 159 | }; 160 | 161 | // Collect (crate-local) IDs of children depending on the kind of parent it is. 162 | let children: Vec<&'a rustdoc_types::Id> = if let Some(parent) = parent { 163 | use rustdoc_types::ItemEnum::*; 164 | match &parent.inner { 165 | _ if parent_id == EMPTY_ITEM_ID => vec![], 166 | Module(m) => m.items.iter().collect(), 167 | ExternCrate { .. } => vec![], 168 | Use(_) => vec![], 169 | Union(u) => u.fields.iter().chain(&u.impls).collect(), 170 | Struct(s) => { 171 | let fields = match &s.kind { 172 | rustdoc_types::StructKind::Unit => vec![], 173 | rustdoc_types::StructKind::Tuple(t) => { 174 | t.iter().filter_map(|x| x.as_ref()).collect() 175 | } 176 | rustdoc_types::StructKind::Plain { fields, .. } => fields.iter().collect(), 177 | }; 178 | fields.into_iter().chain(&s.impls).collect() 179 | } 180 | StructField(ty) => type_ids(ty), 181 | Enum(e) => e.variants.iter().chain(&e.impls).collect(), 182 | Variant(v) => match &v.kind { 183 | rustdoc_types::VariantKind::Plain => vec![], 184 | rustdoc_types::VariantKind::Tuple(t) => { 185 | t.iter().filter_map(|id| id.as_ref()).collect() 186 | } 187 | rustdoc_types::VariantKind::Struct { fields, .. } => fields.iter().collect(), 188 | }, 189 | Function(_) => vec![], 190 | Trait(t) => { 191 | // TODO: also find impls? 192 | t.items.iter().collect() 193 | } 194 | TraitAlias(_) => vec![], 195 | Impl(i) => { 196 | i.items 197 | .iter() 198 | // Add a reference to the trait itself too if it's not an inherent impl: 199 | .chain(i.trait_.as_ref().map(|t| &t.id)) 200 | .collect() 201 | } 202 | TypeAlias(ty) => type_ids(&ty.type_), 203 | Constant { type_, .. } => type_ids(type_), 204 | Static(_) => vec![], 205 | ExternType => vec![], 206 | Macro(_) => vec![], 207 | ProcMacro(_) => vec![], 208 | Primitive(_) => vec![], 209 | AssocConst { .. } => vec![], 210 | AssocType { .. } => vec![], 211 | } 212 | } else { 213 | vec![] 214 | }; 215 | 216 | // Look up and return all the children. The lookup may follow references into other crates. 217 | children 218 | .into_iter() 219 | .filter_map(move |id| self.get_item(parent_id.crate_sibling(id))) 220 | } 221 | 222 | pub fn get_item<'a>(&'a self, id: ItemId<'a>) -> Option<(ItemId<'a>, Item<'a>)> { 223 | if id == EMPTY_ITEM_ID { 224 | return None; 225 | } 226 | let ItemId(local_crate_id, mut local_id) = &id; 227 | let local_crate = self.crates.get(local_crate_id.name)?; 228 | if local_id == EMPTY_ID { 229 | // Fake ID of the crate root. Look up what the root actually is. 230 | local_id = &local_crate.root; 231 | } 232 | if let Some(item) = local_crate.index.get(local_id) { 233 | Some((id, Item::Item(item))) 234 | } else { 235 | // Wasn't found in the local crate's index; look up the summary in paths. 236 | let summary = local_crate.paths.get(local_id)?; 237 | let other_crate = &summary.path[0]; 238 | // Try looking up by path in the other crate's analysis (if we have it). 239 | let other_id = self 240 | .crates 241 | .get(other_crate) 242 | .or_else(|| { 243 | warn!( 244 | "no analysis found for crate {other_crate} (looking for {})", 245 | summary.path.join("::") 246 | ); 247 | None 248 | })? 249 | .paths 250 | .iter() 251 | .find_map(|(id, other)| { 252 | if other.path == summary.path { 253 | Some(id) 254 | } else { 255 | None 256 | } 257 | }) 258 | .or_else(|| { 259 | error!("no item found for {}", summary.path.join("::")); 260 | let bt = Backtrace::capture(); 261 | if bt.status() == BacktraceStatus::Captured { 262 | error!("{bt}"); 263 | } 264 | None 265 | })?; 266 | let item = self.crates[other_crate].index.get(other_id)?; 267 | Some(( 268 | ItemId(CrateId { name: other_crate }, other_id), 269 | Item::Item(item), 270 | )) 271 | } 272 | } 273 | 274 | pub fn get_path<'a>(&'a self, id: ItemId<'a>, name_hint: &str) -> Option<&'a [String]> { 275 | if id == EMPTY_ITEM_ID { 276 | return None; 277 | } 278 | Some( 279 | &self.crates[id.0.name] 280 | .paths 281 | .get(id.1) 282 | .or_else(|| { 283 | warn!("missing path for {id:?} ({name_hint})"); 284 | None 285 | })? 286 | .path[..], 287 | ) 288 | } 289 | } 290 | 291 | fn parse_json(p: &Path) -> anyhow::Result { 292 | let f = File::open(p)?; 293 | let data = serde_json::from_reader(BufReader::new(f))?; 294 | Ok(data) 295 | } 296 | 297 | pub fn type_ids(ty: &rustdoc_types::Type) -> Vec<&rustdoc_types::Id> { 298 | use rustdoc_types::Type::*; 299 | match ty { 300 | ResolvedPath(path) => vec![&path.id], 301 | DynTrait(dt) => dt.traits.iter().map(|t| &t.trait_.id).collect(), 302 | Generic(_) => vec![], 303 | Primitive(_) => vec![], 304 | FunctionPointer(_) => vec![], 305 | Tuple(types) => types.iter().flat_map(type_ids).collect(), 306 | Slice(ty) => type_ids(ty), 307 | Array { type_, .. } => type_ids(type_), 308 | ImplTrait(generics) => generics 309 | .iter() 310 | .filter_map(|g| match g { 311 | rustdoc_types::GenericBound::TraitBound { trait_, .. } => Some(&trait_.id), 312 | rustdoc_types::GenericBound::Outlives(_) => None, 313 | rustdoc_types::GenericBound::Use(_) => None, 314 | }) 315 | .collect(), 316 | Infer => vec![], 317 | RawPointer { type_, .. } => type_ids(type_), 318 | BorrowedRef { type_, .. } => type_ids(type_), 319 | QualifiedPath { 320 | self_type, trait_, .. 321 | } => { 322 | let from_self = type_ids(self_type); 323 | if let Some(t) = trait_ { 324 | [&from_self[..], &[&t.id]].concat() 325 | } else { 326 | from_self 327 | } 328 | } 329 | Pat { type_, .. } => type_ids(type_), 330 | } 331 | } 332 | 333 | fn json_root(workspace_path: &Path) -> PathBuf { 334 | workspace_path.join("target").join(SUBDIR).join("doc") 335 | } 336 | 337 | pub fn get_stdlib_analysis_path(toolchain: Option<&str>) -> anyhow::Result { 338 | let mut cmd = Command::new("rustc"); 339 | if let Some(toolchain) = toolchain { 340 | cmd.arg(format!("+{toolchain}")); 341 | } 342 | 343 | let out = cmd 344 | .arg("--print") 345 | .arg("sysroot") 346 | .output() 347 | .context("Error running 'rustc --print sysroot'")?; 348 | 349 | if !out.status.success() { 350 | let mut msg = format!("Error running 'rustc --print sysroot': {}", out.status).into_bytes(); 351 | msg.extend(b"\nCommand stderr: "); 352 | msg.extend(&out.stderr); 353 | anyhow::bail!(String::from_utf8_lossy(&msg).into_owned()); 354 | } 355 | 356 | let sysroot_path = String::from_utf8(out.stdout).context("'rustc --print sysroot' output")?; 357 | 358 | let path = PathBuf::from(sysroot_path.trim()) 359 | .join("share") 360 | .join("doc") 361 | .join("rust") 362 | .join("json"); 363 | 364 | if fs::metadata(&path)?.is_dir() { 365 | Ok(path) 366 | } else { 367 | Err(anyhow!("{path:?} is something other than a directory")) 368 | } 369 | } 370 | 371 | fn copy_stdlib_json(workspace_path: &Path, toolchain: Option<&str>) -> anyhow::Result<()> { 372 | let src = get_stdlib_analysis_path(toolchain)?; 373 | let dst = json_root(workspace_path); 374 | for res in fs::read_dir(&src).with_context(|| src.display().to_string())? { 375 | let entry = res?; 376 | if entry.file_name().as_encoded_bytes().ends_with(b".json") { 377 | let src_path = entry.path(); 378 | println!("copying {:?}", entry.file_name()); 379 | fs::copy(&src_path, dst.join(entry.file_name())) 380 | .with_context(|| format!("copy {src_path:?}"))?; 381 | } 382 | } 383 | Ok(()) 384 | } 385 | 386 | #[derive(Debug, Clone, PartialEq)] 387 | pub struct CrateId<'a> { 388 | pub name: &'a String, 389 | } 390 | 391 | #[derive(Debug, Clone, PartialEq)] 392 | pub struct ItemId<'a>(CrateId<'a>, &'a rustdoc_types::Id); 393 | 394 | impl<'a> ItemId<'a> { 395 | pub fn crate_root(crate_id: CrateId<'a>) -> Self { 396 | Self(crate_id, EMPTY_ID) 397 | } 398 | 399 | pub fn crate_name(&self) -> &str { 400 | self.0.name 401 | } 402 | 403 | pub fn crate_sibling(&self, other_id: &'a rustdoc_types::Id) -> Self { 404 | Self(CrateId { name: self.0.name }, other_id) 405 | } 406 | } 407 | 408 | #[allow(clippy::large_enum_variant)] 409 | #[derive(Debug, Clone)] 410 | pub enum Item<'a> { 411 | Root, 412 | Item(&'a rustdoc_types::Item), 413 | } 414 | -------------------------------------------------------------------------------- /src/browser_rustdoc.rs: -------------------------------------------------------------------------------- 1 | use crate::analysis::{self, Analysis, Item, ItemId}; 2 | use crate::browser_trait::Browser; 3 | use std::fmt::Write; 4 | 5 | pub struct RustdocBrowser { 6 | analysis: Analysis, 7 | } 8 | 9 | impl RustdocBrowser { 10 | pub fn new(analysis: Analysis) -> Self { 11 | Self { analysis } 12 | } 13 | 14 | fn item_label(&self, id: ItemId, item: &rustdoc_types::Item) -> String { 15 | use rustdoc_types::ItemEnum::*; 16 | let name = item.name.as_deref().unwrap_or(""); 17 | let prefix = match &item.inner { 18 | Module(_) => "mod", 19 | ExternCrate { name, .. } => return format!("extern crate {name}"), 20 | Use(u) => return format!("use {}", u.source) + if u.is_glob { "::*" } else { "" }, 21 | Union(_) => "union", 22 | Struct(_) => "struct", 23 | StructField(f) => return format!("{}: {}", name, type_label(f)), 24 | Enum(_) => "enum", 25 | Variant(v) => { 26 | if let Some(ty) = self.single_element_tuple_variant(v, id) { 27 | // Include the name of the wrapped type in the label. 28 | // list_items() will skip the redundant type later. 29 | return format!("variant {name}({})", type_label(ty)); 30 | } 31 | "variant" 32 | } 33 | Function(_) => "fn", // signature represented by child items 34 | Trait(_) => "trait", 35 | TraitAlias(_) => "trait alias", 36 | Impl(i) => { 37 | return if let Some(trait_) = &i.trait_ { 38 | let path = self 39 | .analysis 40 | .get_path(id.crate_sibling(&trait_.id), &trait_.path); 41 | 42 | /* requires #![feature(let_chains)] 43 | let mut trait_name = if let Some(path) = path && path[0] != id.crate_name() { 44 | path.join("::") 45 | } else { 46 | trait_.name.clone() 47 | };*/ 48 | #[allow(clippy::unnecessary_unwrap)] // until if-let chains are stabilized 49 | let mut trait_name = if path.is_none() || path.unwrap()[0] == id.crate_name() { 50 | // trait in local crate, use trait name 51 | trait_.path.clone() 52 | } else { 53 | // trait in foreign crate, use full path 54 | path.unwrap().join("::") 55 | }; 56 | 57 | if let Some(g) = &trait_.args { 58 | trait_name.push_str(&generic_label(g)); 59 | } 60 | format!("impl {trait_name}") 61 | } else { 62 | "impl Self".to_string() 63 | }; 64 | } 65 | TypeAlias(_) => "type", 66 | Constant { type_, const_: _ } => return format!("const {}: {}", name, type_label(type_)), 67 | Static(s) => return format!("static {}: {}", name, type_label(&s.type_)), 68 | ExternType => "extern type", 69 | Macro(_) => "macro", 70 | ProcMacro(_) => "proc macro", 71 | Primitive(_) => "", 72 | AssocConst { type_, value } => { 73 | return if let Some(value) = value { 74 | format!("const {name}: {} = {value}", type_label(type_)) 75 | } else { 76 | format!("const {name}: {}", type_label(type_)) 77 | } 78 | } 79 | AssocType { type_, .. } => { 80 | return if let Some(type_) = type_ { 81 | format!("type {name} = {}", type_label(type_)) 82 | } else { 83 | format!("type {name}") 84 | }; 85 | } 86 | }; 87 | format!("{prefix} {name}") 88 | } 89 | 90 | fn single_element_tuple_variant<'a>( 91 | &'a self, 92 | v: &'a rustdoc_types::Variant, 93 | id: ItemId<'a>, 94 | ) -> Option<&'a rustdoc_types::Type> { 95 | if let rustdoc_types::VariantKind::Tuple(t) = &v.kind { 96 | if let Some(Some(inner_id)) = t.first() { 97 | if let Some((_id, Item::Item(item))) = 98 | self.analysis.get_item(id.crate_sibling(inner_id)) 99 | { 100 | if let rustdoc_types::ItemEnum::StructField(ty) = &item.inner { 101 | return Some(ty); 102 | } 103 | } 104 | } 105 | } 106 | None 107 | } 108 | } 109 | 110 | impl<'a> Browser for &'a RustdocBrowser { 111 | type Item = Item<'a>; 112 | type ItemId = ItemId<'a>; 113 | 114 | fn list_crates(&self) -> Vec<(String, ItemId<'a>)> { 115 | let mut crates = self 116 | .analysis 117 | .crate_ids() 118 | //.filter(|c| !self.analysis.stdlib_crates.contains(c)) 119 | .map(|item_id| (crate_label(&item_id), item_id)) 120 | .collect::>(); 121 | 122 | sort_by_label(&mut crates); 123 | 124 | crates 125 | } 126 | 127 | fn list_items(&self, parent_id: &ItemId<'a>) -> Vec<(String, (ItemId<'a>, Item<'a>))> { 128 | // If true, skip showing this element's children and show the children of the first child 129 | // instead. Basically, skip one level of nesting. Use when the item is redundant. 130 | let mut use_first_child = false; 131 | 132 | let mut synthetic_items: Vec<(String, (ItemId<'a>, Item<'a>))> = vec![]; 133 | 134 | if let Some((_, Item::Item(parent))) = self.analysis.get_item(parent_id.clone()) { 135 | match &parent.inner { 136 | rustdoc_types::ItemEnum::Variant(v) 137 | if self 138 | .single_element_tuple_variant(v, parent_id.clone()) 139 | .is_some() => 140 | { 141 | // Single-element tuple variant. The only child is a StructField for field "0". 142 | // This adds nothing because the type name is already in the Variant's label. 143 | use_first_child = true; 144 | } 145 | rustdoc_types::ItemEnum::StructField(_) => { 146 | // StructField's only child is the type, which adds nothing, as the type name is 147 | // already in the StructField's label. 148 | use_first_child = true; 149 | } 150 | rustdoc_types::ItemEnum::Function(f) => { 151 | synthetic_items = f 152 | .sig 153 | .inputs 154 | .iter() 155 | // TODO: implement special cases for names of self: 156 | // - self 157 | // - &self 158 | // - &mut self 159 | .map(|(name, ty)| (format!("{name}:"), ty)) 160 | .chain(f.sig.output.as_ref().map(|ty| ("->".to_owned(), ty))) 161 | .map(|(name, ty)| { 162 | let mut ids = analysis::type_ids(ty) 163 | .into_iter() 164 | .map(|id| parent_id.crate_sibling(id)) 165 | .collect::>(); 166 | if ids.is_empty() { 167 | // Inject a fake ID so that the label at least shows up. 168 | ids.push(analysis::EMPTY_ITEM_ID.clone()); 169 | } 170 | (name, ty, ids) 171 | }) 172 | .flat_map(|(name, ty, ids)| { 173 | let use_suffix = ids.len() > 1; 174 | ids.into_iter().enumerate().map(move |(i, id)| { 175 | let tylabel = type_label(ty); 176 | let mut label = format!("{name} {tylabel}"); 177 | if use_suffix { 178 | label += &format!(" (#{i}"); 179 | } 180 | (label, (id, Item::Item(parent))) 181 | }) 182 | }) 183 | .collect::>(); 184 | } 185 | _ => (), 186 | } 187 | } 188 | 189 | let mut items = self 190 | .analysis 191 | .items(parent_id) 192 | .filter_map(|(id, item)| { 193 | let inner = match item { 194 | Item::Root => return None, 195 | Item::Item(item) => item, 196 | }; 197 | 198 | // Remove the clutter of blanket, and synthetic trait impls. 199 | use rustdoc_types::ItemEnum::*; 200 | match &inner.inner { 201 | Impl(i) if i.blanket_impl.is_some() || i.is_synthetic => None, 202 | _ => Some((self.item_label(id.clone(), inner), (id, item))), 203 | } 204 | }) 205 | .collect::>(); 206 | sort_by_label(&mut items); 207 | 208 | if use_first_child && !items.is_empty() { 209 | assert_eq!( 210 | items.len(), 211 | 1, 212 | "use_first_child on non-singleton children list: {items:#?}" 213 | ); 214 | return self.list_items(&items[0].1 .0); 215 | } 216 | 217 | if !synthetic_items.is_empty() { 218 | synthetic_items.extend(items); 219 | items = std::mem::take(&mut synthetic_items); 220 | } 221 | 222 | items 223 | } 224 | 225 | fn get_info(&self, item: &Item<'a>) -> String { 226 | let mut txt = String::new(); 227 | match item { 228 | Item::Item(item) => { 229 | if let Some(docs) = &item.docs { 230 | txt += docs; 231 | txt.push('\n'); 232 | } 233 | if let Some(span) = &item.span { 234 | write!( 235 | txt, 236 | "defined in {:?}\nstarting on line {}", 237 | span.filename, span.begin.0 238 | ) 239 | .unwrap(); 240 | } 241 | } 242 | Item::Root => { 243 | write!(txt, "crate root").unwrap(); 244 | } 245 | } 246 | txt 247 | } 248 | 249 | fn get_debug_info(&self, item: &Item) -> String { 250 | format!("{item:#?}") 251 | } 252 | 253 | fn get_source(&self, item: &Item) -> (String, Option) { 254 | match item { 255 | Item::Item(item) => { 256 | let (txt, line) = get_source_for_item(item); 257 | (txt, Some(line)) 258 | } 259 | Item::Root => (String::new(), None), 260 | } 261 | } 262 | } 263 | 264 | fn get_source_for_item(item: &rustdoc_types::Item) -> (String, usize) { 265 | use std::fs::File; 266 | use std::io::{BufRead, BufReader}; 267 | let Some(span) = &item.span else { 268 | return (String::new(), 0); 269 | }; 270 | match File::open(&span.filename) { 271 | Ok(f) => { 272 | let mut txt = String::new(); 273 | for (i, line) in BufReader::new(f).lines().enumerate() { 274 | write!(txt, "{}: ", i + 1).unwrap(); 275 | txt += &line.unwrap_or_else(|e| format!("")); 276 | txt.push('\n'); 277 | } 278 | let line = span.begin.0 - 1; 279 | (txt, line) 280 | } 281 | Err(e) => (format!("Error opening source: {e}"), 0), 282 | } 283 | } 284 | 285 | fn cmp_labels(a: &str, b: &str) -> std::cmp::Ordering { 286 | // Fields (assuming they contain ": ") go first 287 | a.contains(": ") 288 | .cmp(&b.contains(": ")) 289 | .reverse() // less = goes first 290 | .then_with(|| a.cmp(b)) 291 | } 292 | 293 | #[cfg(test)] 294 | mod test { 295 | use super::*; 296 | 297 | #[test] 298 | fn cmp_test() { 299 | use std::cmp::Ordering::*; 300 | assert_eq!(cmp_labels("a: a", "b: b"), Less); 301 | assert_eq!(cmp_labels("a", "z: z"), Greater); 302 | assert_eq!(cmp_labels("a", "b"), Less); 303 | } 304 | } 305 | 306 | fn sort_by_label(slice: &mut [(String, T)]) { 307 | slice.sort_unstable_by(|(a, _), (b, _)| cmp_labels(a, b)); 308 | } 309 | 310 | fn crate_label(id: &ItemId) -> String { 311 | id.crate_name().to_owned() 312 | } 313 | 314 | fn generic_label(g: &rustdoc_types::GenericArgs) -> String { 315 | use rustdoc_types::{GenericArg, GenericArgs}; 316 | use std::borrow::Cow; 317 | let mut s = String::new(); 318 | match g { 319 | GenericArgs::AngleBracketed { args, constraints } => { 320 | if args.is_empty() { 321 | return s; 322 | } 323 | s.push('<'); 324 | s.push_str( 325 | &args 326 | .iter() 327 | .map(|arg| match arg { 328 | GenericArg::Lifetime(s) => Cow::Borrowed(s.as_str()), 329 | GenericArg::Type(ty) => Cow::Owned(type_label(ty)), 330 | GenericArg::Const(c) => Cow::Owned(format!("{c:?}")), 331 | GenericArg::Infer => Cow::Borrowed("_"), 332 | }) 333 | .collect::>() 334 | .join(", "), 335 | ); 336 | // TODO: dunno what to do with these 337 | s.push_str( 338 | &constraints 339 | .iter() 340 | .map(|c| format!("{c:?}")) 341 | .collect::>() 342 | .join(", "), 343 | ); 344 | s.push('>'); 345 | } 346 | GenericArgs::Parenthesized { inputs, output } => { 347 | s.push('('); 348 | s.push_str(&inputs.iter().map(type_label).collect::>().join(", ")); 349 | s.push(')'); 350 | if let Some(ty) = output { 351 | s.push_str(" -> "); 352 | s.push_str(&type_label(ty)); 353 | } 354 | } 355 | GenericArgs::ReturnTypeNotation => s.push_str(&format!("TODO: {g:?}")), // what is this for? 356 | } 357 | s 358 | } 359 | 360 | fn type_label(ty: &rustdoc_types::Type) -> String { 361 | use rustdoc_types::Type::*; 362 | match ty { 363 | ResolvedPath(p) => { 364 | let mut s = p.path.clone(); 365 | if let Some(args) = &p.args { 366 | s.push_str(&generic_label(args)); 367 | } 368 | s 369 | } 370 | DynTrait(dt) => { 371 | "dyn ".to_owned() 372 | + &dt 373 | .traits 374 | .iter() 375 | .map(|t| { 376 | t.trait_.path.clone() 377 | + &t.trait_ 378 | .args 379 | .as_ref() 380 | .map(|g| generic_label(g)) 381 | .unwrap_or_default() 382 | }) 383 | .collect::>() 384 | .join(" + ") 385 | } 386 | Generic(g) => g.to_owned(), 387 | Primitive(p) => p.to_owned(), 388 | FunctionPointer(fp) => { 389 | let args = fp 390 | .sig 391 | .inputs 392 | .iter() 393 | .map(|(name, ty)| format!("{name}: {}", type_label(ty))) 394 | .collect::>() 395 | .join(", "); 396 | let ret = match &fp.sig.output { 397 | Some(ty) => format!(" -> {}", type_label(ty)), 398 | None => String::new(), 399 | }; 400 | format!("fn({args}){ret}") 401 | } 402 | Tuple(types) => format!( 403 | "({})", 404 | types.iter().map(type_label).collect::>().join(", ") 405 | ), 406 | Slice(ty) => format!("[{}]", type_label(ty)), 407 | Array { type_, len } => format!("[{}; {len}]", type_label(type_)), 408 | ImplTrait(t) => { 409 | use rustdoc_types::GenericBound::*; 410 | use rustdoc_types::PreciseCapturingArg::*; 411 | format!( 412 | "impl {}", 413 | t.iter() 414 | .map(|g| match g { 415 | TraitBound { trait_, .. } => trait_.path.clone(), 416 | Outlives(o) => o.clone(), 417 | Use(u) => { 418 | u.into_iter() 419 | .map(|p| match p { 420 | Lifetime(s) => s, 421 | Param(s) => s, 422 | }) 423 | .cloned() 424 | .collect::>() 425 | .join(", ") 426 | }, 427 | }) 428 | .collect::>() 429 | .join(" + "), 430 | ) 431 | } 432 | Infer => "_".to_owned(), 433 | RawPointer { is_mutable, type_ } => { 434 | format!( 435 | "*{} {}", 436 | if *is_mutable { "mut" } else { "const" }, 437 | type_label(type_), 438 | ) 439 | } 440 | BorrowedRef { 441 | lifetime, 442 | is_mutable, 443 | type_, 444 | } => { 445 | let mut s = "&".to_owned(); 446 | if let Some(l) = lifetime { 447 | s.push_str(l); 448 | s.push(' '); 449 | } 450 | if *is_mutable { 451 | s.push_str("mut "); 452 | } 453 | s.push_str(&type_label(type_)); 454 | s 455 | } 456 | QualifiedPath { 457 | name, 458 | args: _, 459 | self_type, 460 | trait_, 461 | } => { 462 | if let Some(trait_) = trait_ { 463 | format!("<{} as {}>::{name}", type_label(self_type), trait_.path) 464 | } else { 465 | format!("{}::{name}", type_label(self_type)) 466 | } 467 | } 468 | Pat { type_, .. } => type_label(type_), 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /src/browser_trait.rs: -------------------------------------------------------------------------------- 1 | pub trait Browser { 2 | type Item: Clone + Send + Sync; 3 | type ItemId: Clone + Send + Sync; 4 | fn list_crates(&self) -> Vec<(String, Self::ItemId)>; 5 | #[allow(clippy::type_complexity)] 6 | fn list_items(&self, parent_id: &Self::ItemId) -> Vec<(String, (Self::ItemId, Self::Item))>; 7 | fn get_info(&self, item: &Self::Item) -> String; 8 | fn get_debug_info(&self, item: &Self::Item) -> String; 9 | fn get_source(&self, item: &Self::Item) -> (String, Option); 10 | } 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | pub mod analysis; 5 | pub mod browser_rustdoc; 6 | pub mod browser_trait; 7 | pub mod scroll_pad; 8 | pub mod ui; 9 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | use std::path::PathBuf; 3 | use std::sync::Mutex; 4 | 5 | use anyhow::Context; 6 | use clap::Parser; 7 | use lazy_static::lazy_static; 8 | use log::{error, info, Log}; 9 | use rsbrowse::analysis::Analysis; 10 | use rsbrowse::browser_rustdoc::RustdocBrowser; 11 | use rsbrowse::ui; 12 | use tempfile::NamedTempFile; 13 | 14 | #[derive(Debug, Parser)] 15 | #[command(version)] 16 | struct Arguments { 17 | /// Cargo workspace path 18 | #[arg()] 19 | workspace_path: PathBuf, 20 | 21 | /// Select rust toolchain to use. 22 | /// To disable this flag (i.e. if you don't use rustup), set it to empty string. 23 | #[arg(long, default_value = "nightly")] 24 | toolchain: Option, 25 | } 26 | 27 | fn main() -> anyhow::Result<()> { 28 | let mut args = Arguments::parse(); 29 | if args.toolchain.as_deref() == Some("") { 30 | args.toolchain = None; 31 | } 32 | 33 | *LOGGER.sink.lock().unwrap() = Some(Box::new(io::stderr())); 34 | log::set_max_level(log::LevelFilter::max()); 35 | let _ = log::set_logger(&*LOGGER); 36 | 37 | eprintln!("Running Cargo to generate analysis data..."); 38 | Analysis::generate(&args.workspace_path, args.toolchain.as_deref())?; 39 | 40 | eprintln!("Reading analysis data..."); 41 | let analysis = Analysis::load(&args.workspace_path)?; 42 | 43 | std::env::set_current_dir(&args.workspace_path)?; 44 | 45 | let browser = RustdocBrowser::new(analysis); 46 | 47 | // Mega-hax, but doesn't matter because we're not returning from run() anyway. 48 | let browser: &'static RustdocBrowser = Box::leak(Box::new(browser)); 49 | 50 | if let Err(e) = log_to_file() { 51 | eprintln!("failed to set up logging to file: {e}"); 52 | eprintln!("disabling logs"); 53 | *LOGGER.sink.lock().unwrap() = None; 54 | } else { 55 | info!("rsbrowse/{}", env!("CARGO_PKG_VERSION")); 56 | info!( 57 | "git:{}", 58 | option_env!("GIT_COMMIT_HASH").unwrap_or("") 59 | ); 60 | info!("{args:#?}"); 61 | info!("workspace path: {:?}", std::env::current_dir()); 62 | } 63 | 64 | ui::run(browser); 65 | Ok(()) 66 | } 67 | 68 | fn log_to_file() -> anyhow::Result<()> { 69 | let file = NamedTempFile::with_prefix("rsbrowse")?; 70 | let path = file.path().with_file_name("rsbrowse.log"); 71 | let file = file.persist(&path).context("failed to persist logfile")?; 72 | *LOGGER.sink.lock().unwrap() = Some(Box::new(file)); 73 | std::panic::set_hook(Box::new(|panic| { 74 | error!("{panic}"); 75 | // try printing to stderr too 76 | eprintln!("{panic}"); 77 | })); 78 | eprintln!("logs written to {path:?}"); 79 | Ok(()) 80 | } 81 | 82 | lazy_static! { 83 | static ref LOGGER: Logger = Logger { 84 | sink: Mutex::new(None) 85 | }; 86 | } 87 | 88 | struct Logger { 89 | sink: Mutex>>, 90 | } 91 | 92 | impl Log for Logger { 93 | fn enabled(&self, _: &log::Metadata) -> bool { 94 | true 95 | } 96 | 97 | fn log(&self, record: &log::Record) { 98 | if !self.enabled(record.metadata()) { 99 | return; 100 | } 101 | 102 | if let Some(ref mut sink) = *self.sink.lock().unwrap() { 103 | if let Some(path) = record.module_path() { 104 | if path.starts_with("cursive") && record.level() <= log::Level::Debug { 105 | return; 106 | } 107 | } 108 | 109 | let _ = write!(sink, "{}: {}", record.level(), record.args()); 110 | if let Some(path) = record.module_path() { 111 | let _ = write!(sink, " ({path}"); 112 | if let Some(line) = record.line() { 113 | let _ = write!(sink, ":{line}"); 114 | } 115 | let _ = write!(sink, ")"); 116 | } 117 | let _ = writeln!(sink); 118 | } 119 | } 120 | 121 | fn flush(&self) {} 122 | } 123 | -------------------------------------------------------------------------------- /src/scroll_pad.rs: -------------------------------------------------------------------------------- 1 | use cursive::view::{View, ViewWrapper}; 2 | use cursive::Vec2; 3 | 4 | /// Adds one unit of padding to the right side of any view that doesn't require scrolling. 5 | /// This is used to prevent views from having text immediately adjacent to each other (which is 6 | /// hard to read) in the absence of scrollbars separating them. 7 | /// If the view needs a scrollbar, this padding is omitted, because the scrollbar will provide the 8 | /// needed separation. 9 | pub struct ScrollPad { 10 | inner: V, 11 | } 12 | 13 | impl ScrollPad { 14 | pub fn new(inner: V) -> Self { 15 | Self { inner } 16 | } 17 | } 18 | 19 | impl ViewWrapper for ScrollPad { 20 | cursive::wrap_impl!(self.inner: V); 21 | 22 | fn wrap_required_size(&mut self, constraint: Vec2) -> Vec2 { 23 | let mut calc = self.inner.required_size(constraint); 24 | if calc.y <= constraint.y { 25 | // View fits, no scrollbar will be used, so add one unit of padding for separation. 26 | calc.x += 1; 27 | } 28 | calc 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::browser_trait::Browser; 2 | use crate::scroll_pad::ScrollPad; 3 | use cursive::event::Key; 4 | use cursive::traits::*; 5 | use cursive::views::{Dialog, LinearLayout, ScrollView, SelectView, TextView}; 6 | use cursive::{Cursive, CursiveExt, XY}; 7 | use std::borrow::Cow; 8 | 9 | /// How many lines to scroll to before a definition. 10 | const SOURCE_LEADING_CONTEXT_LINES: usize = 5; 11 | 12 | struct UserData { 13 | browser: T, 14 | } 15 | 16 | /// Makes a selectview showing the children of the given parent item in the given crate. 17 | /// Returns None if there are no children to display. 18 | fn make_selectview( 19 | data: &mut UserData, 20 | parent_id: &B::ItemId, 21 | depth: usize, 22 | ) -> Option> { 23 | let items = data.browser.list_items(parent_id); 24 | if items.is_empty() { 25 | return None; 26 | } 27 | 28 | let mut select = SelectView::new(); 29 | for (label, (id, item)) in items { 30 | select.add_item(label, (id, item)); 31 | } 32 | 33 | select.set_on_submit(move |ui, (_id, item)| info_dialog::(ui, item)); 34 | 35 | select.set_on_select(move |ui, (id, _item)| { 36 | add_panel::(ui, id, depth + 1); 37 | }); 38 | 39 | Some(select) 40 | } 41 | 42 | fn info_dialog(ui: &mut Cursive, item: &B::Item) { 43 | let data = ui.user_data::>().unwrap(); 44 | 45 | let info_txt = data.browser.get_info(item); 46 | let (source_txt, start_line) = data.browser.get_source(item); 47 | 48 | let item_dlg = item.clone(); 49 | let info_dialog = Dialog::around( 50 | LinearLayout::vertical() 51 | .child(TextView::new(info_txt).scrollable()) 52 | .child( 53 | TextView::new(source_txt) 54 | .scrollable() 55 | .with_name("source_scroll"), 56 | ) 57 | .scrollable(), 58 | ) 59 | .dismiss_button("ok") 60 | .button("debug", move |ui| { 61 | let data = ui.user_data::>().unwrap(); 62 | let dbg_txt = data.browser.get_debug_info(&item_dlg); 63 | let dbg_dialog = Dialog::around(TextView::new(dbg_txt).scrollable()).dismiss_button("ok"); 64 | ui.add_layer(dbg_dialog); 65 | }); 66 | 67 | ui.add_layer(info_dialog); 68 | 69 | if let Some(start_line) = start_line { 70 | let screen_size = ui.screen_size(); 71 | ui.call_on_name("source_scroll", move |view: &mut ScrollView| { 72 | // HAX: set_offset doesn't work on newly-added views until a layout is done 73 | view.layout(screen_size); 74 | view.set_offset(XY::new( 75 | 0, 76 | start_line.saturating_sub(SOURCE_LEADING_CONTEXT_LINES), 77 | )); 78 | }); 79 | } 80 | } 81 | 82 | fn add_panel(ui: &mut Cursive, parent_id: &B::ItemId, depth: usize) { 83 | ui.call_on_name("horiz_layout", |view: &mut LinearLayout| { 84 | while view.len() > depth { 85 | view.remove_child(view.len() - 1); 86 | } 87 | }); 88 | 89 | let data: &mut UserData = ui.user_data().unwrap(); 90 | 91 | // Expand out all panes to the right, using the first item in each pane, until we run out of 92 | // stuff to show. 93 | // Ideally we wouldn't need to do this immediately and could instead do it on focus changes 94 | // between panes, but Cursive doesn't have any way for a view to respond to being focused, nor 95 | // does its LinearLayout have a callback on switching views. 96 | // Importantly, it's not sufficient to just change things on selection change, because a pane 97 | // with a single item can never have its selection changed, so you'd be stuck there unable to 98 | // go deeper within the tree. So this is why we go ahead and create the next views *right 99 | // away*. 100 | let mut next = vec![]; 101 | let mut local_depth = depth; 102 | let mut local_parent = Cow::Borrowed(parent_id); 103 | while let Some(view) = make_selectview(data, &local_parent, local_depth) { 104 | if let Some((_label, (id, _item))) = view.get_item(0) { 105 | local_depth += 1; 106 | local_parent = Cow::Owned(id.clone()); 107 | } 108 | next.push(view); 109 | } 110 | 111 | if next.is_empty() { 112 | return; 113 | } 114 | 115 | ui.call_on_name("horiz_layout", |horiz_layout: &mut LinearLayout| { 116 | for view in next { 117 | horiz_layout.add_child(ScrollPad::new( 118 | ScrollView::new(view).scroll_y(true).show_scrollbars(true), 119 | )); 120 | } 121 | }); 122 | } 123 | 124 | fn about(ui: &mut Cursive) { 125 | ui.add_layer( 126 | Dialog::around( 127 | TextView::new(format!( 128 | "rsbrowse/{}\n\ 129 | {}by Bill Fraser\n\ 130 | https://github.com/wfraser/rsbrowse", 131 | env!("CARGO_PKG_VERSION"), 132 | if let Some(git) = option_env!("GIT_COMMIT_HASH") { 133 | format!("git:{git}\n") 134 | } else { 135 | String::new() 136 | }, 137 | )) 138 | .h_align(cursive::align::HAlign::Center), 139 | ) 140 | .title("about") 141 | .dismiss_button("ok"), 142 | ) 143 | } 144 | 145 | pub fn run(browser: B) { 146 | let mut ui = Cursive::default(); 147 | 148 | ui.menubar() 149 | .add_leaf("rsbrowse!", about) 150 | .add_delimiter() 151 | .add_leaf("Quit", |ui| ui.quit()) 152 | .add_leaf("(ESC to activate menu)", |_| ()); 153 | ui.set_autohide_menu(false); 154 | ui.add_global_callback(Key::Esc, |ui| ui.select_menubar()); 155 | //ui.add_global_callback(Key::Esc, |ui| ui.quit()); 156 | 157 | ui.set_theme(cursive::theme::Theme::default().with(|theme| { 158 | use cursive::theme::{ 159 | BaseColor::{Black, Green, White}, 160 | Color::{Dark, Light, Rgb}, 161 | PaletteColor, 162 | }; 163 | theme.palette[PaletteColor::Background] = Dark(Black); 164 | theme.palette[PaletteColor::View] = Rgb(32, 32, 32); 165 | theme.palette[PaletteColor::Shadow] = Light(Black); 166 | theme.palette[PaletteColor::Primary] = Dark(White); 167 | theme.palette[PaletteColor::Highlight] = Dark(Green); 168 | })); 169 | 170 | let mut crates_select = SelectView::new(); 171 | for (label, crate_id) in browser.list_crates() { 172 | crates_select.add_item(label, crate_id); 173 | } 174 | 175 | let first_crate = crates_select 176 | .get_item(0) 177 | .map(|(_label, crate_id)| crate_id.clone()); 178 | 179 | // TODO: implement a better live search than this 180 | crates_select.set_autojump(true); 181 | 182 | crates_select.set_on_select(|ui, crate_id| { 183 | add_panel::(ui, crate_id, 1); 184 | }); 185 | 186 | ui.add_fullscreen_layer( 187 | ScrollView::new( 188 | LinearLayout::horizontal() 189 | .child(ScrollPad::new( 190 | ScrollView::new(crates_select).scroll_y(true), 191 | )) 192 | .with_name("horiz_layout"), 193 | ) 194 | .scroll_x(true), 195 | ); 196 | 197 | ui.set_user_data(UserData { browser }); 198 | 199 | // Go ahead and expand the first crate in the list immediately. 200 | if let Some(crate_id) = first_crate { 201 | add_panel::(&mut ui, &crate_id, 1); 202 | } 203 | 204 | ui.run(); 205 | } 206 | -------------------------------------------------------------------------------- /tests/browser_test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | use rsbrowse::analysis::{self, Analysis, Item}; 5 | use rsbrowse::browser_rustdoc::RustdocBrowser; 6 | use rsbrowse::browser_trait::Browser; 7 | use std::path::Path; 8 | 9 | lazy_static! { 10 | static ref BROWSER_STATIC: RustdocBrowser = { 11 | let path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testcrate")); 12 | 13 | let status = std::process::Command::new("cargo") 14 | .arg("clean") 15 | .current_dir(&path) 16 | .status() 17 | .expect("Failed to run 'cargo clean' on test crate"); 18 | if !status.success() { 19 | panic!("Failed to run 'cargo clean' on test crate"); 20 | } 21 | 22 | Analysis::generate(&path, Some("nightly")).expect("Failed to generate analysis data."); 23 | RustdocBrowser::new(Analysis::load(&path).expect("Failed to load analysis")) 24 | }; 25 | static ref BROWSER: &'static RustdocBrowser = &BROWSER_STATIC; 26 | } 27 | 28 | fn iter_labels(items: &[(String, T)]) -> impl Iterator { 29 | items.iter().map(|(label, _)| label.as_str()) 30 | } 31 | 32 | trait VecExt<'a, T> { 33 | fn contains_label(&self, s: &str) -> bool; 34 | fn by_label(&'a self, s: &str) -> &'a T; 35 | fn labels(&self) -> Vec; 36 | } 37 | 38 | impl<'a, T> VecExt<'a, T> for Vec<(String, T)> { 39 | fn contains_label(&self, s: &str) -> bool { 40 | iter_labels(self).any(|label| label == s) 41 | } 42 | fn by_label(&'a self, s: &str) -> &'a T { 43 | &self 44 | .iter() 45 | .find(|(label, _)| label == s) 46 | .expect("not found") 47 | .1 48 | } 49 | fn labels(&self) -> Vec { 50 | iter_labels(self).map(|s| s.to_owned()).collect() 51 | } 52 | } 53 | 54 | fn items_eq(a: &Item, b: &Item) -> bool { 55 | match a { 56 | Item::Root => matches!(b, Item::Root), 57 | Item::Item(a) => match b { 58 | Item::Item(b) => a.id == b.id, 59 | _ => false, 60 | }, 61 | } 62 | } 63 | 64 | trait ItemExt { 65 | fn unwrap_item(&self) -> &rustdoc_types::Item; 66 | } 67 | 68 | impl<'a> ItemExt for Item<'a> { 69 | fn unwrap_item(&self) -> &rustdoc_types::Item { 70 | match self { 71 | Item::Item(item) => item, 72 | _ => panic!("not an Item::Item"), 73 | } 74 | } 75 | } 76 | 77 | #[test] 78 | fn list_items() { 79 | let has_stdlib = analysis::get_stdlib_analysis_path(Some("nightly")).is_ok(); 80 | eprintln!("has_stdlib: {has_stdlib:?}"); 81 | 82 | let crates = BROWSER.list_crates(); 83 | assert_eq!( 84 | crates.labels(), 85 | if has_stdlib { 86 | &[ 87 | "alloc", 88 | "anyhow", 89 | "core", 90 | "externcrate", 91 | "proc_macro", 92 | "std", 93 | "test", 94 | "testcrate", 95 | ][..] 96 | } else { 97 | &["anyhow", "externcrate", "testcrate"][..] 98 | }, 99 | ); 100 | 101 | let crate_id = crates.by_label("testcrate"); 102 | 103 | // Pane 1 104 | 105 | let root_items = BROWSER.list_items(&crate_id); 106 | assert_eq!( 107 | root_items.labels(), 108 | &["mod x", "mod y", "mod z", "trait Trait",] 109 | ); 110 | 111 | // Pane 2 112 | 113 | let mod_x = root_items.by_label("mod x"); 114 | let mod_x_items = BROWSER.list_items(&mod_x.0); 115 | assert_eq!(mod_x_items.labels(), &["enum E", "struct S"]); 116 | 117 | let mod_y = root_items.by_label("mod y"); 118 | let mod_y_items = BROWSER.list_items(&mod_y.0); 119 | assert_eq!(mod_y_items.labels(), &["struct S"]); 120 | 121 | let mod_z = root_items.by_label("mod z"); 122 | let mod_z_items = BROWSER.list_items(&mod_z.0); 123 | assert_eq!(mod_z_items.labels(), &["struct S"]); 124 | 125 | // Assert that the three "struct S" defs are not the same. 126 | assert!(!items_eq( 127 | &mod_x_items.by_label("struct S").1, 128 | &mod_y_items.by_label("struct S").1 129 | )); 130 | assert!(!items_eq( 131 | &mod_y_items.by_label("struct S").1, 132 | &mod_z_items.by_label("struct S").1 133 | )); 134 | 135 | let trait_trait = root_items.by_label("trait Trait"); 136 | let trait_items = BROWSER.list_items(&trait_trait.0); 137 | assert_eq!(trait_items.labels(), &["fn method"]); 138 | 139 | // Pane 3 140 | 141 | let x_e = mod_x_items.by_label("enum E"); 142 | let x_e_items = BROWSER.list_items(&x_e.0); 143 | assert_eq!( 144 | x_e_items.labels(), 145 | &[ 146 | "variant StructVariant", 147 | "variant TupleVariant(S)", 148 | "variant UnitVariant", 149 | ] 150 | ); 151 | 152 | let x_s = mod_x_items.by_label("struct S"); 153 | let x_s_items = BROWSER.list_items(&x_s.0); 154 | assert_eq!( 155 | x_s_items.labels(), 156 | &[ 157 | "fn_field: Box Option>", 158 | "int_field: i32", 159 | "opt_field: Option>", 160 | "string_field: String", 161 | "impl Self", 162 | "impl core::fmt::Display", 163 | "impl externcrate::ExternTrait", 164 | ] 165 | ); 166 | 167 | let y_s = mod_y_items.by_label("struct S"); 168 | let y_s_items = BROWSER.list_items(&y_s.0); 169 | assert_eq!(y_s_items.labels(), &["impl Self", "impl Trait",]); 170 | 171 | let z_s = mod_z_items.by_label("struct S"); 172 | let z_s_items = BROWSER.list_items(&z_s.0); 173 | assert_eq!(z_s_items.labels(), &["impl Trait"]); 174 | 175 | // Pane 4 176 | 177 | let x_e_unit = x_e_items.by_label("variant UnitVariant"); 178 | let x_e_unit_items = BROWSER.list_items(&x_e_unit.0); 179 | assert_eq!(x_e_unit_items.labels(), &[] as &[&str]); 180 | 181 | let x_e_tuple = x_e_items.by_label("variant TupleVariant(S)"); 182 | let x_e_tuple_items = BROWSER.list_items(&x_e_tuple.0); 183 | // Skip the struct field and the struct, go straight to its items: 184 | assert_eq!(x_e_tuple_items.labels(), x_s_items.labels()); 185 | 186 | let x_e_struct = x_e_items.by_label("variant StructVariant"); 187 | let x_e_struct_items = BROWSER.list_items(&x_e_struct.0); 188 | assert_eq!(x_e_struct_items.labels(), &["a: S"]); 189 | 190 | let x_s_self = x_s_items.by_label("impl Self"); 191 | let x_s_self_items = BROWSER.list_items(&x_s_self.0); 192 | assert_eq!(x_s_self_items.labels(), &["fn f"]); 193 | 194 | let x_s_extern = x_s_items.by_label("impl externcrate::ExternTrait"); 195 | let x_s_extern_items = BROWSER.list_items(&x_s_extern.0); 196 | assert_eq!( 197 | x_s_extern_items.labels(), 198 | &["fn required_method", "trait ExternTrait"] 199 | ); 200 | 201 | let y_s_self = y_s_items.by_label("impl Self"); 202 | let y_s_self_items = BROWSER.list_items(&y_s_self.0); 203 | assert_eq!(y_s_self_items.labels(), &["fn spoopadoop"]); 204 | 205 | let y_s_trait = y_s_items.by_label("impl Trait"); 206 | let y_s_trait_items = BROWSER.list_items(&y_s_trait.0); 207 | // includes "fn method" because it overrides the default in the trait: 208 | assert_eq!(y_s_trait_items.labels(), &["fn method", "trait Trait"]); 209 | 210 | let z_s_trait = z_s_items.by_label("impl Trait"); 211 | let z_s_trait_items = BROWSER.list_items(&z_s_trait.0); 212 | // doesn't include "fn method" because it didn't override it: 213 | assert_eq!(z_s_trait_items.labels(), &["trait Trait"]); 214 | 215 | // Pane 5 216 | let x_s_self_f = x_s_self_items.by_label("fn f"); 217 | let x_s_self_f_items = BROWSER.list_items(&x_s_self_f.0); 218 | assert_eq!( 219 | x_s_self_f_items.labels(), 220 | &["self: &Self", "e_arg: E", "-> S",] 221 | ); 222 | } 223 | -------------------------------------------------------------------------------- /tests/externcrate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "externcrate" 3 | version = "0.1.0" 4 | authors = ["Bill Fraser "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /tests/externcrate/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub trait ExternTrait { 2 | /// Implementations must implement this. 3 | fn required_method(&self) -> &'static str; 4 | 5 | /// Implementations may override this, but don't have to. 6 | fn default_method(&self) -> &'static str { 7 | "this is the default impl" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/testcrate/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /tests/testcrate/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 = "anyhow" 7 | version = "1.0.75" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 10 | 11 | [[package]] 12 | name = "externcrate" 13 | version = "0.1.0" 14 | 15 | [[package]] 16 | name = "testcrate" 17 | version = "0.1.0" 18 | dependencies = [ 19 | "anyhow", 20 | "externcrate", 21 | ] 22 | -------------------------------------------------------------------------------- /tests/testcrate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testcrate" 3 | version = "0.1.0" 4 | authors = ["William R. Fraser "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | externcrate = { path = "../externcrate" } 11 | anyhow = "1" 12 | -------------------------------------------------------------------------------- /tests/testcrate/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod x { 2 | pub struct S { 3 | pub int_field: i32, 4 | string_field: String, 5 | opt_field: Option>, 6 | fn_field: Box Option>, 7 | } 8 | 9 | impl S { 10 | pub fn f(&self, e_arg: E) -> S {} 11 | } 12 | 13 | impl std::fmt::Display for S { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | f.write_str("THIS IS AN S STRUCT IN MOD X YO") 16 | } 17 | } 18 | 19 | impl externcrate::ExternTrait for S { 20 | fn required_method(&self) -> &'static str { 21 | "this is my implementation" 22 | } 23 | } 24 | 25 | enum E { 26 | UnitVariant, 27 | TupleVariant(S), 28 | StructVariant { a: S }, 29 | } 30 | } 31 | 32 | pub mod y { 33 | pub struct S; 34 | 35 | impl S { 36 | pub fn spoopadoop(&self) {} 37 | } 38 | 39 | impl crate::Trait for S { 40 | fn method(&self) -> u64 { 41 | 42 42 | } 43 | } 44 | } 45 | 46 | pub mod z { 47 | pub struct S; 48 | 49 | impl crate::Trait for S { 50 | // inherit default implementation for method 51 | } 52 | } 53 | 54 | pub trait Trait { 55 | fn method(&self) -> T { 56 | Default::default() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/testcrate/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("this test is bananas"); 3 | } 4 | --------------------------------------------------------------------------------