├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── README.md ├── autoload └── tree │ ├── exrename.vim │ └── util.vim ├── lua ├── tree.lua └── tree │ ├── custom.lua │ └── float.lua ├── plugin └── tree.vim └── src ├── column.rs ├── errors.rs ├── main.rs ├── tree.rs └── tree_handler.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | bin/ 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litao91/tree-nvim-rs/fa3c1eac2c9ce8daf91088ff35e5bb3d9235fcc7/.gitmodules -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "addr2line" 5 | version = "0.15.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "03345e98af8f3d786b6d9f656ccfa6ac316d954e92bc4841f0bba20789d5fb5a" 8 | dependencies = [ 9 | "gimli", 10 | ] 11 | 12 | [[package]] 13 | name = "adler" 14 | version = "1.0.2" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 17 | 18 | [[package]] 19 | name = "async-attributes" 20 | version = "1.1.2" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" 23 | dependencies = [ 24 | "quote", 25 | "syn", 26 | ] 27 | 28 | [[package]] 29 | name = "async-channel" 30 | version = "1.6.1" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" 33 | dependencies = [ 34 | "concurrent-queue", 35 | "event-listener", 36 | "futures-core", 37 | ] 38 | 39 | [[package]] 40 | name = "async-executor" 41 | version = "1.4.1" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" 44 | dependencies = [ 45 | "async-task", 46 | "concurrent-queue", 47 | "fastrand", 48 | "futures-lite", 49 | "once_cell", 50 | "slab", 51 | ] 52 | 53 | [[package]] 54 | name = "async-global-executor" 55 | version = "2.0.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" 58 | dependencies = [ 59 | "async-channel", 60 | "async-executor", 61 | "async-io", 62 | "async-mutex", 63 | "blocking", 64 | "futures-lite", 65 | "num_cpus", 66 | "once_cell", 67 | ] 68 | 69 | [[package]] 70 | name = "async-io" 71 | version = "1.4.1" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "4bbfd5cf2794b1e908ea8457e6c45f8f8f1f6ec5f74617bf4662623f47503c3b" 74 | dependencies = [ 75 | "concurrent-queue", 76 | "fastrand", 77 | "futures-lite", 78 | "libc", 79 | "log", 80 | "once_cell", 81 | "parking", 82 | "polling", 83 | "slab", 84 | "socket2", 85 | "waker-fn", 86 | "winapi", 87 | ] 88 | 89 | [[package]] 90 | name = "async-lock" 91 | version = "2.4.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" 94 | dependencies = [ 95 | "event-listener", 96 | ] 97 | 98 | [[package]] 99 | name = "async-mutex" 100 | version = "1.4.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" 103 | dependencies = [ 104 | "event-listener", 105 | ] 106 | 107 | [[package]] 108 | name = "async-std" 109 | version = "1.9.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341" 112 | dependencies = [ 113 | "async-attributes", 114 | "async-channel", 115 | "async-global-executor", 116 | "async-io", 117 | "async-lock", 118 | "crossbeam-utils", 119 | "futures-channel", 120 | "futures-core", 121 | "futures-io", 122 | "futures-lite", 123 | "gloo-timers", 124 | "kv-log-macro", 125 | "log", 126 | "memchr", 127 | "num_cpus", 128 | "once_cell", 129 | "pin-project-lite", 130 | "pin-utils", 131 | "slab", 132 | "wasm-bindgen-futures", 133 | ] 134 | 135 | [[package]] 136 | name = "async-task" 137 | version = "4.0.3" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" 140 | 141 | [[package]] 142 | name = "async-trait" 143 | version = "0.1.50" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" 146 | dependencies = [ 147 | "proc-macro2", 148 | "quote", 149 | "syn", 150 | ] 151 | 152 | [[package]] 153 | name = "atomic-waker" 154 | version = "1.0.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" 157 | 158 | [[package]] 159 | name = "autocfg" 160 | version = "1.0.1" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 163 | 164 | [[package]] 165 | name = "backtrace" 166 | version = "0.3.59" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" 169 | dependencies = [ 170 | "addr2line", 171 | "cc", 172 | "cfg-if", 173 | "libc", 174 | "miniz_oxide", 175 | "object", 176 | "rustc-demangle", 177 | ] 178 | 179 | [[package]] 180 | name = "bitflags" 181 | version = "1.2.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 184 | 185 | [[package]] 186 | name = "blocking" 187 | version = "1.0.2" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" 190 | dependencies = [ 191 | "async-channel", 192 | "async-task", 193 | "atomic-waker", 194 | "fastrand", 195 | "futures-lite", 196 | "once_cell", 197 | ] 198 | 199 | [[package]] 200 | name = "bumpalo" 201 | version = "3.6.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" 204 | 205 | [[package]] 206 | name = "byteorder" 207 | version = "1.4.3" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 210 | 211 | [[package]] 212 | name = "bytes" 213 | version = "0.4.12" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" 216 | dependencies = [ 217 | "byteorder", 218 | "iovec", 219 | ] 220 | 221 | [[package]] 222 | name = "cache-padded" 223 | version = "1.1.1" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" 226 | 227 | [[package]] 228 | name = "cc" 229 | version = "1.0.68" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" 232 | dependencies = [ 233 | "jobserver", 234 | ] 235 | 236 | [[package]] 237 | name = "cfg-if" 238 | version = "1.0.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 241 | 242 | [[package]] 243 | name = "chrono" 244 | version = "0.4.19" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 247 | dependencies = [ 248 | "libc", 249 | "num-integer", 250 | "num-traits", 251 | "time", 252 | "winapi", 253 | ] 254 | 255 | [[package]] 256 | name = "concurrent-queue" 257 | version = "1.2.2" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" 260 | dependencies = [ 261 | "cache-padded", 262 | ] 263 | 264 | [[package]] 265 | name = "crossbeam-utils" 266 | version = "0.8.4" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" 269 | dependencies = [ 270 | "autocfg", 271 | "cfg-if", 272 | "lazy_static", 273 | ] 274 | 275 | [[package]] 276 | name = "ctor" 277 | version = "0.1.20" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" 280 | dependencies = [ 281 | "quote", 282 | "syn", 283 | ] 284 | 285 | [[package]] 286 | name = "event-listener" 287 | version = "2.5.1" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" 290 | 291 | [[package]] 292 | name = "fastrand" 293 | version = "1.4.1" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb" 296 | dependencies = [ 297 | "instant", 298 | ] 299 | 300 | [[package]] 301 | name = "form_urlencoded" 302 | version = "1.0.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 305 | dependencies = [ 306 | "matches", 307 | "percent-encoding", 308 | ] 309 | 310 | [[package]] 311 | name = "fs_extra" 312 | version = "1.2.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" 315 | 316 | [[package]] 317 | name = "futures" 318 | version = "0.1.31" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" 321 | 322 | [[package]] 323 | name = "futures" 324 | version = "0.3.15" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" 327 | dependencies = [ 328 | "futures-channel", 329 | "futures-core", 330 | "futures-executor", 331 | "futures-io", 332 | "futures-sink", 333 | "futures-task", 334 | "futures-util", 335 | ] 336 | 337 | [[package]] 338 | name = "futures-channel" 339 | version = "0.3.15" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" 342 | dependencies = [ 343 | "futures-core", 344 | "futures-sink", 345 | ] 346 | 347 | [[package]] 348 | name = "futures-core" 349 | version = "0.3.15" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" 352 | 353 | [[package]] 354 | name = "futures-executor" 355 | version = "0.3.15" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" 358 | dependencies = [ 359 | "futures-core", 360 | "futures-task", 361 | "futures-util", 362 | ] 363 | 364 | [[package]] 365 | name = "futures-io" 366 | version = "0.3.15" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" 369 | 370 | [[package]] 371 | name = "futures-lite" 372 | version = "1.11.3" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb" 375 | dependencies = [ 376 | "fastrand", 377 | "futures-core", 378 | "futures-io", 379 | "memchr", 380 | "parking", 381 | "pin-project-lite", 382 | "waker-fn", 383 | ] 384 | 385 | [[package]] 386 | name = "futures-macro" 387 | version = "0.3.15" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" 390 | dependencies = [ 391 | "autocfg", 392 | "proc-macro-hack", 393 | "proc-macro2", 394 | "quote", 395 | "syn", 396 | ] 397 | 398 | [[package]] 399 | name = "futures-sink" 400 | version = "0.3.15" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" 403 | 404 | [[package]] 405 | name = "futures-task" 406 | version = "0.3.15" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" 409 | 410 | [[package]] 411 | name = "futures-util" 412 | version = "0.3.15" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" 415 | dependencies = [ 416 | "autocfg", 417 | "futures 0.1.31", 418 | "futures-channel", 419 | "futures-core", 420 | "futures-io", 421 | "futures-macro", 422 | "futures-sink", 423 | "futures-task", 424 | "memchr", 425 | "pin-project-lite", 426 | "pin-utils", 427 | "proc-macro-hack", 428 | "proc-macro-nested", 429 | "slab", 430 | "tokio-io", 431 | ] 432 | 433 | [[package]] 434 | name = "gimli" 435 | version = "0.24.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" 438 | 439 | [[package]] 440 | name = "git2" 441 | version = "0.13.20" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba" 444 | dependencies = [ 445 | "bitflags", 446 | "libc", 447 | "libgit2-sys", 448 | "log", 449 | "openssl-probe", 450 | "openssl-sys", 451 | "url", 452 | ] 453 | 454 | [[package]] 455 | name = "gloo-timers" 456 | version = "0.2.1" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" 459 | dependencies = [ 460 | "futures-channel", 461 | "futures-core", 462 | "js-sys", 463 | "wasm-bindgen", 464 | "web-sys", 465 | ] 466 | 467 | [[package]] 468 | name = "hermit-abi" 469 | version = "0.1.18" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 472 | dependencies = [ 473 | "libc", 474 | ] 475 | 476 | [[package]] 477 | name = "idna" 478 | version = "0.2.3" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 481 | dependencies = [ 482 | "matches", 483 | "unicode-bidi", 484 | "unicode-normalization", 485 | ] 486 | 487 | [[package]] 488 | name = "instant" 489 | version = "0.1.9" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" 492 | dependencies = [ 493 | "cfg-if", 494 | ] 495 | 496 | [[package]] 497 | name = "iovec" 498 | version = "0.1.4" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 501 | dependencies = [ 502 | "libc", 503 | ] 504 | 505 | [[package]] 506 | name = "jobserver" 507 | version = "0.1.22" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" 510 | dependencies = [ 511 | "libc", 512 | ] 513 | 514 | [[package]] 515 | name = "js-sys" 516 | version = "0.3.51" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" 519 | dependencies = [ 520 | "wasm-bindgen", 521 | ] 522 | 523 | [[package]] 524 | name = "kv-log-macro" 525 | version = "1.0.7" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 528 | dependencies = [ 529 | "log", 530 | ] 531 | 532 | [[package]] 533 | name = "lazy_static" 534 | version = "1.4.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 537 | 538 | [[package]] 539 | name = "libc" 540 | version = "0.2.95" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" 543 | 544 | [[package]] 545 | name = "libgit2-sys" 546 | version = "0.12.21+1.1.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825" 549 | dependencies = [ 550 | "cc", 551 | "libc", 552 | "libssh2-sys", 553 | "libz-sys", 554 | "openssl-sys", 555 | "pkg-config", 556 | ] 557 | 558 | [[package]] 559 | name = "libssh2-sys" 560 | version = "0.2.21" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee" 563 | dependencies = [ 564 | "cc", 565 | "libc", 566 | "libz-sys", 567 | "openssl-sys", 568 | "pkg-config", 569 | "vcpkg", 570 | ] 571 | 572 | [[package]] 573 | name = "libz-sys" 574 | version = "1.1.3" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" 577 | dependencies = [ 578 | "cc", 579 | "libc", 580 | "pkg-config", 581 | "vcpkg", 582 | ] 583 | 584 | [[package]] 585 | name = "log" 586 | version = "0.4.14" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 589 | dependencies = [ 590 | "cfg-if", 591 | "value-bag", 592 | ] 593 | 594 | [[package]] 595 | name = "matches" 596 | version = "0.1.8" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 599 | 600 | [[package]] 601 | name = "memchr" 602 | version = "2.4.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" 605 | 606 | [[package]] 607 | name = "miniz_oxide" 608 | version = "0.4.4" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 611 | dependencies = [ 612 | "adler", 613 | "autocfg", 614 | ] 615 | 616 | [[package]] 617 | name = "num-integer" 618 | version = "0.1.44" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 621 | dependencies = [ 622 | "autocfg", 623 | "num-traits", 624 | ] 625 | 626 | [[package]] 627 | name = "num-traits" 628 | version = "0.2.14" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 631 | dependencies = [ 632 | "autocfg", 633 | ] 634 | 635 | [[package]] 636 | name = "num_cpus" 637 | version = "1.13.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 640 | dependencies = [ 641 | "hermit-abi", 642 | "libc", 643 | ] 644 | 645 | [[package]] 646 | name = "nvim-rs" 647 | version = "0.2.1-alpha.0" 648 | source = "git+https://github.com/KillTheMule/nvim-rs#38ade7e25cf5a6ef4ce279baf2eb3d91f912ff25" 649 | dependencies = [ 650 | "async-std", 651 | "async-trait", 652 | "futures 0.3.15", 653 | "log", 654 | "rmp", 655 | "rmpv", 656 | ] 657 | 658 | [[package]] 659 | name = "object" 660 | version = "0.24.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" 663 | 664 | [[package]] 665 | name = "once_cell" 666 | version = "1.7.2" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 669 | 670 | [[package]] 671 | name = "openssl-probe" 672 | version = "0.1.4" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" 675 | 676 | [[package]] 677 | name = "openssl-sys" 678 | version = "0.9.63" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98" 681 | dependencies = [ 682 | "autocfg", 683 | "cc", 684 | "libc", 685 | "pkg-config", 686 | "vcpkg", 687 | ] 688 | 689 | [[package]] 690 | name = "parking" 691 | version = "2.0.0" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" 694 | 695 | [[package]] 696 | name = "path-clean" 697 | version = "0.1.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" 700 | 701 | [[package]] 702 | name = "percent-encoding" 703 | version = "2.1.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 706 | 707 | [[package]] 708 | name = "pin-project-lite" 709 | version = "0.2.6" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" 712 | 713 | [[package]] 714 | name = "pin-utils" 715 | version = "0.1.0" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 718 | 719 | [[package]] 720 | name = "pkg-config" 721 | version = "0.3.19" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 724 | 725 | [[package]] 726 | name = "polling" 727 | version = "2.0.3" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "4fc12d774e799ee9ebae13f4076ca003b40d18a11ac0f3641e6f899618580b7b" 730 | dependencies = [ 731 | "cfg-if", 732 | "libc", 733 | "log", 734 | "wepoll-sys", 735 | "winapi", 736 | ] 737 | 738 | [[package]] 739 | name = "proc-macro-hack" 740 | version = "0.5.19" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 743 | 744 | [[package]] 745 | name = "proc-macro-nested" 746 | version = "0.1.7" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 749 | 750 | [[package]] 751 | name = "proc-macro2" 752 | version = "1.0.27" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 755 | dependencies = [ 756 | "unicode-xid", 757 | ] 758 | 759 | [[package]] 760 | name = "quote" 761 | version = "1.0.9" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 764 | dependencies = [ 765 | "proc-macro2", 766 | ] 767 | 768 | [[package]] 769 | name = "rmp" 770 | version = "0.8.10" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "4f55e5fa1446c4d5dd1f5daeed2a4fe193071771a2636274d0d7a3b082aa7ad6" 773 | dependencies = [ 774 | "byteorder", 775 | "num-traits", 776 | ] 777 | 778 | [[package]] 779 | name = "rmpv" 780 | version = "0.4.7" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "7c760afe11955e16121e36485b6b828326c3f0eaff1c31758d96dbeb5cf09fd5" 783 | dependencies = [ 784 | "num-traits", 785 | "rmp", 786 | ] 787 | 788 | [[package]] 789 | name = "rustc-demangle" 790 | version = "0.1.19" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce" 793 | 794 | [[package]] 795 | name = "simplelog" 796 | version = "0.10.0" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0" 799 | dependencies = [ 800 | "chrono", 801 | "log", 802 | "termcolor", 803 | ] 804 | 805 | [[package]] 806 | name = "slab" 807 | version = "0.4.3" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" 810 | 811 | [[package]] 812 | name = "socket2" 813 | version = "0.4.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" 816 | dependencies = [ 817 | "libc", 818 | "winapi", 819 | ] 820 | 821 | [[package]] 822 | name = "syn" 823 | version = "1.0.72" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" 826 | dependencies = [ 827 | "proc-macro2", 828 | "quote", 829 | "unicode-xid", 830 | ] 831 | 832 | [[package]] 833 | name = "termcolor" 834 | version = "1.1.2" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 837 | dependencies = [ 838 | "winapi-util", 839 | ] 840 | 841 | [[package]] 842 | name = "time" 843 | version = "0.1.44" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 846 | dependencies = [ 847 | "libc", 848 | "wasi", 849 | "winapi", 850 | ] 851 | 852 | [[package]] 853 | name = "tinyvec" 854 | version = "1.2.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 857 | dependencies = [ 858 | "tinyvec_macros", 859 | ] 860 | 861 | [[package]] 862 | name = "tinyvec_macros" 863 | version = "0.1.0" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 866 | 867 | [[package]] 868 | name = "tokio-io" 869 | version = "0.1.13" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" 872 | dependencies = [ 873 | "bytes", 874 | "futures 0.1.31", 875 | "log", 876 | ] 877 | 878 | [[package]] 879 | name = "tree-nvim-rs" 880 | version = "0.1.0" 881 | dependencies = [ 882 | "async-std", 883 | "async-trait", 884 | "backtrace", 885 | "chrono", 886 | "fs_extra", 887 | "futures 0.3.15", 888 | "git2", 889 | "log", 890 | "nvim-rs", 891 | "path-clean", 892 | "simplelog", 893 | "unicode-width", 894 | ] 895 | 896 | [[package]] 897 | name = "unicode-bidi" 898 | version = "0.3.5" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 901 | dependencies = [ 902 | "matches", 903 | ] 904 | 905 | [[package]] 906 | name = "unicode-normalization" 907 | version = "0.1.17" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" 910 | dependencies = [ 911 | "tinyvec", 912 | ] 913 | 914 | [[package]] 915 | name = "unicode-width" 916 | version = "0.1.8" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 919 | 920 | [[package]] 921 | name = "unicode-xid" 922 | version = "0.2.2" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 925 | 926 | [[package]] 927 | name = "url" 928 | version = "2.2.2" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 931 | dependencies = [ 932 | "form_urlencoded", 933 | "idna", 934 | "matches", 935 | "percent-encoding", 936 | ] 937 | 938 | [[package]] 939 | name = "value-bag" 940 | version = "1.0.0-alpha.7" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" 943 | dependencies = [ 944 | "ctor", 945 | "version_check", 946 | ] 947 | 948 | [[package]] 949 | name = "vcpkg" 950 | version = "0.2.13" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "025ce40a007e1907e58d5bc1a594def78e5573bb0b1160bc389634e8f12e4faa" 953 | 954 | [[package]] 955 | name = "version_check" 956 | version = "0.9.3" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 959 | 960 | [[package]] 961 | name = "waker-fn" 962 | version = "1.1.0" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" 965 | 966 | [[package]] 967 | name = "wasi" 968 | version = "0.10.0+wasi-snapshot-preview1" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 971 | 972 | [[package]] 973 | name = "wasm-bindgen" 974 | version = "0.2.74" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" 977 | dependencies = [ 978 | "cfg-if", 979 | "wasm-bindgen-macro", 980 | ] 981 | 982 | [[package]] 983 | name = "wasm-bindgen-backend" 984 | version = "0.2.74" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" 987 | dependencies = [ 988 | "bumpalo", 989 | "lazy_static", 990 | "log", 991 | "proc-macro2", 992 | "quote", 993 | "syn", 994 | "wasm-bindgen-shared", 995 | ] 996 | 997 | [[package]] 998 | name = "wasm-bindgen-futures" 999 | version = "0.4.24" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" 1002 | dependencies = [ 1003 | "cfg-if", 1004 | "js-sys", 1005 | "wasm-bindgen", 1006 | "web-sys", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "wasm-bindgen-macro" 1011 | version = "0.2.74" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" 1014 | dependencies = [ 1015 | "quote", 1016 | "wasm-bindgen-macro-support", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "wasm-bindgen-macro-support" 1021 | version = "0.2.74" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" 1024 | dependencies = [ 1025 | "proc-macro2", 1026 | "quote", 1027 | "syn", 1028 | "wasm-bindgen-backend", 1029 | "wasm-bindgen-shared", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "wasm-bindgen-shared" 1034 | version = "0.2.74" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" 1037 | 1038 | [[package]] 1039 | name = "web-sys" 1040 | version = "0.3.51" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" 1043 | dependencies = [ 1044 | "js-sys", 1045 | "wasm-bindgen", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "wepoll-sys" 1050 | version = "3.0.1" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "0fcb14dea929042224824779fbc82d9fab8d2e6d3cbc0ac404de8edf489e77ff" 1053 | dependencies = [ 1054 | "cc", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "winapi" 1059 | version = "0.3.9" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1062 | dependencies = [ 1063 | "winapi-i686-pc-windows-gnu", 1064 | "winapi-x86_64-pc-windows-gnu", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "winapi-i686-pc-windows-gnu" 1069 | version = "0.4.0" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1072 | 1073 | [[package]] 1074 | name = "winapi-util" 1075 | version = "0.1.5" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1078 | dependencies = [ 1079 | "winapi", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "winapi-x86_64-pc-windows-gnu" 1084 | version = "0.4.0" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1087 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tree-nvim-rs" 3 | version = "0.1.0" 4 | authors = ["LI Tao "] 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 | # nvim-rs = "0.1.0" 11 | # TODO: use the version in crate.io. Currently only the master version has the working socket connection 12 | nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", features=["use_async-std"] } 13 | path-clean = "*" 14 | async-trait = "*" 15 | simplelog = "*" 16 | log = "*" 17 | async-std = "*" 18 | backtrace = "*" 19 | unicode-width = "*" 20 | futures = { version = "*", features = ["io-compat"] } 21 | git2 = "*" 22 | chrono = "*" 23 | fs_extra = "*" 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tree-nvim-rs - File explorer for nvim powered by rust 2 | 3 | Inspired by [tree.nvim](https://github.com/zgpio/tree.nvim) and Defx, in fact the nvim/lua part is almost identical to that of `tree.nvim` 4 | 5 | ## Features 6 | 7 | - The performance is comparable to the C++ version (`tree.nvim`) 8 | 9 | ## TODO 10 | 11 | - [x] cd 12 | - [x] copy 13 | - [x] drop 14 | - [x] move 15 | - [ ] open 16 | - [ ] multi 17 | - [ ] remove_trash 18 | - [x] create tree 19 | - [x] open/close, open or close 20 | - [x] new_file 21 | - [x] rename 22 | - [x] delete 23 | - [x] toggle hidden files 24 | - [x] toggle select 25 | - [x] toggle select all 26 | - [x] clear select all 27 | - [x] yank_path 28 | - [ ] open/close recursively 29 | - [x] selection 30 | - [x] resize 31 | - [x] git status 32 | - [x] update git map && load git status on git command 33 | - [ ] search 34 | - [x] redraw 35 | - [ ] test cases 36 | - [x] Custom 37 | - [x] size and time column 38 | - [x] better file name alignment 39 | - [x] Space cell 40 | - [ ] More file types recognization and icon customization 41 | - [ ] Better resizing when toggling buffer 42 | -------------------------------------------------------------------------------- /autoload/tree/exrename.vim: -------------------------------------------------------------------------------- 1 | "============================================================================= 2 | " FILE: exrename.vim 3 | " AUTHOR: Shougo Matsushita 4 | " EDITOR: Alisue 5 | " License: MIT license 6 | "============================================================================= 7 | 8 | let s:PREFIX = has('win32') ? '[exrename]' : '*exrename*' 9 | 10 | function! tree#exrename#create_buffer(candidates, ...) abort 11 | let options = extend({ 12 | \ 'cwd': getcwd(), 13 | \ 'bufnr': bufnr('%'), 14 | \ 'buffer_name': '', 15 | \ 'post_rename_callback': v:null, 16 | \}, get(a:000, 0, {})) 17 | if options.cwd !~# '/$' 18 | " current working directory MUST end with a trailing slash 19 | let options.cwd .= '/' 20 | endif 21 | if options.buffer_name ==# '' 22 | let options.buffer_name = s:PREFIX 23 | else 24 | let options.buffer_name = s:PREFIX . ' - ' . options.buffer_name 25 | endif 26 | 27 | vsplit 28 | redraw 29 | execute 'edit' fnameescape(options.buffer_name) 30 | 31 | setlocal buftype=acwrite 32 | setlocal noswapfile 33 | setfiletype tree_exrename 34 | 35 | syntax match treeExrenameModified '^.*$' 36 | 37 | highlight def link treeExrenameModified Todo 38 | highlight def link treeExrenameOriginal Normal 39 | 40 | let b:exrename = options 41 | 42 | call tree#util#cd(b:exrename.cwd) 43 | 44 | nnoremap q :call exit(bufnr('%')) 45 | augroup tree-exrename 46 | autocmd! * 47 | autocmd BufHidden call s:exit(expand('')) 48 | autocmd BufWriteCmd call s:do_rename() 49 | autocmd CursorMoved,CursorMovedI call s:check_lines() 50 | augroup END 51 | 52 | " Clean up the screen. 53 | silent % delete _ 54 | silent! syntax clear treeExrenameOriginal 55 | 56 | " validate candidates and register 57 | let unique_filenames = [] 58 | let b:exrename.candidates = [] 59 | let b:exrename.filenames = [] 60 | let cnt = 1 61 | for candidate in a:candidates 62 | " make sure that the 'action__path' is absolute path 63 | if !s:is_absolute(candidate.action__path) 64 | let candidate.action__path = b:exrename.cwd . candidate.action__path 65 | endif 66 | " make sure that the 'action__path' exists 67 | if !filewritable(candidate.action__path) 68 | \ && !isdirectory(candidate.action__path) 69 | redraw 70 | echo candidate.action__path 'does not exist. Skip.' 71 | continue 72 | endif 73 | " make sure that the 'action__path' is unique 74 | if index(unique_filenames, candidate.action__path) != -1 75 | redraw 76 | echo candidate.action__path 'is duplicated. Skip.' 77 | continue 78 | endif 79 | " create filename 80 | let filename = candidate.action__path 81 | if stridx(filename, b:exrename.cwd) == 0 82 | let filename = filename[len(b:exrename.cwd) :] 83 | endif 84 | " directory should end with a trailing slash (to distinguish easily) 85 | if isdirectory(candidate.action__path) 86 | let filename .= '/' 87 | endif 88 | 89 | execute 'syntax match treeExrenameOriginal' 90 | \ '/'.printf('^\%%%dl%s$', cnt, 91 | \ escape(s:escape_pattern(filename), '/')).'/' 92 | " register 93 | call add(unique_filenames, candidate.action__path) 94 | call add(b:exrename.candidates, candidate) 95 | call add(b:exrename.filenames, filename) 96 | let cnt += 1 97 | endfor 98 | " write filenames 99 | let [undolevels, &undolevels] = [&undolevels, -1] 100 | try 101 | call setline(1, b:exrename.filenames) 102 | finally 103 | let &undolevels = undolevels 104 | endtry 105 | setlocal nomodified 106 | endfunction 107 | 108 | function! s:escape_pattern(str) abort 109 | return escape(a:str, '~"\.^$[]*') 110 | endfunction 111 | 112 | function! s:is_absolute(path) abort 113 | return a:path =~# '^\%(\a\a\+:\)\|^\%(\a:\|/\)' 114 | endfunction 115 | 116 | function! s:do_rename() abort 117 | if line('$') != len(b:exrename.filenames) 118 | echohl Error | echo 'Invalid rename buffer!' | echohl None 119 | return 120 | endif 121 | 122 | " Rename files. 123 | let linenr = 1 124 | let max = line('$') 125 | while linenr <= max 126 | let filename = b:exrename.filenames[linenr - 1] 127 | 128 | redraw 129 | echo printf('(%'.len(max).'d/%d): %s -> %s', 130 | \ linenr, max, filename, getline(linenr)) 131 | 132 | if filename !=# getline(linenr) 133 | let old_file = b:exrename.candidates[linenr - 1].action__path 134 | let new_file = expand(getline(linenr)) 135 | if new_file !~# '^\%(\a\a\+:\)\|^\%(\a:\|/\)' 136 | let new_file = b:exrename.cwd . new_file 137 | endif 138 | 139 | if rename(old_file, new_file) 140 | " Rename error 141 | continue 142 | endif 143 | 144 | " update b:exrename 145 | let b:exrename.filenames[linenr - 1] = getline(linenr) 146 | let b:exrename.candidates[linenr - 1].action__path = new_file 147 | endif 148 | let linenr += 1 149 | endwhile 150 | 151 | redraw 152 | echo 'Rename done!' 153 | 154 | setlocal nomodified 155 | 156 | if b:exrename.post_rename_callback != v:null 157 | call b:exrename.post_rename_callback(b:exrename) 158 | endif 159 | endfunction 160 | 161 | function! s:exit(bufnr) abort 162 | if !bufexists(a:bufnr) 163 | return 164 | endif 165 | 166 | " Switch buffer. 167 | if winnr('$') != 1 168 | close 169 | else 170 | call s:custom_alternate_buffer() 171 | endif 172 | silent execute 'bdelete!' a:bufnr 173 | endfunction 174 | 175 | function! s:check_lines() abort 176 | if !exists('b:exrename') 177 | return 178 | endif 179 | 180 | if line('$') != len(b:exrename.filenames) 181 | echohl Error | echo 'Invalid rename buffer!' | echohl None 182 | return 183 | endif 184 | endfunction 185 | 186 | function! s:custom_alternate_buffer() abort 187 | if bufnr('%') != bufnr('#') && buflisted(bufnr('#')) 188 | buffer # 189 | endif 190 | 191 | let cnt = 0 192 | let pos = 1 193 | let current = 0 194 | while pos <= bufnr('$') 195 | if buflisted(pos) 196 | if pos == bufnr('%') 197 | let current = cnt 198 | endif 199 | 200 | let cnt += 1 201 | endif 202 | 203 | let pos += 1 204 | endwhile 205 | 206 | if current > cnt / 2 207 | bprevious 208 | else 209 | bnext 210 | endif 211 | endfunction 212 | -------------------------------------------------------------------------------- /autoload/tree/util.vim: -------------------------------------------------------------------------------- 1 | "============================================================================= 2 | " FILE: util.vim 3 | " AUTHOR: Shougo Matsushita 4 | " License: MIT license 5 | "============================================================================= 6 | lua require 'tree' 7 | 8 | function! tree#util#execute_path(command, path) abort 9 | try 10 | execute a:command fnameescape(v:lua.__expand(a:path)) 11 | catch /^Vim\%((\a\+)\)\=:E325/ 12 | " Ignore swap file error 13 | catch 14 | call v:lua.tree.print_error(v:throwpoint) 15 | call v:lua.tree.print_error(v:exception) 16 | endtry 17 | endfunction 18 | 19 | function! tree#util#cd(path) abort 20 | if exists('*chdir') 21 | call chdir(a:path) 22 | else 23 | silent execute (haslocaldir() ? 'lcd' : 'cd') fnameescape(a:path) 24 | endif 25 | endfunction 26 | 27 | function! tree#util#input(prompt, ...) abort 28 | let text = get(a:000, 0, '') 29 | let completion = get(a:000, 1, '') 30 | try 31 | if completion !=# '' 32 | return input(a:prompt, text, completion) 33 | else 34 | return input(a:prompt, text) 35 | endif 36 | catch 37 | " ignore the errors 38 | return '' 39 | endtry 40 | endfunction 41 | 42 | function! tree#util#confirm(msg, choices, default) abort 43 | try 44 | return confirm(a:msg, a:choices, a:default) 45 | catch 46 | " ignore the errors 47 | endtry 48 | 49 | return a:default 50 | endfunction 51 | -------------------------------------------------------------------------------- /lua/tree.lua: -------------------------------------------------------------------------------- 1 | -- vim: set sw=2 sts=4 et tw=78 foldmethod=indent: 2 | -- :luafile % 3 | local a = vim.api 4 | local inspect = vim.inspect 5 | local fn = vim.fn 6 | local eval = vim.api.nvim_eval 7 | local C = vim.api.nvim_command 8 | local cmd = vim.api.nvim_command 9 | local buf_is_loaded = vim.api.nvim_buf_is_loaded 10 | local call = vim.api.nvim_call_function 11 | 12 | local is_windows = fn.has('win32') == 1 or fn.has('win64') == 1 13 | local is_macos = not is_windows and fn.has('win32unix') == 0 and 14 | fn.has('macunix') == 1 15 | local is_linux = fn.has('unix') == 1 and fn.has('macunix') == 0 and 16 | fn.has('win32unix') == 0 17 | local info = debug.getinfo(1, "S") 18 | local sfile = info.source:sub(2) -- remove @ 19 | local project_root = fn.fnamemodify(sfile, ':h:h') 20 | local custom = require('tree/custom') 21 | 22 | -- https://gist.github.com/cwarden/1207556 23 | function catch(what) return what[1] end 24 | 25 | function try(what) 26 | status, result = pcall(what[1]) 27 | if not status then what[2](result) end 28 | return result 29 | end 30 | 31 | local M = {} 32 | 33 | local function default_etc_options() 34 | return { 35 | winheight = 30, 36 | winwidth = 50, 37 | split = 'vertical', -- {"vertical", "horizontal", "no", "tab", "floating"} 38 | winrelative = 'editor', 39 | buffer_name = 'default', 40 | direction = '', 41 | search = '', 42 | new = false, 43 | toggle = true, 44 | wincol = math.modf(vim.o.columns / 4), 45 | winrow = math.modf(vim.o.lines / 3) 46 | } 47 | end 48 | 49 | function M.quit(bufnr) 50 | local etc = M.etc_options[bufnr] 51 | local winnr = call('bufwinnr', {bufnr}) 52 | -- print('winnr: ', winnr) 53 | if winnr < 0 then return end 54 | local prev_winid = 0 55 | if winnr ~= call('winnr', {}) then prev_winid = vim.fn.win_getid() end 56 | 57 | -- move to the tree's win 58 | cmd(string.format('%dwincmd w', winnr)) 59 | if etc.split == 'no' or etc.split == 'tab' then 60 | if etc.prev_bufnr ~= nil and call('bufexists', etc.prev_bufnr) and 61 | etc.prev_bufnr ~= call('bufnr', '%') then 62 | cmd(string.format(string.format('buffer %d', etc.prev_bufnr))) 63 | else 64 | cmd('enew') 65 | end 66 | else 67 | if call('winnr', {'$'}) ~= 1 then 68 | cmd('close') 69 | call('win_gotoid', {prev_winid}) 70 | else 71 | cmd('enew') 72 | end 73 | end 74 | end 75 | 76 | --- Resume tree window. 77 | -- If the window corresponding to bufnrs is available, goto it; 78 | -- otherwise, create a new window. 79 | -- @param bufnrs table: trees bufnrs ordered by recently used. 80 | -- @return nil. 81 | function M.resume(bufnrs) 82 | if bufnrs == nil then return end 83 | if type(bufnrs) == 'number' then bufnrs = {bufnrs} end 84 | 85 | -- check bufnrs 86 | local deadbufs = {} 87 | local treebufs = {} 88 | for _, bufnr in pairs(bufnrs) do 89 | local loaded = buf_is_loaded(bufnr) 90 | if loaded then 91 | table.insert(treebufs, bufnr) 92 | else 93 | table.insert(deadbufs, bufnr) 94 | end 95 | end 96 | -- print("treebufs:", vim.inspect(treebufs)) 97 | 98 | -- TODO: send delete notify when -1. 99 | for _, bufnr in pairs(treebufs) do 100 | local winid = call('bufwinid', {bufnr}) 101 | if winid > 0 then 102 | if M.etc_options[bufnr].toggle then 103 | -- print('toggle') 104 | M.quit(bufnr) 105 | else 106 | -- print('goto winid', winid) 107 | call('win_gotoid', {winid}) 108 | end 109 | return 110 | end 111 | end 112 | 113 | local bufnr = treebufs[1] 114 | local etc = M.etc_options[bufnr] 115 | local resize_cmd, str 116 | -- local no_split = false 117 | -- if cfg.split == 'no' or cfg.split == 'tab' or cfg.split == 'floating' then 118 | -- no_split = true 119 | -- end 120 | local vertical = '' 121 | local command = 'sbuffer' 122 | if etc.split == 'tab' then cmd 'tabnew' end 123 | if etc.split == 'vertical' then 124 | vertical = 'vertical' 125 | resize_cmd = string.format('vertical resize %d', etc.winwidth) 126 | elseif etc.split == 'horizontal' then 127 | resize_cmd = string.format('resize %d', etc.winheight) 128 | elseif etc.split == 'floating' then 129 | local winid = a.nvim_open_win(bufnr, true, { 130 | relative = 'editor', 131 | anchor = 'NW', 132 | row = 0, -- etc.winrow 133 | col = 0, -- etc.wincol 134 | width = etc.winwidth, 135 | height = etc.winheight 136 | }) 137 | else 138 | command = 'buffer' 139 | end 140 | 141 | if etc.split ~= 'floating' then 142 | local direction = 'topleft' 143 | if etc.direction == 'botright' then direction = 'botright' end 144 | str = string.format("silent keepalt %s %s %s %d", direction, vertical, 145 | command, bufnr) 146 | 147 | cmd(str) 148 | 149 | if resize_cmd ~= nil then cmd(resize_cmd) end 150 | end 151 | 152 | cmd "se nonu" 153 | cmd "se nornu" 154 | cmd "se nolist" 155 | cmd "se signcolumn=no" 156 | a.nvim_win_set_option(winid, 'wrap', false) 157 | end 158 | 159 | function M.resize(size, bufnr) 160 | print(size) 161 | local resize_cmd 162 | local etc = M.etc_options[bufnr]; 163 | if etc.split == 'vertical' then 164 | etc.winwidth = size 165 | resize_cmd = string.format('vertical resize %d', etc.winwidth) 166 | elseif etc.split == 'horizontal' then 167 | etc.winheight = size 168 | resize_cmd = string.format('resize %d', etc.winheight) 169 | end 170 | if resize_cmd ~= nil then cmd(resize_cmd) end 171 | end 172 | 173 | --- Drop file. 174 | --- If the window corresponding to file is available, goto it; 175 | --- otherwise, goto prev window and edit file. 176 | -- @param file string: file absolute path 177 | -- @return nil 178 | function M.drop(args, file) 179 | local arg = args[1] or 'edit' 180 | local bufnr = call('bufnr', {file}) 181 | local winids = call('win_findbuf', {bufnr}) 182 | -- print(vim.inspect(winids)) 183 | if #winids == 1 then 184 | call('win_gotoid', {winids[1]}) 185 | else 186 | local prev_winnr = call('winnr', {'#'}) 187 | local prev_winid = call('win_getid', {prev_winnr}) 188 | call('win_gotoid', {prev_winid}) 189 | local str = string.format("%s %s", arg, file) 190 | cmd(str) 191 | end 192 | end 193 | 194 | --- Used to process files with the same name 195 | -- def check_overwrite(view: View, dest: Path, src: Path) -> Path: 196 | -- dest/src: {mtime=, path=, size=} 197 | function M.pre_paste(pos, src, dest) 198 | -- print(vim.inspect(dest)) 199 | local d_mtime = dest.mtime 200 | local s_mtime = src.mtime 201 | 202 | local slocaltime = os.date("%Y-%m-%d %H:%M:%S", s_mtime) 203 | local dlocaltime = os.date("%Y-%m-%d %H:%M:%S", d_mtime) 204 | -- time.strftime("%c", time.localtime(s_mtime)) 205 | local msg1 = string.format(' src: %s %d bytes\n', src.path, src.size) 206 | local msg2 = string.format(' %s\n', slocaltime) 207 | local msg3 = string.format('dest: %s %d bytes\n', dest.path, dest.size) 208 | local msg4 = string.format(' %s\n', dlocaltime) 209 | local msg = msg1 .. msg2 .. msg3 .. msg4 210 | -- print_message(msg) 211 | 212 | local msg = msg .. 213 | string.format('%s already exists. Overwrite?', dest.path) 214 | local choice = call('confirm', 215 | {msg, '&Force\n&No\n&Rename\n&Time\n&Underbar', 0}) 216 | local ret = '' 217 | if choice == 0 then 218 | return 219 | elseif choice == 1 then 220 | ret = dest.path 221 | elseif choice == 2 then 222 | ret = '' 223 | elseif choice == 3 then 224 | -- ('dir' if src.is_dir() else 'file') 225 | local msg = string.format('%s -> ', src.path) 226 | ret = call('input', {msg, dest.path, 'file'}) 227 | elseif choice == 4 and d_mtime < s_mtime then 228 | ret = src.path 229 | elseif choice == 5 then 230 | ret = dest.path .. '_' 231 | end 232 | 233 | -- TODO: notify ret to server -- 234 | rpcrequest('function', {"paste", {pos, src.path, ret}}, true) 235 | end 236 | 237 | --- Confirm remove files. 238 | -- @param bufnr Number of tree buffer 239 | -- @param rmfiles List of remove files 240 | -- @return nil 241 | function M.pre_remove(bufnr, rmfiles) 242 | -- print(vim.inspect(info)) 243 | local cnt = #rmfiles 244 | local msg = string.format('Are you sure to remove %d files?\n', cnt) 245 | for _, f in ipairs(rmfiles) do msg = msg .. f .. '\n' end 246 | local choice = call('confirm', {msg, '&Yes\n&No\n&Cancel', 0}) 247 | 248 | if choice == 1 then 249 | rpcrequest('function', {"remove", {bufnr, choice}}, true) 250 | end 251 | end 252 | 253 | function M.buf_attach(buf) 254 | a.nvim_buf_attach(buf, false, { 255 | on_detach = function() 256 | rpcrequest('function', {"on_detach", buf}, true) 257 | M.alive_buf_cnt = M.alive_buf_cnt - 1 258 | M.etc_options[buf] = nil 259 | end 260 | }) 261 | end 262 | 263 | -- [first, last] 264 | function table.slice(tbl, first, last, step) 265 | local sliced = {} 266 | for i = first or 1, last or #tbl, step or 1 do 267 | sliced[#sliced + 1] = tbl[i] 268 | end 269 | return sliced 270 | end 271 | -------------------- start of util.vim -------------------- 272 | --- keymap is shared for all tree buffer 273 | -- `:map ` to show keymap 274 | keymap = '' 275 | M.callback = {} 276 | function M.keymap(lhs, ...) 277 | -- TODO: call directly uses lua callback 278 | local action_set = { 279 | copy = true, 280 | paste = true, 281 | move = true, 282 | drop = true, 283 | open_tree = true, 284 | close_tree = true, 285 | open_or_close_tree = true, 286 | open_directory = true, 287 | cd = true, 288 | call = true, 289 | new_file = true, 290 | rename = true, 291 | toggle_select = true, 292 | remove = true, 293 | toggle_ignored_files = true, 294 | yank_path = true, 295 | clear_select_all = true, 296 | toggle_select_all = true, 297 | redraw = true, 298 | resize = true, 299 | update_git_map = true 300 | } 301 | local action_list = {...} 302 | local autocmd = [[augroup tree_keymap 303 | autocmd! 304 | autocmd FileType tree call Tree_set_keymap() 305 | augroup END 306 | func! Tree_set_keymap() abort 307 | ]] 308 | local head = [[nnoremap ]] .. lhs .. ' ' 309 | local str = '' 310 | local expr = false 311 | for i, action in ipairs(action_list) do 312 | local op, args 313 | if type(action) == 'table' then 314 | op = action[1] 315 | args = table.slice(action, 2) 316 | else 317 | op = action 318 | args = {} 319 | end 320 | for i, arg in ipairs(args) do 321 | if type(arg) == 'function' then 322 | M.callback[lhs] = arg 323 | expr = true 324 | -- NOTE: When the parameter of action is function, it should be evaluated every time 325 | -- print(string.format('arg: %s is function', vim.inspect(arg))) 326 | end 327 | end 328 | -- print(i, vim.inspect(action)) 329 | if action_set[op] then 330 | if op == 'call' then 331 | str = str .. 332 | string.format( 333 | [[:lua tree.call(tree.callback["%s"])]], 334 | vim.fn.escape(lhs, '\\')) 335 | else 336 | if expr then 337 | str = str .. string.format( 338 | [[:call v:lua.call_async_action(%s, luaeval('tree.callback["%s"]()'))]], 339 | fn.string(op), vim.fn.escape(lhs, '\\')) 340 | else 341 | str = str .. 342 | string.format( 343 | [[:call v:lua.call_async_action(%s, %s)]], 344 | fn.string(op), fn.string(args)) 345 | end 346 | end 347 | elseif vim.fn.exists(':' .. op) == 2 then 348 | str = str .. ':' .. op .. '' 349 | else 350 | -- TODO: Support vim action parameters 351 | str = str .. op 352 | end 353 | end 354 | keymap = keymap .. head .. str .. "\n" 355 | autocmd = autocmd .. keymap .. "\nendf" 356 | vim.api.nvim_exec(autocmd, false) 357 | end 358 | function M.string(expr) 359 | if type(expr) == 'string' then 360 | return expr 361 | else 362 | return vim.fn.string(expr) 363 | end 364 | end 365 | 366 | function M.call_tree(command, args) 367 | local paths, context = __parse_options(args) 368 | try { 369 | function() 370 | call_async_action('redraw', {}) -- trigger exception when server dead 371 | M.start(paths, context) 372 | end, catch { 373 | function(error) 374 | print('restart tree.nvim server') 375 | M.channel_id = nil 376 | M.start(paths, context) 377 | end 378 | } 379 | } 380 | end 381 | 382 | -- @param f function 383 | function M.call(f) 384 | local ctx = M.get_candidate() 385 | -- a.nvim_call_function(f, {ctx}) 386 | f(ctx) 387 | end 388 | 389 | function M.print_error(s) 390 | a.nvim_command(string.format( 391 | "echohl Error | echomsg '[tree] %s' | echohl None", 392 | M.string(s))) 393 | end 394 | 395 | local function __re_unquoted_match(match) 396 | -- Don't match a:match if it is located in-between unescaped single or double quotes 397 | return match .. 398 | [[\v\ze([^"'\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"|'([^'\\]*\\.)*[^'\\]*'))*[^"']*$]] 399 | end 400 | function M.convert2list(expr) 401 | if vim.tbl_islist(expr) then 402 | return expr 403 | else 404 | return {expr} 405 | end 406 | end 407 | function __parse_options(cmdline) 408 | local args = {} 409 | local options = {} 410 | local match = vim.fn.match 411 | 412 | -- Eval 413 | if match(cmdline, [[\\\@= 0 then 439 | if type(template_opts[name]) == type(42) then 440 | options[name] = vim.fn.str2nr(value) 441 | else 442 | options[name] = value 443 | end 444 | else 445 | table.insert(args, arg) 446 | end 447 | end 448 | 449 | return args, options 450 | end 451 | 452 | function __expand(path) 453 | if path:find('^~') then path = vim.fn.fnamemodify(path, ':p') end 454 | return __substitute_path_separator(path) 455 | end 456 | 457 | function __remove_quote_pairs(s) 458 | -- remove leading/ending quote pairs 459 | local t = s 460 | if (t[1] == '"' and t[#t] == '"') or (t[1] == "'" and t[#t] == "'") then 461 | t = t:sub(2, #t - 1) 462 | else 463 | t = vim.fn.substitute(s, [[\\\(.\)]], "\\1", 'g') 464 | end 465 | return t 466 | end 467 | 468 | function __substitute_path_separator(path) 469 | if is_windows then 470 | return vim.fn.substitute(path, '\\', '/', 'g') 471 | else 472 | return path 473 | end 474 | end 475 | function map_filter(func, t) 476 | vim.validate {func = {func, 'c'}, t = {t, 't'}} 477 | 478 | local rettab = {} 479 | for k, v in pairs(t) do if func(k, v) then rettab[k] = v end end 480 | return rettab 481 | end 482 | function __expand_complete(path) 483 | if path:find('^~') then 484 | path = vim.fn.fnamemodify(path, ':p') 485 | elseif vim.fn.match(path, [[^\$\h\w*]]) ~= -1 then 486 | path = vim.fn 487 | .substitute(path, [[^\$\h\w*]], [[\=eval(submatch(0))]], '') 488 | end 489 | return __substitute_path_separator(path) 490 | end 491 | function complete(arglead, cmdline, cursorpos) 492 | local copy = vim.fn.copy 493 | local _ = {} 494 | 495 | if arglead:find('^-') then 496 | -- Option names completion. 497 | local bool_options = vim.tbl_keys( 498 | map_filter( 499 | function(k, v) 500 | return type(v) == 'boolean' 501 | end, copy(user_options()))) 502 | local bt = vim.tbl_map(function(v) 503 | return '-' .. vim.fn.tr(v, '_', '-') 504 | end, copy(bool_options)) 505 | vim.list_extend(_, bt) 506 | local string_options = vim.tbl_keys( 507 | map_filter( 508 | function(k, v) 509 | return type(v) ~= 'boolean' 510 | end, copy(user_options()))) 511 | local st = vim.tbl_map(function(v) 512 | return '-' .. vim.fn.tr(v, '_', '-') .. '=' 513 | end, copy(string_options)) 514 | vim.list_extend(_, st) 515 | 516 | -- Add "-no-" option names completion. 517 | local nt = vim.tbl_map(function(v) 518 | return '-no-' .. vim.fn.tr(v, '_', '-') 519 | end, copy(bool_options)) 520 | vim.list_extend(_, nt) 521 | else 522 | local al = __expand_complete(arglead) 523 | -- Path names completion. 524 | local files = vim.tbl_filter(function(v) 525 | return vim.fn.stridx(v:lower(), al:lower()) == 0 526 | end, vim.tbl_map(__substitute_path_separator, 527 | vim.fn.glob(arglead .. '*', true, true))) 528 | files = vim.tbl_map(__expand_complete, vim.tbl_filter( 529 | function(v) 530 | return vim.fn.isdirectory(v) == 1 531 | end, files)) 532 | if arglead:find('^~') then 533 | local home_pattern = '^' .. __expand_complete('~') 534 | files = vim.tbl_map(function(v) 535 | return vim.fn.substitute(v, home_pattern, '~/', '') 536 | end, files) 537 | end 538 | files = vim.tbl_map(function(v) 539 | return vim.fn.escape(v .. '/', ' \\') 540 | end, files) 541 | vim.list_extend(_, files) 542 | end 543 | 544 | return vim.fn.uniq(vim.fn.sort(vim.tbl_filter( 545 | function(v) 546 | return vim.fn.stridx(v, arglead) == 0 547 | end, _))) 548 | end 549 | -- Test case 550 | -- -columns=mark:git:indent:icon:filename:size:time -winwidth=40 -listed `expand('%:p:h')` 551 | -- -buffer-name=\`foo\` -split=vertical -direction=topleft -winwidth=40 -listed `expand('%:p:h')` 552 | function __eval_cmdline(cmdline) 553 | local cl = '' 554 | local prev_match = 0 555 | local eval_pos = vim.fn.match(cmdline, [[\\\@= 0 do 557 | if eval_pos - prev_match > 0 then 558 | cl = cl .. cmdline:sub(prev_match + 1, eval_pos) 559 | end 560 | prev_match = vim.fn.matchend(cmdline, [[\\\@= 0 then cl = cl .. cmdline:sub(prev_match + 1) end 569 | 570 | return cl 571 | end 572 | function M.new_file(args) 573 | print(inspect(args)) 574 | ret = fn.input(args.prompt, args.text, args.completion) 575 | print(ret) 576 | rpcrequest('function', {"new_file", {ret, args.bufnr}}, true) 577 | end 578 | 579 | function M.error(str) 580 | local cmd = string.format('echomsg "[tree] %s"', str) 581 | a.nvim_command('echohl Error') 582 | a.nvim_command(cmd) 583 | a.nvim_command('echohl None') 584 | end 585 | function M.warning(str) 586 | local cmd = string.format('echomsg "[tree] %s"', str) 587 | a.nvim_command('echohl WarningMsg') 588 | a.nvim_command(cmd) 589 | a.nvim_command('echohl None') 590 | end 591 | function M.print_message(str) 592 | local cmd = string.format('echo "[tree] %s"', str) 593 | a.nvim_command(cmd) 594 | end 595 | 596 | function M.run_commands_batch(args) 597 | for i = 1, #args do a.nvim_command(args[i]) end 598 | end 599 | 600 | function M.hl_lines(bufnr, icon_ns_id, args) 601 | for i = 1, #args, 4 do 602 | hl_group = args[i] 603 | start_pos = args[i + 1] 604 | end_pos = args[i + 2] 605 | row = args[i + 3] 606 | a.nvim_buf_add_highlight(bufnr, icon_ns_id, hl_group, row, start_pos, 607 | end_pos) 608 | end 609 | end 610 | 611 | function rpcrequest(method, args, is_async) 612 | if not M.channel_id then 613 | -- TODO: temporary 614 | M.error("tree.channel_id doesn't exists") 615 | return -1 616 | end 617 | 618 | local channel_id = M.channel_id 619 | if is_async then 620 | return vim.rpcnotify(channel_id, method, args) 621 | else 622 | return vim.rpcrequest(channel_id, method, args) 623 | end 624 | end 625 | 626 | function M.linux() return is_linux end 627 | function M.windows() return is_windows end 628 | function M.macos() return is_macos end 629 | -- Open a file. 630 | function M.open(filename) 631 | local filename = vim.fn.fnamemodify(filename, ':p') 632 | local system = vim.fn.system 633 | local shellescape = vim.fn.shellescape 634 | local executable = vim.fn.executable 635 | local exists = vim.fn.exists 636 | local printf = string.format 637 | 638 | -- Detect desktop environment. 639 | if tree.windows() then 640 | -- For URI only. 641 | -- Note: 642 | -- # and % required to be escaped (:help cmdline-special) 643 | a.nvim_command(printf( 644 | "silent execute '!start rundll32 url.dll,FileProtocolHandler %s'", 645 | vim.fn.escape(filename, '#%'))) 646 | elseif vim.fn.has('win32unix') == 1 then 647 | -- Cygwin. 648 | system(printf('cygstart %s', shellescape(filename))) 649 | elseif executable('xdg-open') == 1 then 650 | -- Linux. 651 | system(printf('%s %s &', 'xdg-open', shellescape(filename))) 652 | elseif exists('$KDE_FULL_SESSION') == 1 and vim.env['KDE_FULL_SESSION'] == 653 | 'true' then 654 | -- KDE. 655 | system(printf('%s %s &', 'kioclient exec', shellescape(filename))) 656 | elseif exists('$GNOME_DESKTOP_SESSION_ID') == 1 then 657 | -- GNOME. 658 | system(printf('gnome-open %s &', shellescape(filename))) 659 | elseif executable('exo-open') == 1 then 660 | -- Xfce. 661 | system(printf('exo-open %s &', shellescape(filename))) 662 | elseif tree.macos() and executable('open') == 1 then 663 | -- Mac OS. 664 | system(printf('open %s &', shellescape(filename))) 665 | else 666 | -- Give up. 667 | M.print_error('Not supported.') 668 | end 669 | end 670 | -------------------- end of util.vim -------------------- 671 | 672 | -------------------- start of init.vim -------------------- 673 | M.servername = nil 674 | local function init_channel() 675 | if fn.has('nvim-0.5') == 0 then 676 | print('tree requires nvim 0.5+.') 677 | return true 678 | end 679 | 680 | local servername = vim.v.servername 681 | local cmd 682 | -- NOTE: ~ cant expand in {cmd} arg of jobstart 683 | if M.linux() then 684 | cmd = {project_root .. '/bin/tree', servername} 685 | elseif M.windows() then 686 | local ip = '127.0.0.1' 687 | if not M.servername then 688 | local port = 6666 689 | while not M.servername do 690 | try { 691 | function() 692 | vim.fn.serverstart(ip .. ':' .. tostring(port)) 693 | M.servername = port 694 | end, catch {function(error) 695 | port = port + 1 696 | end} 697 | } 698 | end 699 | end 700 | cmd = {project_root .. '\\bin\\tree.exe', tostring(M.servername)} 701 | elseif M.macos() then 702 | cmd = {project_root .. '/bin/tree', servername} 703 | end 704 | -- print('bin:', bin) 705 | -- print('servername:', servername) 706 | -- print(inspect(cmd)) 707 | fn.jobstart(cmd) 708 | local N = 250 709 | local i = 0 710 | while i < N and M.channel_id == nil do 711 | C('sleep 4m') 712 | i = i + 1 713 | end 714 | -- print(string.format('Wait for server %dms', i*4)) 715 | return true 716 | end 717 | 718 | local function initialize() 719 | if M.channel_id then return end 720 | 721 | init_channel() 722 | -- NOTE: Exec VimL snippets in lua. 723 | a.nvim_exec([[ 724 | augroup tree 725 | autocmd! 726 | augroup END 727 | ]], false) 728 | 729 | -- TODO: g:tree#_histories 730 | M.tree_histories = {} 731 | end 732 | 733 | -- options = core + etc 734 | function user_options() 735 | return vim.tbl_extend('force', { 736 | auto_cd = false, 737 | auto_recursive_level = 0, 738 | columns = 'mark:indent:icon:filename:size', 739 | ignored_files = '.*', 740 | listed = false, 741 | profile = false, 742 | resume = false, 743 | root_marker = '[in]: ', 744 | session_file = '', 745 | show_ignored_files = false, 746 | sort = 'filename' 747 | }, default_etc_options()) 748 | end 749 | 750 | local function internal_options() 751 | local s = fn.getpos("'<")[2] 752 | local e = fn.getpos("'>")[2] 753 | cmd 'delmarks <' 754 | cmd 'delmarks >' 755 | return { 756 | cursor = fn.line('.'), 757 | -- drives={}, 758 | prev_bufnr = fn.bufnr('%'), 759 | prev_winid = fn.win_getid(), 760 | visual_start = s, 761 | visual_end = e 762 | } 763 | end 764 | -- Transfer action context to server when perform action 765 | -- Transfer core options when _tree_start 766 | local function init_context(user_context) 767 | local ctx = {} 768 | local local_custom = vim.deepcopy(custom.get()) 769 | -- NOTE: Avoid empty custom.column being converted to vector 770 | if vim.tbl_isempty(local_custom.column) then local_custom.column = nil end 771 | if local_custom.option._ then 772 | ctx = vim.tbl_extend('force', ctx, local_custom.option._) 773 | local_custom.option._ = nil 774 | end 775 | if local_custom.option.buffer_name then 776 | ctx = vim.tbl_extend('force', ctx, local_custom.option.buffer_name) 777 | end 778 | ctx = vim.tbl_extend('force', ctx, user_context) 779 | ctx.custom = local_custom 780 | return ctx 781 | end 782 | 783 | local function action_context() 784 | local context = internal_options() 785 | return context 786 | end 787 | 788 | -------------------- end of init.vim -------------------- 789 | 790 | -------------------- start of tree.vim -------------------- 791 | -- NOTE: The buffer creation is done by the lua side 792 | M.alive_buf_cnt = 0 793 | M.etc_options = {} 794 | local count = 0 795 | function M.start(_paths, user_ctx) 796 | initialize() 797 | local ctx = init_context(user_ctx) 798 | local paths = fn.map(_paths, "fnamemodify(v:val, ':p')") 799 | if #paths == 0 then paths = {fn.getcwd()} end 800 | if M.alive_buf_cnt < 1 or user_ctx.new then 801 | local buf = a.nvim_create_buf(false, true) 802 | local bufname = "Tree-" .. tostring(count) 803 | a.nvim_buf_set_name(buf, bufname) 804 | count = count + 1 805 | M.alive_buf_cnt = M.alive_buf_cnt + 1 806 | local etc = default_etc_options() 807 | for k, _ in pairs(etc) do if ctx[k] then etc[k] = ctx[k] end end 808 | M.etc_options[buf] = etc 809 | ctx.bufnr = buf 810 | end 811 | rpcrequest('_tree_start', {paths, ctx}, false) 812 | -- TODO: search path 813 | -- if context['search'] !=# '' 814 | -- call tree#call_action('search', [context['search']]) 815 | -- endif 816 | end 817 | 818 | function call_async_action(action, ...) 819 | if vim.bo.filetype ~= 'tree' then return end 820 | 821 | local context = action_context() 822 | local args = ... 823 | if type(args) ~= 'table' then args = {...} end 824 | rpcrequest('_tree_async_action', {action, args, context}, true) 825 | end 826 | 827 | function M.get_candidate() 828 | if vim.bo.filetype ~= 'tree' then return {} end 829 | 830 | local context = internal_options() 831 | return rpcrequest('_tree_get_candidate', {context}, false) 832 | end 833 | function M.is_directory() return 834 | fn.get(M.get_candidate(), 'is_directory', false) end 835 | function M.is_opened_tree() 836 | return fn.get(M.get_candidate(), 'is_opened_tree', false) 837 | end 838 | 839 | function M.get_context() 840 | if vim.bo.filetype ~= 'tree' then return {} end 841 | 842 | return rpcrequest('_tree_get_context', {}, false) 843 | end 844 | -------------------- end of tree.vim -------------------- 845 | 846 | if _TEST then 847 | -- Note: we prefix it with an underscore, such that the test function and real function have 848 | -- different names. Otherwise an accidental call in the code to `M.FirstToUpper` would 849 | -- succeed in tests, but later fail unexpectedly in production 850 | M._set_custom = set_custom 851 | M._init_context = init_context 852 | M._initialize = initialize 853 | M.__expand_complete = __expand_complete 854 | M.custom = custom 855 | end 856 | 857 | tree = M 858 | return M 859 | -------------------------------------------------------------------------------- /lua/tree/custom.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.get() 4 | if not M.custom then 5 | M.custom = { 6 | column = {}, 7 | option = {}, 8 | source = {}, 9 | } 10 | end 11 | return M.custom 12 | end 13 | 14 | -- use name:value or dict extend dest table 15 | local function set(dest, name_or_dict, value) 16 | if type(name_or_dict) == 'table' then 17 | dest = vim.tbl_extend('force', dest, name_or_dict) 18 | else 19 | dest[name_or_dict] = value 20 | end 21 | return dest 22 | end 23 | 24 | function M.column(column_name, name_or_dict, ...) 25 | local custom = M.get().column 26 | 27 | for i, key in ipairs(vim.split(column_name, '%s*,%s*')) do 28 | if not custom[key] then 29 | custom[key] = {} 30 | end 31 | custom[key] = set(custom[key], name_or_dict, ...) 32 | end 33 | end 34 | 35 | function M.option(buffer_name, name_or_dict, ...) 36 | local custom = M.get().option 37 | 38 | for i, key in ipairs(vim.split(buffer_name, '%s*,%s*')) do 39 | if not custom[key] then 40 | custom[key] = {} 41 | end 42 | custom[key] = set(custom[key], name_or_dict, ...) 43 | end 44 | end 45 | 46 | function M.source(source_name, name_or_dict, ...) 47 | local custom = M.get().source 48 | 49 | for i, key in ipairs(vim.fn.split(source_name, [[\s*,\s*]])) do 50 | if not custom[key] then 51 | custom[key] = {} 52 | end 53 | custom[key] = set(custom[key], name_or_dict, ...) 54 | end 55 | end 56 | 57 | if _TEST then 58 | M._set = set 59 | end 60 | 61 | return M 62 | -------------------------------------------------------------------------------- /lua/tree/float.lua: -------------------------------------------------------------------------------- 1 | -- vim: set sw=2 sts=4 et tw=78 foldlevel=0 foldmethod=marker: 2 | local fn = vim.fn 3 | local api = vim.api 4 | local cmd = vim.api.nvim_command 5 | local function buildContent(info) 6 | local marker = { 7 | ft='', 8 | date='', 9 | size='' 10 | } 11 | 12 | local content = {} 13 | 14 | for k, v in pairs(info) do 15 | table.insert(content, string.format('%4s: %s', k, v)) 16 | end 17 | 18 | return content 19 | end 20 | 21 | local function winPos(width, height) 22 | local bottom_line = fn.line('w0') + fn.winheight(0) - 1 23 | local curr_pos = fn.getpos('.') 24 | local rownr = curr_pos[2] 25 | local colnr = curr_pos[3] 26 | local columns = vim.o.columns 27 | -- a long wrap line 28 | if colnr > columns then 29 | colnr = colnr % columns 30 | rownr = rownr + colnr / columns 31 | end 32 | 33 | local vert, row, hor, col 34 | if rownr + height <= bottom_line then 35 | vert = 'N' 36 | row = 1 37 | else 38 | vert = 'S' 39 | row = 0 40 | end 41 | 42 | if colnr + width <= columns then 43 | hor = 'W' 44 | col = 0 45 | else 46 | hor = 'E' 47 | col = 1 48 | end 49 | 50 | return row, col, vert, hor 51 | end 52 | 53 | local function winSize(info, max_width, max_height) 54 | local width = 0 55 | local height = 0 56 | 57 | for i, line in ipairs(info) do 58 | local line_width = fn.strdisplaywidth(line) 59 | if line_width > max_width then 60 | width = max_width 61 | height = height + line_width / max_width + 1 62 | else 63 | width = fn.max({line_width, width}) 64 | height = height + 1 65 | end 66 | end 67 | 68 | if height > max_height then 69 | height = max_height 70 | end 71 | return width, height 72 | end 73 | 74 | function closePopup() 75 | local winnr = fn.winnr('$') 76 | for i=1,winnr do 77 | if fn.getbufvar(fn.winbufnr(i), '&filetype') == 'tree-float' then 78 | cmd(i .. 'wincmd c') 79 | cmd('autocmd! TreeClosePopup * ') 80 | return 81 | end 82 | end 83 | end 84 | 85 | function Tree_display(info) 86 | local content = buildContent(info) 87 | local tree_popup_max_height 88 | local tree_popup_max_width 89 | local max_height = tree_popup_max_height or 0.6*vim.o.lines 90 | local max_width = tree_popup_max_width or 0.6*vim.o.columns 91 | max_height = fn.float2nr(max_height) 92 | max_width = fn.float2nr(max_width) 93 | local width, height = winSize(content, max_width, max_height) 94 | local row, col, vert, hor = winPos(width, height) 95 | 96 | -- for i in range(len(content)) do 97 | -- let line = content[i] 98 | -- end 99 | 100 | -- `width + 2`? ==> set foldcolumn=1 101 | local options = { 102 | relative='cursor', 103 | anchor=vert .. hor, 104 | row=row, 105 | col=col, 106 | width=width + 2, 107 | height=height, 108 | } 109 | api.nvim_open_win(fn.bufnr('%'), true, options) 110 | cmd('enew!') 111 | fn.append(0, content) 112 | 113 | api.nvim_exec([[ 114 | normal gg 115 | nmap q :close 116 | setlocal foldcolumn=1 117 | setlocal buftype=nofile 118 | setlocal bufhidden=wipe 119 | setlocal signcolumn=no 120 | setlocal filetype=tree-float 121 | setlocal noautoindent 122 | setlocal nosmartindent 123 | setlocal wrap 124 | setlocal nobuflisted 125 | setlocal noswapfile 126 | setlocal nocursorline 127 | setlocal nonumber 128 | setlocal norelativenumber 129 | setlocal nospell 130 | if has('nvim') 131 | setlocal winhighlight=Normal:treeFloatingNormal 132 | setlocal winhighlight=FoldColumn:treeFloatingNormal 133 | endif 134 | noautocmd wincmd p 135 | 136 | augroup TreeClosePopup 137 | autocmd! 138 | autocmd CursorMoved,CursorMovedI,InsertEnter,BufLeave,WinLeave call v:lua.closePopup() 139 | augroup END 140 | ]], false) 141 | 142 | end 143 | -- print(vim.inspect(buildContent({date='2020-03-08', ft='txt'}))) 144 | -- call v:lua.Tree_display({ 'date': '2020-03-08', 'ft': 'txt' }) 145 | -- lua Tree_display({ date='2020-03-08', ft='txt', size='1024KB' }) 146 | -- autocmd CursorHold lua Tree_display({ date='2020-03-08', ft='txt', size='1024KB' }) 147 | -------------------------------------------------------------------------------- /plugin/tree.vim: -------------------------------------------------------------------------------- 1 | "============================================================================= 2 | " FILE: tree.vim 3 | " AUTHOR: Shougo Matsushita 4 | " License: MIT license 5 | "============================================================================= 6 | 7 | if exists('g:loaded_tree_rs') 8 | finish 9 | endif 10 | let g:loaded_tree_rs = 1 11 | 12 | command! -nargs=* -range -bar -complete=customlist,v:lua.complete 13 | \ Tree 14 | \ call luaeval('require("tree").call_tree("Tree", _A)', ) 15 | -------------------------------------------------------------------------------- /src/column.rs: -------------------------------------------------------------------------------- 1 | use crate::tree::Tree; 2 | use chrono::{DateTime, Local}; 3 | use git2::Status; 4 | use log::*; 5 | use std::convert::From; 6 | use std::ffi::OsStr; 7 | use std::fs::Metadata; 8 | 9 | #[derive(Eq, PartialEq, Clone)] 10 | pub enum Icon { 11 | FolderClosed, 12 | FolderOpened, 13 | FolderSymlink, 14 | File, 15 | FileSymlink, 16 | FileHidden, 17 | Excel, 18 | Word, 19 | Ppt, 20 | Stylus, 21 | Sass, 22 | Html, 23 | Xml, 24 | Ejs, 25 | Css, 26 | Webpack, 27 | Markdown, 28 | Json, 29 | Javascript, 30 | Javascriptreact, 31 | Ruby, 32 | Php, 33 | Python, 34 | Coffee, 35 | Mustache, 36 | Conf, 37 | Image, 38 | Ico, 39 | Twig, 40 | C, 41 | H, 42 | Haskell, 43 | Lua, 44 | Java, 45 | Terminal, 46 | Ml, 47 | Diff, 48 | Sql, 49 | Clojure, 50 | Edn, 51 | Scala, 52 | Go, 53 | Dart, 54 | Firefox, 55 | Vs, 56 | Perl, 57 | Rss, 58 | Fsharp, 59 | Rust, 60 | Dlang, 61 | Erlang, 62 | Elixir, 63 | Mix, 64 | Vim, 65 | Ai, 66 | Psd, 67 | Psb, 68 | Typescript, 69 | Typescriptreact, 70 | Julia, 71 | Puppet, 72 | Vue, 73 | Swift, 74 | Gitconfig, 75 | Bashrc, 76 | Favicon, 77 | Docker, 78 | Gruntfile, 79 | Gulpfile, 80 | Dropbox, 81 | License, 82 | Procfile, 83 | Jquery, 84 | Angular, 85 | Backbone, 86 | Requirejs, 87 | Materialize, 88 | Mootools, 89 | Vagrant, 90 | Svg, 91 | Font, 92 | Text, 93 | Archive, 94 | Unknown, 95 | } 96 | 97 | impl From<&str> for Icon { 98 | fn from(s: &str) -> Icon { 99 | match s { 100 | "styl" => Icon::Stylus, 101 | "sass" => Icon::Sass, 102 | "scss" => Icon::Sass, 103 | "htm" => Icon::Html, 104 | "html" => Icon::Html, 105 | "slim" => Icon::Html, 106 | "xml" => Icon::Xml, 107 | "xaml" => Icon::Xml, 108 | "ejs" => Icon::Ejs, 109 | "css" => Icon::Css, 110 | "less" => Icon::Css, 111 | "md" => Icon::Markdown, 112 | "mdx" => Icon::Markdown, 113 | "markdown" => Icon::Markdown, 114 | "rmd" => Icon::Markdown, 115 | "json" => Icon::Json, 116 | "js" => Icon::Javascript, 117 | "es6" => Icon::Javascript, 118 | "jsx" => Icon::Javascriptreact, 119 | "rb" => Icon::Ruby, 120 | "ru" => Icon::Ruby, 121 | "php" => Icon::Php, 122 | "py" => Icon::Python, 123 | "pyc" => Icon::Python, 124 | "pyo" => Icon::Python, 125 | "pyd" => Icon::Python, 126 | "coffee" => Icon::Coffee, 127 | "mustache" => Icon::Mustache, 128 | "hbs" => Icon::Mustache, 129 | "config" => Icon::Conf, 130 | "conf" => Icon::Conf, 131 | "ini" => Icon::Conf, 132 | "yml" => Icon::Conf, 133 | "yaml" => Icon::Conf, 134 | "toml" => Icon::Conf, 135 | "jpg" => Icon::Image, 136 | "jpeg" => Icon::Image, 137 | "bmp" => Icon::Image, 138 | "png" => Icon::Image, 139 | "gif" => Icon::Image, 140 | "ico" => Icon::Ico, 141 | "twig" => Icon::Twig, 142 | "cpp" => Icon::C, 143 | "c++" => Icon::C, 144 | "cxx" => Icon::C, 145 | "cc" => Icon::C, 146 | "cp" => Icon::C, 147 | "c" => Icon::C, 148 | "h" => Icon::H, 149 | "hpp" => Icon::H, 150 | "hxx" => Icon::H, 151 | "hs" => Icon::Haskell, 152 | "lhs" => Icon::Haskell, 153 | "lua" => Icon::Lua, 154 | "java" => Icon::Java, 155 | "jar" => Icon::Java, 156 | "sh" => Icon::Terminal, 157 | "fish" => Icon::Terminal, 158 | "bash" => Icon::Terminal, 159 | "zsh" => Icon::Terminal, 160 | "ksh" => Icon::Terminal, 161 | "csh" => Icon::Terminal, 162 | "awk" => Icon::Terminal, 163 | "ps1" => Icon::Terminal, 164 | "bat" => Icon::Terminal, 165 | "cmd" => Icon::Terminal, 166 | "ml" => Icon::Ml, 167 | "mli" => Icon::Ml, 168 | "diff" => Icon::Diff, 169 | "db" => Icon::Sql, 170 | "sql" => Icon::Sql, 171 | "dump" => Icon::Sql, 172 | "accdb" => Icon::Sql, 173 | "clj" => Icon::Clojure, 174 | "cljc" => Icon::Clojure, 175 | "cljs" => Icon::Clojure, 176 | "edn" => Icon::Edn, 177 | "scala" => Icon::Scala, 178 | "go" => Icon::Go, 179 | "dart" => Icon::Dart, 180 | "xul" => Icon::Firefox, 181 | "sln" => Icon::Vs, 182 | "suo" => Icon::Vs, 183 | "pl" => Icon::Perl, 184 | "pm" => Icon::Perl, 185 | "t" => Icon::Perl, 186 | "rss" => Icon::Rss, 187 | "f#" => Icon::Fsharp, 188 | "fsscript" => Icon::Fsharp, 189 | "fsx" => Icon::Fsharp, 190 | "fs" => Icon::Fsharp, 191 | "fsi" => Icon::Fsharp, 192 | "rs" => Icon::Rust, 193 | "rlib" => Icon::Rust, 194 | "d" => Icon::Dlang, 195 | "erl" => Icon::Erlang, 196 | "hrl" => Icon::Erlang, 197 | "ex" => Icon::Elixir, 198 | "exs" => Icon::Elixir, 199 | "exx" => Icon::Elixir, 200 | "leex" => Icon::Elixir, 201 | "vim" => Icon::Vim, 202 | "ai" => Icon::Ai, 203 | "psd" => Icon::Psd, 204 | "psb" => Icon::Psd, 205 | "ts" => Icon::Typescript, 206 | "tsx" => Icon::Javascriptreact, 207 | "jl" => Icon::Julia, 208 | "pp" => Icon::Puppet, 209 | "vue" => Icon::Vue, 210 | "swift" => Icon::Swift, 211 | "xcplayground" => Icon::Swift, 212 | "svg" => Icon::Svg, 213 | "otf" => Icon::Font, 214 | "ttf" => Icon::Font, 215 | "fnt" => Icon::Font, 216 | "txt" => Icon::Text, 217 | "text" => Icon::Text, 218 | "zip" => Icon::Archive, 219 | "tar" => Icon::Archive, 220 | "gz" => Icon::Archive, 221 | "gzip" => Icon::Archive, 222 | "rar" => Icon::Archive, 223 | "7z" => Icon::Archive, 224 | "iso" => Icon::Archive, 225 | "doc" => Icon::Word, 226 | "docx" => Icon::Word, 227 | "docm" => Icon::Word, 228 | "csv" => Icon::Excel, 229 | "xls" => Icon::Excel, 230 | "xlsx" => Icon::Excel, 231 | "xlsm" => Icon::Excel, 232 | "ppt" => Icon::Ppt, 233 | "pptx" => Icon::Ppt, 234 | "pptm" => Icon::Ppt, 235 | _ => Icon::Unknown, 236 | } 237 | } 238 | } 239 | 240 | impl Icon { 241 | pub fn hl_group_name(&self) -> &str { 242 | match *self { 243 | Icon::FolderClosed => "tree_icon_FolderClosed", 244 | Icon::FolderOpened => "tree_icon_FolderOpened", 245 | Icon::FolderSymlink => "tree_icon_FolderSymlink", 246 | Icon::File => "tree_icon_File", 247 | Icon::FileSymlink => "tree_icon_FileSymlink", 248 | Icon::FileHidden => "tree_icon_FileHidden", 249 | Icon::Excel => "tree_icon_Excel", 250 | Icon::Word => "tree_icon_Word", 251 | Icon::Ppt => "tree_icon_Ppt", 252 | Icon::Stylus => "tree_icon_Stylus", 253 | Icon::Sass => "tree_icon_Sass", 254 | Icon::Html => "tree_icon_Html", 255 | Icon::Xml => "tree_icon_Xml", 256 | Icon::Ejs => "tree_icon_Ejs", 257 | Icon::Css => "tree_icon_Css", 258 | Icon::Webpack => "tree_icon_Webpack", 259 | Icon::Markdown => "tree_icon_Markdown", 260 | Icon::Json => "tree_icon_Json", 261 | Icon::Javascript => "tree_icon_Javascript", 262 | Icon::Javascriptreact => "tree_icon_Javascriptreact", 263 | Icon::Ruby => "tree_icon_Ruby", 264 | Icon::Php => "tree_icon_Php", 265 | Icon::Python => "tree_icon_Python", 266 | Icon::Coffee => "tree_icon_Coffee", 267 | Icon::Mustache => "tree_icon_Mustache", 268 | Icon::Conf => "tree_icon_Conf", 269 | Icon::Image => "tree_icon_Image", 270 | Icon::Ico => "tree_icon_Ico", 271 | Icon::Twig => "tree_icon_Twig", 272 | Icon::C => "tree_icon_C", 273 | Icon::H => "tree_icon_H", 274 | Icon::Haskell => "tree_icon_Haskell", 275 | Icon::Lua => "tree_icon_Lua", 276 | Icon::Java => "tree_icon_Java", 277 | Icon::Terminal => "tree_icon_Terminal", 278 | Icon::Ml => "tree_icon_Ml", 279 | Icon::Diff => "tree_icon_Diff", 280 | Icon::Sql => "tree_icon_Sql", 281 | Icon::Clojure => "tree_icon_Clojure", 282 | Icon::Edn => "tree_icon_Edn", 283 | Icon::Scala => "tree_icon_Scala", 284 | Icon::Go => "tree_icon_Go", 285 | Icon::Dart => "tree_icon_Dart", 286 | Icon::Firefox => "tree_icon_Firefox", 287 | Icon::Vs => "tree_icon_Vs", 288 | Icon::Perl => "tree_icon_Perl", 289 | Icon::Rss => "tree_icon_Rss", 290 | Icon::Fsharp => "tree_icon_Fsharp", 291 | Icon::Rust => "tree_icon_Rust", 292 | Icon::Dlang => "tree_icon_Dlang", 293 | Icon::Erlang => "tree_icon_Erlang", 294 | Icon::Elixir => "tree_icon_Elixir", 295 | Icon::Mix => "tree_icon_Mix", 296 | Icon::Vim => "tree_icon_Vim", 297 | Icon::Ai => "tree_icon_Ai", 298 | Icon::Psd => "tree_icon_Psd", 299 | Icon::Psb => "tree_icon_Psb", 300 | Icon::Typescript => "tree_icon_Typescript", 301 | Icon::Typescriptreact => "tree_icon_Typescriptreact", 302 | Icon::Julia => "tree_icon_Julia", 303 | Icon::Puppet => "tree_icon_Puppet", 304 | Icon::Vue => "tree_icon_Vue", 305 | Icon::Swift => "tree_icon_Swift", 306 | Icon::Gitconfig => "tree_icon_Gitconfig", 307 | Icon::Bashrc => "tree_icon_Bashrc", 308 | Icon::Favicon => "tree_icon_Favicon", 309 | Icon::Docker => "tree_icon_Docker", 310 | Icon::Gruntfile => "tree_icon_Gruntfile", 311 | Icon::Gulpfile => "tree_icon_Gulpfile", 312 | Icon::Dropbox => "tree_icon_Dropbox", 313 | Icon::License => "tree_icon_License", 314 | Icon::Procfile => "tree_icon_Procfile", 315 | Icon::Jquery => "tree_icon_Jquery", 316 | Icon::Angular => "tree_icon_Angular", 317 | Icon::Backbone => "tree_icon_Backbone", 318 | Icon::Requirejs => "tree_icon_Requirejs", 319 | Icon::Materialize => "tree_icon_Materialize", 320 | Icon::Mootools => "tree_icon_Mootools", 321 | Icon::Vagrant => "tree_icon_Vagrant", 322 | Icon::Svg => "tree_icon_Svg", 323 | Icon::Font => "tree_icon_Font", 324 | Icon::Text => "tree_icon_Text", 325 | Icon::Archive => "tree_icon_Archive", 326 | Icon::Unknown => "tree_icon_Unknonwn", 327 | } 328 | } 329 | pub fn as_glyph_and_color(&self) -> (&str, &str) { 330 | match *self { 331 | Icon::FolderClosed => ("", "#00afaf"), 332 | Icon::FolderOpened => ("", "#00afaf"), 333 | Icon::FolderSymlink => ("", "#00afaf"), 334 | Icon::File => ("", "#999999"), 335 | Icon::FileSymlink => ("", "#999999"), 336 | Icon::FileHidden => ("﬒", "#999999"), 337 | Icon::Excel => ("", "#207245"), 338 | Icon::Word => ("", "#185abd"), 339 | Icon::Ppt => ("", "#cb4a32"), 340 | Icon::Stylus => ("", "#8dc149"), 341 | Icon::Sass => ("", "#f55385"), 342 | Icon::Html => ("", "#e37933"), 343 | Icon::Xml => ("謹", "#e37933"), 344 | Icon::Ejs => ("", "#cbcb41"), 345 | Icon::Css => ("", "#519aba"), 346 | Icon::Webpack => ("ﰩ", "#519aba"), 347 | Icon::Markdown => ("", "#519aba"), 348 | Icon::Json => ("", "#cbcb41"), 349 | Icon::Javascript => ("", "#cbcb41"), 350 | Icon::Javascriptreact => ("", "#519aba"), 351 | Icon::Ruby => ("", "#cc3e44"), 352 | Icon::Php => ("", "#a074c4"), 353 | Icon::Python => ("", "#519aba"), 354 | Icon::Coffee => ("", "#cbcb41"), 355 | Icon::Mustache => ("", "#e37933"), 356 | Icon::Conf => ("", "#6d8086"), 357 | Icon::Image => ("", "#a074c4"), 358 | Icon::Ico => ("", "#cbcb41"), 359 | Icon::Twig => ("", "#8dc149"), 360 | Icon::C => ("", "#519aba"), 361 | Icon::H => ("", "#a074c4"), 362 | Icon::Haskell => ("", "#a074c4"), 363 | Icon::Lua => ("", "#519aba"), 364 | Icon::Java => ("", "#cc3e44"), 365 | Icon::Terminal => ("", "#4d5a5e"), 366 | Icon::Ml => ("λ", "#e37933"), 367 | Icon::Diff => ("", "#41535b"), 368 | Icon::Sql => ("", "#f55385"), 369 | Icon::Clojure => ("", "#8dc149"), 370 | Icon::Edn => ("", "#519aba"), 371 | Icon::Scala => ("", "#cc3e44"), 372 | Icon::Go => ("", "#519aba"), 373 | Icon::Dart => ("", "#03589C"), 374 | Icon::Firefox => ("", "#e37933"), 375 | Icon::Vs => ("", "#854CC7"), 376 | Icon::Perl => ("", "#519aba"), 377 | Icon::Rss => ("", "#FB9D3B"), 378 | Icon::Fsharp => ("", "#519aba"), 379 | Icon::Rust => ("", "#519aba"), 380 | Icon::Dlang => ("", "#cc3e44"), 381 | Icon::Erlang => ("", "#A90533"), 382 | Icon::Elixir => ("", "#a074c4"), 383 | Icon::Mix => ("", "#cc3e44"), 384 | Icon::Vim => ("", "#019833"), 385 | Icon::Ai => ("", "#cbcb41"), 386 | Icon::Psd => ("", "#519aba"), 387 | Icon::Psb => ("", "#519aba"), 388 | Icon::Typescript => ("", "#519aba"), 389 | Icon::Typescriptreact => ("", "#519aba"), 390 | Icon::Julia => ("", "#a074c4"), 391 | Icon::Puppet => ("", "#cbcb41"), 392 | Icon::Vue => ("﵂", "#8dc149"), 393 | Icon::Swift => ("", "#e37933"), 394 | Icon::Gitconfig => ("", "#41535b"), 395 | Icon::Bashrc => ("", "#4d5a5e"), 396 | Icon::Favicon => ("", "#cbcb41"), 397 | Icon::Docker => ("", "#519aba"), 398 | Icon::Gruntfile => ("", "#e37933"), 399 | Icon::Gulpfile => ("", "#cc3e44"), 400 | Icon::Dropbox => ("", "#0061FE"), 401 | Icon::License => ("", "#cbcb41"), 402 | Icon::Procfile => ("", "#a074c4"), 403 | Icon::Jquery => ("", "#1B75BB"), 404 | Icon::Angular => ("", "#E23237"), 405 | Icon::Backbone => ("", "#0071B5"), 406 | Icon::Requirejs => ("", "#F44A41"), 407 | Icon::Materialize => ("", "#EE6E73"), 408 | Icon::Mootools => ("", "#ECECEC"), 409 | Icon::Vagrant => ("", "#1563FF"), 410 | Icon::Svg => ("ﰟ", "#FFB13B"), 411 | Icon::Font => ("", "#999999"), 412 | Icon::Text => ("", "#999999"), 413 | Icon::Archive => ("", "#cc3e44"), 414 | Icon::Unknown => ("", "#999999"), 415 | } 416 | } 417 | } 418 | 419 | pub static GUI_COLORS: &[GuiColor] = &[ 420 | GuiColor::BROWN, 421 | GuiColor::AQUA, 422 | GuiColor::BLUE, 423 | GuiColor::DARKBLUE, 424 | GuiColor::PURPLE, 425 | GuiColor::LIGHTPURPLE, 426 | GuiColor::RED, 427 | GuiColor::BEIGE, 428 | GuiColor::YELLOW, 429 | GuiColor::ORANGE, 430 | GuiColor::DARKORANGE, 431 | GuiColor::PINK, 432 | GuiColor::SALMON, 433 | GuiColor::GREEN, 434 | GuiColor::LIGHTGREEN, 435 | GuiColor::WHITE, 436 | ]; 437 | 438 | pub static ICONS: &[Icon] = &[ 439 | Icon::FolderClosed, 440 | Icon::FolderOpened, 441 | Icon::FolderSymlink, 442 | Icon::File, 443 | Icon::FileSymlink, 444 | Icon::FileHidden, 445 | Icon::Excel, 446 | Icon::Word, 447 | Icon::Ppt, 448 | Icon::Stylus, 449 | Icon::Sass, 450 | Icon::Html, 451 | Icon::Xml, 452 | Icon::Ejs, 453 | Icon::Css, 454 | Icon::Webpack, 455 | Icon::Markdown, 456 | Icon::Json, 457 | Icon::Javascript, 458 | Icon::Javascriptreact, 459 | Icon::Ruby, 460 | Icon::Php, 461 | Icon::Python, 462 | Icon::Coffee, 463 | Icon::Mustache, 464 | Icon::Conf, 465 | Icon::Image, 466 | Icon::Ico, 467 | Icon::Twig, 468 | Icon::C, 469 | Icon::H, 470 | Icon::Haskell, 471 | Icon::Lua, 472 | Icon::Java, 473 | Icon::Terminal, 474 | Icon::Ml, 475 | Icon::Diff, 476 | Icon::Sql, 477 | Icon::Clojure, 478 | Icon::Edn, 479 | Icon::Scala, 480 | Icon::Go, 481 | Icon::Dart, 482 | Icon::Firefox, 483 | Icon::Vs, 484 | Icon::Perl, 485 | Icon::Rss, 486 | Icon::Fsharp, 487 | Icon::Rust, 488 | Icon::Dlang, 489 | Icon::Erlang, 490 | Icon::Elixir, 491 | Icon::Mix, 492 | Icon::Vim, 493 | Icon::Ai, 494 | Icon::Psd, 495 | Icon::Psb, 496 | Icon::Typescript, 497 | Icon::Typescriptreact, 498 | Icon::Julia, 499 | Icon::Puppet, 500 | Icon::Vue, 501 | Icon::Swift, 502 | Icon::Gitconfig, 503 | Icon::Bashrc, 504 | Icon::Favicon, 505 | Icon::Docker, 506 | Icon::Gruntfile, 507 | Icon::Gulpfile, 508 | Icon::Dropbox, 509 | Icon::License, 510 | Icon::Procfile, 511 | Icon::Jquery, 512 | Icon::Angular, 513 | Icon::Backbone, 514 | Icon::Requirejs, 515 | Icon::Materialize, 516 | Icon::Mootools, 517 | Icon::Vagrant, 518 | Icon::Svg, 519 | Icon::Font, 520 | Icon::Text, 521 | Icon::Archive, 522 | Icon::Unknown, 523 | ]; 524 | 525 | fn get_git_indicator(status: Status) -> (&'static str, GuiColor) { 526 | match status { 527 | Status::WT_NEW => ("✭", GuiColor::WHITE), 528 | Status::WT_MODIFIED => ("✹", GuiColor::YELLOW), 529 | Status::INDEX_MODIFIED => ("✚", GuiColor::GREEN), 530 | Status::WT_RENAMED => ("➜", GuiColor::YELLOW), 531 | Status::IGNORED => ("☒", GuiColor::WHITE), 532 | Status::CONFLICTED => ("═", GuiColor::RED), 533 | Status::WT_DELETED => ("✖", GuiColor::RED), 534 | _ => { 535 | info!("Unknown status: {:?}", status); 536 | ("?", GuiColor::WHITE) 537 | } 538 | } 539 | } 540 | 541 | static READ_ONLY_ICON: &'static str = "✗"; 542 | static SELECTED_ICON: &'static str = "✓"; 543 | 544 | #[derive(PartialEq, Eq, Clone, Hash, Debug)] 545 | pub enum ColumnType { 546 | MARK, 547 | INDENT, 548 | GIT, 549 | ICON, 550 | FILENAME, 551 | SIZE, 552 | TIME, 553 | SPACE, 554 | } 555 | 556 | impl From<&str> for ColumnType { 557 | fn from(s: &str) -> Self { 558 | match s { 559 | "mark" => ColumnType::MARK, 560 | "indent" => ColumnType::INDENT, 561 | "git" => ColumnType::GIT, 562 | "icon" => ColumnType::ICON, 563 | "filename" => ColumnType::FILENAME, 564 | "size" => ColumnType::SIZE, 565 | "time" => ColumnType::TIME, 566 | "space" => ColumnType::SPACE, 567 | _ => panic!("Error! unknown column type: {}", s), 568 | } 569 | } 570 | } 571 | 572 | pub enum GuiColor { 573 | BROWN, 574 | AQUA, 575 | BLUE, 576 | DARKBLUE, 577 | PURPLE, 578 | LIGHTPURPLE, 579 | RED, 580 | BEIGE, 581 | YELLOW, 582 | ORANGE, 583 | DARKORANGE, 584 | PINK, 585 | SALMON, 586 | GREEN, 587 | LIGHTGREEN, 588 | WHITE, 589 | } 590 | 591 | impl GuiColor { 592 | pub fn color_val(&self) -> &str { 593 | match *self { 594 | GuiColor::BROWN => "#905532", 595 | GuiColor::AQUA => "#3AFFDB", 596 | GuiColor::BLUE => "#689FB6", 597 | GuiColor::DARKBLUE => "#44788E", 598 | GuiColor::PURPLE => "#834F79", 599 | GuiColor::LIGHTPURPLE => "#834F79", 600 | GuiColor::RED => "#AE403F", 601 | GuiColor::BEIGE => "#F5C06F", 602 | GuiColor::YELLOW => "#F09F17", 603 | GuiColor::ORANGE => "#D4843E", 604 | GuiColor::DARKORANGE => "#F16529", 605 | GuiColor::PINK => "#CB6F6F", 606 | GuiColor::SALMON => "#EE6E73", 607 | GuiColor::GREEN => "#8FAA54", 608 | GuiColor::LIGHTGREEN => "#31B53E", 609 | GuiColor::WHITE => "#FFFFFF", 610 | } 611 | } 612 | 613 | pub fn hl_group_name(&self) -> &str { 614 | match *self { 615 | GuiColor::BROWN => "tree_color_brow", 616 | GuiColor::AQUA => "tree_color_aqua", 617 | GuiColor::BLUE => "tree_color_blue", 618 | GuiColor::DARKBLUE => "tree_color_darkblue", 619 | GuiColor::PURPLE => "tree_color_purple", 620 | GuiColor::LIGHTPURPLE => "tree_color_lightpurple", 621 | GuiColor::RED => "tree_color_red", 622 | GuiColor::BEIGE => "tree_color_beige", 623 | GuiColor::YELLOW => "tree_color_yellow", 624 | GuiColor::ORANGE => "tree_color_orange", 625 | GuiColor::DARKORANGE => "tree_color_darkorange", 626 | GuiColor::PINK => "tree_color_pink", 627 | GuiColor::SALMON => "tree_color_salmon", 628 | GuiColor::GREEN => "tree_color_green", 629 | GuiColor::LIGHTGREEN => "tree_color_lightgreen", 630 | GuiColor::WHITE => "tree_color_white", 631 | } 632 | } 633 | } 634 | 635 | #[derive(Debug)] 636 | pub struct FileItem { 637 | pub path: std::path::PathBuf, 638 | pub metadata: Metadata, 639 | pub level: isize, 640 | pub parent: Option, // the index of the parent in the Tree::fileitems 641 | pub last: bool, 642 | pub id: usize, 643 | // pub git_map: HashMap, 644 | } 645 | pub type FileItemPtr = std::sync::Arc; 646 | 647 | impl FileItem { 648 | pub fn new(path: std::path::PathBuf, metadata: Metadata, id: usize) -> Self { 649 | Self { 650 | path, 651 | metadata, 652 | level: -1, 653 | parent: None, 654 | last: false, 655 | id, 656 | } 657 | } 658 | 659 | pub fn extension(&self) -> Option<&str> { 660 | self.path.extension().and_then(OsStr::to_str) 661 | } 662 | } 663 | 664 | #[derive(Debug)] 665 | pub struct ColumnCell { 666 | pub col_start: usize, 667 | pub col_end: usize, 668 | pub byte_start: usize, 669 | pub byte_end: usize, 670 | pub text: String, 671 | pub hl_group: Option, 672 | } 673 | 674 | impl ColumnCell { 675 | pub fn new(tree: &Tree, fileitem: &FileItem, ty: ColumnType, is_root_cell: bool) -> Self { 676 | let mut text; 677 | let mut hl_group = None; 678 | let path_str = fileitem.path.to_str().unwrap(); 679 | match ty { 680 | ColumnType::MARK => { 681 | if fileitem.metadata.permissions().readonly() { 682 | text = String::from(READ_ONLY_ICON); 683 | hl_group = Some(String::from(GuiColor::BROWN.hl_group_name())) 684 | } else if tree.is_item_selected(fileitem.id) { 685 | text = String::from(SELECTED_ICON); 686 | hl_group = Some(String::from(GuiColor::GREEN.hl_group_name())) 687 | } else { 688 | text = String::from(" "); 689 | } 690 | } 691 | ColumnType::INDENT => { 692 | let mut icon_idx: i32 = -1; 693 | let mut indent_idx: i32 = -1; 694 | for (i, col) in tree.config.columns.iter().enumerate() { 695 | if *col == ColumnType::ICON { 696 | icon_idx = i as i32; 697 | } 698 | if *col == ColumnType::INDENT { 699 | indent_idx = i as i32; 700 | } 701 | } 702 | let margin = icon_idx - indent_idx - 1; 703 | let margin_val = if margin >= 0 { margin as usize } else { 0usize }; 704 | let prefix = unsafe { String::from_utf8_unchecked(vec![b' '; margin_val * 2]) }; 705 | let mut inversed_elements: Vec<&str> = Vec::new(); 706 | if fileitem.level > 0 { 707 | if fileitem.last { 708 | inversed_elements.push("└ "); 709 | } else { 710 | inversed_elements.push("│ "); 711 | } 712 | inversed_elements.push(prefix.as_str()); 713 | let max_level = fileitem.level - 1; 714 | let mut i = 0; 715 | let mut parent = &fileitem.parent; 716 | while let Some(pf) = parent { 717 | if i >= max_level { 718 | break; 719 | } 720 | if pf.last { 721 | inversed_elements.push(" "); 722 | } else { 723 | inversed_elements.push("│ "); 724 | } 725 | inversed_elements.push(prefix.as_str()); 726 | parent = &pf.parent; 727 | i = i + 1; 728 | } 729 | } 730 | text = String::new(); 731 | while let Some(top) = inversed_elements.pop() { 732 | text.push_str(top); 733 | } 734 | } 735 | ColumnType::GIT => { 736 | if let Some(status) = tree.git_map.get(path_str) { 737 | let (icon, color) = get_git_indicator(*status); 738 | text = String::from(icon); 739 | hl_group = Some(color.hl_group_name().to_owned()); 740 | } else { 741 | text = String::from(" "); 742 | } 743 | } 744 | ColumnType::ICON => { 745 | if fileitem.metadata.is_dir() { 746 | text = String::new(); 747 | let dir_opened = tree.is_item_opened(path_str); 748 | if !is_root_cell { 749 | let icon; 750 | if dir_opened { 751 | icon = Icon::FolderOpened; 752 | } else if fileitem.metadata.file_type().is_symlink() { 753 | icon = Icon::FolderSymlink; 754 | } else { 755 | icon = Icon::FolderClosed; 756 | } 757 | hl_group = Some(icon.hl_group_name().to_owned()); 758 | text.push_str(icon.as_glyph_and_color().0); 759 | } 760 | } else { 761 | let extension_icon = match fileitem.extension() { 762 | Some(extension) => Icon::from(extension), 763 | None => Icon::Unknown, 764 | }; 765 | hl_group = Some(extension_icon.hl_group_name().to_owned()); 766 | text = extension_icon.as_glyph_and_color().0.to_owned(); 767 | } 768 | } 769 | ColumnType::FILENAME => { 770 | hl_group = Some(GuiColor::WHITE.hl_group_name().to_owned()); 771 | if is_root_cell { 772 | text = tree.config.root_marker.clone(); 773 | text.push_str(path_str); 774 | } else { 775 | text = String::from(fileitem.path.file_name().and_then(OsStr::to_str).unwrap()); 776 | if fileitem.metadata.is_dir() { 777 | text.push('/'); 778 | hl_group = Some(String::from(GuiColor::BLUE.hl_group_name())); 779 | } 780 | } 781 | } 782 | ColumnType::SIZE => { 783 | if fileitem.metadata.is_dir() { 784 | text = String::from(" "); 785 | } else { 786 | let sz = fileitem.metadata.len(); 787 | text = if sz < 1024 { 788 | format!("{: >4} B ", sz) 789 | } else if 1024 <= sz && sz < 1024 * 1024 { 790 | format!("{: >4} KB", sz >> 10) 791 | } else if 1024 * 1024 <= sz && sz < 1024 * 1024 * 1024 { 792 | format!("{: >4} MB", sz >> 20) 793 | } else if 1024 * 1024 * 1024 <= sz && sz < 1024u64 * 1024 * 1024 * 1024 { 794 | format!("{: >4} GB", sz >> 30) 795 | } else if 1024u64 * 1024 * 1024 * 1024 <= sz 796 | && sz < 1024u64 * 1024 * 1024 * 1024 * 1024 797 | { 798 | format!("{: >4} TB", sz >> 40) 799 | } else { 800 | unreachable!(); 801 | } 802 | } 803 | } 804 | ColumnType::TIME => { 805 | hl_group = Some(GuiColor::BLUE.hl_group_name().to_owned()); 806 | let modified_dt: DateTime = fileitem.metadata.modified().unwrap().into(); 807 | text = format!("{}", modified_dt.format("%Y-%m-%d")); 808 | } 809 | ColumnType::SPACE => { 810 | text = String::from(" "); 811 | } 812 | }; 813 | Self { 814 | col_start: 0, 815 | col_end: 0, 816 | byte_start: 0, 817 | byte_end: 0, 818 | text, 819 | hl_group, 820 | } 821 | } 822 | } 823 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | macro_rules! define_error { 5 | ($($err:ident)*) => { $( 6 | #[derive(Debug)] 7 | pub struct $err { 8 | details: String, 9 | } 10 | 11 | impl $err { 12 | pub fn new(msg: &str) -> Self { 13 | Self { 14 | details: msg.to_string(), 15 | } 16 | } 17 | 18 | pub fn from_string(details: String) -> Self { 19 | Self { 20 | details 21 | } 22 | } 23 | } 24 | 25 | impl fmt::Display for $err { 26 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 27 | write!(f, "{}", self.details) 28 | } 29 | } 30 | 31 | impl Error for $err { 32 | fn description(&self) -> &str { 33 | &self.details 34 | } 35 | } 36 | 37 | )*} 38 | } 39 | 40 | define_error!(ArgError); 41 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use async_std; 2 | use backtrace::Backtrace; 3 | use futures::io::AsyncWrite; 4 | use futures::io::WriteHalf; 5 | use log::*; 6 | 7 | #[cfg(unix)] 8 | use async_std::os::unix::net::UnixStream; 9 | use nvim_rs::{create::async_std as create, Neovim, Value}; 10 | use simplelog::{ConfigBuilder, LevelFilter, WriteLogger}; 11 | use std::env; 12 | use std::error::Error; 13 | mod column; 14 | mod errors; 15 | mod tree; 16 | mod tree_handler; 17 | use tree_handler::TreeHandler; 18 | 19 | fn init_logging() -> Result<(), Box> { 20 | use std::env::VarError; 21 | 22 | let log_level_filter = match env::var("LOG_LEVEL") 23 | .unwrap_or(String::from("info")) 24 | .to_lowercase() 25 | .as_ref() 26 | { 27 | "debug" => LevelFilter::Debug, 28 | "error" => LevelFilter::Error, 29 | "info" => LevelFilter::Info, 30 | "off" => LevelFilter::Off, 31 | "trace" => LevelFilter::Trace, 32 | "warn" => LevelFilter::Warn, 33 | _ => LevelFilter::Off, 34 | }; 35 | 36 | let config = ConfigBuilder::new() 37 | .set_max_level(LevelFilter::Info) 38 | .build(); 39 | 40 | let filepath = match env::var("LOG_FILE") { 41 | Err(err) => match err { 42 | VarError::NotPresent => return Ok(()), 43 | e @ VarError::NotUnicode(_) => { 44 | return Err(Box::new(e)); 45 | } 46 | }, 47 | Ok(path) => path.to_owned(), 48 | }; 49 | 50 | let log_file = std::fs::OpenOptions::new() 51 | .write(true) 52 | .create(true) 53 | .append(true) 54 | .open(filepath)?; 55 | 56 | WriteLogger::init(log_level_filter, config, log_file)?; 57 | 58 | Ok(()) 59 | } 60 | 61 | fn panic_hook() { 62 | use std::panic; 63 | 64 | panic::set_hook(Box::new(|p| { 65 | let backtrace = Backtrace::new(); 66 | error!("panic {:?}\n{:?}", p, backtrace); 67 | })); 68 | } 69 | 70 | /// Initialize the neovim channel 71 | /// It sets up the channel_id and and highlight groups 72 | async fn init_channel(nvim: &Neovim) 73 | where 74 | T: Sync + Send + Unpin + AsyncWrite, 75 | { 76 | let chan = nvim.get_api_info().await.unwrap()[0].as_i64().unwrap(); 77 | debug!("setting chan to {}", chan); 78 | nvim.execute_lua("require('tree').channel_id = ...", vec![Value::from(chan)]) 79 | .await 80 | .unwrap(); 81 | info!("Set chan to {} done!", chan); 82 | 83 | let mut commands = Vec::new(); 84 | for icon in column::ICONS { 85 | let name = icon.hl_group_name(); 86 | let color = icon.as_glyph_and_color().1; 87 | let cmd = format!("hi {} guifg={}", name, color); 88 | commands.push(Value::from(cmd)); 89 | } 90 | 91 | for color in column::GUI_COLORS { 92 | let cmd = format!("hi {} guifg={}", color.hl_group_name(), color.color_val(),); 93 | commands.push(Value::from(cmd)); 94 | } 95 | nvim.execute_lua("require('tree').run_commands_batch(...)", vec![Value::from(commands)]).await.unwrap(); 96 | } 97 | 98 | async fn run(args: Vec) { 99 | debug!("args: {:?}", args); 100 | let server = args[1].clone(); 101 | // create the neovim session with TreeHandler 102 | let (nvim, io_handler) = create::new_unix_socket( 103 | server, 104 | TreeHandler::>::default(), 105 | ) 106 | .await 107 | .unwrap(); 108 | // set tree#_channel_id 109 | init_channel(&nvim).await; 110 | 111 | match io_handler.await { 112 | Err(err) => { 113 | if !err.is_reader_error() { 114 | // One last try, since there wasn't an error with writing to the stream 115 | nvim.err_writeln(&format!("Error: '{}'", err)) 116 | .await 117 | .unwrap_or_else(|e| { 118 | // We could inspect this error to see what was happening, and maybe 119 | // retry, but at this point it's probably best to assume the worst 120 | // and print a friendly and supportive message to our 121 | // users 122 | error!("Well, dang... '{}'", e); 123 | }); 124 | } 125 | 126 | if !err.is_channel_closed() { 127 | // Closed channel usually means neovim quit itself, or this plugin was 128 | // told to quit by closing the channel, so it's not always an error 129 | // condition. 130 | error!("Error: '{}'", err); 131 | } 132 | } 133 | Ok(()) => {} 134 | } 135 | } 136 | 137 | #[async_std::main] 138 | async fn main() { 139 | let _ = init_logging(); 140 | panic_hook(); 141 | let args: Vec = env::args().collect(); 142 | debug!("No fork"); 143 | run(args).await; 144 | debug!("Done!"); 145 | } 146 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::column::ColumnType; 2 | use crate::column::{ColumnCell, FileItem, FileItemPtr}; 3 | use crate::errors::ArgError; 4 | use async_std::sync::{Arc, Mutex, RwLock}; 5 | use fs_extra; 6 | use futures::io::AsyncWrite; 7 | use git2::{Repository, Status}; 8 | use log::*; 9 | use nvim_rs::{ 10 | exttypes::{Buffer, Window}, 11 | Neovim, Value, 12 | }; 13 | use path_clean::PathClean; 14 | use std::cmp::Ordering; 15 | use std::collections::HashMap; 16 | use std::collections::HashSet; 17 | use std::convert::From; 18 | use std::env; 19 | use std::fmt; 20 | use std::fmt::Debug; 21 | use std::io; 22 | use std::path::{Path, PathBuf}; 23 | use unicode_width::UnicodeWidthStr; 24 | 25 | pub fn absolute_path

(path: P) -> io::Result 26 | where 27 | P: AsRef, 28 | { 29 | let path = path.as_ref(); 30 | if path.is_absolute() { 31 | Ok(path.to_path_buf().clean()) 32 | } else { 33 | Ok(env::current_dir()?.join(path).clean()) 34 | } 35 | } 36 | 37 | #[derive(Default, Debug, Clone)] 38 | pub struct Context { 39 | pub cursor: u64, 40 | pub drives: Vec, 41 | pub visual_start: u64, 42 | pub visual_end: u64, 43 | pub prev_bufnr: Option, 44 | } 45 | 46 | impl Context { 47 | pub fn update(&mut self, key: &str, val: Value) { 48 | match key { 49 | "cursor" => match val { 50 | Value::Integer(v) => { 51 | self.cursor = if let Some(v) = v.as_u64() { 52 | v 53 | } else { 54 | error!("Can't convert value {} to u64", val); 55 | return; 56 | } 57 | } 58 | _ => { 59 | error!("Unknown value: {}", val); 60 | } 61 | }, 62 | "visual_start" => match val { 63 | Value::Integer(v) => { 64 | self.visual_start = if let Some(v) = v.as_u64() { 65 | v 66 | } else { 67 | error!("Can't convert value {} to u64", val); 68 | return; 69 | } 70 | } 71 | _ => { 72 | error!("Unknown value: {}", val); 73 | } 74 | }, 75 | "visual_end" => match val { 76 | Value::Integer(v) => { 77 | self.visual_end = if let Some(v) = v.as_u64() { 78 | v 79 | } else { 80 | error!("Can't convert value {} to u64", val); 81 | return; 82 | } 83 | } 84 | _ => { 85 | error!("Unknown value: {}", val); 86 | } 87 | }, 88 | _ => { 89 | warn!("Context: Unsupported member: {}", key); 90 | } 91 | } 92 | } 93 | } 94 | 95 | pub enum ClipboardMode { 96 | COPY, 97 | MOVE, 98 | } 99 | 100 | static CLIPBOARD_MODE: RwLock = RwLock::new(ClipboardMode::COPY); 101 | static CLIPBOARD: RwLock> = RwLock::new(Vec::new()); 102 | 103 | // State parameters for Tree 104 | #[derive(Debug)] 105 | pub struct Config { 106 | pub auto_cd: bool, 107 | pub auto_recursive_level: u16, 108 | pub columns: Vec, 109 | pub ignored_files: String, 110 | pub show_ignored_files: bool, 111 | pub profile: bool, 112 | pub root_marker: String, 113 | 114 | pub search: String, 115 | pub session_file: String, 116 | pub sort: String, 117 | 118 | pub listed: bool, 119 | } 120 | 121 | impl Default for Config { 122 | fn default() -> Self { 123 | Self { 124 | auto_cd: false, 125 | auto_recursive_level: 0, 126 | columns: vec![ 127 | ColumnType::MARK, 128 | ColumnType::INDENT, 129 | ColumnType::GIT, 130 | ColumnType::ICON, 131 | ColumnType::FILENAME, 132 | ColumnType::SIZE, 133 | ColumnType::TIME, 134 | ], 135 | ignored_files: String::new(), 136 | show_ignored_files: false, 137 | profile: false, 138 | root_marker: "[in]: ".to_owned(), 139 | search: String::new(), 140 | session_file: String::new(), 141 | sort: String::new(), 142 | 143 | listed: false, 144 | } 145 | } 146 | } 147 | 148 | fn val_to_u16(v: &Value) -> Result> { 149 | if let Some(v_str) = v.as_str() { 150 | Ok(v_str.parse::()?) 151 | } else { 152 | match v.as_u64() { 153 | Some(v) => Ok(v as u16), 154 | None => Err(Box::new(crate::errors::ArgError::new( 155 | "Type mismatch: str u64", 156 | ))), 157 | } 158 | } 159 | } 160 | 161 | fn val_to_string(v: &Value) -> Result> { 162 | if let Some(v_str) = v.as_str() { 163 | Ok(v_str.to_owned()) 164 | } else { 165 | Err(Box::new(crate::errors::ArgError::new( 166 | "Type mismatch: str expected", 167 | ))) 168 | } 169 | } 170 | 171 | fn val_to_bool(v: &Value) -> Result> { 172 | if let Some(v_str) = v.as_str() { 173 | Ok(v_str.parse::()?) 174 | } else { 175 | match v.as_bool() { 176 | Some(v) => Ok(v), 177 | None => match v.as_i64() { 178 | Some(vi) => Ok(vi == 1), 179 | None => Err(Box::new(crate::errors::ArgError::from_string(format!( 180 | "Type mismatch: bool expected, but {:?} found", 181 | v 182 | )))), 183 | }, 184 | } 185 | } 186 | } 187 | 188 | impl Config { 189 | pub fn update( 190 | &mut self, 191 | cfg: &HashMap, 192 | ) -> Result<(), Box> { 193 | // TODO: handle type mismatch 194 | for (k, v) in cfg { 195 | info!("k: {:?}, v: {:?}", k, v); 196 | match k.as_str() { 197 | "auto_recursive_level" => self.auto_recursive_level = val_to_u16(v)?, 198 | "auto_cd" => { 199 | self.auto_cd = val_to_bool(v).map_err(|e| { 200 | ArgError::from_string(format!("auto_cd need boolean type: {:?}", e)) 201 | })? 202 | } 203 | "listed" => { 204 | self.listed = val_to_bool(v).map_err(|e| { 205 | ArgError::from_string(format!("Config: auto_cd need boolean type: {:?}", e)) 206 | })? 207 | } 208 | "profile" => { 209 | self.profile = val_to_bool(v).map_err(|e| { 210 | ArgError::from_string(format!("profile need boolean type: {:?}", e)) 211 | })? 212 | } 213 | "show_ignored_files" => { 214 | self.show_ignored_files = val_to_bool(v).map_err(|e| { 215 | ArgError::from_string(format!( 216 | "show_ignored_files need boolean type: {:?}", 217 | e 218 | )) 219 | })? 220 | } 221 | "root_marker" => self.root_marker = val_to_string(v)?, 222 | "ignored_files" => self.ignored_files = val_to_string(v)?, 223 | "search" => self.search = val_to_string(v)?, 224 | "session_file" => self.session_file = val_to_string(v)?, 225 | "sort" => self.sort = val_to_string(v)?, 226 | "columns" => { 227 | self.columns.clear(); 228 | for col in match v.as_str() { 229 | Some(v) => v.split(":"), 230 | None => { 231 | return Err(Box::new(crate::errors::ArgError::new("Str type expected"))) 232 | } 233 | } { 234 | // info!("col:{}", col); 235 | self.columns.push(ColumnType::from(col)); 236 | } 237 | } 238 | _ => warn!("Config: Unsupported member: {}", k), 239 | }; 240 | } 241 | Ok(()) 242 | } 243 | } 244 | 245 | const KSTOP: usize = 60; 246 | 247 | pub struct Tree { 248 | pub bufnr: Value, // use bufnr to avoid tedious generic code 249 | pub icon_ns_id: i64, 250 | pub config: Config, 251 | selected_items: HashSet, 252 | file_items: Vec, 253 | expand_store: HashMap, 254 | col_map: HashMap>, 255 | targets: Vec, 256 | cursor_history: HashMap, 257 | git_repo: Option>, 258 | pub git_map: HashMap, 259 | } 260 | 261 | impl Debug for Tree { 262 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 263 | write!( 264 | f, 265 | "Tree {{ bufnr: {:?}, icon_ns_id: {:?}, config: {:?} }}", 266 | self.bufnr, self.icon_ns_id, self.config 267 | ) 268 | } 269 | } 270 | 271 | impl Tree { 272 | pub async fn new( 273 | bufnr: Value, 274 | buf: &Buffer, 275 | nvim: &Neovim, 276 | icon_ns_id: i64, 277 | ) -> Result> { 278 | buf.set_option("ft", Value::from("tree")).await?; 279 | buf.set_option("modifiable", Value::from(false)).await?; 280 | nvim.command("lua tree = require('tree')").await?; 281 | nvim.execute_lua("tree.buf_attach(...)", vec![buf.get_value().clone()]) 282 | .await?; 283 | Ok(Self { 284 | bufnr, 285 | icon_ns_id, 286 | config: Default::default(), 287 | file_items: Default::default(), 288 | expand_store: Default::default(), 289 | col_map: Default::default(), 290 | targets: Default::default(), 291 | cursor_history: Default::default(), 292 | selected_items: Default::default(), 293 | git_repo: None, 294 | git_map: Default::default(), 295 | }) 296 | } 297 | pub fn is_item_opened(&self, path: &str) -> bool { 298 | match self.expand_store.get(path) { 299 | Some(v) => *v, 300 | None => false, 301 | } 302 | } 303 | pub fn is_item_selected(&self, idx: usize) -> bool { 304 | self.selected_items.contains(&idx) 305 | } 306 | pub fn init_git_repo>(&mut self, path: P) { 307 | match Repository::discover(path) { 308 | Ok(repo) => self.git_repo = Some(Mutex::new(repo)), 309 | Err(e) => { 310 | info!("Not a git repo: {:?}", e); 311 | } 312 | } 313 | } 314 | pub fn update_git_map(&mut self) { 315 | if self.git_repo.is_none() { 316 | self.init_git_repo(&self.file_items[0].path.clone()) 317 | } 318 | if let Some(ref mutex) = self.git_repo { 319 | if let Some(ref repo) = mutex.try_lock() { 320 | self.git_map.clear(); 321 | match repo.statuses(None) { 322 | Ok(statuses) => { 323 | let work_dir = repo.workdir().unwrap(); 324 | for status in statuses.iter() { 325 | self.git_map.insert( 326 | work_dir 327 | .join(status.path().unwrap()) 328 | .to_str() 329 | .unwrap() 330 | .to_owned(), 331 | status.status(), 332 | ); 333 | } 334 | info!("git_map: {:?}", self.git_map); 335 | } 336 | Err(e) => error!("Fail to get status: {:?}", e), 337 | } 338 | } else { 339 | info!("We failed the race!"); 340 | } 341 | } else { 342 | info!("Git not enabled"); 343 | } 344 | } 345 | pub async fn action( 346 | &mut self, 347 | nvim: &Neovim, 348 | action: &str, 349 | args: Value, 350 | ctx: Context, 351 | ) { 352 | info!( 353 | "Action: {:?}, \n args: {:?}, \n ctx: {:?}", 354 | action, args, ctx 355 | ); 356 | match match action { 357 | "drop" => self.action_drop(nvim, args, ctx).await, 358 | "open_tree" => self.action_open_tree(nvim, args, ctx).await, 359 | "close_tree" => self.action_close_tree(nvim, args, ctx).await, 360 | "open_or_close_tree" => self.action_open_or_close_tree(nvim, args, ctx).await, 361 | "open_directory" => self.action_open_directory(nvim, args, ctx).await, 362 | "cd" => self.action_cd(nvim, args, ctx).await, 363 | "call" => self.action_call(nvim, args, ctx).await, 364 | "new_file" => self.action_new_file(nvim, args, ctx).await, 365 | "rename" => self.action_rename(nvim, args, ctx).await, 366 | "toggle_select" => self.action_toggle_select(nvim, args, ctx).await, 367 | "remove" => self.action_remove(nvim, args, ctx).await, 368 | "toggle_ignored_files" => self.action_show_ignored(nvim, args, ctx).await, 369 | "yank_path" => self.action_yank_path(nvim, args, ctx).await, 370 | "clear_select_all" => self.action_clear_select_all(nvim, args, ctx).await, 371 | "toggle_select_all" => self.action_toggle_select_all(nvim, args, ctx).await, 372 | "redraw" => self.action_redraw(nvim, args, ctx).await, 373 | "resize" => self.action_resize(nvim, args, ctx).await, 374 | "update_git_map" => self.action_update_git_map(nvim, args, ctx).await, 375 | "copy" => self.action_copy(nvim, args, ctx).await, 376 | "move" => self.action_move(nvim, args, ctx).await, 377 | "paste" => self.action_paste(nvim, args, ctx).await, 378 | _ => { 379 | error!("Unknown action: {}", action); 380 | return; 381 | } 382 | } { 383 | Ok(_) => {} 384 | Err(e) => error!("err: {:?}", e), 385 | } 386 | } 387 | 388 | pub fn save_cursor(&mut self, ctx: &Context) { 389 | if let Some(item) = self.file_items.get(0) { 390 | if let Some(path) = item.path.to_str() { 391 | self.cursor_history.insert(path.to_owned(), ctx.cursor); 392 | } 393 | } 394 | } 395 | 396 | pub async fn cwd_input( 397 | nvim: &Neovim, 398 | cwd: &str, 399 | prompt: &str, 400 | text: &str, 401 | completion: &str, 402 | ) -> Result> { 403 | let save_cwd = nvim.call_function("getcwd", vec![]).await?; 404 | info!("cwd: {:?}", save_cwd); 405 | nvim.call_function("tree#util#cd", vec![Value::from(cwd)]) 406 | .await?; 407 | 408 | let filename = if let Value::String(v) = nvim 409 | .call_function( 410 | "tree#util#input", 411 | vec![ 412 | Value::from(prompt), 413 | Value::from(text), 414 | Value::from(completion), 415 | ], 416 | ) 417 | .await? 418 | { 419 | v.into_str().unwrap() 420 | } else { 421 | return Err(Box::new(ArgError::new("Wrong return type"))); 422 | }; 423 | 424 | nvim.call_function("tree#util#cd", vec![save_cwd]).await?; 425 | 426 | Ok(filename) 427 | } 428 | 429 | pub async fn confirm( 430 | nvim: &Neovim, 431 | question: String, 432 | ) -> Result> { 433 | if let Value::Integer(v) = nvim 434 | .call_function( 435 | "tree#util#confirm", 436 | vec![ 437 | Value::from(question), 438 | Value::from("&Yes\n&No\n&Cancel"), 439 | Value::from(2), 440 | ], 441 | ) 442 | .await? 443 | { 444 | Ok(v.as_i64().unwrap() == 1) 445 | } else { 446 | Err(Box::new(ArgError::new("Invalid return type"))) 447 | } 448 | } 449 | 450 | pub async fn redraw_subtree( 451 | &mut self, 452 | nvim: &Neovim, 453 | parent_idx: usize, 454 | force: bool, 455 | ) -> Result<(), Box> { 456 | let cur = match self.file_items.get(parent_idx) { 457 | Some(c) => c, 458 | None => return Err(Box::new(ArgError::new("Invalid index"))), 459 | } 460 | .clone(); 461 | 462 | let idx = cur.id; 463 | let base_level = cur.level; 464 | let start = cur.id + 1; 465 | let mut end = start; 466 | for fi in &self.file_items[start..] { 467 | if fi.level <= base_level { 468 | break; 469 | } 470 | end += 1; 471 | } 472 | 473 | info!("remove range [{}, {})", start, end); 474 | let new_end; 475 | if force { 476 | self.remove_items_and_cells(start, end)?; 477 | let mut child_items = Vec::new(); 478 | self.entry_info_recursively_sync(cur.clone(), &mut child_items, idx + 1)?; 479 | let child_item_size = child_items.len(); 480 | self.insert_items_and_cells(start, child_items)?; 481 | new_end = start + child_item_size; 482 | } else { 483 | let cells = self.make_cells(&self.file_items[start..end], start == 0); 484 | for (col, cells) in cells { 485 | if !self.col_map.contains_key(&col) { 486 | self.col_map.insert(col.clone(), Vec::new()); 487 | } 488 | self.col_map 489 | .get_mut(&col) 490 | .unwrap() 491 | .splice(start..end, cells); 492 | } 493 | new_end = end; 494 | } 495 | // the new end after adding the new file 496 | info!("redraw range [{}, {})", start, new_end); 497 | // update lines (zero based) 498 | let ret = (start..new_end).map(|i| self.makeline(i)).collect(); 499 | self.buf_set_lines(nvim, start as i64, end as i64, true, ret) 500 | .await?; 501 | self.hl_lines(&nvim, start, new_end).await?; 502 | Ok(()) 503 | } 504 | 505 | pub async fn action_redraw( 506 | &mut self, 507 | nvim: &Neovim, 508 | _arg: Value, 509 | _ctx: Context, 510 | ) -> Result<(), Box> { 511 | self.redraw_subtree(nvim, 0, true).await?; 512 | Ok(()) 513 | } 514 | 515 | pub async fn action_resize( 516 | &mut self, 517 | nvim: &Neovim, 518 | arg: Value, 519 | _ctx: Context, 520 | ) -> Result<(), Box> { 521 | let mut args = match arg { 522 | Value::Array(v) => v, 523 | _ => { 524 | Err(ArgError::new("Invalid arg type"))?; 525 | return Ok(()); 526 | } 527 | }; 528 | if args.is_empty() { 529 | return Ok(()); 530 | } 531 | args.push(self.bufnr.clone()); 532 | info!(" args for resize: {:?}", args); 533 | // nvim.execute_lua("tree.print_message(...)", vec![Value::from("hello".to_owned())]).await?; 534 | nvim.execute_lua("tree.resize(...)", args).await?; 535 | Ok(()) 536 | } 537 | 538 | pub async fn action_yank_path( 539 | &mut self, 540 | nvim: &Neovim, 541 | _arg: Value, 542 | ctx: Context, 543 | ) -> Result<(), Box> { 544 | let paths_str = if self.selected_items.is_empty() { 545 | self.file_items[ctx.cursor as usize - 1] 546 | .path 547 | .to_str() 548 | .unwrap() 549 | .to_owned() 550 | } else { 551 | self.selected_items 552 | .iter() 553 | .map(|x| self.file_items[*x].path.to_str().unwrap().to_owned()) 554 | .collect::>() 555 | .join("\n") 556 | }; 557 | nvim.call_function( 558 | "setreg", 559 | vec![Value::from("+"), Value::from(paths_str.as_str())], 560 | ) 561 | .await?; 562 | nvim.execute_lua("tree.print_message(...)", vec![Value::from(paths_str)]) 563 | .await?; 564 | Ok(()) 565 | } 566 | 567 | pub async fn action_show_ignored( 568 | &mut self, 569 | nvim: &Neovim, 570 | _arg: Value, 571 | _ctx: Context, 572 | ) -> Result<(), Box> { 573 | self.config.show_ignored_files = !self.config.show_ignored_files; 574 | self.redraw_subtree(nvim, 0, true).await?; 575 | Ok(()) 576 | } 577 | 578 | pub async fn action_remove( 579 | &mut self, 580 | nvim: &Neovim, 581 | arg: Value, 582 | ctx: Context, 583 | ) -> Result<(), Box> { 584 | let args = match arg { 585 | Value::Array(v) => v, 586 | _ => { 587 | Err(ArgError::new("Invalid arg type"))?; 588 | return Ok(()); 589 | } 590 | }; 591 | 592 | let force = match args.get(0) { 593 | Some(Value::String(v)) => v.as_str().unwrap() == "true", 594 | _ => false, 595 | }; 596 | let targets: Vec<&FileItem> = if self.selected_items.is_empty() { 597 | vec![&self.file_items[ctx.cursor as usize - 1].as_ref()] 598 | } else { 599 | self.selected_items 600 | .iter() 601 | .map(|x| self.file_items[*x].as_ref()) 602 | .collect() 603 | }; 604 | if !force { 605 | let message = if targets.len() == 1 { 606 | format!( 607 | "Are you sure you want to delete {}?", 608 | targets[0].path.to_str().unwrap() 609 | ) 610 | } else { 611 | format!("Are you sure you want to delete {} files?", targets.len()) 612 | }; 613 | if !Self::confirm(nvim, message).await? { 614 | info!("Remove cancelled"); 615 | return Ok(()); 616 | } 617 | } 618 | for target in targets { 619 | if target.metadata.is_dir() { 620 | std::fs::remove_dir_all(&target.path)?; 621 | } else { 622 | std::fs::remove_file(&target.path)?; 623 | } 624 | } 625 | // redraw the entire tree 626 | self.redraw_subtree(nvim, 0, true).await?; 627 | 628 | Ok(()) 629 | } 630 | pub async fn action_toggle_select( 631 | &mut self, 632 | nvim: &Neovim, 633 | _arg: Value, 634 | ctx: Context, 635 | ) -> Result<(), Box> { 636 | let idx = (ctx.cursor - 1) as usize; 637 | if self.selected_items.contains(&idx) { 638 | self.selected_items.remove(&idx); 639 | } else { 640 | self.selected_items.insert(idx); 641 | } 642 | 643 | // soft redraw a single line 644 | self.update_cells(idx, idx + 1); 645 | let ret = vec![self.makeline(idx)]; 646 | self.buf_set_lines(nvim, idx as i64, idx as i64 + 1, true, ret) 647 | .await?; 648 | self.hl_lines(&nvim, idx, idx + 1).await?; 649 | 650 | Ok(()) 651 | } 652 | 653 | pub async fn action_clear_select_all( 654 | &mut self, 655 | nvim: &Neovim, 656 | _arg: Value, 657 | _ctx: Context, 658 | ) -> Result<(), Box> { 659 | self.selected_items.clear(); 660 | self.redraw_subtree(nvim, 0, false).await?; 661 | Ok(()) 662 | } 663 | 664 | pub async fn action_toggle_select_all( 665 | &mut self, 666 | nvim: &Neovim, 667 | _arg: Value, 668 | _ctx: Context, 669 | ) -> Result<(), Box> { 670 | for i in 0..self.file_items.len() { 671 | if !self.selected_items.remove(&i) { 672 | self.selected_items.insert(i); 673 | } 674 | } 675 | self.redraw_subtree(nvim, 0, false).await?; 676 | Ok(()) 677 | } 678 | 679 | pub async fn action_rename( 680 | &mut self, 681 | nvim: &Neovim, 682 | _arg: Value, 683 | ctx: Context, 684 | ) -> Result<(), Box> { 685 | info!("{:?}", _arg); 686 | let idx = ctx.cursor as usize - 1; 687 | let cur = &self.file_items[idx]; 688 | let old_path = cur.path.to_str().unwrap(); 689 | let cwd = self.file_items[0].path.to_str().unwrap(); 690 | let msg = format!("New name: {} -> ", old_path); 691 | let new_filename = Self::cwd_input(nvim, cwd, &msg, old_path, "file").await?; 692 | if new_filename.is_empty() { 693 | return Ok(()); 694 | } 695 | // let new_path = fs::canonicalize(cur.path.join(new_filename)).await?; 696 | let new_path = cur.path.join(new_filename); 697 | if new_path == cur.path { 698 | return Ok(()); 699 | } 700 | info!("New path: {:?}", new_path); 701 | 702 | if new_path.exists() { 703 | let message = Value::from(format!("{} already exists", new_path.to_str().unwrap())); 704 | nvim.execute_lua("tree.print_message(...)", vec![message]) 705 | .await?; 706 | return Err(Box::new(ArgError::new("File exists!"))); 707 | } 708 | std::fs::rename(&cur.path, new_path)?; 709 | // TODO: no need to redraw the entire tree, we can redraw the parent and the target's 710 | // parent 711 | self.redraw_subtree(nvim, 0, true).await?; 712 | 713 | Ok(()) 714 | } 715 | 716 | pub async fn action_new_file( 717 | &mut self, 718 | nvim: &Neovim, 719 | _arg: Value, 720 | ctx: Context, 721 | ) -> Result<(), Box> { 722 | let idx = ctx.cursor as usize - 1; 723 | let cur = &self.file_items[idx]; 724 | let cur_path_str = cur.path.to_str().unwrap(); 725 | let idx_to_redraw; 726 | // idx == 0 => is_root 727 | let cwd = if self.is_item_opened(cur_path_str) || idx == 0 { 728 | idx_to_redraw = idx; 729 | cur_path_str 730 | } else if let Some(p) = cur.parent.as_ref() { 731 | idx_to_redraw = p.id; 732 | p.path.to_str().unwrap() 733 | } else { 734 | return Err(Box::new(ArgError::new( 735 | "can't find correct position to create new file", 736 | ))); 737 | }; 738 | let new_filename = 739 | Self::cwd_input(nvim, &cwd, "Please input a new filename: ", "", "file").await?; 740 | let is_dir = new_filename.ends_with('/'); 741 | let mut filename = std::path::PathBuf::from(cwd); 742 | filename.push(new_filename); 743 | info!("New file name: {:?}", filename); 744 | let message = Value::from(format!("{} already exists", filename.to_str().unwrap())); 745 | if filename.exists() { 746 | nvim.execute_lua("tree.print_message(...)", vec![message]) 747 | .await?; 748 | return Err(Box::new(ArgError::new("File exists!"))); 749 | } 750 | if is_dir { 751 | std::fs::create_dir(filename)?; 752 | } else { 753 | let mut parent = filename.clone(); 754 | parent.pop(); 755 | std::fs::create_dir_all(parent)?; 756 | std::fs::File::create(filename)?; 757 | } 758 | 759 | self.redraw_subtree(nvim, idx_to_redraw, true).await?; 760 | 761 | Ok(()) 762 | } 763 | pub async fn action_call( 764 | &mut self, 765 | nvim: &Neovim, 766 | arg: Value, 767 | ctx: Context, 768 | ) -> Result<(), Box> { 769 | let args = match arg { 770 | Value::Array(v) => v, 771 | _ => { 772 | Err(ArgError::new("Invalid arg type"))?; 773 | return Ok(()); 774 | } 775 | }; 776 | let func = if let Some(Value::String(v)) = args.get(0) { 777 | v.as_str().unwrap() 778 | } else { 779 | return Err(Box::new(ArgError::new("func not defined"))); 780 | }; 781 | let cur = &self.file_items[ctx.cursor as usize - 1]; 782 | 783 | let ctx = Value::Map(vec![( 784 | Value::from("targets"), 785 | Value::Array(vec![Value::from(cur.path.to_str().unwrap())]), 786 | )]); 787 | nvim.call_function(func, vec![ctx]).await?; 788 | Ok(()) 789 | } 790 | 791 | pub async fn action_cd( 792 | &mut self, 793 | nvim: &Neovim, 794 | arg: Value, 795 | ctx: Context, 796 | ) -> Result<(), Box> { 797 | self.save_cursor(&ctx); 798 | let args = match arg { 799 | Value::Array(v) => v, 800 | _ => { 801 | Err(ArgError::new("Invalid arg type"))?; 802 | return Ok(()); 803 | } 804 | }; 805 | if !args.is_empty() { 806 | let dir = if let Some(d) = args[0].as_str() { 807 | d 808 | } else { 809 | Err(ArgError::new("Dir should be of type String"))?; 810 | return Ok(()); 811 | }; 812 | if dir == ".." { 813 | match self.file_items[0].path.clone().parent() { 814 | Some(p) => self.change_root(p.to_str().unwrap(), nvim).await?, 815 | None => {} 816 | } 817 | } else if dir == "." { 818 | let cur_idx = ctx.cursor as usize - 1; 819 | let cur = match self.file_items.get(cur_idx) { 820 | Some(i) => i, 821 | None => { 822 | Err(ArgError::new("invalid cursor pos"))?; 823 | return Ok(()); 824 | } 825 | }; 826 | let cur_path_str = cur.path.to_str().unwrap(); 827 | let cmd = if self.is_item_opened(cur_path_str) { 828 | format!("cd {}", cur_path_str) 829 | } else { 830 | format!("cd {}", dir) 831 | }; 832 | nvim.command(&cmd).await? 833 | } else { 834 | self.change_root(dir, nvim).await?; 835 | } 836 | } 837 | Ok(()) 838 | } 839 | /// Open like :drop 840 | pub async fn action_update_git_map( 841 | &mut self, 842 | nvim: &Neovim, 843 | _args: Value, 844 | _ctx: Context, 845 | ) -> Result<(), Box> { 846 | if self.config.columns.contains(&ColumnType::GIT) { 847 | self.update_git_map(); 848 | self.redraw_subtree(nvim, 0, false).await?; 849 | } 850 | Ok(()) 851 | } 852 | 853 | /// Open like :drop 854 | pub async fn action_drop( 855 | &mut self, 856 | nvim: &Neovim, 857 | args: Value, 858 | ctx: Context, 859 | ) -> Result<(), Box> { 860 | let info: String; 861 | let should_change_root; 862 | if let Some(cur) = self.file_items.get(ctx.cursor as usize - 1) { 863 | info = cur.path.to_str().unwrap().to_owned(); 864 | if cur.metadata.is_dir() { 865 | should_change_root = true; 866 | } else { 867 | should_change_root = false; 868 | } 869 | } else { 870 | return Err(Box::new(ArgError::new("drop: invalid cursor position"))); 871 | } 872 | if should_change_root { 873 | self.change_root(&info, nvim).await?; 874 | } else { 875 | nvim.execute_lua("tree.drop(...)", vec![args, Value::from(info)]) 876 | .await?; 877 | } 878 | Ok(()) 879 | } 880 | 881 | pub async fn close_tree( 882 | &mut self, 883 | nvim: &Neovim, 884 | idx: usize, 885 | ) -> Result<(), Box> { 886 | // skip the root 887 | if idx == 0 { 888 | return Ok(()); 889 | } 890 | 891 | // get the current 892 | let target = match self.file_items.get(idx) { 893 | Some(fi) => fi, 894 | None => { 895 | return Err(Box::new(ArgError::from_string(format!( 896 | "Index out of bound: {}", 897 | idx 898 | )))); 899 | } 900 | } 901 | .clone(); 902 | let path_str = match target.path.to_str() { 903 | Some(path) => path, 904 | None => { 905 | return Err(Box::new(ArgError::new("filename error"))); 906 | } 907 | }; 908 | let is_opened = match self.expand_store.get(path_str) { 909 | Some(v) => *v, 910 | None => false, 911 | }; 912 | if target.metadata.is_dir() && is_opened { 913 | self.expand_store.remove(path_str); 914 | let start = idx + 1; 915 | let base_level = target.level; 916 | let mut end = start; 917 | for fi in &self.file_items[start..] { 918 | if fi.level <= base_level { 919 | break; 920 | } 921 | end += 1; 922 | } 923 | self.remove_items_and_cells(start, end)?; 924 | self.update_cells(idx, idx + 1); 925 | let ret = vec![self.makeline(idx)]; 926 | self.buf_set_lines(nvim, idx as i64, end as i64, true, ret) 927 | .await?; 928 | self.hl_lines(&nvim, idx, idx + 1).await?; 929 | } 930 | 931 | Ok(()) 932 | } 933 | 934 | pub async fn open_tree( 935 | &mut self, 936 | nvim: &Neovim, 937 | idx: usize, 938 | ) -> Result<(), Box> { 939 | // don't open root item 940 | if idx == 0 { 941 | return Ok(()); 942 | } 943 | let cur = match self.file_items.get(idx) { 944 | Some(fi) => fi, 945 | None => { 946 | return Err(Box::new(ArgError::from_string(format!( 947 | "Index out of bound: {}", 948 | idx 949 | )))); 950 | } 951 | } 952 | .clone(); 953 | let path_str = match cur.path.to_str() { 954 | Some(path) => path, 955 | None => { 956 | return Err(Box::new(ArgError::new("filename error"))); 957 | } 958 | }; 959 | let is_opened = match self.expand_store.get(path_str) { 960 | Some(v) => *v, 961 | None => false, 962 | }; 963 | 964 | if cur.metadata.is_dir() && !is_opened { 965 | let mut child_fileitem = Vec::new(); 966 | self.entry_info_recursively_sync(cur.clone(), &mut child_fileitem, idx + 1)?; 967 | self.expand_store.insert(path_str.to_owned(), true); 968 | // icon should be open 969 | self.update_cells(idx, idx + 1); 970 | let child_item_size = child_fileitem.len(); 971 | self.insert_items_and_cells(idx + 1, child_fileitem)?; 972 | // update lines 973 | let end = idx + child_item_size + 1; 974 | let ret = (idx..end).map(|i| self.makeline(i)).collect(); 975 | self.buf_set_lines(nvim, idx as i64, (idx + 1) as i64, true, ret) 976 | .await?; 977 | self.hl_lines(&nvim, idx, idx + 1 + child_item_size).await?; 978 | } 979 | Ok(()) 980 | } 981 | pub async fn action_close_tree( 982 | &mut self, 983 | nvim: &Neovim, 984 | _args: Value, 985 | ctx: Context, 986 | ) -> Result<(), Box> { 987 | let idx = ctx.cursor as usize - 1; 988 | let target = match self.file_items.get(idx) { 989 | Some(fi) => fi, 990 | None => { 991 | return Err(Box::new(ArgError::from_string(format!( 992 | "item not found: {}", 993 | idx 994 | )))); 995 | } 996 | }; 997 | if target.metadata.is_dir() && self.is_item_opened(target.path.to_str().unwrap()) { 998 | self.close_tree(nvim, idx).await 999 | } else if let Some(p) = target.parent.clone() { 1000 | self.close_tree(nvim, p.id).await?; 1001 | match nvim 1002 | .call("cursor", vec![Value::from(p.id + 1), Value::from(1)]) 1003 | .await? 1004 | { 1005 | Err(e) => error!("{:?}", e), 1006 | _ => {} 1007 | }; 1008 | Ok(()) 1009 | } else { 1010 | Ok(()) 1011 | } 1012 | } 1013 | 1014 | pub async fn action_open_or_close_tree( 1015 | &mut self, 1016 | nvim: &Neovim, 1017 | _args: Value, 1018 | ctx: Context, 1019 | ) -> Result<(), Box> { 1020 | let idx = ctx.cursor as usize - 1; 1021 | let target = match self.file_items.get(idx) { 1022 | Some(fi) => fi, 1023 | None => { 1024 | return Err(Box::new(ArgError::from_string(format!( 1025 | "item not found: {}", 1026 | idx 1027 | )))); 1028 | } 1029 | }; 1030 | 1031 | if target.metadata.is_dir() && self.is_item_opened(target.path.to_str().unwrap()) { 1032 | self.close_tree(nvim, idx).await?; 1033 | } else { 1034 | self.open_tree(nvim, idx).await?; 1035 | } 1036 | Ok(()) 1037 | } 1038 | 1039 | pub async fn action_open_directory( 1040 | &mut self, 1041 | nvim: &Neovim, 1042 | _args: Value, 1043 | ctx: Context, 1044 | ) -> Result<(), Box> { 1045 | let idx = ctx.cursor as usize - 1; 1046 | let target = match self.file_items.get(idx) { 1047 | Some(fi) => fi, 1048 | None => { 1049 | return Err(Box::new(ArgError::from_string(format!( 1050 | "item not found: {}", 1051 | idx 1052 | )))); 1053 | } 1054 | }; 1055 | if target.metadata.is_dir() && idx != 0 { 1056 | let target_path = target.path.to_str().unwrap().to_owned(); 1057 | self.change_root(&target_path, nvim).await?; 1058 | } 1059 | Ok(()) 1060 | } 1061 | 1062 | pub async fn action_open_tree( 1063 | &mut self, 1064 | nvim: &Neovim, 1065 | _args: Value, 1066 | ctx: Context, 1067 | ) -> Result<(), Box> { 1068 | let idx = ctx.cursor as usize - 1; 1069 | self.open_tree(nvim, idx).await 1070 | } 1071 | 1072 | pub fn update_cells(&mut self, sl: usize, el: usize) { 1073 | // self.update_git_map(); 1074 | let cells = self.make_cells(&self.file_items[sl..el], sl == 0); 1075 | for (col, cells) in cells { 1076 | if !self.col_map.contains_key(&col) { 1077 | self.col_map.insert(col.clone(), Vec::new()); 1078 | } 1079 | self.col_map.get_mut(&col).unwrap().splice(sl..el, cells); 1080 | } 1081 | } 1082 | 1083 | pub fn get_context_value(&self, cursor: usize) -> Value { 1084 | let idx = cursor - 1; 1085 | let ft = self.file_items.get(idx).unwrap(); 1086 | info!("get context of: {:?}", ft.path); 1087 | Value::Map(vec![ 1088 | ( 1089 | Value::from("is_directory"), 1090 | Value::from(ft.metadata.is_dir()), 1091 | ), 1092 | ( 1093 | Value::from("is_opened_tree"), 1094 | Value::from(self.is_item_opened(ft.path.to_str().unwrap())), 1095 | ), 1096 | (Value::from("level"), Value::from(ft.level)), 1097 | ]) 1098 | } 1099 | 1100 | pub async fn change_root( 1101 | &mut self, 1102 | path_str: &str, 1103 | nvim: &Neovim, 1104 | ) -> Result<(), Box> { 1105 | let path = std::path::Path::new(path_str); 1106 | if !path.is_dir() { 1107 | return Ok(()); 1108 | } 1109 | let root_path = absolute_path(path)?; 1110 | // if we have loaded git repo previously, we need to update 1111 | // it. Otherwise we won't do a hard reload in the future 1112 | if self.git_repo.is_some() { 1113 | self.init_git_repo(&root_path); 1114 | // self.update_git_map(); 1115 | } 1116 | let root_path_str = if let Some(p) = root_path.to_str() { 1117 | p 1118 | } else { 1119 | return Err(Box::new(ArgError::from_string(format!( 1120 | "Invalid path {:?}", 1121 | root_path 1122 | )))); 1123 | }; 1124 | let last_cursor = match self.cursor_history.get(root_path_str) { 1125 | Some(v) => Some(*v), 1126 | None => None, 1127 | }; 1128 | self.expand_store.insert(root_path_str.to_owned(), true); 1129 | 1130 | self.targets.clear(); 1131 | self.col_map.clear(); 1132 | self.file_items.clear(); 1133 | 1134 | let filemeta = std::fs::metadata(root_path_str)?; 1135 | let mut fileitems = vec![Arc::new(FileItem::new(root_path, filemeta, 0))]; 1136 | 1137 | // recursively what the directory and build up the tree 1138 | self.entry_info_recursively_sync(fileitems[0].clone(), &mut fileitems, 1)?; 1139 | 1140 | self.insert_items_and_cells(0, fileitems)?; 1141 | 1142 | let ret = (0..self.file_items.len()) 1143 | .map(|i| self.makeline(i)) 1144 | .collect(); 1145 | self.buf_set_lines(nvim, 0, -1, true, ret).await?; 1146 | self.hl_lines(&nvim, 0, self.file_items.len()).await?; 1147 | if let Some(v) = last_cursor { 1148 | let win = Window::new(Value::from(0), nvim.clone()); 1149 | let cursor_pos = if v as usize >= self.file_items.len() { 1150 | 0_i64 1151 | } else { 1152 | v as i64 1153 | }; 1154 | match win.set_cursor((cursor_pos, 0)).await { 1155 | Ok(_) => {} 1156 | Err(e) => warn!("Fail to set cursor position {}: {:?}", cursor_pos, e), 1157 | }; 1158 | } 1159 | Ok(()) 1160 | } 1161 | 1162 | fn make_cells( 1163 | &self, 1164 | items: &[FileItemPtr], 1165 | first_item_is_root: bool, 1166 | ) -> Vec<(ColumnType, Vec)> { 1167 | let mut r = Vec::new(); 1168 | for col in &self.config.columns { 1169 | r.push((col.clone(), Vec::new())) 1170 | } 1171 | let mut is_first = true; 1172 | for fileitem in items { 1173 | let mut start = 0; 1174 | let mut byte_start = 0; 1175 | let is_root = first_item_is_root && is_first; 1176 | for i in 0..self.config.columns.len() { 1177 | let col = &self.config.columns[i]; 1178 | let mut cell = ColumnCell::new(self, fileitem, col.clone(), is_root); 1179 | cell.byte_start = byte_start; 1180 | cell.byte_end = byte_start + cell.text.len(); 1181 | cell.col_start = start; 1182 | cell.col_end = start + UnicodeWidthStr::width(cell.text.as_str()); 1183 | // NOTE: alignment 1184 | if *col == ColumnType::FILENAME { 1185 | let stop = KSTOP as i64 - cell.col_end as i64; 1186 | if stop > 0 { 1187 | cell.col_end += stop as usize; 1188 | cell.byte_end += stop as usize; 1189 | } else if is_root && KSTOP > cell.col_start + 5 { 1190 | // TODO: implement this 1191 | } 1192 | } 1193 | let sep = if *col == ColumnType::INDENT { 0 } else { 1 }; 1194 | start = cell.col_end + sep; 1195 | byte_start = cell.byte_end + sep; 1196 | r[i].1.push(cell); 1197 | } 1198 | is_first = false; 1199 | } 1200 | r 1201 | } 1202 | 1203 | /* 1204 | fn remove_cells(&mut self, start: usize, end: usize) { 1205 | for (_, val) in self.col_map.iter_mut() { 1206 | val.splice(start..end, vec![]); 1207 | } 1208 | } 1209 | */ 1210 | 1211 | fn remove_items_and_cells(&mut self, start: usize, end: usize) -> Result<(), ArgError> { 1212 | // remove the items in between 1213 | for (_, val) in self.col_map.iter_mut() { 1214 | val.splice(start..end, vec![]); 1215 | } 1216 | self.file_items.splice(start..end, vec![]); 1217 | for i in start..end { 1218 | self.selected_items.remove(&i); 1219 | } 1220 | 1221 | // items after the deleted 1222 | if start < self.file_items.len() { 1223 | for i in start..self.file_items.len() { 1224 | let fi = self.file_items[i].as_ref(); 1225 | // replace the old id with the new id for selected items 1226 | if self.selected_items.remove(&fi.id) { 1227 | self.selected_items.insert(i); 1228 | } 1229 | // This should be safe here, &mut self (guaranteed by rust) is the only mutable borrowing 1230 | // and fileitem won't live outside self (it's private, and the only place referring 1231 | // fileitem other than the fileitem list is the "parent" field of other fileitem 1232 | unsafe { 1233 | (&mut *(fi as *const FileItem as *mut FileItem)).id = i; 1234 | } 1235 | } 1236 | } 1237 | 1238 | Ok(()) 1239 | } 1240 | 1241 | // insert at the pos 1242 | fn insert_items_and_cells( 1243 | &mut self, 1244 | pos: usize, 1245 | items: Vec, 1246 | ) -> Result<(), ArgError> { 1247 | if pos > self.file_items.len() { 1248 | return Err(ArgError::new("pos larger than the fileitem size")); 1249 | } 1250 | let is_first_item_root = pos == 0; 1251 | // insert items 1252 | let size_to_insert = items.len(); 1253 | self.file_items.splice(pos..pos, items.iter().cloned()); 1254 | // update the indices 1255 | if pos + size_to_insert < self.file_items.len() { 1256 | for i in pos + size_to_insert..self.file_items.len() { 1257 | let fi = self.file_items[i].as_ref(); 1258 | if self.selected_items.remove(&fi.id) { 1259 | self.selected_items.insert(i); 1260 | } 1261 | // This should be safe here, &mut self is the only mutable borrowing 1262 | // and fileitem won't live outside self 1263 | unsafe { 1264 | (&mut *(fi as *const FileItem as *mut FileItem)).id = i; 1265 | } 1266 | } 1267 | } 1268 | 1269 | // make cells 1270 | let cells = self.make_cells(&items, is_first_item_root); 1271 | // insert the cells 1272 | for (col, cells) in cells { 1273 | if !self.col_map.contains_key(&col) { 1274 | self.col_map.insert(col.clone(), Vec::new()); 1275 | } 1276 | self.col_map.get_mut(&col).unwrap().splice(pos..pos, cells); 1277 | } 1278 | Ok(()) 1279 | } 1280 | 1281 | // set the content of the buffer 1282 | async fn buf_set_lines( 1283 | &self, 1284 | nvim: &Neovim, 1285 | start: i64, 1286 | end: i64, 1287 | strict: bool, 1288 | replacement: Vec, 1289 | ) -> Result<(), Box> { 1290 | let buf = Buffer::new(self.bufnr.clone(), nvim.clone()); 1291 | buf.set_option("modifiable", Value::from(true)).await?; 1292 | buf.set_lines(start, end, strict, replacement).await?; 1293 | buf.set_option("modifiable", Value::from(false)).await?; 1294 | Ok(()) 1295 | } 1296 | 1297 | // NOTE: tests show that the sync version is much faster than the async version 1298 | // using tokio::fs 1299 | fn entry_info_recursively_sync<'a>( 1300 | &'a self, 1301 | item: Arc, 1302 | fileitem_lst: &'a mut Vec, 1303 | mut start_id: usize, 1304 | ) -> Result> { 1305 | let mut entries: Vec<_> = std::fs::read_dir(&item.path)? 1306 | .map(|x| x.unwrap()) 1307 | .filter(|x| { 1308 | self.config.show_ignored_files 1309 | || !(x.file_name().to_str().unwrap().starts_with('.')) 1310 | }) 1311 | .map(|x| { 1312 | let meta = x.metadata().unwrap(); 1313 | (x, meta) 1314 | }) 1315 | .collect(); 1316 | entries.sort_by(|l, r| { 1317 | if l.1.is_dir() && !r.1.is_dir() { 1318 | Ordering::Less 1319 | } else if !l.1.is_dir() && r.1.is_dir() { 1320 | Ordering::Greater 1321 | } else { 1322 | l.0.file_name().cmp(&r.0.file_name()) 1323 | } 1324 | }); 1325 | let level = item.level + 1; 1326 | let mut i = 0; 1327 | let count = entries.len(); 1328 | for entry in entries { 1329 | let mut fileitem = FileItem::new(absolute_path(entry.0.path())?, entry.1, start_id); 1330 | start_id += 1; 1331 | fileitem.level = level; 1332 | fileitem.parent = Some(item.clone()); 1333 | if i == count - 1 { 1334 | fileitem.last = true; 1335 | } 1336 | i += 1; 1337 | if let Some(expand) = self.expand_store.get(fileitem.path.to_str().unwrap()) { 1338 | if *expand { 1339 | let ft_ptr = Arc::new(fileitem); 1340 | fileitem_lst.push(ft_ptr.clone()); 1341 | start_id = 1342 | self.entry_info_recursively_sync(ft_ptr.clone(), fileitem_lst, start_id)? 1343 | } else { 1344 | fileitem_lst.push(Arc::new(fileitem)); 1345 | } 1346 | } else { 1347 | fileitem_lst.push(Arc::new(fileitem)); 1348 | } 1349 | } 1350 | Ok(start_id) 1351 | } 1352 | 1353 | /* 1354 | fn entry_info_recursively<'a>( 1355 | &'a self, 1356 | item: Arc, 1357 | fileitem_lst: &'a mut Vec, 1358 | mut start_id: usize, 1359 | ) -> Pin>> + 'a + Send>> { 1360 | Box::pin(async move { 1361 | let mut read_dir = tokio::fs::read_dir(&item.path).await?; 1362 | let mut dir_entries = Vec::new(); 1363 | // filter: dirs, files, no dot and dot dot 1364 | while let Some(entry) = read_dir.next_entry().await? { 1365 | // skip hidden file or dot or dot dot 1366 | if fs_utils::is_dot_or_dotdot(&entry) 1367 | || (!self.config.show_ignored_files && fs_utils::is_hidden(&entry)) 1368 | { 1369 | continue; 1370 | } 1371 | dir_entries.push(entry); 1372 | // entries.push((entry, metadata)); 1373 | } 1374 | if dir_entries.len() <= 0 { 1375 | return Ok(start_id); 1376 | } 1377 | let metadata = futures::future::join_all(dir_entries.iter().map(|x| x.metadata())) 1378 | .await 1379 | .into_iter() 1380 | .map(|x| x.unwrap()); 1381 | let mut entries: Vec<_> = dir_entries.into_iter().zip(metadata.into_iter()).collect(); 1382 | // directory first, name order 1383 | entries.sort_by(|l, r| { 1384 | if l.1.is_dir() && !r.1.is_dir() { 1385 | Ordering::Less 1386 | } else if !l.1.is_dir() && r.1.is_dir() { 1387 | Ordering::Greater 1388 | } else { 1389 | l.0.file_name().cmp(&r.0.file_name()) 1390 | } 1391 | }); 1392 | let level = item.level + 1; 1393 | let mut i = 0; 1394 | let count = entries.len(); 1395 | for entry in entries { 1396 | let mut fileitem = FileItem::new( 1397 | tokio::fs::canonicalize(entry.0.path()).await?, 1398 | entry.1, 1399 | start_id, 1400 | ); 1401 | start_id += 1; 1402 | fileitem.level = level; 1403 | fileitem.parent = Some(item.clone()); 1404 | if i == count - 1 { 1405 | fileitem.last = true; 1406 | } 1407 | i += 1; 1408 | if let Some(expand) = self.expand_store.get(fileitem.path.to_str().unwrap()) { 1409 | if *expand { 1410 | let ft_ptr = Arc::new(fileitem); 1411 | fileitem_lst.push(ft_ptr.clone()); 1412 | start_id = self.entry_info_recursively_sync( 1413 | ft_ptr.clone(), 1414 | fileitem_lst, 1415 | start_id, 1416 | )?; 1417 | } else { 1418 | fileitem_lst.push(Arc::new(fileitem)); 1419 | } 1420 | } else { 1421 | fileitem_lst.push(Arc::new(fileitem)); 1422 | } 1423 | } 1424 | Ok(start_id) 1425 | }) 1426 | } 1427 | */ 1428 | 1429 | fn makeline(&self, pos: usize) -> String { 1430 | let mut start = 0; 1431 | let mut line = String::new(); 1432 | for col in &self.config.columns { 1433 | let cell = &self.col_map[col][pos]; 1434 | unsafe { 1435 | line.push_str(&String::from_utf8_unchecked(vec![ 1436 | b' '; 1437 | cell.col_start - start 1438 | ])); 1439 | } 1440 | line.push_str(&cell.text); 1441 | let len = cell.byte_end - cell.byte_start - cell.text.len(); 1442 | let space_after = unsafe { String::from_utf8_unchecked(vec![b' '; len]) }; 1443 | line.push_str(&space_after); 1444 | start = cell.col_end; 1445 | } 1446 | line 1447 | } 1448 | 1449 | // [sl, el) 1450 | async fn hl_lines( 1451 | &self, 1452 | nvim: &Neovim, 1453 | sl: usize, 1454 | el: usize, 1455 | ) -> Result<(), Box> { 1456 | let mut hl_args = Vec::::new(); 1457 | let icon_ns_id = self.icon_ns_id; 1458 | for i in sl..el { 1459 | for col in &self.config.columns { 1460 | let cell = &self.col_map.get(col).unwrap()[i]; 1461 | if let Some(hl_group) = cell.hl_group.clone() { 1462 | // let buf = Buffer::new(self.bufnr.clone(), nvim.clone()); 1463 | let start = cell.byte_start as i64; 1464 | let end = (cell.byte_start + cell.text.len()) as i64; 1465 | hl_args.push(Value::from(hl_group)); 1466 | hl_args.push(Value::from(start)); 1467 | hl_args.push(Value::from(end)); 1468 | hl_args.push(Value::from(i)); 1469 | // async_std::task::spawn(async move { 1470 | // let hl_group = hl_group; 1471 | // buf.add_highlight(icon_ns_id, &hl_group, i as i64, start, end) 1472 | // .await 1473 | // .unwrap(); 1474 | // }); 1475 | } 1476 | } 1477 | } 1478 | let args = vec![ 1479 | self.bufnr.clone(), 1480 | Value::from(icon_ns_id), 1481 | Value::from(hl_args), 1482 | ]; 1483 | let nvim_c = nvim.clone(); 1484 | async_std::task::spawn(async move { 1485 | nvim_c 1486 | .execute_lua("tree.hl_lines(...)", args) 1487 | .await 1488 | .unwrap(); 1489 | }); 1490 | Ok(()) 1491 | } 1492 | 1493 | pub async fn action_copy( 1494 | &mut self, 1495 | nvim: &Neovim, 1496 | _arg: Value, 1497 | ctx: Context, 1498 | ) -> Result<(), Box> { 1499 | { 1500 | (*CLIPBOARD_MODE.write().await) = ClipboardMode::COPY; 1501 | } 1502 | self.copy_or_move(ctx).await?; 1503 | nvim.execute_lua( 1504 | "tree.print_message(...)", 1505 | vec![Value::from("Copy to clipboard")], 1506 | ) 1507 | .await?; 1508 | Ok(()) 1509 | } 1510 | 1511 | pub async fn action_move( 1512 | &mut self, 1513 | nvim: &Neovim, 1514 | _arg: Value, 1515 | ctx: Context, 1516 | ) -> Result<(), Box> { 1517 | { 1518 | (*CLIPBOARD_MODE.write().await) = ClipboardMode::MOVE; 1519 | } 1520 | self.copy_or_move(ctx).await?; 1521 | nvim.execute_lua( 1522 | "tree.print_message(...)", 1523 | vec![Value::from("Move to clipboard")], 1524 | ) 1525 | .await?; 1526 | Ok(()) 1527 | } 1528 | 1529 | pub async fn copy_or_move(&self, ctx: Context) -> Result<(), Box> { 1530 | let mut clipboard = CLIPBOARD.write().await; 1531 | clipboard.clear(); 1532 | if self.selected_items.is_empty() { 1533 | clipboard.push(self.file_items[ctx.cursor as usize - 1].path.clone()); 1534 | } else { 1535 | clipboard.extend( 1536 | self.selected_items 1537 | .iter() 1538 | .map(|x| self.file_items[*x].path.clone()), 1539 | ) 1540 | } 1541 | 1542 | Ok(()) 1543 | } 1544 | pub async fn action_paste( 1545 | &mut self, 1546 | nvim: &Neovim, 1547 | _arg: Value, 1548 | ctx: Context, 1549 | ) -> Result<(), Box> { 1550 | let clipboard_empty = { CLIPBOARD.read().await.is_empty() }; 1551 | if clipboard_empty { 1552 | nvim.execute_lua( 1553 | "tree.print_message(...)", 1554 | vec![Value::from("Nothing in clipboard")], 1555 | ) 1556 | .await?; 1557 | return Ok(()); 1558 | } 1559 | let items: Vec<_> = { CLIPBOARD.read().await.iter().map(|x| x.clone()).collect() }; 1560 | for item in items { 1561 | if !item.exists() { 1562 | continue; 1563 | } 1564 | let cur = self.file_items[ctx.cursor as usize - 1].as_ref(); 1565 | let dest_fname = item.file_name().unwrap().to_str().unwrap().to_owned(); 1566 | let cur_dir = cur.path.parent().unwrap().to_path_buf(); 1567 | let mut dest_file = cur_dir.clone(); 1568 | dest_file.push(PathBuf::from(dest_fname).as_path()); 1569 | info!("dest_file: {:?}", dest_file); 1570 | if dest_file.exists() { 1571 | let dest_meta = std::fs::metadata(&dest_file)?; 1572 | let src_meta = std::fs::metadata(&item)?; 1573 | let dest = Value::from(vec![ 1574 | ( 1575 | Value::from("mtime"), 1576 | Value::from( 1577 | dest_meta 1578 | .modified()? 1579 | .duration_since(std::time::SystemTime::UNIX_EPOCH)? 1580 | .as_secs(), 1581 | ), 1582 | ), 1583 | ( 1584 | Value::from("path"), 1585 | Value::from(dest_file.as_os_str().to_str().unwrap()), 1586 | ), 1587 | (Value::from("size"), Value::from(dest_meta.len())), 1588 | ]); 1589 | let src = Value::from(vec![ 1590 | ( 1591 | Value::from("mtime"), 1592 | Value::from( 1593 | src_meta 1594 | .modified()? 1595 | .duration_since(std::time::SystemTime::UNIX_EPOCH)? 1596 | .as_secs(), 1597 | ), 1598 | ), 1599 | ( 1600 | Value::from("path"), 1601 | Value::from(item.as_os_str().to_str().unwrap()), 1602 | ), 1603 | (Value::from("size"), Value::from(src_meta.len())), 1604 | ]); 1605 | nvim.execute_lua( 1606 | "tree.pre_paste(...)", 1607 | vec![ 1608 | Value::from(vec![self.bufnr.clone(), Value::from(ctx.cursor as u64 - 1)]), 1609 | src, 1610 | dest, 1611 | ], 1612 | ) 1613 | .await?; 1614 | } else { 1615 | self.func_paste( 1616 | nvim, 1617 | ctx.cursor - 1, 1618 | item.as_os_str().to_str().unwrap(), 1619 | dest_file.as_os_str().to_str().unwrap(), 1620 | ) 1621 | .await?; 1622 | } 1623 | } 1624 | 1625 | Ok(()) 1626 | } 1627 | 1628 | pub async fn func_paste( 1629 | &mut self, 1630 | nvim: &Neovim, 1631 | idx: u64, 1632 | src: &str, 1633 | dest: &str, 1634 | ) -> Result<(), Box> { 1635 | let mode; 1636 | { 1637 | let clipboard_mode = CLIPBOARD_MODE.read().await; 1638 | match *clipboard_mode { 1639 | ClipboardMode::COPY => { 1640 | mode = ClipboardMode::COPY; 1641 | } 1642 | ClipboardMode::MOVE => { 1643 | mode = ClipboardMode::MOVE; 1644 | } 1645 | } 1646 | } 1647 | let from_path = Path::new(src); 1648 | let to_path = Path::new(dest); 1649 | let is_dir = std::fs::metadata(from_path).unwrap().is_dir(); 1650 | match mode { 1651 | ClipboardMode::COPY => { 1652 | if is_dir { 1653 | fs_extra::dir::copy(&from_path, &to_path, &fs_extra::dir::CopyOptions::new())?; 1654 | } else { 1655 | std::fs::copy(from_path, to_path)?; 1656 | } 1657 | let idx_to_redraw = 1658 | if let Some(parent) = self.file_items[idx as usize].parent.as_ref() { 1659 | parent.id 1660 | } else { 1661 | 0 1662 | }; 1663 | self.redraw_subtree(nvim, idx_to_redraw, true).await?; 1664 | } 1665 | ClipboardMode::MOVE => { 1666 | std::fs::rename(from_path, to_path)?; 1667 | self.redraw_subtree(nvim, 0, true).await?; 1668 | } 1669 | } 1670 | Ok(()) 1671 | } 1672 | } 1673 | -------------------------------------------------------------------------------- /src/tree_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ArgError; 2 | use crate::tree::Context; 3 | use crate::tree::Tree; 4 | use async_std::sync::Arc; 5 | use async_std::sync::RwLock; 6 | use async_trait::async_trait; 7 | use futures::io::AsyncWrite; 8 | use log::*; 9 | use nvim_rs::{exttypes::Buffer, Handler, Neovim, Value}; 10 | use std::borrow::BorrowMut; 11 | use std::collections::HashMap; 12 | use std::convert::From; 13 | 14 | fn bufnr_val_to_tuple(val: &Value) -> Option<(i8, Vec)> { 15 | match val { 16 | Value::Integer(v) => Some((0, vec![v.as_u64().unwrap() as u8])), 17 | Value::Ext(v1, v2) => Some((*v1, v2.clone())), 18 | _ => None, 19 | } 20 | } 21 | 22 | // fn tuple_to_bufnr_val(v: &(i8, Vec)) -> Value { 23 | // Value::Ext(v.0.clone(), v.1.clone()) 24 | // } 25 | 26 | #[derive(Default, Debug)] 27 | pub struct TreeHandlerData { 28 | // cfg_map: HashMap, 29 | bufnr_to_tree: HashMap<(i8, Vec), Tree>, 30 | tree_bufs: Vec, // recently used order 31 | // buffer: Option::Writer>>, 32 | buf_count: u32, 33 | prev_bufnr: Option, 34 | } 35 | 36 | type TreeHandlerDataPtr = Arc>; 37 | 38 | /// Handling requests and notifications from neovim 39 | pub struct TreeHandler { 40 | _phantom: std::marker::PhantomData, // ugly, but otherwise the compiler will complain, need to workout a more elegant way 41 | data: TreeHandlerDataPtr, 42 | } 43 | 44 | impl Clone for TreeHandler { 45 | fn clone(&self) -> Self { 46 | Self { 47 | data: self.data.clone(), // the shared state 48 | _phantom: Default::default(), 49 | } 50 | } 51 | } 52 | 53 | impl Default for TreeHandler { 54 | fn default() -> Self { 55 | Self { 56 | data: Arc::new(RwLock::new(TreeHandlerData::default())), 57 | _phantom: Default::default(), 58 | } 59 | } 60 | } 61 | 62 | impl TreeHandler { 63 | async fn create_namespace( 64 | nvim: &Neovim<::Writer>, 65 | ) -> Result> { 66 | let ns_id = nvim.create_namespace("tree_icon").await?; 67 | Ok(ns_id) 68 | } 69 | 70 | async fn create_tree( 71 | data: &mut TreeHandlerData, 72 | nvim: &Neovim<::Writer>, 73 | bufnr: Value, 74 | path: &str, 75 | cfg_map: HashMap, 76 | ) -> Result<(), Box> { 77 | let buf = Buffer::new(bufnr.clone(), nvim.clone()); 78 | // new namespace and new buffer for the new tree 79 | let ns_id = Self::create_namespace(nvim).await?; 80 | 81 | let mut tree = Tree::new(bufnr.clone(), &buf, &nvim, ns_id).await?; 82 | { 83 | tree.config.update(&cfg_map)?; 84 | } 85 | 86 | let start = std::time::Instant::now(); 87 | tree.change_root(path, &nvim).await?; 88 | info!("change root took: {} secs", start.elapsed().as_secs_f64()); 89 | 90 | buf.set_option("buflisted", Value::from(tree.config.listed)) 91 | .await?; 92 | 93 | data.bufnr_to_tree 94 | .insert(bufnr_val_to_tuple(&bufnr).unwrap(), tree); 95 | data.tree_bufs.push(bufnr.clone()); 96 | data.prev_bufnr = Some(bufnr.clone()); 97 | 98 | // let start = std::time::Instant::now(); 99 | // let nvim = nvim.clone(); 100 | // async_std::task::spawn(async move { 101 | nvim.execute_lua("tree.resume(...)", vec![bufnr]) 102 | .await 103 | .unwrap(); 104 | // }); 105 | // info!("resume took: {} secs", start.elapsed().as_secs_f64()); 106 | Ok(()) 107 | } 108 | 109 | /// starts the tree, either create a new one or using the existing one 110 | async fn start_tree( 111 | data: &mut TreeHandlerData, 112 | nvim: &Neovim<::Writer>, 113 | path: String, 114 | cfg_map: HashMap, 115 | ) -> Result<(), Box> { 116 | let bufnr = cfg_map.get("bufnr"); 117 | 118 | if let Some(bufnr) = bufnr { 119 | info!("creating new tree at {}", bufnr); 120 | // let start = std::time::Instant::now(); 121 | Self::create_tree(data, nvim, bufnr.clone(), &path, cfg_map).await?; 122 | // info!("Create tree took {} secs", start.elapsed().as_secs_f64()); 123 | } else { 124 | let bufnr_vals; 125 | { 126 | // only a few items, wouldn't be a problem 127 | let prev_bufnr = match &data.prev_bufnr { 128 | Some(nr) => nr, 129 | None => return Err(Box::new(ArgError::new("prev_bufnr not defined"))), 130 | } 131 | .clone(); 132 | let tree = match data 133 | .bufnr_to_tree 134 | .get_mut(&bufnr_val_to_tuple(&prev_bufnr).unwrap()) 135 | { 136 | Some(t) => t, 137 | None => return Err(Box::new(ArgError::new("unknown tree"))), 138 | }; 139 | tree.config.update(&cfg_map)?; 140 | data.tree_bufs.retain(|v| v != &prev_bufnr); 141 | data.tree_bufs.push(prev_bufnr); 142 | bufnr_vals = Value::Array(data.tree_bufs.iter().rev().cloned().collect()); 143 | } 144 | nvim.execute_lua("tree.resume(...)", vec![bufnr_vals]) 145 | .await?; 146 | } 147 | Ok(()) 148 | } 149 | } 150 | 151 | #[async_trait] 152 | impl Handler for TreeHandler { 153 | type Writer = W; 154 | async fn handle_request( 155 | &self, 156 | name: String, 157 | mut args: Vec, 158 | nvim: Neovim, 159 | ) -> Result { 160 | info!("Request: {}, {:?}", name, args); 161 | 162 | match name.as_ref() { 163 | "_tree_start" => { 164 | let vl = match &mut args[0] { 165 | Value::Array(v) => v, 166 | _ => return Err(Value::from("Error: invalid arg type")), 167 | }; 168 | let context = match vl.pop() { 169 | Some(Value::Map(v)) => v, 170 | _ => return Err(Value::from("Error: invalid arg type")), 171 | }; 172 | let method_args = match vl.pop() { 173 | Some(Value::Array(v)) => v, 174 | _ => return Err(Value::from("Error: invalid arg type")), 175 | }; 176 | if args.len() <= 0 { 177 | return Err(Value::from("Error: path is required for _tree_start")); 178 | } 179 | let mut cfg_map = HashMap::new(); 180 | for (k, v) in context { 181 | let key = match k { 182 | Value::String(v) => v.into_str().unwrap(), 183 | _ => return Err(Value::from(format!("Key should be of type string"))), 184 | }; 185 | cfg_map.insert(key, v); 186 | } 187 | 188 | let path = match &method_args[0] { 189 | Value::String(s) => s.as_str().unwrap().to_owned(), 190 | _ => return Err(Value::from("Error: path should be string")), 191 | }; 192 | info!("path: {}, cfg_map: {:?}", path, cfg_map); 193 | /* 194 | tokio::spawn(async move { 195 | if let Err(e) = Self::start_tree(data, nvim, path, cfg_map).await { 196 | error!("Start tree error: {:?}", e); 197 | }; 198 | }); 199 | Ok(Value::Nil) 200 | */ 201 | let start = std::time::Instant::now(); 202 | { 203 | let mut d = self.data.write().await; 204 | info!("Wait for lock took {} secs", start.elapsed().as_secs_f64()); 205 | match Self::start_tree(d.borrow_mut(), &nvim, path, cfg_map).await { 206 | Err(e) => Err(Value::from(format!("Error: {:?}", e))), 207 | _ => { 208 | info!( 209 | "Start tree took {} secs, at bufnr {:?}", 210 | start.elapsed().as_secs_f64(), 211 | d.prev_bufnr 212 | ); 213 | Ok(Value::Nil) 214 | } 215 | } 216 | } 217 | } 218 | "_tree_get_candidate" => { 219 | let buf = match nvim.get_current_buf().await { 220 | Ok(v) => v, 221 | Err(e) => { 222 | return Err(Value::from(format!("Can't get current buffer: {:?}", e))); 223 | } 224 | }; 225 | let bufnr = match buf.get_value() { 226 | Value::Ext(v0, v1) => (*v0, v1.clone()), 227 | _ => { 228 | return Err(Value::from(format!("Type for current buffer error"))); 229 | } 230 | }; 231 | let cursor = match nvim.call_function("line", vec![Value::from(".")]).await { 232 | Ok(Value::Integer(v)) => match v.as_u64() { 233 | Some(i) => i as usize, 234 | None => { 235 | return Err(Value::from(format!("Type for current line error"))); 236 | } 237 | }, 238 | _ => { 239 | return Err(Value::from(format!("Type for current line error"))); 240 | } 241 | }; 242 | info!("bufnr: {:?}, cursor {}", bufnr, cursor); 243 | let d = self.data.read().await; 244 | if let Some(tree) = d.bufnr_to_tree.get(&bufnr) { 245 | Ok(Value::from(tree.get_context_value(cursor))) 246 | } else { 247 | Err(Value::from("Can't find view")) 248 | } 249 | } 250 | _ => Err(Value::from(format!("Unknown method: {}", name))), 251 | } 252 | } 253 | 254 | async fn handle_notify( 255 | &self, 256 | name: String, 257 | mut args: Vec, 258 | neovim: Neovim, 259 | ) { 260 | info!("Notify {}: {:?}", name, args); 261 | let vl = std::mem::replace(args.get_mut(0).unwrap(), Value::Nil); 262 | let mut vl = match vl { 263 | Value::Array(v) => v, 264 | _ => { 265 | error!("Invalid argument type"); 266 | return; 267 | } 268 | }; 269 | info!("vl: {:?}", vl); 270 | if name == "_tree_async_action" && !args.is_empty() { 271 | if vl.len() != 3 { 272 | error!("Arg num should be 3 but got {}", vl.len()); 273 | } 274 | 275 | let mut ctx = Context::default(); 276 | 277 | // 3rd update context 278 | match vl.pop().unwrap() { 279 | Value::Map(context_val) => { 280 | for (k, v) in context_val { 281 | let key = match k { 282 | Value::String(v) => match v.into_str() { 283 | Some(vv) => vv, 284 | None => { 285 | error!("Can't convert to str"); 286 | continue; 287 | } 288 | }, 289 | _ => { 290 | error!("Key should be of type string"); 291 | continue; 292 | } 293 | }; 294 | ctx.update(&key, v); 295 | } 296 | } 297 | _ => { 298 | error!("Context must be of map"); 299 | return; 300 | } 301 | }; 302 | // 2nd 303 | let act_args = vl.pop().unwrap(); 304 | 305 | let action = match vl.pop().unwrap() { 306 | Value::String(a) => a.into_str().unwrap(), 307 | _ => { 308 | error!("action must be of string type"); 309 | return; 310 | } 311 | }; 312 | 313 | info!("async action: {}", action); 314 | 315 | { 316 | let start = std::time::Instant::now(); 317 | let mut d = self.data.write().await; 318 | info!( 319 | "Waited took {} secs for lock", 320 | start.elapsed().as_secs_f64() 321 | ); 322 | if let Some(bufnr) = d.prev_bufnr.clone() { 323 | if let Some(tree) = d 324 | .bufnr_to_tree 325 | .get_mut(&bufnr_val_to_tuple(&bufnr).unwrap()) 326 | { 327 | let start = std::time::Instant::now(); 328 | tree.action(&neovim, &action, act_args, ctx).await; 329 | info!( 330 | "Action {} took {} secs", 331 | action, 332 | start.elapsed().as_secs_f64() 333 | ); 334 | } 335 | } 336 | } 337 | } 338 | 339 | if name == "_tree_async_func" { 340 | let func_name = args[0].as_str().unwrap(); 341 | if func_name == "paste" { 342 | let fargs = args[1].as_array().unwrap(); 343 | let pos = fargs[0].as_array().unwrap(); 344 | let src = fargs[1].as_str().unwrap(); 345 | let dest = fargs[2].as_str().unwrap(); 346 | let buf = pos[0].as_u64().unwrap(); 347 | let line = pos[1].as_u64().unwrap(); 348 | { 349 | let mut d = self.data.write().await; 350 | if let Some(tree) = d 351 | .bufnr_to_tree 352 | .get_mut(&bufnr_val_to_tuple(&Value::from(buf)).unwrap()) 353 | { 354 | match tree.func_paste(&neovim, line, src, dest).await { 355 | Ok(_) => {} 356 | Err(e) => { 357 | error!("paste error: {:?}", e); 358 | } 359 | } 360 | } 361 | } 362 | } 363 | } 364 | } 365 | } 366 | --------------------------------------------------------------------------------