diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..0ac9c905ac4bd553295572aa14fdec6d190f06ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +/target +/manifest.yml +/migrate.yml diff --git a/.gitignore b/.gitignore index 9a0348bfcef9876e21a3d22dbc3db582aa9c2db6..d096dc01da8a69d51c1a00fc44a85b5f67ebcbd8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /target /zed.xcworkspace .DS_Store +/script/node_modules +/server/.env.toml +/server/static/styles.css diff --git a/Cargo.lock b/Cargo.lock index 2a01fdf7826c0f558ca9ed457cb7ad85cf63ce0c..36c855e9e049d43ba153efc14c711f35fd8aee36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -40,7 +40,7 @@ checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" dependencies = [ "aes-soft", "aesni", - "cipher", + "cipher 0.2.5", ] [[package]] @@ -51,7 +51,7 @@ checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" dependencies = [ "aead", "aes", - "cipher", + "cipher 0.2.5", "ctr", "ghash", "subtle", @@ -63,8 +63,8 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ - "cipher", - "opaque-debug", + "cipher 0.2.5", + "opaque-debug 0.3.0", ] [[package]] @@ -73,8 +73,36 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ - "cipher", - "opaque-debug", + "cipher 0.2.5", + "opaque-debug 0.3.0", +] + +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + +[[package]] +name = "ahash" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796540673305a66d127804eef19ad696f1f204b8c1025aaca4958c17eab32877" +dependencies = [ + "getrandom 0.2.2", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom 0.2.2", + "once_cell", + "version_check", ] [[package]] @@ -86,6 +114,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192ec435945d87bc2f70992b4d818154b5feede43c09fb7592146374eac90a6" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -97,9 +140,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.38" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" [[package]] name = "ar" @@ -125,6 +168,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "async-channel" version = "1.6.1" @@ -136,6 +189,30 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-compression" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443ccbb270374a2b1055fc72da40e1f237809cd6bb0e97e66d264cd138473a6" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "futures-io", + "memchr", + "pin-project-lite 0.2.4", +] + +[[package]] +name = "async-dup" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c" +dependencies = [ + "futures-io", + "simple-mutex", +] + [[package]] name = "async-executor" version = "1.4.0" @@ -177,6 +254,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "async-h1" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5142de15b549749cce62923a50714b0d7b77f5090ced141599e78899865451" +dependencies = [ + "async-channel", + "async-dup", + "async-std", + "byte-pool", + "futures-core", + "http-types", + "httparse", + "lazy_static", + "log", + "pin-project", +] + [[package]] name = "async-io" version = "1.3.1" @@ -243,16 +338,86 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "async-rustls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f38092e8f467f47aadaff680903c7cbfeee7926b058d7f40af2dd4c878fbdee" +dependencies = [ + "futures-lite", + "rustls 0.18.1", + "webpki", +] + +[[package]] +name = "async-rustls" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c86f33abd5a4f3e2d6d9251a9e0c6a7e52eb1113caf893dae8429bf4a53f378" +dependencies = [ + "futures-lite", + "rustls 0.19.1", + "webpki", +] + +[[package]] +name = "async-session" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f" +dependencies = [ + "anyhow", + "async-std", + "async-trait", + "base64 0.12.3", + "bincode", + "blake3", + "chrono", + "hmac 0.8.1", + "kv-log-macro", + "rand 0.7.3", + "serde 1.0.125", + "serde_json 1.0.64", + "sha2", +] + +[[package]] +name = "async-sqlx-session" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b66fb8c6ffbf26cdba6705c36f086b6f02f0b4778b6a348134302a2a00730bc" +dependencies = [ + "async-session", + "async-std", + "sqlx 0.4.2", +] + +[[package]] +name = "async-sse" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10" +dependencies = [ + "async-channel", + "async-std", + "http-types", + "log", + "memchr", + "pin-project-lite 0.1.12", +] + [[package]] name = "async-std" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341" dependencies = [ + "async-attributes", "async-channel", "async-global-executor", "async-io", "async-lock", + "async-process", "crossbeam-utils", "futures-channel", "futures-core", @@ -264,7 +429,7 @@ dependencies = [ "memchr", "num_cpus", "once_cell", - "pin-project-lite", + "pin-project-lite 0.2.4", "pin-utils", "slab", "wasm-bindgen-futures", @@ -283,7 +448,7 @@ checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400" dependencies = [ "futures-core", "futures-io", - "rustls", + "rustls 0.19.1", "webpki", "webpki-roots", ] @@ -309,10 +474,19 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite", + "pin-project-lite 0.2.4", "tungstenite", ] +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits 0.2.14", +] + [[package]] name = "atomic" version = "0.5.0" @@ -383,6 +557,21 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde 1.0.125", +] + [[package]] name = "bindgen" version = "0.58.1" @@ -392,7 +581,7 @@ dependencies = [ "bitflags 1.2.1", "cexpr", "clang-sys", - "clap", + "clap 2.33.3", "env_logger", "lazy_static", "lazycell", @@ -418,6 +607,18 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -429,19 +630,55 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 0.1.10", + "constant_time_eq", + "crypto-mac 0.8.0", + "digest 0.9.0", +] + [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -458,6 +695,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "brotli" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f29919120f08613aadcd4383764e00526fc9f18b6c0895814faeed0dd78613e" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1052e1c3b8d4d80eb84a8b94f0a1498797b5fb96314c001156a1c761940ef4ec" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "0.2.15" @@ -467,12 +725,34 @@ dependencies = [ "memchr", ] +[[package]] +name = "build_const" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" + [[package]] name = "bumpalo" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +[[package]] +name = "byte-pool" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca" +dependencies = [ + "crossbeam-queue", + "stable_deref_trait", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "bytemuck" version = "1.5.1" @@ -523,7 +803,7 @@ dependencies = [ "ar", "cab", "chrono", - "clap", + "clap 2.33.3", "dirs 1.0.5", "error-chain", "glob 0.2.11", @@ -543,6 +823,28 @@ dependencies = [ "walkdir", ] +[[package]] +name = "cargo-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0226944a63d1bf35a3b5f948dd7c59e263db83695c9e8bffc4037de02e30f1d7" +dependencies = [ + "serde 1.0.125", +] + +[[package]] +name = "cargo_metadata" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714a157da7991e23d90686b9524b9e12e0407a108647f52e9328f4b3d51ac7f" +dependencies = [ + "cargo-platform", + "semver 0.11.0", + "semver-parser 0.10.2", + "serde 1.0.125", + "serde_json 1.0.64", +] + [[package]] name = "cc" version = "1.0.67" @@ -555,7 +857,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" dependencies = [ - "nom", + "nom 5.1.2", ] [[package]] @@ -589,6 +891,7 @@ dependencies = [ "libc", "num-integer", "num-traits 0.2.14", + "serde 1.0.125", "time 0.1.44", "winapi 0.3.9", ] @@ -605,7 +908,16 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" dependencies = [ - "generic-array", + "generic-array 0.14.4", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.4", ] [[package]] @@ -629,11 +941,43 @@ dependencies = [ "atty", "bitflags 1.2.1", "strsim 0.8.0", - "textwrap", + "textwrap 0.11.0", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +dependencies = [ + "atty", + "bitflags 1.2.1", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim 0.10.0", + "termcolor", + "textwrap 0.12.1", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -652,6 +996,18 @@ dependencies = [ "cc", ] +[[package]] +name = "coarsetime" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2918e2ffa91a49dabbba4965fe38a37a1ba0b6953a29e32cc250a8d59cd42232" +dependencies = [ + "libc", + "once_cell", + "wasi 0.10.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + [[package]] name = "cocoa" version = "0.24.0" @@ -687,6 +1043,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "comrak" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b423acba50d5016684beaf643f9991e622633a4c858be6885653071c2da2b0c6" +dependencies = [ + "clap 2.33.3", + "entities", + "lazy_static", + "pest", + "pest_derive", + "regex", + "shell-words", + "twoway", + "typed-arena", + "unicode_categories", + "xdg", +] + [[package]] name = "concurrent-queue" version = "1.2.2" @@ -696,6 +1071,12 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "const-oid" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22" + [[package]] name = "const_fn" version = "0.4.8" @@ -717,7 +1098,7 @@ dependencies = [ "aes-gcm", "base64 0.13.0", "hkdf", - "hmac", + "hmac 0.10.1", "percent-encoding", "rand 0.8.3", "sha2", @@ -789,6 +1170,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -855,16 +1245,54 @@ dependencies = [ "loom", ] +[[package]] +name = "crypto-bigint" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4" +dependencies = [ + "generic-array 0.14.4", + "rand_core 0.6.2", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + [[package]] name = "crypto-mac" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ - "generic-array", + "generic-array 0.14.4", "subtle", ] +[[package]] +name = "ct-codecs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + [[package]] name = "ctor" version = "0.1.20" @@ -881,7 +1309,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" dependencies = [ - "cipher", + "cipher 0.2.5", ] [[package]] @@ -960,13 +1388,31 @@ dependencies = [ "byteorder", ] +[[package]] +name = "der" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f215f706081a44cb702c71c39a52c05da637822e9c1645a50b7202689e982d" +dependencies = [ + "const-oid", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -1027,6 +1473,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dtoa" version = "0.4.8" @@ -1051,12 +1503,49 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd4afd79212583ff429b913ad6605242ed7eec277e950b1438f300748f948f4" +[[package]] +name = "ecdsa" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cb0ed2d2ce37766ac86c05f66973ace8c51f7f1533bedce8fb79e2b54b3f14" +dependencies = [ + "der", + "elliptic-curve", + "hmac 0.11.0", + "signature", +] + +[[package]] +name = "ed25519-compact" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf396058cc7285b342f9a10ed7a377f088942396c46c4c9a7eb4f0782cb1171" +dependencies = [ + "getrandom 0.2.2", +] + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "elliptic-curve" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e5c176479da93a0983f0a6fdc3c1b8e7d5be0d7fe3fe05a99f15b96582b9a8" +dependencies = [ + "crypto-bigint", + "ff", + "generic-array 0.14.4", + "group", + "pkcs8", + "rand_core 0.6.2", + "subtle", + "zeroize", +] + [[package]] name = "encoding" version = "0.2.33" @@ -1130,6 +1619,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + [[package]] name = "enum_primitive" version = "0.1.1" @@ -1152,6 +1647,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde 1.0.125", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -1197,6 +1701,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "fastrand" version = "1.4.0" @@ -1206,6 +1716,32 @@ dependencies = [ "instant", ] +[[package]] +name = "femme" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af1a24f391a5a94d756db5092c6576aad494b88a71a5a36b20c67b63e0df034" +dependencies = [ + "cfg-if 0.1.10", + "js-sys", + "log", + "serde 1.0.125", + "serde_derive", + "serde_json 1.0.64", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "ff" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63eec06c61e487eecf0f7e6e6372e596a81922c28d33e645d6983ca6493a1af0" +dependencies = [ + "rand_core 0.6.2", + "subtle", +] + [[package]] name = "filetime" version = "0.2.14" @@ -1371,6 +1907,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futures" version = "0.3.12" @@ -1430,7 +1972,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite", + "pin-project-lite 0.2.4", "waker-fn", ] @@ -1471,7 +2013,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.4", "pin-utils", "proc-macro-hack", "proc-macro-nested", @@ -1491,6 +2033,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.4" @@ -1519,8 +2070,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.10.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1529,7 +2082,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" dependencies = [ - "opaque-debug", + "opaque-debug 0.3.0", "polyval", ] @@ -1593,6 +2146,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-task", + "backtrace", "bindgen", "block", "cc", @@ -1619,7 +2173,6 @@ dependencies = [ "rand 0.8.3", "replace_with", "resvg", - "scoped-pool", "seahash", "serde 1.0.125", "serde_json 1.0.64", @@ -1640,11 +2193,66 @@ dependencies = [ "syn", ] +[[package]] +name = "group" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" +dependencies = [ + "ff", + "rand_core 0.6.2", + "subtle", +] + +[[package]] +name = "handlebars" +version = "3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error", + "serde 1.0.125", + "serde_json 1.0.64", +] + [[package]] name = "hashbrown" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.7", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.4", +] + +[[package]] +name = "hashlink" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" +dependencies = [ + "hashbrown 0.9.1", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] [[package]] name = "heck" @@ -1664,14 +2272,30 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" dependencies = [ - "digest", - "hmac", + "digest 0.9.0", + "hmac 0.10.1", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", ] [[package]] @@ -1680,8 +2304,36 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ - "crypto-mac", - "digest", + "crypto-mac 0.10.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac-sha256" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcdc571e566521512579aab40bf807c5066e1765fb36857f16ed7595c13567c6" +dependencies = [ + "digest 0.9.0", +] + +[[package]] +name = "hmac-sha512" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e806677ce663d0a199541030c816847b36e8dc095f70dae4a4f4ad63da5383" +dependencies = [ + "digest 0.9.0", ] [[package]] @@ -1732,7 +2384,7 @@ dependencies = [ "cookie", "futures-lite", "infer", - "pin-project-lite", + "pin-project-lite 0.2.4", "rand 0.7.3", "serde 1.0.125", "serde_json 1.0.64", @@ -1816,7 +2468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" dependencies = [ "autocfg 1.0.1", - "hashbrown", + "hashbrown 0.9.1", ] [[package]] @@ -1929,6 +2581,40 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt-simple" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5630483dc02fd2274f6d1ec91209811f1a3bc442da4d76a31872d6a02cf9e9" +dependencies = [ + "anyhow", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "rand 0.8.3", + "rsa", + "serde 1.0.125", + "serde_json 1.0.64", + "thiserror", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a26a4a8e8b0ab315c687767b543c923c9667a1f2bf42a42818d1453891c7c1" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa", + "elliptic-curve", + "sha2", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1972,6 +2658,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags 1.2.1", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.98" @@ -2073,12 +2772,29 @@ dependencies = [ "libc", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matches" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + [[package]] name = "md5" version = "0.3.8" @@ -2205,6 +2921,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "nom" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "num-bigint" version = "0.4.0" @@ -2294,6 +3023,38 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e47cfc4c0a1a519d9a025ebfbac3a2439d1b5cdf397d72dcb79b11d9920dab" +dependencies = [ + "base64 0.13.0", + "chrono", + "getrandom 0.2.2", + "http", + "rand 0.8.3", + "serde 1.0.125", + "serde_json 1.0.64", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + +[[package]] +name = "oauth2-surf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a041fdfcfb6aac56f08d021befc1493bde6e6699fef2364e74f0d9adedd27b" +dependencies = [ + "anyhow", + "http", + "oauth2", + "surf", + "thiserror", +] + [[package]] name = "objc" version = "0.2.7" @@ -2325,6 +3086,12 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -2351,12 +3118,29 @@ dependencies = [ ] [[package]] -name = "ordered-float" -version = "2.1.1" +name = "ordered-float" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "766f840da25490628d8e63e529cd21c014f6600c6b8517add12a6fa6167a6218" +dependencies = [ + "num-traits 0.2.14", +] + +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + +[[package]] +name = "p256" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766f840da25490628d8e63e529cd21c014f6600c6b8517add12a6fa6167a6218" +checksum = "d053368e1bae4c8a672953397bd1bd7183dde1c72b0b7612a15719173148d186" dependencies = [ - "num-traits 0.2.14", + "ecdsa", + "elliptic-curve", + "sha2", ] [[package]] @@ -2390,6 +3174,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "password-hash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1a5d4e9c205d2c1ae73b84aab6240e98218c0e72e63b50422cfb2d1ca952282" +dependencies = [ + "base64ct", + "rand_core 0.6.2", + "subtle", +] + [[package]] name = "pathfinder_color" version = "0.5.0" @@ -2418,6 +3213,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "crypto-mac 0.11.0", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -2441,6 +3245,49 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1 0.8.2", +] + [[package]] name = "petgraph" version = "0.5.1" @@ -2515,6 +3362,12 @@ dependencies = [ "syn", ] +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + [[package]] name = "pin-project-lite" version = "0.2.4" @@ -2527,6 +3380,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d156817ae0125e8aa5067710b0db24f0984830614f99875a70aa5e3b74db69" +dependencies = [ + "base64ct", + "der", + "spki", + "zeroize", +] + [[package]] name = "pkg-config" version = "0.3.19" @@ -2595,7 +3460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" dependencies = [ "cpuid-bool", - "opaque-debug", + "opaque-debug 0.3.0", "universal-hash", ] @@ -2621,6 +3486,30 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -2712,6 +3601,12 @@ dependencies = [ "prost 0.7.0 (git+https://github.com/tokio-rs/prost?rev=6cf97ea422b09d98de34643c4dda2d4f8b7e23e6)", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.9" @@ -2721,6 +3616,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.4.6" @@ -3076,6 +3977,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" +[[package]] +name = "route-recognizer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" + [[package]] name = "roxmltree" version = "0.14.1" @@ -3092,7 +3999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ef841a26fc5d040ced0417c6c6a64ee851f42489df11cdf0218e545b6f8d28" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "lazy_static", "num-bigint-dig", "num-integer", @@ -3168,7 +4075,20 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", +] + +[[package]] +name = "rustls" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +dependencies = [ + "base64 0.12.3", + "log", + "ring", + "sct", + "webpki", ] [[package]] @@ -3215,6 +4135,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "salsa20" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7c5f10864beba947e1a1b43f3ef46c8cc58d1c2ae549fa471713e8ff60787a" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3234,13 +4163,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "scoped-pool" -version = "0.0.1" -dependencies = [ - "crossbeam-channel", -] - [[package]] name = "scoped-tls" version = "1.0.0" @@ -3259,6 +4181,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scrypt" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879588d8f90906e73302547e20fffefdd240eb3e0e744e142321f5d49dea0518" +dependencies = [ + "base64ct", + "hmac 0.11.0", + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.6.1" @@ -3281,7 +4217,17 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser", + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.2", + "serde 1.0.125", ] [[package]] @@ -3290,6 +4236,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "0.9.15" @@ -3340,6 +4295,15 @@ dependencies = [ "serde 1.0.125", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f6109f0506e20f7e0f910e51a0079acf41da8e0694e6442527c4ddf5a2b158" +dependencies = [ + "serde 1.0.125", +] + [[package]] name = "serde_qs" version = "0.7.2" @@ -3385,17 +4349,29 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + [[package]] name = "sha-1" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -3416,13 +4392,19 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", ] +[[package]] +name = "shell-words" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" + [[package]] name = "shlex" version = "1.0.0" @@ -3448,12 +4430,31 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.2", +] + [[package]] name = "similar" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" +[[package]] +name = "simple-mutex" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38aabbeafa6f6dead8cebf246fe9fae1f9215c8d29b3a69f93bd62a9e4a3dcd6" +dependencies = [ + "event-listener", +] + [[package]] name = "simple_asn1" version = "0.5.3" @@ -3493,82 +4494,290 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] -name = "slab" -version = "0.4.3" +name = "slab" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + +[[package]] +name = "sluice" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fa0333a60ff2e3474a6775cc611840c2a55610c831dd366503474c02f1a28f5" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "smol" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spinning_top" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75adad84ee84b521fb2cca2d4fd0f1dab1d8d026bda3c5bea4ca63b5f9f9293c" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "987637c5ae6b3121aba9d513f869bd2bff11c4cc086c22473befd6649c0bd521" +dependencies = [ + "der", +] + +[[package]] +name = "sqlformat" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" +dependencies = [ + "lazy_static", + "maplit", + "nom 6.2.1", + "regex", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a98f9bf17b690f026b6fec565293a995b46dfbd6293debcb654dcffd2d1b34" +dependencies = [ + "sqlx-core 0.4.2", + "sqlx-macros 0.4.2", +] + +[[package]] +name = "sqlx" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "ba82f79b31f30acebf19905bcd8b978f46891b9d0723f578447361a8910b6584" +dependencies = [ + "sqlx-core 0.5.5", + "sqlx-macros 0.5.5", +] [[package]] -name = "sluice" -version = "0.5.4" +name = "sqlx-core" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa0333a60ff2e3474a6775cc611840c2a55610c831dd366503474c02f1a28f5" +checksum = "36bb6a2ca3345a86493bc3b71eabc2c6c16a8bb1aa476cf5303bee27f67627d7" dependencies = [ + "ahash 0.6.3", + "atoi", + "base64 0.13.0", + "bitflags 1.2.1", + "byteorder", + "bytes 0.5.6", + "chrono", + "crc", + "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", + "either", "futures-channel", "futures-core", - "futures-io", + "futures-util", + "hashlink 0.6.0", + "hex", + "hmac 0.10.1", + "itoa 0.4.7", + "libc", + "log", + "md-5", + "memchr", + "once_cell", + "parking_lot", + "percent-encoding", + "rand 0.7.3", + "rustls 0.18.1", + "serde 1.0.125", + "serde_json 1.0.64", + "sha-1 0.9.6", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt 0.2.0", + "stringprep", + "thiserror", + "url", + "webpki", + "webpki-roots", + "whoami", ] [[package]] -name = "smallvec" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" - -[[package]] -name = "smol" -version = "1.2.5" +name = "sqlx-core" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4" +checksum = "7f23af36748ec8ea8d49ef8499839907be41b0b1178a4e82b8cb45d29f531dc9" dependencies = [ - "async-channel", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-net", - "async-process", - "blocking", - "futures-lite", + "ahash 0.7.4", + "atoi", + "base64 0.13.0", + "bitflags 1.2.1", + "byteorder", + "bytes 1.0.1", + "crc", + "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", + "dirs 3.0.1", + "either", + "futures-channel", + "futures-core", + "futures-util", + "hashlink 0.7.0", + "hex", + "hmac 0.10.1", + "itoa 0.4.7", + "libc", + "log", + "md-5", + "memchr", "once_cell", + "parking_lot", + "percent-encoding", + "rand 0.8.3", + "rustls 0.19.1", + "serde 1.0.125", + "serde_json 1.0.64", + "sha-1 0.9.6", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt 0.5.5", + "stringprep", + "thiserror", + "url", + "webpki", + "webpki-roots", + "whoami", ] [[package]] -name = "socket2" -version = "0.3.19" +name = "sqlx-macros" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "2b5ada8b3b565331275ce913368565a273a74faf2a34da58c4dc010ce3286844" dependencies = [ - "cfg-if 1.0.0", - "libc", - "winapi 0.3.9", + "cargo_metadata", + "dotenv", + "either", + "futures", + "heck", + "lazy_static", + "proc-macro2", + "quote", + "serde_json 1.0.64", + "sha2", + "sqlx-core 0.4.2", + "sqlx-rt 0.2.0", + "syn", + "url", ] [[package]] -name = "socket2" -version = "0.4.0" +name = "sqlx-macros" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +checksum = "47e4a2349d1ffd60a03ca0de3f116ba55d7f406e55a0d84c64a5590866d94c06" dependencies = [ - "libc", - "winapi 0.3.9", + "dotenv", + "either", + "futures", + "heck", + "once_cell", + "proc-macro2", + "quote", + "sha2", + "sqlx-core 0.5.5", + "sqlx-rt 0.5.5", + "syn", + "url", ] [[package]] -name = "spin" -version = "0.5.2" +name = "sqlx-rt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "63fc5454c9dd7aaea3a0eeeb65ca40d06d0d8e7413a8184f7c3a3ffa5056190b" +dependencies = [ + "async-rustls 0.1.2", + "async-std", +] [[package]] -name = "spinning_top" -version = "0.2.4" +name = "sqlx-rt" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75adad84ee84b521fb2cca2d4fd0f1dab1d8d026bda3c5bea4ca63b5f9f9293c" +checksum = "8199b421ecf3493ee9ef3e7bc90c904844cfb2ea7ea2f57347a93f52bfd3e057" dependencies = [ - "lock_api", + "async-rustls 0.2.0", + "async-std", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "standback" version = "0.2.17" @@ -3633,6 +4842,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.7.0" @@ -3645,6 +4864,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.0" @@ -3667,12 +4892,18 @@ dependencies = [ "log", "mime_guess", "once_cell", - "pin-project-lite", + "pin-project-lite 0.2.4", "serde 1.0.125", "serde_json 1.0.64", "web-sys", ] +[[package]] +name = "sval" +version = "1.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" + [[package]] name = "svg_fmt" version = "0.4.1" @@ -3728,6 +4959,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.33" @@ -3802,6 +5039,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.24" @@ -3831,6 +5077,41 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tide" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0" +dependencies = [ + "async-h1", + "async-session", + "async-sse", + "async-std", + "async-trait", + "femme", + "futures-util", + "http-client", + "http-types", + "kv-log-macro", + "log", + "pin-project-lite 0.2.4", + "route-recognizer", + "serde 1.0.125", + "serde_json 1.0.64", +] + +[[package]] +name = "tide-compress" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d59e3885ecbc547a611d81e501b51bb5f52abd44c3eb3b733ac3c44ff2f2619" +dependencies = [ + "async-compression", + "futures-lite", + "http-types", + "tide", +] + [[package]] name = "time" version = "0.1.44" @@ -3948,7 +5229,7 @@ checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite", + "pin-project-lite 0.2.4", "tracing-attributes", "tracing-core", ] @@ -4028,18 +5309,46 @@ dependencies = [ "input_buffer", "log", "rand 0.8.3", - "sha-1", + "sha-1 0.9.6", "thiserror", "url", "utf-8", ] +[[package]] +name = "twoway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" +dependencies = [ + "memchr", + "unchecked-index", +] + +[[package]] +name = "typed-arena" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" + [[package]] name = "typenum" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unicase" version = "2.6.0" @@ -4115,6 +5424,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unindent" version = "0.1.7" @@ -4127,7 +5442,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" dependencies = [ - "generic-array", + "generic-array 0.14.4", "subtle", ] @@ -4199,6 +5514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" dependencies = [ "ctor", + "sval", "version_check", ] @@ -4262,6 +5578,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" dependencies = [ "cfg-if 1.0.0", + "serde 1.0.125", + "serde_json 1.0.64", "wasm-bindgen-macro", ] @@ -4378,6 +5696,16 @@ dependencies = [ "libc", ] +[[package]] +name = "whoami" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.2.8" @@ -4430,6 +5758,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "xattr" version = "0.2.2" @@ -4439,6 +5773,12 @@ dependencies = [ "libc", ] +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" + [[package]] name = "xmlparser" version = "0.13.3" @@ -4494,28 +5834,46 @@ dependencies = [ "tree-sitter-rust", "unindent", "url", - "zed-rpc", + "zrpc", ] [[package]] -name = "zed-rpc" +name = "zed-server" version = "0.1.0" dependencies = [ "anyhow", - "async-lock", + "async-sqlx-session", + "async-std", + "async-trait", "async-tungstenite", "base64 0.13.0", + "clap 3.0.0-beta.2", + "comrak", + "either", + "envy", "futures", - "log", + "gpui", + "handlebars", + "http-auth-basic", + "jwt-simple", + "lazy_static", + "oauth2", + "oauth2-surf", "parking_lot", "postage", - "prost 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "prost-build", "rand 0.8.3", - "rsa", + "rust-embed", + "scrypt", "serde 1.0.125", - "smol", - "tempdir", + "serde_json 1.0.64", + "sha-1 0.9.6", + "sqlx 0.5.5", + "surf", + "tide", + "tide-compress", + "toml 0.5.8", + "zed", + "zrpc", ] [[package]] @@ -4538,3 +5896,24 @@ dependencies = [ "syn", "synstructure", ] + +[[package]] +name = "zrpc" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-lock", + "async-tungstenite", + "base64 0.13.0", + "futures", + "log", + "parking_lot", + "postage", + "prost 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "prost-build", + "rand 0.8.3", + "rsa", + "serde 1.0.125", + "smol", + "tempdir", +] diff --git a/Cargo.toml b/Cargo.toml index 8c87fd2d417cfbe70e54d65b654478a6a7559976..45734a7dce0f829f5e1dcf1a7c23966cbdcde429 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,9 @@ [workspace] -members = ["zed", "zed-rpc", "gpui", "gpui_macros", "fsevent", "scoped_pool"] +members = ["fsevent", "gpui", "gpui_macros", "server", "zed", "zrpc"] [patch.crates-io] async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "d72771a19f4143530b1cfd23808e344f1276e176" } - # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" } cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6f168a9a91e5081b68dd56a8ab805abf67efd9e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# syntax = docker/dockerfile:1.2 + +FROM rust as builder +WORKDIR app +RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - +RUN apt-get install -y nodejs +COPY . . + +# Install script dependencies +RUN --mount=type=cache,target=./script/node_modules \ + cd ./script && npm install --quiet + +# Build CSS +RUN --mount=type=cache,target=./script/node_modules \ + script/build-css --release + +# Compile server +RUN --mount=type=cache,target=./script/node_modules \ + --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=./target \ + cargo build --release --bin zed-server + +# Copy server binary out of cached directory +RUN --mount=type=cache,target=./target \ + cp /app/target/release/zed-server /app/zed-server + +# Copy server binary to the runtime image +FROM debian:buster-slim as runtime +RUN apt-get update; \ + apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates +WORKDIR app +COPY --from=builder /app/zed-server /app +ENTRYPOINT ["/app/zed-server"] diff --git a/Dockerfile.migrator b/Dockerfile.migrator new file mode 100644 index 0000000000000000000000000000000000000000..76b7e2b729478e8bae1b5ef2e997118f7d431a29 --- /dev/null +++ b/Dockerfile.migrator @@ -0,0 +1,15 @@ +# syntax = docker/dockerfile:1.2 + +FROM rust as builder +WORKDIR app +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=./target \ + cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.5 + +FROM debian:buster-slim as runtime +RUN apt-get update; \ + apt-get install -y --no-install-recommends libssl1.1 +WORKDIR app +COPY --from=builder /app/bin/sqlx /app +COPY ./server/migrations /app/migrations +ENTRYPOINT ["/app/sqlx", "migrate", "run"] diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 4288795d923f58cc05290f5b29d3e374ba9cb3e2..7b8f6f9c15e484406c7c9e5cd1e805ff10003139 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -6,24 +6,24 @@ version = "0.1.0" [dependencies] async-task = "4.0.3" +backtrace = "0.3" ctor = "0.1" etagere = "0.2" -gpui_macros = {path = "../gpui_macros"} +gpui_macros = { path = "../gpui_macros" } log = "0.4" num_cpus = "1.13" ordered-float = "2.1.1" parking_lot = "0.11.1" pathfinder_color = "0.5" pathfinder_geometry = "0.5" -postage = {version = "0.4.1", features = ["futures-traits"]} +postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" replace_with = "0.1.7" resvg = "0.14" -scoped-pool = {path = "../scoped_pool"} seahash = "4.1" -serde = {version = "1.0.125", features = ["derive"]} +serde = { version = "1.0.125", features = ["derive"] } serde_json = "1.0.64" -smallvec = {version = "1.6", features = ["union"]} +smallvec = { version = "1.6", features = ["union"] } smol = "1.2" tiny-skia = "0.5" tree-sitter = "0.19" @@ -45,7 +45,7 @@ cocoa = "0.24" core-foundation = "0.9" core-graphics = "0.22.2" core-text = "19.2" -font-kit = {git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1"} +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" } foreign-types = "0.3" log = "0.4" metal = "0.21.0" diff --git a/gpui/grammars/context-predicate/Cargo.toml b/gpui/grammars/context-predicate/Cargo.toml index 84d18b218013a1f57102c8c7b1ef3e15203fdf0c..9e3316c0f2300f5bcbd48497f8d1db2d6efb2b6b 100644 --- a/gpui/grammars/context-predicate/Cargo.toml +++ b/gpui/grammars/context-predicate/Cargo.toml @@ -7,14 +7,8 @@ categories = ["parsing", "text-editors"] repository = "https://github.com/tree-sitter/tree-sitter-javascript" edition = "2018" license = "MIT" - build = "bindings/rust/build.rs" -include = [ - "bindings/rust/*", - "grammar.js", - "queries/*", - "src/*", -] +include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"] [lib] path = "bindings/rust/lib.rs" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index b81848bfa29e144e61be94563eb58fdcb10c04e9..b356a883f61312aff1cf903beeb14cb7a7749667 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -607,7 +607,6 @@ impl MutableAppContext { values: Default::default(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background, - thread_pool: scoped_pool::Pool::new(num_cpus::get(), "app"), font_cache: Arc::new(FontCache::new(fonts)), }, actions: HashMap::new(), @@ -1485,7 +1484,6 @@ pub struct AppContext { values: RwLock>>, background: Arc, ref_counts: Arc>, - thread_pool: scoped_pool::Pool, font_cache: Arc, } @@ -1530,10 +1528,6 @@ impl AppContext { &self.font_cache } - pub fn thread_pool(&self) -> &scoped_pool::Pool { - &self.thread_pool - } - pub fn value(&self, id: usize) -> ValueHandle { let key = (TypeId::of::(), id); let mut values = self.values.write(); @@ -1716,10 +1710,6 @@ impl<'a, T: Entity> ModelContext<'a, T> { &self.app.cx.background } - pub fn thread_pool(&self) -> &scoped_pool::Pool { - &self.app.cx.thread_pool - } - pub fn halt_stream(&mut self) { self.halt_stream = true; } diff --git a/gpui/src/executor.rs b/gpui/src/executor.rs index 19f2803cb3e74210034ae0e9920aef60d5322ccc..a7c2eaaecc92c6668578c1a6b10bcf426cc6ad8e 100644 --- a/gpui/src/executor.rs +++ b/gpui/src/executor.rs @@ -1,17 +1,19 @@ use anyhow::{anyhow, Result}; use async_task::Runnable; pub use async_task::Task; +use backtrace::{Backtrace, BacktraceFmt, BytesOrWideString}; use parking_lot::Mutex; use rand::prelude::*; use smol::{channel, prelude::*, Executor}; use std::{ + fmt::{self, Debug}, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, - mpsc::SyncSender, + mpsc::Sender, Arc, }, thread, @@ -32,7 +34,6 @@ pub enum Background { Deterministic(Arc), Production { executor: Arc>, - threads: usize, _stop: channel::Sender<()>, }, } @@ -40,9 +41,9 @@ pub enum Background { struct DeterministicState { rng: StdRng, seed: u64, - scheduled: Vec, - spawned_from_foreground: Vec, - waker: Option>, + scheduled: Vec<(Runnable, Backtrace)>, + spawned_from_foreground: Vec<(Runnable, Backtrace)>, + waker: Option>, } pub struct Deterministic(Arc>); @@ -63,14 +64,16 @@ impl Deterministic { T: 'static, F: Future + 'static, { + let backtrace = Backtrace::new_unresolved(); let scheduled_once = AtomicBool::new(false); let state = self.0.clone(); let (runnable, task) = async_task::spawn_local(future, move |runnable| { let mut state = state.lock(); + let backtrace = backtrace.clone(); if scheduled_once.fetch_or(true, SeqCst) { - state.scheduled.push(runnable); + state.scheduled.push((runnable, backtrace)); } else { - state.spawned_from_foreground.push(runnable); + state.spawned_from_foreground.push((runnable, backtrace)); } if let Some(waker) = state.waker.as_ref() { waker.send(()).ok(); @@ -85,10 +88,11 @@ impl Deterministic { T: 'static + Send, F: 'static + Send + Future, { + let backtrace = Backtrace::new_unresolved(); let state = self.0.clone(); let (runnable, task) = async_task::spawn(future, move |runnable| { let mut state = state.lock(); - state.scheduled.push(runnable); + state.scheduled.push((runnable, backtrace.clone())); if let Some(waker) = state.waker.as_ref() { waker.send(()).ok(); } @@ -102,7 +106,7 @@ impl Deterministic { T: 'static, F: Future + 'static, { - let (wake_tx, wake_rx) = std::sync::mpsc::sync_channel(32); + let (wake_tx, wake_rx) = std::sync::mpsc::channel(); let state = self.0.clone(); state.lock().waker = Some(wake_tx); @@ -113,6 +117,7 @@ impl Deterministic { }) .detach(); + let mut trace = Trace::default(); loop { if let Ok(value) = output_rx.try_recv() { state.lock().waker = None; @@ -126,9 +131,13 @@ impl Deterministic { .rng .gen_range(0..state.scheduled.len() + state.spawned_from_foreground.len()); if ix < state.scheduled.len() { - state.scheduled.remove(ix) + let (_, backtrace) = &state.scheduled[ix]; + trace.record(&state, backtrace.clone()); + state.scheduled.remove(ix).0 } else { - state.spawned_from_foreground.remove(0) + let (_, backtrace) = &state.spawned_from_foreground[0]; + trace.record(&state, backtrace.clone()); + state.spawned_from_foreground.remove(0).0 } }; @@ -137,6 +146,129 @@ impl Deterministic { } } +#[derive(Default)] +struct Trace { + executed: Vec, + scheduled: Vec>, + spawned_from_foreground: Vec>, +} + +impl Trace { + fn record(&mut self, state: &DeterministicState, executed: Backtrace) { + self.scheduled.push( + state + .scheduled + .iter() + .map(|(_, backtrace)| backtrace.clone()) + .collect(), + ); + self.spawned_from_foreground.push( + state + .spawned_from_foreground + .iter() + .map(|(_, backtrace)| backtrace.clone()) + .collect(), + ); + self.executed.push(executed); + } + + fn resolve(&mut self) { + for backtrace in &mut self.executed { + backtrace.resolve(); + } + + for backtraces in &mut self.scheduled { + for backtrace in backtraces { + backtrace.resolve(); + } + } + + for backtraces in &mut self.spawned_from_foreground { + for backtrace in backtraces { + backtrace.resolve(); + } + } + } +} + +impl Debug for Trace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct FirstCwdFrameInBacktrace<'a>(&'a Backtrace); + + impl<'a> Debug for FirstCwdFrameInBacktrace<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + let cwd = std::env::current_dir().unwrap(); + let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| { + fmt::Display::fmt(&path, fmt) + }; + let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path); + for frame in self.0.frames() { + let mut formatted_frame = fmt.frame(); + if frame + .symbols() + .iter() + .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd))) + { + formatted_frame.backtrace_frame(frame)?; + break; + } + } + fmt.finish() + } + } + + for ((backtrace, scheduled), spawned_from_foreground) in self + .executed + .iter() + .zip(&self.scheduled) + .zip(&self.spawned_from_foreground) + { + writeln!(f, "Scheduled")?; + for backtrace in scheduled { + writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?; + } + if scheduled.is_empty() { + writeln!(f, "None")?; + } + writeln!(f, "==========")?; + + writeln!(f, "Spawned from foreground")?; + for backtrace in spawned_from_foreground { + writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?; + } + if spawned_from_foreground.is_empty() { + writeln!(f, "None")?; + } + writeln!(f, "==========")?; + + writeln!(f, "Run: {:?}", FirstCwdFrameInBacktrace(backtrace))?; + writeln!(f, "+++++++++++++++++++")?; + } + + Ok(()) + } +} + +impl Drop for Trace { + fn drop(&mut self) { + let trace_on_panic = if let Ok(trace_on_panic) = std::env::var("EXECUTOR_TRACE_ON_PANIC") { + trace_on_panic == "1" || trace_on_panic == "true" + } else { + false + }; + let trace_always = if let Ok(trace_always) = std::env::var("EXECUTOR_TRACE_ALWAYS") { + trace_always == "1" || trace_always == "true" + } else { + false + }; + + if trace_always || (trace_on_panic && thread::panicking()) { + self.resolve(); + dbg!(self); + } + } +} + impl Foreground { pub fn platform(dispatcher: Arc) -> Result { if dispatcher.is_main_thread() { @@ -191,9 +323,8 @@ impl Background { pub fn new() -> Self { let executor = Arc::new(Executor::new()); let stop = channel::unbounded::<()>(); - let threads = num_cpus::get(); - for i in 0..threads { + for i in 0..2 * num_cpus::get() { let executor = executor.clone(); let stop = stop.1.clone(); thread::Builder::new() @@ -204,16 +335,12 @@ impl Background { Self::Production { executor, - threads, _stop: stop.0, } } - pub fn threads(&self) -> usize { - match self { - Self::Deterministic(_) => 1, - Self::Production { threads, .. } => *threads, - } + pub fn num_cpus(&self) -> usize { + num_cpus::get() } pub fn spawn(&self, future: F) -> Task diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 216ed79b327039225be7421ffb850a796bfbf616..b8f77bcebd5f8e789cea2779366d7d58b7318ac9 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -30,4 +30,3 @@ pub use presenter::{ AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; -pub use scoped_pool; diff --git a/gpui_macros/Cargo.toml b/gpui_macros/Cargo.toml index b718e953da48b36ab422b4e43af47a982ac70a26..a5d7373463b8d50b5e205cba1e96eb738602a5a8 100644 --- a/gpui_macros/Cargo.toml +++ b/gpui_macros/Cargo.toml @@ -9,4 +9,4 @@ proc-macro = true [dependencies] syn = "1.0" quote = "1.0" -proc-macro2 = "1.0" \ No newline at end of file +proc-macro2 = "1.0" diff --git a/scoped_pool/Cargo.toml b/scoped_pool/Cargo.toml deleted file mode 100644 index a2e5a1206f758c2054fa5f6c6570f2366867c02d..0000000000000000000000000000000000000000 --- a/scoped_pool/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "scoped-pool" -version = "0.0.1" -license = "MIT" -edition = "2018" - -[dependencies] -crossbeam-channel = "0.5" diff --git a/scoped_pool/src/lib.rs b/scoped_pool/src/lib.rs deleted file mode 100644 index da4d193e07cee1b84157bc166bcc9a0d40ead92a..0000000000000000000000000000000000000000 --- a/scoped_pool/src/lib.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crossbeam_channel as chan; -use std::{marker::PhantomData, mem::transmute, thread}; - -#[derive(Clone)] -pub struct Pool { - req_tx: chan::Sender, - thread_count: usize, -} - -pub struct Scope<'a> { - req_count: usize, - req_tx: chan::Sender, - resp_tx: chan::Sender<()>, - resp_rx: chan::Receiver<()>, - phantom: PhantomData<&'a ()>, -} - -struct Request { - callback: Box, - resp_tx: chan::Sender<()>, -} - -impl Pool { - pub fn new(thread_count: usize, name: impl AsRef) -> Self { - let (req_tx, req_rx) = chan::unbounded(); - for i in 0..thread_count { - thread::Builder::new() - .name(format!("scoped_pool {} {}", name.as_ref(), i)) - .spawn({ - let req_rx = req_rx.clone(); - move || loop { - match req_rx.recv() { - Err(_) => break, - Ok(Request { callback, resp_tx }) => { - callback(); - resp_tx.send(()).ok(); - } - } - } - }) - .expect("scoped_pool: failed to spawn thread"); - } - Self { - req_tx, - thread_count, - } - } - - pub fn thread_count(&self) -> usize { - self.thread_count - } - - pub fn scoped<'scope, F, R>(&self, scheduler: F) -> R - where - F: FnOnce(&mut Scope<'scope>) -> R, - { - let (resp_tx, resp_rx) = chan::bounded(1); - let mut scope = Scope { - resp_tx, - resp_rx, - req_count: 0, - phantom: PhantomData, - req_tx: self.req_tx.clone(), - }; - let result = scheduler(&mut scope); - scope.wait(); - result - } -} - -impl<'scope> Scope<'scope> { - pub fn execute(&mut self, callback: F) - where - F: FnOnce() + Send + 'scope, - { - // Transmute the callback's lifetime to be 'static. This is safe because in ::wait, - // we block until all the callbacks have been called and dropped. - let callback = unsafe { - transmute::, Box>( - Box::new(callback), - ) - }; - - self.req_count += 1; - self.req_tx - .send(Request { - callback, - resp_tx: self.resp_tx.clone(), - }) - .unwrap(); - } - - fn wait(&self) { - for _ in 0..self.req_count { - self.resp_rx.recv().unwrap(); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::{Arc, Mutex}; - - #[test] - fn test_execute() { - let pool = Pool::new(3, "test"); - - { - let vec = Mutex::new(Vec::new()); - pool.scoped(|scope| { - for _ in 0..3 { - scope.execute(|| { - for i in 0..5 { - vec.lock().unwrap().push(i); - } - }); - } - }); - - let mut vec = vec.into_inner().unwrap(); - vec.sort_unstable(); - assert_eq!(vec, [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) - } - } - - #[test] - fn test_clone_send_and_execute() { - let pool = Pool::new(3, "test"); - - let mut threads = Vec::new(); - for _ in 0..3 { - threads.push(thread::spawn({ - let pool = pool.clone(); - move || { - let vec = Mutex::new(Vec::new()); - pool.scoped(|scope| { - for _ in 0..3 { - scope.execute(|| { - for i in 0..5 { - vec.lock().unwrap().push(i); - } - }); - } - }); - let mut vec = vec.into_inner().unwrap(); - vec.sort_unstable(); - assert_eq!(vec, [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) - } - })); - } - - for thread in threads { - thread.join().unwrap(); - } - } - - #[test] - fn test_share_and_execute() { - let pool = Arc::new(Pool::new(3, "test")); - - let mut threads = Vec::new(); - for _ in 0..3 { - threads.push(thread::spawn({ - let pool = pool.clone(); - move || { - let vec = Mutex::new(Vec::new()); - pool.scoped(|scope| { - for _ in 0..3 { - scope.execute(|| { - for i in 0..5 { - vec.lock().unwrap().push(i); - } - }); - } - }); - let mut vec = vec.into_inner().unwrap(); - vec.sort_unstable(); - assert_eq!(vec, [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) - } - })); - } - - for thread in threads { - thread.join().unwrap(); - } - } -} diff --git a/script/build-css b/script/build-css new file mode 100755 index 0000000000000000000000000000000000000000..77be6635a40d0c310cc2fad29fcce48adf643a16 --- /dev/null +++ b/script/build-css @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +cd ./script +[ -d node_modules ] || npm install +if [[ $1 == --release ]]; then + export NODE_ENV=production # Purge unused styles in --release mode +fi +npx tailwindcss build ../server/styles.css --output ../server/static/styles.css diff --git a/script/deploy b/script/deploy new file mode 100755 index 0000000000000000000000000000000000000000..1e648336d44a8d4e7f99c5164ecda87357ec3f3f --- /dev/null +++ b/script/deploy @@ -0,0 +1,17 @@ +#!/bin/bash + +# Prerequisites: +# +# - Log in to the DigitalOcean docker registry +# doctl registry login +# +# - Set the default K8s context to production +# doctl kubernetes cluster kubeconfig save zed-1 + +set -e + +IMAGE_ID=registry.digitalocean.com/zed/zed-server + +docker build . --tag $IMAGE_ID +docker push $IMAGE_ID +kubectl rollout restart deployment zed diff --git a/script/deploy-migration b/script/deploy-migration new file mode 100755 index 0000000000000000000000000000000000000000..251e7a4518cac6f4c27cbf1f5d92a8dfb79c9852 --- /dev/null +++ b/script/deploy-migration @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +IMAGE_ID=registry.digitalocean.com/zed/zed-migrator + +docker build . \ + --file ./Dockerfile.migrator \ + --tag $IMAGE_ID +docker push $IMAGE_ID +kubectl apply -f ./server/migrate.yml diff --git a/script/package-lock.json b/script/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..ba5f9c360e6fbb97c2afe4fae890acd53da45b63 --- /dev/null +++ b/script/package-lock.json @@ -0,0 +1,2452 @@ +{ + "name": "script", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/typography": "^0.4.0", + "tailwindcss-cli": "^0.1.2" + } + }, + "node_modules/@fullhuman/postcss-purgecss": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", + "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==", + "dev": true, + "dependencies": { + "purgecss": "^3.1.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.4.0.tgz", + "integrity": "sha512-3BfOYT5MYNEq81Ism3L2qu/HRP2Q5vWqZtZRQqQrthHuaTK9qpuPfbMT5WATjAM5J1OePKBaI5pLoX4S1JGNMQ==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "peerDependencies": { + "tailwindcss": "2.0.0-alpha.24 || ^2.0.0" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz", + "integrity": "sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA==", + "dev": true, + "dependencies": { + "browserslist": "^4.16.3", + "caniuse-lite": "^1.0.30001196", + "colorette": "^1.2.2", + "fraction.js": "^4.0.13", + "normalize-range": "^0.1.2", + "postcss-value-parser": "^4.1.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001228", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", + "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "node_modules/detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "dependencies": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", + "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.3.728", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.728.tgz", + "integrity": "sha512-SHv4ziXruBpb1Nz4aTuqEHBYi/9GNCJMYIJgDEXrp/2V01nFXMNFUTli5Z85f5ivSkioLilQatqBYFB44wNJrA==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.0.tgz", + "integrity": "sha512-o9lSKpK0TDqDwTL24Hxqi6I99s942l6TYkfl6WvGWgLOIFz/YonSGKfiSeMadoiNvTfqnfOa9mjb5SGVbBK9/w==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/glob-base/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "dev": true, + "dependencies": { + "lodash.toarray": "^4.4.0" + } + }, + "node_modules/node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", + "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.2.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz", + "integrity": "sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==", + "dev": true, + "dependencies": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map": "^0.6.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-functions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", + "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=", + "dev": true, + "dependencies": { + "glob": "^7.1.2", + "object-assign": "^4.1.1", + "postcss": "^6.0.9", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/postcss-functions/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-functions/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-functions/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/postcss-functions/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/postcss-functions/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-functions/node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-functions/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-functions/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", + "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1", + "postcss": "^8.1.6" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-nested": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.5.tgz", + "integrity": "sha512-GSRXYz5bccobpTzLQZXOnSOfKl6TwVr5CyAQJUPub4nuRJSOECK5AqurxVgmtxP48p0Kc/ndY/YyS1yqldX0Ew==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.1.13" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/purgecss": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz", + "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==", + "dev": true, + "dependencies": { + "commander": "^6.0.0", + "glob": "^7.0.0", + "postcss": "^8.2.1", + "postcss-selector-parser": "^6.0.2" + }, + "bin": { + "purgecss": "bin/purgecss.js" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dev": true, + "dependencies": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.1.2.tgz", + "integrity": "sha512-T5t+wwd+/hsOyRw2HJuFuv0LTUm3MUdHm2DJ94GPVgzqwPPFa9XxX0KlwLWupUuiOUj6uiKURCzYPHFcuPch/w==", + "dev": true, + "dependencies": { + "@fullhuman/postcss-purgecss": "^3.1.3", + "bytes": "^3.0.0", + "chalk": "^4.1.0", + "chokidar": "^3.5.1", + "color": "^3.1.3", + "detective": "^5.2.0", + "didyoumean": "^1.2.1", + "dlv": "^1.1.3", + "fast-glob": "^3.2.5", + "fs-extra": "^9.1.0", + "html-tags": "^3.1.0", + "lodash": "^4.17.21", + "lodash.topath": "^4.5.2", + "modern-normalize": "^1.0.0", + "node-emoji": "^1.8.1", + "normalize-path": "^3.0.0", + "object-hash": "^2.1.1", + "parse-glob": "^3.0.4", + "postcss-functions": "^3", + "postcss-js": "^3.0.3", + "postcss-nested": "5.0.5", + "postcss-selector-parser": "^6.0.4", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "quick-lru": "^5.1.1", + "reduce-css-calc": "^2.1.8", + "resolve": "^1.20.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "autoprefixer": "^10.0.2", + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss-cli": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tailwindcss-cli/-/tailwindcss-cli-0.1.2.tgz", + "integrity": "sha512-17NuGSHKTr4twN1BFxuoTArMcBQH+7YL6x4PHFnmWsGNOX45O4Roc8EdMVhSSH2rQoSDoLvR4TmlfddMon3yKg==", + "dev": true, + "dependencies": { + "autoprefixer": "^10.0.2", + "postcss": "^8.1.8", + "tailwindcss": "^2.0.1" + }, + "bin": { + "tailwindcss-cli": "index.js" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + } + }, + "dependencies": { + "@fullhuman/postcss-purgecss": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", + "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==", + "dev": true, + "requires": { + "purgecss": "^3.1.3" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + } + }, + "@tailwindcss/typography": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.4.0.tgz", + "integrity": "sha512-3BfOYT5MYNEq81Ism3L2qu/HRP2Q5vWqZtZRQqQrthHuaTK9qpuPfbMT5WATjAM5J1OePKBaI5pLoX4S1JGNMQ==", + "dev": true, + "requires": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "autoprefixer": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz", + "integrity": "sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA==", + "dev": true, + "requires": { + "browserslist": "^4.16.3", + "caniuse-lite": "^1.0.30001196", + "colorette": "^1.2.2", + "fraction.js": "^4.0.13", + "normalize-range": "^0.1.2", + "postcss-value-parser": "^4.1.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001228", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", + "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "dev": true + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "didyoumean": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", + "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.728", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.728.tgz", + "integrity": "sha512-SHv4ziXruBpb1Nz4aTuqEHBYi/9GNCJMYIJgDEXrp/2V01nFXMNFUTli5Z85f5ivSkioLilQatqBYFB44wNJrA==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fraction.js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.0.tgz", + "integrity": "sha512-o9lSKpK0TDqDwTL24Hxqi6I99s942l6TYkfl6WvGWgLOIFz/YonSGKfiSeMadoiNvTfqnfOa9mjb5SGVbBK9/w==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, + "lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "dev": true + }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true + }, + "node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "dev": true, + "requires": { + "lodash.toarray": "^4.4.0" + } + }, + "node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-hash": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", + "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "dev": true + }, + "postcss": { + "version": "8.2.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz", + "integrity": "sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map": "^0.6.1" + } + }, + "postcss-functions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", + "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=", + "dev": true, + "requires": { + "glob": "^7.1.2", + "object-assign": "^4.1.1", + "postcss": "^6.0.9", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", + "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1", + "postcss": "^8.1.6" + } + }, + "postcss-nested": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.5.tgz", + "integrity": "sha512-GSRXYz5bccobpTzLQZXOnSOfKl6TwVr5CyAQJUPub4nuRJSOECK5AqurxVgmtxP48p0Kc/ndY/YyS1yqldX0Ew==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "purgecss": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz", + "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==", + "dev": true, + "requires": { + "commander": "^6.0.0", + "glob": "^7.0.0", + "postcss": "^8.2.1", + "postcss-selector-parser": "^6.0.2" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tailwindcss": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.1.2.tgz", + "integrity": "sha512-T5t+wwd+/hsOyRw2HJuFuv0LTUm3MUdHm2DJ94GPVgzqwPPFa9XxX0KlwLWupUuiOUj6uiKURCzYPHFcuPch/w==", + "dev": true, + "requires": { + "@fullhuman/postcss-purgecss": "^3.1.3", + "bytes": "^3.0.0", + "chalk": "^4.1.0", + "chokidar": "^3.5.1", + "color": "^3.1.3", + "detective": "^5.2.0", + "didyoumean": "^1.2.1", + "dlv": "^1.1.3", + "fast-glob": "^3.2.5", + "fs-extra": "^9.1.0", + "html-tags": "^3.1.0", + "lodash": "^4.17.21", + "lodash.topath": "^4.5.2", + "modern-normalize": "^1.0.0", + "node-emoji": "^1.8.1", + "normalize-path": "^3.0.0", + "object-hash": "^2.1.1", + "parse-glob": "^3.0.4", + "postcss-functions": "^3", + "postcss-js": "^3.0.3", + "postcss-nested": "5.0.5", + "postcss-selector-parser": "^6.0.4", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "quick-lru": "^5.1.1", + "reduce-css-calc": "^2.1.8", + "resolve": "^1.20.0" + } + }, + "tailwindcss-cli": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tailwindcss-cli/-/tailwindcss-cli-0.1.2.tgz", + "integrity": "sha512-17NuGSHKTr4twN1BFxuoTArMcBQH+7YL6x4PHFnmWsGNOX45O4Roc8EdMVhSSH2rQoSDoLvR4TmlfddMon3yKg==", + "dev": true, + "requires": { + "autoprefixer": "^10.0.2", + "postcss": "^8.1.8", + "tailwindcss": "^2.0.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + } + } +} diff --git a/script/package.json b/script/package.json new file mode 100644 index 0000000000000000000000000000000000000000..12c3e782c831b61fb1e3bc1a3444499801cafa89 --- /dev/null +++ b/script/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "@tailwindcss/typography": "^0.4.0", + "tailwindcss-cli": "^0.1.2" + } +} diff --git a/script/server b/script/server new file mode 100755 index 0000000000000000000000000000000000000000..491932c9525276d78dbc70ab58986f7850ecec12 --- /dev/null +++ b/script/server @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +cd server +cargo run $@ diff --git a/script/sqlx b/script/sqlx new file mode 100755 index 0000000000000000000000000000000000000000..080e0d843a213dc1ac4e5dbddf5c89df8ff4c086 --- /dev/null +++ b/script/sqlx @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +# Install sqlx-cli if needed +[[ "$(sqlx --version)" == "sqlx-cli 0.5.5" ]] || cargo install sqlx-cli --version 0.5.5 + +# Export contents of .env.toml +eval "$(cargo run --bin dotenv)" + +# Run sqlx command +sqlx $@ diff --git a/script/tailwind.config.js b/script/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..fc8127ef6e6aa812006f05502a60a633b444c65a --- /dev/null +++ b/script/tailwind.config.js @@ -0,0 +1,44 @@ +module.exports = { + theme: { + fontFamily: { + display: [ + "Visby CF", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", + "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji" + ], + body: [ + "Open Sans", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", + "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji" + ], + }, + extend: { + typography: (theme) => ({ + DEFAULT: { + css: { + h1: { + fontFamily: theme("fontFamily.display").join(", ") + }, + h2: { + fontFamily: theme("fontFamily.display").join(", ") + }, + h3: { + fontFamily: theme("fontFamily.display").join(", ") + }, + h4: { + fontFamily: theme("fontFamily.display").join(", ") + } + } + } + }) + } + }, + variants: { + }, + plugins: [ + require('@tailwindcss/typography'), + ], + purge: [ + "../server/templates/**/*.hbs" + ] +} \ No newline at end of file diff --git a/server/.env.template.toml b/server/.env.template.toml new file mode 100644 index 0000000000000000000000000000000000000000..b5e030fad4f4ec78cd51ebdf01c40c4aca3f0964 --- /dev/null +++ b/server/.env.template.toml @@ -0,0 +1,11 @@ +DATABASE_URL = "postgres://postgres@localhost/zed" +SESSION_SECRET = "6E1GS6IQNOLIBKWMEVWF1AFO4H78KNU8" + +HTTP_PORT = 8080 + +# Available at https://github.com/organizations/zed-industries/settings/apps/zed-local-development +GITHUB_APP_ID = 115633 +GITHUB_CLIENT_ID = "Iv1.768076c9becc75c4" +GITHUB_CLIENT_SECRET = "" +GITHUB_PRIVATE_KEY = """\ +""" diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..6d26f66054912768dc708b1d56ccbe8a25791614 --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,50 @@ +[package] +authors = ["Nathan Sobo "] +default-run = "zed-server" +edition = "2018" +name = "zed-server" +version = "0.1.0" + +[dependencies] +anyhow = "1.0.40" +async-std = { version = "1.8.0", features = ["attributes"] } +async-trait = "0.1.50" +async-tungstenite = "0.14" +base64 = "0.13" +clap = "=3.0.0-beta.2" +comrak = "0.10" +either = "1.6" +envy = "0.4.2" +futures = "0.3" +handlebars = "3.5" +http-auth-basic = "0.1.3" +jwt-simple = "0.10.0" +oauth2 = { version = "4.0.0", default_features = false } +oauth2-surf = "0.1.1" +parking_lot = "0.11.1" +postage = { version = "0.4.1", features = ["futures-traits"] } +rand = "0.8" +rust-embed = "5.9.0" +scrypt = "0.7" +serde = { version = "1.0", features = ["derive"] } +sha-1 = "0.9" +surf = "2.2.0" +tide = "0.16.0" +tide-compress = "0.9.0" +toml = "0.5.8" +zrpc = { path = "../zrpc" } + +[dependencies.async-sqlx-session] +version = "0.3.0" +features = ["pg", "rustls"] +default-features = false + +[dependencies.sqlx] +version = "0.5.2" +features = ["runtime-async-std-rustls", "postgres"] + +[dev-dependencies] +gpui = { path = "../gpui" } +zed = { path = "../zed", features = ["test-support"] } +lazy_static = "1.4" +serde_json = { version = "1.0.64", features = ["preserve_order"] } diff --git a/server/Procfile b/server/Procfile new file mode 100644 index 0000000000000000000000000000000000000000..74cb9a094b3ed89dcc70c67294553ac571866b8f --- /dev/null +++ b/server/Procfile @@ -0,0 +1,2 @@ +web: ./target/release/zed-server +release: ./target/release/sqlx migrate run diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000000000000000000000000000000000000..031e0d024460478573a0f261c73ff75f32237ff6 --- /dev/null +++ b/server/README.md @@ -0,0 +1,17 @@ +# Zed Server + +This crate is what we run at https://zed.dev. + +It contains our web presence as well as the backend logic for collaboration, to which we connect from the Zed client via a websocket. + +## Templates + +We use handlebars templates that are interpreted at runtime. When running in debug mode, you can change templates and see the latest content without restarting the server. This is enabled by the `rust-embed` crate, which we use to access the contents of the `/templates` folder at runtime. In debug mode it reads contents from the file system, but in release the templates will be embedded in the server binary. + +## Static assets + +We also use `rust-embed` to access the contents of the `/static` folder via the `/static/*` route. The app will pick up changes to the contents of this folder when running in debug mode. + +## CSS + +This site uses Tailwind CSS, which means our stylesheets don't need to change very frequently. We check `static/styles.css` into the repository, but it's actually compiled from `/styles.css` via `script/build-css`. This script runs the Tailwind compilation flow to regenerate `static/styles.css` via PostCSS. diff --git a/server/basic.conf b/server/basic.conf new file mode 100644 index 0000000000000000000000000000000000000000..c6db392dbaabe9121a62d1c155dadb7a9ddcf235 --- /dev/null +++ b/server/basic.conf @@ -0,0 +1,12 @@ + +[Interface] +PrivateKey = B5Fp/yVfP0QYlb+YJv9ea+EMI1mWODPD3akh91cVjvc= +Address = fdaa:0:2ce3:a7b:bea:0:a:2/120 +DNS = fdaa:0:2ce3::3 + +[Peer] +PublicKey = RKAYPljEJiuaELNDdQIEJmQienT9+LRISfIHwH45HAw= +AllowedIPs = fdaa:0:2ce3::/48 +Endpoint = ord1.gateway.6pn.dev:51820 +PersistentKeepalive = 15 + diff --git a/server/manifest.yml b/server/manifest.yml new file mode 100644 index 0000000000000000000000000000000000000000..9f7f4260c41d4f72fc3c49c92f2d58a16fb4ca8e --- /dev/null +++ b/server/manifest.yml @@ -0,0 +1,71 @@ +--- +kind: Service +apiVersion: v1 +metadata: + name: zed + annotations: + service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: "606e2db9-2b58-4ae7-b12c-a0c7d56af49b" +spec: + type: LoadBalancer + selector: + app: zed + ports: + - name: web + protocol: TCP + port: 443 + targetPort: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zed +spec: + replicas: 1 + selector: + matchLabels: + app: zed + template: + metadata: + labels: + app: zed + spec: + containers: + - name: zed + image: registry.digitalocean.com/zed/zed-server + ports: + - containerPort: 8080 + protocol: TCP + env: + - name: HTTP_PORT + value: "8080" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: database + key: url + - name: SESSION_SECRET + valueFrom: + secretKeyRef: + name: session + key: secret + - name: GITHUB_APP_ID + valueFrom: + secretKeyRef: + name: github + key: appId + - name: GITHUB_CLIENT_ID + valueFrom: + secretKeyRef: + name: github + key: clientId + - name: GITHUB_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: github + key: clientSecret + - name: GITHUB_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: github + key: privateKey diff --git a/server/migrate.yml b/server/migrate.yml new file mode 100644 index 0000000000000000000000000000000000000000..58badca60de2d859109ca9826d69167ee1806461 --- /dev/null +++ b/server/migrate.yml @@ -0,0 +1,17 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: migrate +spec: + template: + spec: + restartPolicy: Never + containers: + - name: migrator + image: registry.digitalocean.com/zed/zed-migrator + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: database + key: url diff --git a/server/migrations/20210527024318_initial_schema.sql b/server/migrations/20210527024318_initial_schema.sql new file mode 100644 index 0000000000000000000000000000000000000000..5882d2de394c0b77a7da65b9f767c746dc4d1808 --- /dev/null +++ b/server/migrations/20210527024318_initial_schema.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS "sessions" ( + "id" VARCHAR NOT NULL PRIMARY KEY, + "expires" TIMESTAMP WITH TIME ZONE NULL, + "session" TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS "users" ( + "id" SERIAL PRIMARY KEY, + "github_login" VARCHAR, + "admin" BOOLEAN +); + +CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login"); + +CREATE TABLE IF NOT EXISTS "signups" ( + "id" SERIAL PRIMARY KEY, + "github_login" VARCHAR, + "email_address" VARCHAR, + "about" TEXT +); + +INSERT INTO users (github_login, admin) +VALUES + ('nathansobo', true), + ('maxbrunsfeld', true), + ('as-cii', true); diff --git a/server/migrations/20210607190313_create_access_tokens.sql b/server/migrations/20210607190313_create_access_tokens.sql new file mode 100644 index 0000000000000000000000000000000000000000..60745a98bae9ac8bc3e2016e598480e74e0b6473 --- /dev/null +++ b/server/migrations/20210607190313_create_access_tokens.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS "access_tokens" ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER REFERENCES users (id), + "hash" VARCHAR(128) +); + +CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id"); diff --git a/server/src/admin.rs b/server/src/admin.rs new file mode 100644 index 0000000000000000000000000000000000000000..3f379ff56f9a1e2f2c5d34d41b20a24dfa683c7a --- /dev/null +++ b/server/src/admin.rs @@ -0,0 +1,160 @@ +use crate::{auth::RequestExt as _, AppState, DbPool, LayoutData, Request, RequestExt as _}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use sqlx::{Executor, FromRow}; +use std::sync::Arc; +use surf::http::mime; + +#[async_trait] +pub trait RequestExt { + async fn require_admin(&self) -> tide::Result<()>; +} + +#[async_trait] +impl RequestExt for Request { + async fn require_admin(&self) -> tide::Result<()> { + let current_user = self + .current_user() + .await? + .ok_or_else(|| tide::Error::from_str(401, "not logged in"))?; + + if current_user.is_admin { + Ok(()) + } else { + Err(tide::Error::from_str( + 403, + "authenticated user is not an admin", + )) + } + } +} + +pub fn add_routes(app: &mut tide::Server>) { + app.at("/admin").get(get_admin_page); + app.at("/users").post(post_user); + app.at("/users/:id").put(put_user); + app.at("/users/:id/delete").post(delete_user); + app.at("/signups/:id/delete").post(delete_signup); +} + +#[derive(Serialize)] +struct AdminData { + #[serde(flatten)] + layout: Arc, + users: Vec, + signups: Vec, +} + +#[derive(Debug, FromRow, Serialize)] +pub struct User { + pub id: i32, + pub github_login: String, + pub admin: bool, +} + +#[derive(Debug, FromRow, Serialize)] +pub struct Signup { + pub id: i32, + pub github_login: String, + pub email_address: String, + pub about: String, +} + +async fn get_admin_page(mut request: Request) -> tide::Result { + request.require_admin().await?; + + let data = AdminData { + layout: request.layout_data().await?, + users: sqlx::query_as("SELECT * FROM users ORDER BY github_login ASC") + .fetch_all(request.db()) + .await?, + signups: sqlx::query_as("SELECT * FROM signups ORDER BY id DESC") + .fetch_all(request.db()) + .await?, + }; + + Ok(tide::Response::builder(200) + .body(request.state().render_template("admin.hbs", &data)?) + .content_type(mime::HTML) + .build()) +} + +async fn post_user(mut request: Request) -> tide::Result { + request.require_admin().await?; + + #[derive(Deserialize)] + struct Form { + github_login: String, + #[serde(default)] + admin: bool, + } + + let form = request.body_form::
().await?; + let github_login = form + .github_login + .strip_prefix("@") + .unwrap_or(&form.github_login); + + if !github_login.is_empty() { + create_user(request.db(), github_login, form.admin).await?; + } + + Ok(tide::Redirect::new("/admin").into()) +} + +async fn put_user(mut request: Request) -> tide::Result { + request.require_admin().await?; + + let user_id = request.param("id")?.parse::()?; + + #[derive(Deserialize)] + struct Body { + admin: bool, + } + + let body: Body = request.body_json().await?; + + request + .db() + .execute( + sqlx::query("UPDATE users SET admin = $1 WHERE id = $2;") + .bind(body.admin) + .bind(user_id), + ) + .await?; + + Ok(tide::Response::builder(200).build()) +} + +async fn delete_user(request: Request) -> tide::Result { + request.require_admin().await?; + + let user_id = request.param("id")?.parse::()?; + request + .db() + .execute(sqlx::query("DELETE FROM users WHERE id = $1;").bind(user_id)) + .await?; + + Ok(tide::Redirect::new("/admin").into()) +} + +pub async fn create_user(db: &DbPool, github_login: &str, admin: bool) -> tide::Result { + let id: i32 = + sqlx::query_scalar("INSERT INTO users (github_login, admin) VALUES ($1, $2) RETURNING id;") + .bind(github_login) + .bind(admin) + .fetch_one(db) + .await?; + Ok(id) +} + +async fn delete_signup(request: Request) -> tide::Result { + request.require_admin().await?; + let signup_id = request.param("id")?.parse::()?; + request + .db() + .execute(sqlx::query("DELETE FROM signups WHERE id = $1;").bind(signup_id)) + .await?; + + Ok(tide::Redirect::new("/admin").into()) +} diff --git a/server/src/assets.rs b/server/src/assets.rs new file mode 100644 index 0000000000000000000000000000000000000000..a53be8ed95f23d9f0666cf6a2b24faecb47fc2e6 --- /dev/null +++ b/server/src/assets.rs @@ -0,0 +1,31 @@ +use crate::{AppState, Request}; +use anyhow::anyhow; +use rust_embed::RustEmbed; +use std::sync::Arc; +use tide::{http::mime, Server}; + +#[derive(RustEmbed)] +#[folder = "static"] +struct Static; + +pub fn add_routes(app: &mut Server>) { + app.at("/static/*path").get(get_static_asset); +} + +async fn get_static_asset(request: Request) -> tide::Result { + let path = request.param("path").unwrap(); + let content = Static::get(path).ok_or_else(|| anyhow!("asset not found at {}", path))?; + + let content_type = if path.starts_with("svg") { + mime::SVG + } else if path.starts_with("styles") { + mime::CSS + } else { + mime::BYTE_STREAM + }; + + Ok(tide::Response::builder(200) + .content_type(content_type) + .body(content.as_ref()) + .build()) +} diff --git a/server/src/auth.rs b/server/src/auth.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a7107e550c2adc4838a703308554c93ea21464d --- /dev/null +++ b/server/src/auth.rs @@ -0,0 +1,336 @@ +use super::errors::TideResultExt; +use crate::{github, rpc, AppState, DbPool, Request, RequestExt as _}; +use anyhow::{anyhow, Context}; +use async_std::stream::StreamExt; +use async_trait::async_trait; +pub use oauth2::basic::BasicClient as Client; +use oauth2::{ + AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, RedirectUrl, + TokenResponse as _, TokenUrl, +}; +use rand::thread_rng; +use scrypt::{ + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Scrypt, +}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use std::{borrow::Cow, convert::TryFrom, sync::Arc}; +use surf::Url; +use tide::Server; +use zrpc::{auth as zed_auth, proto, Peer}; + +static CURRENT_GITHUB_USER: &'static str = "current_github_user"; +static GITHUB_AUTH_URL: &'static str = "https://github.com/login/oauth/authorize"; +static GITHUB_TOKEN_URL: &'static str = "https://github.com/login/oauth/access_token"; + +#[derive(Serialize)] +pub struct User { + pub github_login: String, + pub avatar_url: String, + pub is_insider: bool, + pub is_admin: bool, +} + +pub struct VerifyToken; + +#[derive(Clone, Copy)] +pub struct UserId(pub i32); + +#[async_trait] +impl tide::Middleware> for VerifyToken { + async fn handle( + &self, + mut request: Request, + next: tide::Next<'_, Arc>, + ) -> tide::Result { + let mut auth_header = request + .header("Authorization") + .ok_or_else(|| anyhow!("no authorization header"))? + .last() + .as_str() + .split_whitespace(); + + let user_id: i32 = auth_header + .next() + .ok_or_else(|| anyhow!("missing user id in authorization header"))? + .parse()?; + let access_token = auth_header + .next() + .ok_or_else(|| anyhow!("missing access token in authorization header"))?; + + let state = request.state().clone(); + + let mut password_hashes = + sqlx::query_scalar::<_, String>("SELECT hash FROM access_tokens WHERE user_id = $1") + .bind(&user_id) + .fetch_many(&state.db); + + let mut credentials_valid = false; + while let Some(password_hash) = password_hashes.next().await { + if let either::Either::Right(password_hash) = password_hash? { + if verify_access_token(&access_token, &password_hash)? { + credentials_valid = true; + break; + } + } + } + + if credentials_valid { + request.set_ext(UserId(user_id)); + Ok(next.run(request).await) + } else { + Err(anyhow!("invalid credentials").into()) + } + } +} + +#[async_trait] +pub trait RequestExt { + async fn current_user(&self) -> tide::Result>; +} + +#[async_trait] +impl RequestExt for Request { + async fn current_user(&self) -> tide::Result> { + if let Some(details) = self.session().get::(CURRENT_GITHUB_USER) { + #[derive(FromRow)] + struct UserRow { + admin: bool, + } + + let user_row: Option = + sqlx::query_as("SELECT admin FROM users WHERE github_login = $1") + .bind(&details.login) + .fetch_optional(self.db()) + .await?; + + let is_insider = user_row.is_some(); + let is_admin = user_row.map_or(false, |row| row.admin); + + Ok(Some(User { + github_login: details.login, + avatar_url: details.avatar_url, + is_insider, + is_admin, + })) + } else { + Ok(None) + } + } +} + +#[async_trait] +pub trait PeerExt { + async fn sign_out( + self: &Arc, + connection_id: zrpc::ConnectionId, + state: &AppState, + ) -> tide::Result<()>; +} + +#[async_trait] +impl PeerExt for Peer { + async fn sign_out( + self: &Arc, + connection_id: zrpc::ConnectionId, + state: &AppState, + ) -> tide::Result<()> { + self.disconnect(connection_id).await; + let worktree_ids = state.rpc.write().await.remove_connection(connection_id); + for worktree_id in worktree_ids { + let state = state.rpc.read().await; + if let Some(worktree) = state.worktrees.get(&worktree_id) { + rpc::broadcast(connection_id, worktree.connection_ids(), |conn_id| { + self.send( + conn_id, + proto::RemovePeer { + worktree_id, + peer_id: connection_id.0, + }, + ) + }) + .await?; + } + } + Ok(()) + } +} + +pub fn build_client(client_id: &str, client_secret: &str) -> Client { + Client::new( + ClientId::new(client_id.to_string()), + Some(oauth2::ClientSecret::new(client_secret.to_string())), + AuthUrl::new(GITHUB_AUTH_URL.into()).unwrap(), + Some(TokenUrl::new(GITHUB_TOKEN_URL.into()).unwrap()), + ) +} + +pub fn add_routes(app: &mut Server>) { + app.at("/sign_in").get(get_sign_in); + app.at("/sign_out").post(post_sign_out); + app.at("/auth_callback").get(get_auth_callback); +} + +#[derive(Debug, Deserialize)] +struct NativeAppSignInParams { + native_app_port: String, + native_app_public_key: String, +} + +async fn get_sign_in(mut request: Request) -> tide::Result { + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + request + .session_mut() + .insert("pkce_verifier", pkce_verifier)?; + + let mut redirect_url = Url::parse(&format!( + "{}://{}/auth_callback", + request + .header("X-Forwarded-Proto") + .and_then(|values| values.get(0)) + .map(|value| value.as_str()) + .unwrap_or("http"), + request.host().unwrap() + ))?; + + let app_sign_in_params: Option = request.query().ok(); + if let Some(query) = app_sign_in_params { + redirect_url + .query_pairs_mut() + .clear() + .append_pair("native_app_port", &query.native_app_port) + .append_pair("native_app_public_key", &query.native_app_public_key); + } + + let (auth_url, csrf_token) = request + .state() + .auth_client + .authorize_url(CsrfToken::new_random) + .set_redirect_uri(Cow::Owned(RedirectUrl::from_url(redirect_url))) + .set_pkce_challenge(pkce_challenge) + .url(); + + request + .session_mut() + .insert("auth_csrf_token", csrf_token)?; + + Ok(tide::Redirect::new(auth_url).into()) +} + +async fn get_auth_callback(mut request: Request) -> tide::Result { + #[derive(Debug, Deserialize)] + struct Query { + code: String, + state: String, + + #[serde(flatten)] + native_app_sign_in_params: Option, + } + + let query: Query = request.query()?; + + let pkce_verifier = request + .session() + .get("pkce_verifier") + .ok_or_else(|| anyhow!("could not retrieve pkce_verifier from session"))?; + + let csrf_token = request + .session() + .get::("auth_csrf_token") + .ok_or_else(|| anyhow!("could not retrieve auth_csrf_token from session"))?; + + if &query.state != csrf_token.secret() { + return Err(anyhow!("csrf token does not match").into()); + } + + let github_access_token = request + .state() + .auth_client + .exchange_code(AuthorizationCode::new(query.code)) + .set_pkce_verifier(pkce_verifier) + .request_async(oauth2_surf::http_client) + .await + .context("failed to exchange oauth code")? + .access_token() + .secret() + .clone(); + + let user_details = request + .state() + .github_client + .user(github_access_token) + .details() + .await + .context("failed to fetch user")?; + + let user_id: Option = sqlx::query_scalar("SELECT id from users where github_login = $1") + .bind(&user_details.login) + .fetch_optional(request.db()) + .await?; + + request + .session_mut() + .insert(CURRENT_GITHUB_USER, user_details.clone())?; + + // When signing in from the native app, generate a new access token for the current user. Return + // a redirect so that the user's browser sends this access token to the locally-running app. + if let Some((user_id, app_sign_in_params)) = user_id.zip(query.native_app_sign_in_params) { + let access_token = create_access_token(request.db(), user_id).await?; + let native_app_public_key = + zed_auth::PublicKey::try_from(app_sign_in_params.native_app_public_key.clone()) + .context("failed to parse app public key")?; + let encrypted_access_token = native_app_public_key + .encrypt_string(&access_token) + .context("failed to encrypt access token with public key")?; + + return Ok(tide::Redirect::new(&format!( + "http://127.0.0.1:{}?user_id={}&access_token={}", + app_sign_in_params.native_app_port, user_id, encrypted_access_token, + )) + .into()); + } + + Ok(tide::Redirect::new("/").into()) +} + +async fn post_sign_out(mut request: Request) -> tide::Result { + request.session_mut().remove(CURRENT_GITHUB_USER); + Ok(tide::Redirect::new("/").into()) +} + +pub async fn create_access_token(db: &DbPool, user_id: i32) -> tide::Result { + let access_token = zed_auth::random_token(); + let access_token_hash = + hash_access_token(&access_token).context("failed to hash access token")?; + sqlx::query("INSERT INTO access_tokens (user_id, hash) values ($1, $2)") + .bind(user_id) + .bind(access_token_hash) + .fetch_optional(db) + .await?; + Ok(access_token) +} + +fn hash_access_token(token: &str) -> tide::Result { + // Avoid slow hashing in debug mode. + let params = if cfg!(debug_assertions) { + scrypt::Params::new(1, 1, 1).unwrap() + } else { + scrypt::Params::recommended() + }; + + Ok(Scrypt + .hash_password( + token.as_bytes(), + None, + params, + &SaltString::generate(thread_rng()), + )? + .to_string()) +} + +pub fn verify_access_token(token: &str, hash: &str) -> tide::Result { + let hash = PasswordHash::new(hash)?; + Ok(Scrypt.verify_password(token.as_bytes(), &hash).is_ok()) +} diff --git a/server/src/bin/dotenv.rs b/server/src/bin/dotenv.rs new file mode 100644 index 0000000000000000000000000000000000000000..c093bcb6e9fcd833181346adf9d041a535286731 --- /dev/null +++ b/server/src/bin/dotenv.rs @@ -0,0 +1,20 @@ +use anyhow::anyhow; +use std::fs; + +fn main() -> anyhow::Result<()> { + let env: toml::map::Map = toml::de::from_str( + &fs::read_to_string("./.env.toml").map_err(|_| anyhow!("no .env.toml file found"))?, + )?; + + for (key, value) in env { + let value = match value { + toml::Value::String(value) => value, + toml::Value::Integer(value) => value.to_string(), + toml::Value::Float(value) => value.to_string(), + _ => panic!("unsupported TOML value in .env.toml for key {}", key), + }; + println!("export {}=\"{}\"", key, value); + } + + Ok(()) +} diff --git a/server/src/env.rs b/server/src/env.rs new file mode 100644 index 0000000000000000000000000000000000000000..58c29b0205f598e720e8ba7f38b983d31082b5c2 --- /dev/null +++ b/server/src/env.rs @@ -0,0 +1,20 @@ +use anyhow::anyhow; +use std::fs; + +pub fn load_dotenv() -> anyhow::Result<()> { + let env: toml::map::Map = toml::de::from_str( + &fs::read_to_string("./.env.toml").map_err(|_| anyhow!("no .env.toml file found"))?, + )?; + + for (key, value) in env { + let value = match value { + toml::Value::String(value) => value, + toml::Value::Integer(value) => value.to_string(), + toml::Value::Float(value) => value.to_string(), + _ => panic!("unsupported TOML value in .env.toml for key {}", key), + }; + std::env::set_var(key, value); + } + + Ok(()) +} diff --git a/server/src/errors.rs b/server/src/errors.rs new file mode 100644 index 0000000000000000000000000000000000000000..1dbb44361fb8b8035d229d571fa81c0e7baf2b70 --- /dev/null +++ b/server/src/errors.rs @@ -0,0 +1,73 @@ +use crate::{AppState, LayoutData, Request, RequestExt}; +use async_trait::async_trait; +use serde::Serialize; +use std::sync::Arc; +use tide::http::mime; + +pub struct Middleware; + +#[async_trait] +impl tide::Middleware> for Middleware { + async fn handle( + &self, + mut request: Request, + next: tide::Next<'_, Arc>, + ) -> tide::Result { + let app = request.state().clone(); + let layout_data = request.layout_data().await?; + + let mut response = next.run(request).await; + + #[derive(Serialize)] + struct ErrorData { + #[serde(flatten)] + layout: Arc, + status: u16, + reason: &'static str, + } + + if !response.status().is_success() { + response.set_body(app.render_template( + "error.hbs", + &ErrorData { + layout: layout_data, + status: response.status().into(), + reason: response.status().canonical_reason(), + }, + )?); + response.set_content_type(mime::HTML); + } + + Ok(response) + } +} + +// Allow tide Results to accept context like other Results do when +// using anyhow. +pub trait TideResultExt { + fn context(self, cx: C) -> Self + where + C: std::fmt::Display + Send + Sync + 'static; + + fn with_context(self, f: F) -> Self + where + C: std::fmt::Display + Send + Sync + 'static, + F: FnOnce() -> C; +} + +impl TideResultExt for tide::Result { + fn context(self, cx: C) -> Self + where + C: std::fmt::Display + Send + Sync + 'static, + { + self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(cx))) + } + + fn with_context(self, f: F) -> Self + where + C: std::fmt::Display + Send + Sync + 'static, + F: FnOnce() -> C, + { + self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(f()))) + } +} diff --git a/server/src/expiring.rs b/server/src/expiring.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba974dc8e094311ee885e0918ee2e433db1716d7 --- /dev/null +++ b/server/src/expiring.rs @@ -0,0 +1,43 @@ +use std::{future::Future, time::Instant}; + +use async_std::sync::Mutex; + +#[derive(Default)] +pub struct Expiring(Mutex>>); + +pub struct ExpiringState { + value: T, + expires_at: Instant, +} + +impl Expiring { + pub async fn get_or_refresh(&self, f: F) -> tide::Result + where + F: FnOnce() -> G, + G: Future>, + { + let mut state = self.0.lock().await; + + if let Some(state) = state.as_mut() { + if Instant::now() >= state.expires_at { + let (value, expires_at) = f().await?; + state.value = value.clone(); + state.expires_at = expires_at; + Ok(value) + } else { + Ok(state.value.clone()) + } + } else { + let (value, expires_at) = f().await?; + *state = Some(ExpiringState { + value: value.clone(), + expires_at, + }); + Ok(value) + } + } + + pub async fn clear(&self) { + self.0.lock().await.take(); + } +} diff --git a/server/src/github.rs b/server/src/github.rs new file mode 100644 index 0000000000000000000000000000000000000000..c7122b6e10f72beb58b3d734f23ccc50d3b60313 --- /dev/null +++ b/server/src/github.rs @@ -0,0 +1,265 @@ +use crate::expiring::Expiring; +use anyhow::{anyhow, Context}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + future::Future, + sync::Arc, + time::{Duration, Instant}, +}; +use surf::{http::Method, RequestBuilder, Url}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Release { + pub tag_name: String, + pub name: String, + pub body: String, + pub draft: bool, + pub assets: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Asset { + pub name: String, + pub url: String, +} + +pub struct AppClient { + id: usize, + private_key: String, + jwt_bearer_header: Expiring, +} + +#[derive(Deserialize)] +struct Installation { + #[allow(unused)] + id: usize, +} + +impl AppClient { + #[cfg(test)] + pub fn test() -> Arc { + Arc::new(Self { + id: Default::default(), + private_key: Default::default(), + jwt_bearer_header: Default::default(), + }) + } + + pub fn new(id: usize, private_key: String) -> Arc { + Arc::new(Self { + id, + private_key, + jwt_bearer_header: Default::default(), + }) + } + + pub async fn repo(self: &Arc, nwo: String) -> tide::Result { + let installation: Installation = self + .request( + Method::Get, + &format!("/repos/{}/installation", &nwo), + |refresh| self.bearer_header(refresh), + ) + .await?; + + Ok(RepoClient { + app: self.clone(), + nwo, + installation_id: installation.id, + installation_token_header: Default::default(), + }) + } + + pub fn user(self: &Arc, access_token: String) -> UserClient { + UserClient { + app: self.clone(), + access_token, + } + } + + async fn request( + &self, + method: Method, + path: &str, + get_auth_header: F, + ) -> tide::Result + where + T: DeserializeOwned, + F: Fn(bool) -> G, + G: Future>, + { + let mut retried = false; + + loop { + let response = RequestBuilder::new( + method, + Url::parse(&format!("https://api.github.com{}", path))?, + ) + .header("Accept", "application/vnd.github.v3+json") + .header("Authorization", get_auth_header(retried).await?) + .recv_json() + .await; + + if let Err(error) = response.as_ref() { + if error.status() == 401 && !retried { + retried = true; + continue; + } + } + + return response; + } + } + + async fn bearer_header(&self, refresh: bool) -> tide::Result { + if refresh { + self.jwt_bearer_header.clear().await; + } + + self.jwt_bearer_header + .get_or_refresh(|| async { + use jwt_simple::{algorithms::RS256KeyPair, prelude::*}; + use std::time; + + let key_pair = RS256KeyPair::from_pem(&self.private_key) + .with_context(|| format!("invalid private key {:?}", self.private_key))?; + let mut claims = Claims::create(Duration::from_mins(10)); + claims.issued_at = Some(Clock::now_since_epoch() - Duration::from_mins(1)); + claims.issuer = Some(self.id.to_string()); + let token = key_pair.sign(claims).context("failed to sign claims")?; + let expires_at = time::Instant::now() + time::Duration::from_secs(9 * 60); + + Ok((format!("Bearer {}", token), expires_at)) + }) + .await + } + + async fn installation_token_header( + &self, + header: &Expiring, + installation_id: usize, + refresh: bool, + ) -> tide::Result { + if refresh { + header.clear().await; + } + + header + .get_or_refresh(|| async { + #[derive(Debug, Deserialize)] + struct AccessToken { + token: String, + } + + let access_token: AccessToken = self + .request( + Method::Post, + &format!("/app/installations/{}/access_tokens", installation_id), + |refresh| self.bearer_header(refresh), + ) + .await?; + + let header = format!("Token {}", access_token.token); + let expires_at = Instant::now() + Duration::from_secs(60 * 30); + + Ok((header, expires_at)) + }) + .await + } +} + +pub struct RepoClient { + app: Arc, + nwo: String, + installation_id: usize, + installation_token_header: Expiring, +} + +impl RepoClient { + #[cfg(test)] + pub fn test(app_client: &Arc) -> Self { + Self { + app: app_client.clone(), + nwo: String::new(), + installation_id: 0, + installation_token_header: Default::default(), + } + } + + pub async fn releases(&self) -> tide::Result> { + self.get(&format!("/repos/{}/releases?per_page=100", self.nwo)) + .await + } + + pub async fn release_asset(&self, tag: &str, name: &str) -> tide::Result { + let release: Release = self + .get(&format!("/repos/{}/releases/tags/{}", self.nwo, tag)) + .await?; + + let asset = release + .assets + .iter() + .find(|asset| asset.name == name) + .ok_or_else(|| anyhow!("no asset found with name {}", name))?; + + let request = surf::get(&asset.url) + .header("Accept", "application/octet-stream'") + .header( + "Authorization", + self.installation_token_header(false).await?, + ); + let client = surf::client().with(surf::middleware::Redirect::new(5)); + let mut response = client.send(request).await?; + + Ok(response.take_body()) + } + + async fn get(&self, path: &str) -> tide::Result { + self.request::(Method::Get, path).await + } + + async fn request(&self, method: Method, path: &str) -> tide::Result { + Ok(self + .app + .request(method, path, |refresh| { + self.installation_token_header(refresh) + }) + .await?) + } + + async fn installation_token_header(&self, refresh: bool) -> tide::Result { + self.app + .installation_token_header( + &self.installation_token_header, + self.installation_id, + refresh, + ) + .await + } +} + +pub struct UserClient { + app: Arc, + access_token: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct User { + pub login: String, + pub avatar_url: String, +} + +impl UserClient { + pub async fn details(&self) -> tide::Result { + Ok(self + .app + .request(Method::Get, "/user", |_| async { + Ok(self.access_token_header()) + }) + .await?) + } + + fn access_token_header(&self) -> String { + format!("Token {}", self.access_token) + } +} diff --git a/server/src/home.rs b/server/src/home.rs new file mode 100644 index 0000000000000000000000000000000000000000..b4b8c24bf607302db7c15b109bd784bf38e667e2 --- /dev/null +++ b/server/src/home.rs @@ -0,0 +1,112 @@ +use crate::{ + auth::RequestExt as _, github::Release, AppState, LayoutData, Request, RequestExt as _, +}; +use comrak::ComrakOptions; +use serde::{Deserialize, Serialize}; +use sqlx::Executor as _; +use std::sync::Arc; +use tide::{http::mime, log, Server}; + +pub fn add_routes(app: &mut Server>) { + app.at("/").get(get_home); + app.at("/signups").post(post_signup); + app.at("/releases/:tag_name/:name").get(get_release_asset); +} + +async fn get_home(mut request: Request) -> tide::Result { + #[derive(Serialize)] + struct HomeData { + #[serde(flatten)] + layout: Arc, + releases: Option>, + } + + let mut data = HomeData { + layout: request.layout_data().await?, + releases: None, + }; + + if let Some(user) = request.current_user().await? { + if user.is_insider { + data.releases = Some( + request + .state() + .repo_client + .releases() + .await? + .into_iter() + .filter_map(|mut release| { + if release.draft { + None + } else { + let mut options = ComrakOptions::default(); + options.render.unsafe_ = true; // Allow raw HTML in the markup. We control these release notes anyway. + release.body = comrak::markdown_to_html(&release.body, &options); + Some(release) + } + }) + .collect(), + ); + } + } + + Ok(tide::Response::builder(200) + .body(request.state().render_template("home.hbs", &data)?) + .content_type(mime::HTML) + .build()) +} + +async fn post_signup(mut request: Request) -> tide::Result { + #[derive(Debug, Deserialize)] + struct Form { + github_login: String, + email_address: String, + about: String, + } + + let mut form: Form = request.body_form().await?; + form.github_login = form + .github_login + .strip_prefix("@") + .map(str::to_string) + .unwrap_or(form.github_login); + + log::info!("Signup submitted: {:?}", form); + + // Save signup in the database + request + .db() + .execute( + sqlx::query( + "INSERT INTO signups (github_login, email_address, about) VALUES ($1, $2, $3);", + ) + .bind(&form.github_login) + .bind(&form.email_address) + .bind(&form.about), + ) + .await?; + + let layout_data = request.layout_data().await?; + Ok(tide::Response::builder(200) + .body( + request + .state() + .render_template("signup.hbs", &layout_data)?, + ) + .content_type(mime::HTML) + .build()) +} + +async fn get_release_asset(request: Request) -> tide::Result { + let body = request + .state() + .repo_client + .release_asset(request.param("tag_name")?, request.param("name")?) + .await?; + + Ok(tide::Response::builder(200) + .header("Cache-Control", "no-transform") + .content_type(mime::BYTE_STREAM) + .body(body) + .build()) +} diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..ebd52b0a8bd0fd1e42beeaa505245852f545dd6f --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,197 @@ +mod admin; +mod assets; +mod auth; +mod env; +mod errors; +mod expiring; +mod github; +mod home; +mod rpc; +mod team; +#[cfg(test)] +mod tests; + +use self::errors::TideResultExt as _; +use anyhow::{Context, Result}; +use async_sqlx_session::PostgresSessionStore; +use async_std::{net::TcpListener, sync::RwLock as AsyncRwLock}; +use async_trait::async_trait; +use auth::RequestExt as _; +use handlebars::{Handlebars, TemplateRenderError}; +use parking_lot::RwLock; +use rust_embed::RustEmbed; +use serde::{Deserialize, Serialize}; +use sqlx::postgres::{PgPool, PgPoolOptions}; +use std::sync::Arc; +use surf::http::cookies::SameSite; +use tide::{log, sessions::SessionMiddleware}; +use tide_compress::CompressMiddleware; +use zrpc::Peer; + +type Request = tide::Request>; +type DbPool = PgPool; + +#[derive(RustEmbed)] +#[folder = "templates"] +struct Templates; + +#[derive(Default, Deserialize)] +pub struct Config { + pub http_port: u16, + pub database_url: String, + pub session_secret: String, + pub github_app_id: usize, + pub github_client_id: String, + pub github_client_secret: String, + pub github_private_key: String, +} + +pub struct AppState { + db: sqlx::PgPool, + handlebars: RwLock>, + auth_client: auth::Client, + github_client: Arc, + repo_client: github::RepoClient, + rpc: AsyncRwLock, + config: Config, +} + +impl AppState { + async fn new(config: Config) -> tide::Result> { + let db = PgPoolOptions::new() + .max_connections(5) + .connect(&config.database_url) + .await + .context("failed to connect to postgres database")?; + + let github_client = + github::AppClient::new(config.github_app_id, config.github_private_key.clone()); + let repo_client = github_client + .repo("zed-industries/zed".into()) + .await + .context("failed to initialize github client")?; + + let this = Self { + db, + handlebars: Default::default(), + auth_client: auth::build_client(&config.github_client_id, &config.github_client_secret), + github_client, + repo_client, + rpc: Default::default(), + config, + }; + this.register_partials(); + Ok(Arc::new(this)) + } + + fn register_partials(&self) { + for path in Templates::iter() { + if let Some(partial_name) = path + .strip_prefix("partials/") + .and_then(|path| path.strip_suffix(".hbs")) + { + let partial = Templates::get(path.as_ref()).unwrap(); + self.handlebars + .write() + .register_partial(partial_name, std::str::from_utf8(partial.as_ref()).unwrap()) + .unwrap() + } + } + } + + fn render_template( + &self, + path: &'static str, + data: &impl Serialize, + ) -> Result { + #[cfg(debug_assertions)] + self.register_partials(); + + self.handlebars.read().render_template( + std::str::from_utf8(Templates::get(path).unwrap().as_ref()).unwrap(), + data, + ) + } +} + +#[async_trait] +trait RequestExt { + async fn layout_data(&mut self) -> tide::Result>; + fn db(&self) -> &DbPool; +} + +#[async_trait] +impl RequestExt for Request { + async fn layout_data(&mut self) -> tide::Result> { + if self.ext::>().is_none() { + self.set_ext(Arc::new(LayoutData { + current_user: self.current_user().await?, + })); + } + Ok(self.ext::>().unwrap().clone()) + } + + fn db(&self) -> &DbPool { + &self.state().db + } +} + +#[derive(Serialize)] +struct LayoutData { + current_user: Option, +} + +#[async_std::main] +async fn main() -> tide::Result<()> { + log::start(); + + if let Err(error) = env::load_dotenv() { + log::error!( + "error loading .env.toml (this is expected in production): {}", + error + ); + } + + let config = envy::from_env::().expect("error loading config"); + let state = AppState::new(config).await?; + let rpc = Peer::new(); + run_server( + state.clone(), + rpc, + TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port)).await?, + ) + .await?; + Ok(()) +} + +pub async fn run_server( + state: Arc, + rpc: Arc, + listener: TcpListener, +) -> tide::Result<()> { + let mut web = tide::with_state(state.clone()); + web.with(CompressMiddleware::new()); + web.with( + SessionMiddleware::new( + PostgresSessionStore::new_with_table_name(&state.config.database_url, "sessions") + .await + .unwrap(), + state.config.session_secret.as_bytes(), + ) + .with_same_site_policy(SameSite::Lax), // Required obtain our session in /auth_callback + ); + web.with(errors::Middleware); + home::add_routes(&mut web); + team::add_routes(&mut web); + admin::add_routes(&mut web); + auth::add_routes(&mut web); + assets::add_routes(&mut web); + + let mut app = tide::with_state(state.clone()); + rpc::add_routes(&mut app, &rpc); + app.at("/").nest(web); + + app.listen(listener).await?; + + Ok(()) +} diff --git a/server/src/rpc.rs b/server/src/rpc.rs new file mode 100644 index 0000000000000000000000000000000000000000..3c189833b252e2354e43683e63f4e2fba6db54c6 --- /dev/null +++ b/server/src/rpc.rs @@ -0,0 +1,652 @@ +use crate::auth::{self, UserId}; + +use super::{auth::PeerExt as _, AppState}; +use anyhow::anyhow; +use async_std::task; +use async_tungstenite::{ + tungstenite::{protocol::Role, Error as WebSocketError, Message as WebSocketMessage}, + WebSocketStream, +}; +use sha1::{Digest as _, Sha1}; +use std::{ + collections::{HashMap, HashSet}, + future::Future, + mem, + sync::Arc, + time::Instant, +}; +use surf::StatusCode; +use tide::log; +use tide::{ + http::headers::{HeaderName, CONNECTION, UPGRADE}, + Request, Response, +}; +use zrpc::{ + auth::random_token, + proto::{self, EnvelopedMessage}, + ConnectionId, Peer, Router, TypedEnvelope, +}; + +type ReplicaId = u16; + +#[derive(Default)] +pub struct State { + connections: HashMap, + pub worktrees: HashMap, + next_worktree_id: u64, +} + +struct ConnectionState { + _user_id: i32, + worktrees: HashSet, +} + +pub struct WorktreeState { + host_connection_id: Option, + guest_connection_ids: HashMap, + active_replica_ids: HashSet, + access_token: String, + root_name: String, + entries: HashMap, +} + +impl WorktreeState { + pub fn connection_ids(&self) -> Vec { + self.guest_connection_ids + .keys() + .copied() + .chain(self.host_connection_id) + .collect() + } + + fn host_connection_id(&self) -> tide::Result { + Ok(self + .host_connection_id + .ok_or_else(|| anyhow!("host disconnected from worktree"))?) + } +} + +impl State { + // Add a new connection associated with a given user. + pub fn add_connection(&mut self, connection_id: ConnectionId, _user_id: i32) { + self.connections.insert( + connection_id, + ConnectionState { + _user_id, + worktrees: Default::default(), + }, + ); + } + + // Remove the given connection and its association with any worktrees. + pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Vec { + let mut worktree_ids = Vec::new(); + if let Some(connection_state) = self.connections.remove(&connection_id) { + for worktree_id in connection_state.worktrees { + if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { + if worktree.host_connection_id == Some(connection_id) { + worktree_ids.push(worktree_id); + } else if let Some(replica_id) = + worktree.guest_connection_ids.remove(&connection_id) + { + worktree.active_replica_ids.remove(&replica_id); + worktree_ids.push(worktree_id); + } + } + } + } + worktree_ids + } + + // Add the given connection as a guest of the given worktree + pub fn join_worktree( + &mut self, + connection_id: ConnectionId, + worktree_id: u64, + access_token: &str, + ) -> Option<(ReplicaId, &WorktreeState)> { + if let Some(worktree_state) = self.worktrees.get_mut(&worktree_id) { + if access_token == worktree_state.access_token { + if let Some(connection_state) = self.connections.get_mut(&connection_id) { + connection_state.worktrees.insert(worktree_id); + } + + let mut replica_id = 1; + while worktree_state.active_replica_ids.contains(&replica_id) { + replica_id += 1; + } + worktree_state.active_replica_ids.insert(replica_id); + worktree_state + .guest_connection_ids + .insert(connection_id, replica_id); + Some((replica_id, worktree_state)) + } else { + None + } + } else { + None + } + } + + fn read_worktree( + &self, + worktree_id: u64, + connection_id: ConnectionId, + ) -> tide::Result<&WorktreeState> { + let worktree = self + .worktrees + .get(&worktree_id) + .ok_or_else(|| anyhow!("worktree not found"))?; + + if worktree.host_connection_id == Some(connection_id) + || worktree.guest_connection_ids.contains_key(&connection_id) + { + Ok(worktree) + } else { + Err(anyhow!( + "{} is not a member of worktree {}", + connection_id, + worktree_id + ))? + } + } + + fn write_worktree( + &mut self, + worktree_id: u64, + connection_id: ConnectionId, + ) -> tide::Result<&mut WorktreeState> { + let worktree = self + .worktrees + .get_mut(&worktree_id) + .ok_or_else(|| anyhow!("worktree not found"))?; + + if worktree.host_connection_id == Some(connection_id) + || worktree.guest_connection_ids.contains_key(&connection_id) + { + Ok(worktree) + } else { + Err(anyhow!( + "{} is not a member of worktree {}", + connection_id, + worktree_id + ))? + } + } +} + +trait MessageHandler<'a, M: proto::EnvelopedMessage> { + type Output: 'a + Send + Future>; + + fn handle( + &self, + message: TypedEnvelope, + rpc: &'a Arc, + app_state: &'a Arc, + ) -> Self::Output; +} + +impl<'a, M, F, Fut> MessageHandler<'a, M> for F +where + M: proto::EnvelopedMessage, + F: Fn(TypedEnvelope, &'a Arc, &'a Arc) -> Fut, + Fut: 'a + Send + Future>, +{ + type Output = Fut; + + fn handle( + &self, + message: TypedEnvelope, + rpc: &'a Arc, + app_state: &'a Arc, + ) -> Self::Output { + (self)(message, rpc, app_state) + } +} + +fn on_message(router: &mut Router, rpc: &Arc, app_state: &Arc, handler: H) +where + M: EnvelopedMessage, + H: 'static + Clone + Send + Sync + for<'a> MessageHandler<'a, M>, +{ + let rpc = rpc.clone(); + let handler = handler.clone(); + let app_state = app_state.clone(); + router.add_message_handler(move |message| { + let rpc = rpc.clone(); + let handler = handler.clone(); + let app_state = app_state.clone(); + async move { + let sender_id = message.sender_id; + let message_id = message.message_id; + let start_time = Instant::now(); + log::info!( + "RPC message received. id: {}.{}, type:{}", + sender_id, + message_id, + M::NAME + ); + if let Err(err) = handler.handle(message, &rpc, &app_state).await { + log::error!("error handling message: {:?}", err); + } else { + log::info!( + "RPC message handled. id:{}.{}, duration:{:?}", + sender_id, + message_id, + start_time.elapsed() + ); + } + + Ok(()) + } + }); +} + +pub fn add_rpc_routes(router: &mut Router, state: &Arc, rpc: &Arc) { + on_message(router, rpc, state, share_worktree); + on_message(router, rpc, state, join_worktree); + on_message(router, rpc, state, update_worktree); + on_message(router, rpc, state, close_worktree); + on_message(router, rpc, state, open_buffer); + on_message(router, rpc, state, close_buffer); + on_message(router, rpc, state, update_buffer); + on_message(router, rpc, state, buffer_saved); + on_message(router, rpc, state, save_buffer); +} + +pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { + let mut router = Router::new(); + add_rpc_routes(&mut router, app.state(), rpc); + let router = Arc::new(router); + + let rpc = rpc.clone(); + app.at("/rpc").with(auth::VerifyToken).get(move |request: Request>| { + let user_id = request.ext::().copied(); + let rpc = rpc.clone(); + let router = router.clone(); + async move { + const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + let connection_upgrade = header_contains_ignore_case(&request, CONNECTION, "upgrade"); + let upgrade_to_websocket = header_contains_ignore_case(&request, UPGRADE, "websocket"); + let upgrade_requested = connection_upgrade && upgrade_to_websocket; + + if !upgrade_requested { + return Ok(Response::new(StatusCode::UpgradeRequired)); + } + + let header = match request.header("Sec-Websocket-Key") { + Some(h) => h.as_str(), + None => return Err(anyhow!("expected sec-websocket-key"))?, + }; + + let mut response = Response::new(StatusCode::SwitchingProtocols); + response.insert_header(UPGRADE, "websocket"); + response.insert_header(CONNECTION, "Upgrade"); + let hash = Sha1::new().chain(header).chain(WEBSOCKET_GUID).finalize(); + response.insert_header("Sec-Websocket-Accept", base64::encode(&hash[..])); + response.insert_header("Sec-Websocket-Version", "13"); + + let http_res: &mut tide::http::Response = response.as_mut(); + let upgrade_receiver = http_res.recv_upgrade().await; + let addr = request.remote().unwrap_or("unknown").to_string(); + let state = request.state().clone(); + let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?.0; + task::spawn(async move { + if let Some(stream) = upgrade_receiver.await { + let stream = WebSocketStream::from_raw_socket(stream, Role::Server, None).await; + handle_connection(rpc, router, state, addr, stream, user_id).await; + } + }); + + Ok(response) + } + }); +} + +pub async fn handle_connection( + rpc: Arc, + router: Arc, + state: Arc, + addr: String, + stream: Conn, + user_id: i32, +) where + Conn: 'static + + futures::Sink + + futures::Stream> + + Send + + Unpin, +{ + log::info!("accepted rpc connection: {:?}", addr); + let (connection_id, handle_io, handle_messages) = rpc.add_connection(stream, router).await; + state + .rpc + .write() + .await + .add_connection(connection_id, user_id); + + let handle_messages = async move { + handle_messages.await; + Ok(()) + }; + + if let Err(e) = futures::try_join!(handle_messages, handle_io) { + log::error!("error handling rpc connection {:?} - {:?}", addr, e); + } + + log::info!("closing connection to {:?}", addr); + if let Err(e) = rpc.sign_out(connection_id, &state).await { + log::error!("error signing out connection {:?} - {:?}", addr, e); + } +} + +async fn share_worktree( + mut request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let mut state = state.rpc.write().await; + let worktree_id = state.next_worktree_id; + state.next_worktree_id += 1; + let access_token = random_token(); + let worktree = request + .payload + .worktree + .as_mut() + .ok_or_else(|| anyhow!("missing worktree"))?; + let entries = mem::take(&mut worktree.entries) + .into_iter() + .map(|entry| (entry.id, entry)) + .collect(); + state.worktrees.insert( + worktree_id, + WorktreeState { + host_connection_id: Some(request.sender_id), + guest_connection_ids: Default::default(), + active_replica_ids: Default::default(), + access_token: access_token.clone(), + root_name: mem::take(&mut worktree.root_name), + entries, + }, + ); + + rpc.respond( + request.receipt(), + proto::ShareWorktreeResponse { + worktree_id, + access_token, + }, + ) + .await?; + Ok(()) +} + +async fn join_worktree( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let worktree_id = request.payload.worktree_id; + let access_token = &request.payload.access_token; + + let mut state = state.rpc.write().await; + if let Some((peer_replica_id, worktree)) = + state.join_worktree(request.sender_id, worktree_id, access_token) + { + let mut peers = Vec::new(); + if let Some(host_connection_id) = worktree.host_connection_id { + peers.push(proto::Peer { + peer_id: host_connection_id.0, + replica_id: 0, + }); + } + for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids { + if *peer_conn_id != request.sender_id { + peers.push(proto::Peer { + peer_id: peer_conn_id.0, + replica_id: *peer_replica_id as u32, + }); + } + } + + broadcast(request.sender_id, worktree.connection_ids(), |conn_id| { + rpc.send( + conn_id, + proto::AddPeer { + worktree_id, + peer: Some(proto::Peer { + peer_id: request.sender_id.0, + replica_id: peer_replica_id as u32, + }), + }, + ) + }) + .await?; + rpc.respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: Some(proto::Worktree { + root_name: worktree.root_name.clone(), + entries: worktree.entries.values().cloned().collect(), + }), + replica_id: peer_replica_id as u32, + peers, + }, + ) + .await?; + } else { + rpc.respond( + request.receipt(), + proto::OpenWorktreeResponse { + worktree_id, + worktree: None, + replica_id: 0, + peers: Vec::new(), + }, + ) + .await?; + } + + Ok(()) +} + +async fn update_worktree( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + { + let mut state = state.rpc.write().await; + let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; + for entry_id in &request.payload.removed_entries { + worktree.entries.remove(&entry_id); + } + + for entry in &request.payload.updated_entries { + worktree.entries.insert(entry.id, entry.clone()); + } + } + + broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await?; + Ok(()) +} + +async fn close_worktree( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let connection_ids; + { + let mut state = state.rpc.write().await; + let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; + connection_ids = worktree.connection_ids(); + if worktree.host_connection_id == Some(request.sender_id) { + worktree.host_connection_id = None; + } else if let Some(replica_id) = worktree.guest_connection_ids.remove(&request.sender_id) { + worktree.active_replica_ids.remove(&replica_id); + } + } + + broadcast(request.sender_id, connection_ids, |conn_id| { + rpc.send( + conn_id, + proto::RemovePeer { + worktree_id: request.payload.worktree_id, + peer_id: request.sender_id.0, + }, + ) + }) + .await?; + + Ok(()) +} + +async fn open_buffer( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let receipt = request.receipt(); + let worktree_id = request.payload.worktree_id; + let host_connection_id = state + .rpc + .read() + .await + .read_worktree(worktree_id, request.sender_id)? + .host_connection_id()?; + + let response = rpc + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?; + rpc.respond(receipt, response).await?; + Ok(()) +} + +async fn close_buffer( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let host_connection_id = state + .rpc + .read() + .await + .read_worktree(request.payload.worktree_id, request.sender_id)? + .host_connection_id()?; + + rpc.forward_send(request.sender_id, host_connection_id, request.payload) + .await?; + + Ok(()) +} + +async fn save_buffer( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let host; + let guests; + { + let state = state.rpc.read().await; + let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?; + host = worktree.host_connection_id()?; + guests = worktree + .guest_connection_ids + .keys() + .copied() + .collect::>(); + } + + let sender = request.sender_id; + let receipt = request.receipt(); + let response = rpc + .forward_request(sender, host, request.payload.clone()) + .await?; + + broadcast(host, guests, |conn_id| { + let response = response.clone(); + async move { + if conn_id == sender { + rpc.respond(receipt, response).await + } else { + rpc.forward_send(host, conn_id, response).await + } + } + }) + .await?; + + Ok(()) +} + +async fn update_buffer( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await +} + +async fn buffer_saved( + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await +} + +async fn broadcast_in_worktree( + worktree_id: u64, + request: TypedEnvelope, + rpc: &Arc, + state: &Arc, +) -> tide::Result<()> { + let connection_ids = state + .rpc + .read() + .await + .read_worktree(worktree_id, request.sender_id)? + .connection_ids(); + + broadcast(request.sender_id, connection_ids, |conn_id| { + rpc.forward_send(request.sender_id, conn_id, request.payload.clone()) + }) + .await?; + + Ok(()) +} + +pub async fn broadcast( + sender_id: ConnectionId, + receiver_ids: Vec, + mut f: F, +) -> anyhow::Result<()> +where + F: FnMut(ConnectionId) -> T, + T: Future>, +{ + let futures = receiver_ids + .into_iter() + .filter(|id| *id != sender_id) + .map(|id| f(id)); + futures::future::try_join_all(futures).await?; + Ok(()) +} + +fn header_contains_ignore_case( + request: &tide::Request, + header_name: HeaderName, + value: &str, +) -> bool { + request + .header(header_name) + .map(|h| { + h.as_str() + .split(',') + .any(|s| s.trim().eq_ignore_ascii_case(value.trim())) + }) + .unwrap_or(false) +} diff --git a/server/src/team.rs b/server/src/team.rs new file mode 100644 index 0000000000000000000000000000000000000000..d04e6db668f04ba4f3782aec8f0d5fd82594336d --- /dev/null +++ b/server/src/team.rs @@ -0,0 +1,15 @@ +use crate::{AppState, Request, RequestExt}; +use std::sync::Arc; +use tide::http::mime; + +pub fn add_routes(app: &mut tide::Server>) { + app.at("/team").get(get_team); +} + +async fn get_team(mut request: Request) -> tide::Result { + let data = request.layout_data().await?; + Ok(tide::Response::builder(200) + .body(request.state().render_template("team.hbs", &data)?) + .content_type(mime::HTML) + .build()) +} diff --git a/server/src/tests.rs b/server/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..041a5adcea301910d82539b74c4396120640d4e1 --- /dev/null +++ b/server/src/tests.rs @@ -0,0 +1,599 @@ +use crate::{ + admin, auth, github, + rpc::{self, add_rpc_routes}, + AppState, Config, +}; +use async_std::task; +use gpui::TestAppContext; +use rand::prelude::*; +use serde_json::json; +use sqlx::{ + migrate::{MigrateDatabase, Migrator}, + postgres::PgPoolOptions, + Executor as _, Postgres, +}; +use std::{path::Path, sync::Arc}; +use zed::{ + editor::Editor, + language::LanguageRegistry, + rpc::Client, + settings, + test::Channel, + worktree::{FakeFs, Fs as _, Worktree}, +}; +use zrpc::{ForegroundRouter, Peer, Router}; + +#[gpui::test] +async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + let (window_b, _) = cx_b.add_window(|_| EmptyView); + let settings = settings::channel(&cx_b.font_cache()).unwrap().1; + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id()); + worktree_a + .condition(&cx_a, |tree, _| { + tree.peers() + .values() + .any(|replica_id| *replica_id == replica_id_b) + }) + .await; + + // Open the same file as client B and client A. + let buffer_b = worktree_b + .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx)) + .await + .unwrap(); + buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); + worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx))); + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx)) + .await + .unwrap(); + + // Create a selection set as client B and see that selection set as client A. + let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx)); + buffer_a + .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1) + .await; + + // Edit the buffer as client B and see that edit as client A. + editor_b.update(&mut cx_b, |editor, cx| { + editor.insert(&"ok, ".to_string(), cx) + }); + buffer_a + .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") + .await; + + // Remove the selection set as client B, see those selections disappear as client A. + cx_b.update(move |_| drop(editor_b)); + buffer_a + .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) + .await; + + // Close the buffer as client A, see that the buffer is closed. + drop(buffer_a); + worktree_a + .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx)) + .await; + + // Dropping the worktree removes client B from client A's peers. + cx_b.update(move |_| drop(worktree_b)); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().is_empty()) + .await; +} + +#[gpui::test] +async fn test_propagate_saves_and_fs_changes_in_shared_worktree( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + mut cx_c: TestAppContext, +) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 3 clients. + let mut server = TestServer::start().await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + let client_c = server.create_client(&mut cx_c, "user_c").await; + + let fs = Arc::new(FakeFs::new()); + + // Share a worktree as client A. + fs.insert_tree( + "/a", + json!({ + "file1": "", + "file2": "" + }), + ) + .await; + + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs.clone(), + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as clients B and C. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token.clone(), + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let worktree_c = Worktree::open_remote( + client_c.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_c.to_async(), + ) + .await + .unwrap(); + + // Open and edit a buffer as both guests B and C. + let buffer_b = worktree_b + .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + let buffer_c = worktree_c + .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); + buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx)); + + // Open and edit that buffer as the host. + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx)) + .await + .unwrap(); + + buffer_a + .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") + .await; + buffer_a.update(&mut cx_a, |buf, cx| { + buf.edit([buf.len()..buf.len()], "i-am-a", cx) + }); + + // Wait for edits to propagate + buffer_a + .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + buffer_b + .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + buffer_c + .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") + .await; + + // Edit the buffer as the host and concurrently save as guest B. + let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap()); + buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx)); + save_b.await.unwrap(); + assert_eq!( + fs.load("/a/file1".as_ref()).await.unwrap(), + "hi-a, i-am-c, i-am-b, i-am-a" + ); + buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty())); + buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty())); + buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await; + + // Make changes on host's file system, see those changes on the guests. + fs.rename("/a/file2".as_ref(), "/a/file3".as_ref()) + .await + .unwrap(); + fs.insert_file(Path::new("/a/file4"), "4".into()) + .await + .unwrap(); + + worktree_b + .condition(&cx_b, |tree, _| tree.file_count() == 3) + .await; + worktree_c + .condition(&cx_c, |tree, _| tree.file_count() == 3) + .await; + worktree_b.read_with(&cx_b, |tree, _| { + assert_eq!( + tree.paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + &["file1", "file3", "file4"] + ) + }); + worktree_c.read_with(&cx_c, |tree, _| { + assert_eq!( + tree.paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + &["file1", "file3", "file4"] + ) + }); +} + +#[gpui::test] +async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.save(Path::new("/a.txt"), &"a-contents".into()) + .await + .unwrap(); + let worktree_a = Worktree::open_local( + "/".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let buffer_b = worktree_b + .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) + .await + .unwrap(); + let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime); + + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx)); + buffer_b.read_with(&cx_b, |buf, _| { + assert!(buf.is_dirty()); + assert!(!buf.has_conflict()); + }); + + buffer_b + .update(&mut cx_b, |buf, cx| buf.save(cx)) + .unwrap() + .await + .unwrap(); + worktree_b + .condition(&cx_b, |_, cx| { + buffer_b.read(cx).file().unwrap().mtime != mtime + }) + .await; + buffer_b.read_with(&cx_b, |buf, _| { + assert!(!buf.is_dirty()); + assert!(!buf.has_conflict()); + }); + + buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx)); + buffer_b.read_with(&cx_b, |buf, _| { + assert!(buf.is_dirty()); + assert!(!buf.has_conflict()); + }); +} + +#[gpui::test] +async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.save(Path::new("/a.txt"), &"a-contents".into()) + .await + .unwrap(); + let worktree_a = Worktree::open_local( + "/".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let buffer_a = worktree_a + .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) + .await + .unwrap(); + let buffer_b = cx_b + .background() + .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); + + task::yield_now().await; + buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); + + let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); + let buffer_b = buffer_b.await.unwrap(); + buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; +} + +#[gpui::test] +async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_a, "user_b").await; + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + "/a".as_ref(), + lang_registry.clone(), + fs, + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_id, worktree_token) = worktree_a + .update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().share(client_a.clone(), cx) + }) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let _worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + worktree_token, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 1) + .await; + + // Drop client B's connection and ensure client A observes client B leaving the worktree. + client_b.disconnect().await.unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 0) + .await; +} + +struct TestServer { + peer: Arc, + app_state: Arc, + db_name: String, + router: Arc, +} + +impl TestServer { + async fn start() -> Self { + let mut rng = StdRng::from_entropy(); + let db_name = format!("zed-test-{}", rng.gen::()); + let app_state = Self::build_app_state(&db_name).await; + let peer = Peer::new(); + let mut router = Router::new(); + add_rpc_routes(&mut router, &app_state, &peer); + Self { + peer, + router: Arc::new(router), + app_state, + db_name, + } + } + + async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client { + let user_id = admin::create_user(&self.app_state.db, name, false) + .await + .unwrap(); + let lang_registry = Arc::new(LanguageRegistry::new()); + let client = Client::new(lang_registry.clone()); + let mut client_router = ForegroundRouter::new(); + cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router)); + + let (client_conn, server_conn) = Channel::bidirectional(); + cx.background() + .spawn(rpc::handle_connection( + self.peer.clone(), + self.router.clone(), + self.app_state.clone(), + name.to_string(), + server_conn, + user_id, + )) + .detach(); + client + .add_connection(client_conn, Arc::new(client_router), cx.to_async()) + .await + .unwrap(); + + // Reset the executor because running SQL queries has a non-deterministic impact on it. + cx.foreground().reset(); + client + } + + async fn build_app_state(db_name: &str) -> Arc { + let mut config = Config::default(); + config.session_secret = "a".repeat(32); + config.database_url = format!("postgres://postgres@localhost/{}", db_name); + + Self::create_db(&config.database_url).await; + let db = PgPoolOptions::new() + .max_connections(5) + .connect(&config.database_url) + .await + .expect("failed to connect to postgres database"); + let migrator = Migrator::new(Path::new("./migrations")).await.unwrap(); + migrator.run(&db).await.unwrap(); + + let github_client = github::AppClient::test(); + Arc::new(AppState { + db, + handlebars: Default::default(), + auth_client: auth::build_client("", ""), + repo_client: github::RepoClient::test(&github_client), + github_client, + rpc: Default::default(), + config, + }) + } + + async fn create_db(url: &str) { + // Enable tests to run in parallel by serializing the creation of each test database. + lazy_static::lazy_static! { + static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(()); + } + + let _lock = DB_CREATION.lock().await; + Postgres::create_database(url) + .await + .expect("failed to create test database"); + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + task::block_on(async { + self.peer.reset().await; + self.app_state + .db + .execute( + format!( + " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();", + self.db_name, + ) + .as_str(), + ) + .await + .unwrap(); + self.app_state.db.close().await; + Postgres::drop_database(&self.app_state.config.database_url) + .await + .unwrap(); + }); + } +} + +struct EmptyView; + +impl gpui::Entity for EmptyView { + type Event = (); +} + +impl gpui::View for EmptyView { + fn ui_name() -> &'static str { + "empty view" + } + + fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { + gpui::Element::boxed(gpui::elements::Empty) + } +} diff --git a/server/static/fonts/VisbyCF-Bold.eot b/server/static/fonts/VisbyCF-Bold.eot new file mode 100644 index 0000000000000000000000000000000000000000..74eab1431ab793d2c1f2eb209e6c4d2541227628 Binary files /dev/null and b/server/static/fonts/VisbyCF-Bold.eot differ diff --git a/server/static/fonts/VisbyCF-Bold.woff b/server/static/fonts/VisbyCF-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..d82871881ca2ee3355d0ee90365075997cd3cbc0 Binary files /dev/null and b/server/static/fonts/VisbyCF-Bold.woff differ diff --git a/server/static/fonts/VisbyCF-Bold.woff2 b/server/static/fonts/VisbyCF-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b0a74210a473925e6327c76353668253016dc018 Binary files /dev/null and b/server/static/fonts/VisbyCF-Bold.woff2 differ diff --git a/server/static/fonts/VisbyCF-BoldOblique.eot b/server/static/fonts/VisbyCF-BoldOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..65e87d4dd86728d3e321bb77989d872f7e3d70ef Binary files /dev/null and b/server/static/fonts/VisbyCF-BoldOblique.eot differ diff --git a/server/static/fonts/VisbyCF-BoldOblique.woff b/server/static/fonts/VisbyCF-BoldOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..a7c88a46e45dcb928c5a8854087a0abbc25ae07e Binary files /dev/null and b/server/static/fonts/VisbyCF-BoldOblique.woff differ diff --git a/server/static/fonts/VisbyCF-BoldOblique.woff2 b/server/static/fonts/VisbyCF-BoldOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7d41ec56d52d49cba604e986edacbb27b1bbf649 Binary files /dev/null and b/server/static/fonts/VisbyCF-BoldOblique.woff2 differ diff --git a/server/static/fonts/VisbyCF-DemiBold.eot b/server/static/fonts/VisbyCF-DemiBold.eot new file mode 100644 index 0000000000000000000000000000000000000000..a692e69a1994753cfcd965d752a00e462a78841b Binary files /dev/null and b/server/static/fonts/VisbyCF-DemiBold.eot differ diff --git a/server/static/fonts/VisbyCF-DemiBold.woff b/server/static/fonts/VisbyCF-DemiBold.woff new file mode 100644 index 0000000000000000000000000000000000000000..da11fc410782ac9ca12f25f96e47b2a20ed41f55 Binary files /dev/null and b/server/static/fonts/VisbyCF-DemiBold.woff differ diff --git a/server/static/fonts/VisbyCF-DemiBold.woff2 b/server/static/fonts/VisbyCF-DemiBold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..725279b90df763a3a1946338661d9f79a0aab43d Binary files /dev/null and b/server/static/fonts/VisbyCF-DemiBold.woff2 differ diff --git a/server/static/fonts/VisbyCF-DemiBoldOblique.eot b/server/static/fonts/VisbyCF-DemiBoldOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..87dc93e1004740020c46d86d604caa9fb0478d3e Binary files /dev/null and b/server/static/fonts/VisbyCF-DemiBoldOblique.eot differ diff --git a/server/static/fonts/VisbyCF-DemiBoldOblique.woff b/server/static/fonts/VisbyCF-DemiBoldOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..c39d362b2a2bb310c9f9ac6ce0541f2ad243126a Binary files /dev/null and b/server/static/fonts/VisbyCF-DemiBoldOblique.woff differ diff --git a/server/static/fonts/VisbyCF-DemiBoldOblique.woff2 b/server/static/fonts/VisbyCF-DemiBoldOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c4e4969e8e583217a51594037e63e2a5b6ef7f14 Binary files /dev/null and b/server/static/fonts/VisbyCF-DemiBoldOblique.woff2 differ diff --git a/server/static/fonts/VisbyCF-ExtraBold.eot b/server/static/fonts/VisbyCF-ExtraBold.eot new file mode 100644 index 0000000000000000000000000000000000000000..d5d1ca31d8bb13504eba2d26d620308c67d12a8f Binary files /dev/null and b/server/static/fonts/VisbyCF-ExtraBold.eot differ diff --git a/server/static/fonts/VisbyCF-ExtraBold.woff b/server/static/fonts/VisbyCF-ExtraBold.woff new file mode 100644 index 0000000000000000000000000000000000000000..490769c8cb2f784183b2768798550861085667e1 Binary files /dev/null and b/server/static/fonts/VisbyCF-ExtraBold.woff differ diff --git a/server/static/fonts/VisbyCF-ExtraBold.woff2 b/server/static/fonts/VisbyCF-ExtraBold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ccec4d68d94cfe2e9a21294c1c755b77412f8655 Binary files /dev/null and b/server/static/fonts/VisbyCF-ExtraBold.woff2 differ diff --git a/server/static/fonts/VisbyCF-ExtraBoldOblique.eot b/server/static/fonts/VisbyCF-ExtraBoldOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..c12bf4cead036ad53a7bde286ad37d1606e714ca Binary files /dev/null and b/server/static/fonts/VisbyCF-ExtraBoldOblique.eot differ diff --git a/server/static/fonts/VisbyCF-ExtraBoldOblique.woff b/server/static/fonts/VisbyCF-ExtraBoldOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..f5fd18cc5958eae3853c6c6535db2c0f997134d4 Binary files /dev/null and b/server/static/fonts/VisbyCF-ExtraBoldOblique.woff differ diff --git a/server/static/fonts/VisbyCF-ExtraBoldOblique.woff2 b/server/static/fonts/VisbyCF-ExtraBoldOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d11da0a31c1a3cafe13651bdefa7369195c409f5 Binary files /dev/null and b/server/static/fonts/VisbyCF-ExtraBoldOblique.woff2 differ diff --git a/server/static/fonts/VisbyCF-Heavy.eot b/server/static/fonts/VisbyCF-Heavy.eot new file mode 100644 index 0000000000000000000000000000000000000000..edd568786396f98a79ad658fce21d55310c2cf26 Binary files /dev/null and b/server/static/fonts/VisbyCF-Heavy.eot differ diff --git a/server/static/fonts/VisbyCF-Heavy.woff b/server/static/fonts/VisbyCF-Heavy.woff new file mode 100644 index 0000000000000000000000000000000000000000..59c7ce86564509f3d7ea99b148bf204321a890d7 Binary files /dev/null and b/server/static/fonts/VisbyCF-Heavy.woff differ diff --git a/server/static/fonts/VisbyCF-Heavy.woff2 b/server/static/fonts/VisbyCF-Heavy.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..49baf8e42a58ea9489e8cd42d75ca16da083a237 Binary files /dev/null and b/server/static/fonts/VisbyCF-Heavy.woff2 differ diff --git a/server/static/fonts/VisbyCF-HeavyOblique.eot b/server/static/fonts/VisbyCF-HeavyOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..3ce587fe2c0d33973fa5978e7eeba7ff793e85fe Binary files /dev/null and b/server/static/fonts/VisbyCF-HeavyOblique.eot differ diff --git a/server/static/fonts/VisbyCF-HeavyOblique.woff b/server/static/fonts/VisbyCF-HeavyOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..f51d3f9433ea7d5d5a5a171027fe6f45a88cde7d Binary files /dev/null and b/server/static/fonts/VisbyCF-HeavyOblique.woff differ diff --git a/server/static/fonts/VisbyCF-HeavyOblique.woff2 b/server/static/fonts/VisbyCF-HeavyOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2b477bea7065bafc39fb88817fce98f34bd57843 Binary files /dev/null and b/server/static/fonts/VisbyCF-HeavyOblique.woff2 differ diff --git a/server/static/fonts/VisbyCF-Light.eot b/server/static/fonts/VisbyCF-Light.eot new file mode 100644 index 0000000000000000000000000000000000000000..d1e64eddefdfc3b61ece1f99bf4f4eb63f540885 Binary files /dev/null and b/server/static/fonts/VisbyCF-Light.eot differ diff --git a/server/static/fonts/VisbyCF-Light.woff b/server/static/fonts/VisbyCF-Light.woff new file mode 100644 index 0000000000000000000000000000000000000000..06f8cc058c694ba33f3382db70b9951f8fe387d9 Binary files /dev/null and b/server/static/fonts/VisbyCF-Light.woff differ diff --git a/server/static/fonts/VisbyCF-Light.woff2 b/server/static/fonts/VisbyCF-Light.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..110303f496c486df6f6394d23a5ecc0105f539b9 Binary files /dev/null and b/server/static/fonts/VisbyCF-Light.woff2 differ diff --git a/server/static/fonts/VisbyCF-LightOblique.eot b/server/static/fonts/VisbyCF-LightOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..5f803f1c7235b8b060b956de78f704be0e1d8851 Binary files /dev/null and b/server/static/fonts/VisbyCF-LightOblique.eot differ diff --git a/server/static/fonts/VisbyCF-LightOblique.woff b/server/static/fonts/VisbyCF-LightOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..afdbb176a208bf9eec1640365e4ce6f71ad468fa Binary files /dev/null and b/server/static/fonts/VisbyCF-LightOblique.woff differ diff --git a/server/static/fonts/VisbyCF-LightOblique.woff2 b/server/static/fonts/VisbyCF-LightOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7cc0d7fcc56b3eae57d108f3794abd03ec7311ca Binary files /dev/null and b/server/static/fonts/VisbyCF-LightOblique.woff2 differ diff --git a/server/static/fonts/VisbyCF-Medium.eot b/server/static/fonts/VisbyCF-Medium.eot new file mode 100644 index 0000000000000000000000000000000000000000..3162546b322743cba1dbd7b21849e54b3be37076 Binary files /dev/null and b/server/static/fonts/VisbyCF-Medium.eot differ diff --git a/server/static/fonts/VisbyCF-Medium.woff b/server/static/fonts/VisbyCF-Medium.woff new file mode 100644 index 0000000000000000000000000000000000000000..2ba79e63ab545eb3c8e4e9143352bb24e80b49ae Binary files /dev/null and b/server/static/fonts/VisbyCF-Medium.woff differ diff --git a/server/static/fonts/VisbyCF-Medium.woff2 b/server/static/fonts/VisbyCF-Medium.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e02074269c170d70b47938838416ab2eccfa6ea8 Binary files /dev/null and b/server/static/fonts/VisbyCF-Medium.woff2 differ diff --git a/server/static/fonts/VisbyCF-MediumOblique.eot b/server/static/fonts/VisbyCF-MediumOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..9a40e3d154e85c3bf15c8bf33d4d3654d882618d Binary files /dev/null and b/server/static/fonts/VisbyCF-MediumOblique.eot differ diff --git a/server/static/fonts/VisbyCF-MediumOblique.woff b/server/static/fonts/VisbyCF-MediumOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..70610de8d63fd3e4a97f12672c17c6e2ab66e603 Binary files /dev/null and b/server/static/fonts/VisbyCF-MediumOblique.woff differ diff --git a/server/static/fonts/VisbyCF-MediumOblique.woff2 b/server/static/fonts/VisbyCF-MediumOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b4a887af037769e2c37ad029c41829c476594b6e Binary files /dev/null and b/server/static/fonts/VisbyCF-MediumOblique.woff2 differ diff --git a/server/static/fonts/VisbyCF-Regular.eot b/server/static/fonts/VisbyCF-Regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..4984ee655b403dc05aa119956512f45b6b020e76 Binary files /dev/null and b/server/static/fonts/VisbyCF-Regular.eot differ diff --git a/server/static/fonts/VisbyCF-Regular.woff b/server/static/fonts/VisbyCF-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..a064e5b39782846b53aab0487531515167ac1e4e Binary files /dev/null and b/server/static/fonts/VisbyCF-Regular.woff differ diff --git a/server/static/fonts/VisbyCF-Regular.woff2 b/server/static/fonts/VisbyCF-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d11dcf1da939610a941319074105289e54f8f6b8 Binary files /dev/null and b/server/static/fonts/VisbyCF-Regular.woff2 differ diff --git a/server/static/fonts/VisbyCF-RegularOblique.eot b/server/static/fonts/VisbyCF-RegularOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..88b0b22daf2c48ae0b2543d326473422947eb92c Binary files /dev/null and b/server/static/fonts/VisbyCF-RegularOblique.eot differ diff --git a/server/static/fonts/VisbyCF-RegularOblique.woff b/server/static/fonts/VisbyCF-RegularOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..30edfb697fb08f627123ce4fcbddc3c7199513e5 Binary files /dev/null and b/server/static/fonts/VisbyCF-RegularOblique.woff differ diff --git a/server/static/fonts/VisbyCF-RegularOblique.woff2 b/server/static/fonts/VisbyCF-RegularOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..254537fb1b094bf1f5c5d0b5316c89818ad8fb2b Binary files /dev/null and b/server/static/fonts/VisbyCF-RegularOblique.woff2 differ diff --git a/server/static/fonts/VisbyCF-Thin.eot b/server/static/fonts/VisbyCF-Thin.eot new file mode 100644 index 0000000000000000000000000000000000000000..28910a2fa64433bd4fb63691fedaa01c2d941d19 Binary files /dev/null and b/server/static/fonts/VisbyCF-Thin.eot differ diff --git a/server/static/fonts/VisbyCF-Thin.woff b/server/static/fonts/VisbyCF-Thin.woff new file mode 100644 index 0000000000000000000000000000000000000000..8c46eace0308f746cae195284591f5b0b30e6ba9 Binary files /dev/null and b/server/static/fonts/VisbyCF-Thin.woff differ diff --git a/server/static/fonts/VisbyCF-Thin.woff2 b/server/static/fonts/VisbyCF-Thin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a8efa01c036a2c7b57e5eff556ba432439b7d56f Binary files /dev/null and b/server/static/fonts/VisbyCF-Thin.woff2 differ diff --git a/server/static/fonts/VisbyCF-ThinOblique.eot b/server/static/fonts/VisbyCF-ThinOblique.eot new file mode 100644 index 0000000000000000000000000000000000000000..566303e2f4b88239f89cf12f77ce1d177499bb3b Binary files /dev/null and b/server/static/fonts/VisbyCF-ThinOblique.eot differ diff --git a/server/static/fonts/VisbyCF-ThinOblique.woff b/server/static/fonts/VisbyCF-ThinOblique.woff new file mode 100644 index 0000000000000000000000000000000000000000..82c946d60759ba0d25fcad9b40fd58b81d85883c Binary files /dev/null and b/server/static/fonts/VisbyCF-ThinOblique.woff differ diff --git a/server/static/fonts/VisbyCF-ThinOblique.woff2 b/server/static/fonts/VisbyCF-ThinOblique.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9c92ce05cca008dad878e242d1218f5fd401ea78 Binary files /dev/null and b/server/static/fonts/VisbyCF-ThinOblique.woff2 differ diff --git a/server/static/images/favicon.png b/server/static/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..074140e0627270a23e1b8574d9ab8f178f759ab6 Binary files /dev/null and b/server/static/images/favicon.png differ diff --git a/server/static/svg/hero.svg b/server/static/svg/hero.svg new file mode 100644 index 0000000000000000000000000000000000000000..0678b702635c988b51a407b6deda0f32c2d1a420 --- /dev/null +++ b/server/static/svg/hero.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/server/styles.css b/server/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..ab10493e32ab146ccec53f5a29c179785288c32a --- /dev/null +++ b/server/styles.css @@ -0,0 +1,108 @@ +/* This file is compiled to /assets/styles/tailwind.css via script/tailwind */ + +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap'); + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-Thin.woff2') format('woff2'), + url('/static/fonts/VisbyCF-Thin.woff') format('woff'); + font-weight: 100; + font-style: normal; +} + + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-Light.woff2') format('woff2'), + url('/static/fonts/VisbyCF-Light.woff') format('woff'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-Regular.woff2') format('woff2'), + url('/static/fonts/VisbyCF-Regular.woff') format('woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-Medium.woff2') format('woff2'), + url('/static/fonts/VisbyCF-Medium.woff') format('woff'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-DemiBold.woff2') format('woff2'), + url('/static/fonts/VisbyCF-DemiBold.woff') format('woff'); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-Bold.woff2') format('woff2'), + url('/static/fonts/VisbyCF-Bold.woff') format('woff'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-ExtraBold.woff2') format('woff2'), + url('/static/fonts/VisbyCF-ExtraBold.woff') format('woff'); + font-weight: 800; + font-style: normal; +} + +@font-face { + font-family: 'Visby CF'; + src: + url('/static/fonts/VisbyCF-Heavy.woff2') format('woff2'), + url('/static/fonts/VisbyCF-Heavy.woff') format('woff'); + font-weight: 900; + font-style: normal; +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + @responsive { + .bg-dotgrid-sm { + background: + linear-gradient(90deg, theme('colors.gray.50') 38px, transparent 1%) center, + linear-gradient(theme('colors.gray.50') 38px, transparent 1%) center, + theme('colors.gray.600'); + background-size: 40px 40px; + } + + .bg-dotgrid-md { + background: + linear-gradient(90deg, theme('colors.gray.50') 58px, transparent 1%) center, + linear-gradient(theme('colors.gray.50') 58px, transparent 1%) center, + theme('colors.gray.600'); + background-size: 60px 60px; + } + + .bg-dotgrid-lg { + background: + linear-gradient(90deg, theme('colors.gray.50') 88px, transparent 1%) center, + linear-gradient(theme('colors.gray.50') 88px, transparent 1%) center, + theme('colors.gray.600'); + background-size: 90px 90px; + } + } +} \ No newline at end of file diff --git a/server/templates/admin.hbs b/server/templates/admin.hbs new file mode 100644 index 0000000000000000000000000000000000000000..4dbce023bb8096de291b3e4477aaf41a15f74a89 --- /dev/null +++ b/server/templates/admin.hbs @@ -0,0 +1,81 @@ +{{#> layout }} + + +
+
+

Users

+ + + + + + + + + + + + + + + {{#each users}} + + + + + + + + {{/each}} +
GitHub LoginAdmin
+ + + + + +
+ {{github_login}} + + + + +
+ +

Signups

+ + {{#each signups}} + + + + + + + + + {{/each}} +
{{github_login}}{{email_address}}{{about}} + +
+
+
+{{/layout}} \ No newline at end of file diff --git a/server/templates/docs.hbs b/server/templates/docs.hbs new file mode 100644 index 0000000000000000000000000000000000000000..cb5f0410897fa1ab44c33855160e41d42bbb8c7c --- /dev/null +++ b/server/templates/docs.hbs @@ -0,0 +1,41 @@ +{{#> layout }} + +
+
+

Bypassing code signing restrictions

+
+
+

+ We haven't yet applied to Apple for the required certificate to sign our application bundle, which + means there's a small speed bump when you run our app. +

+

+ Instead of double-clicking the app, right click it and choose Open. +

+

+ You need to attempt open the app twice. On the second attempt, you should see the option + to open the application anyway in the dialog. +

+
+ Screen Shot 2021-06-02 at 2 38 12 PM + Screen Shot 2021-06-02 at 2 38 19 PM +
+ +

Key bindings

+
+
+
+
cmd-shift-L
+
+
+ Split selection into lines +
+
+
+
+
+ +{{/layout}} \ No newline at end of file diff --git a/server/templates/error.hbs b/server/templates/error.hbs new file mode 100644 index 0000000000000000000000000000000000000000..6013b2de80eb80ffd9e28be522beda5a522b88bc --- /dev/null +++ b/server/templates/error.hbs @@ -0,0 +1,7 @@ +{{#> layout }} +
+
+ Sorry, we encountered a {{status}} error: {{reason}}. +
+
+{{/layout}} \ No newline at end of file diff --git a/server/templates/home.hbs b/server/templates/home.hbs new file mode 100644 index 0000000000000000000000000000000000000000..796fc232234cfed34d7c0aeee4afd5f870f6a9ee --- /dev/null +++ b/server/templates/home.hbs @@ -0,0 +1,69 @@ +{{#> layout }} +{{#if releases}} + +
+
+ {{#each releases}} +
+
+
+ VERSION {{name}} +
+ + DOWNLOAD + +
+
+ {{{body}}} +
+
+ {{/each}} +
+
+ +{{else}} + +
+ +
+ +
+
+

+ We’re the team behind GitHub’s Atom text editor, and we’re building something new: +

+ +

+ Zed is a fully-native desktop code editor focused on high performance, + clean design, and seamless collaboration. +

+ +

+ We’re in early development, but we’d like to build a small community of developers who care deeply about + their tools and are willing to give us feedback. We'll be sharing alpha builds with community members and + telling our story along the way. +

+ +

+ If you’re interested in joining us, please let us know. +

+
+ +
+ + + + +
+
+ +{{/if}} +{{/layout}} \ No newline at end of file diff --git a/server/templates/partials/layout.hbs b/server/templates/partials/layout.hbs new file mode 100644 index 0000000000000000000000000000000000000000..1e4a3561bda5f10287617c1f2a5e4c2a39c13628 --- /dev/null +++ b/server/templates/partials/layout.hbs @@ -0,0 +1,62 @@ + + + + + + Zed Industries + + + + + + +
+
+ + ZEDINDUSTRIES + +
+ + Team + + {{#if current_user}} + {{#if current_user.is_admin }} + + Admin + + {{/if}} +
+ + +
+ {{else}} + + Log in + + {{/if}} +
+
+ + {{> @partial-block}} + + + \ No newline at end of file diff --git a/server/templates/signup.hbs b/server/templates/signup.hbs new file mode 100644 index 0000000000000000000000000000000000000000..738cf2e6e6b485f9194b87ca34af17fbdd453424 --- /dev/null +++ b/server/templates/signup.hbs @@ -0,0 +1,19 @@ +{{#> layout }} +
+
+
+ THANKS +
+
+

+ Thanks a ton for your interest! We'll add you to our list and let you know when we have something ready + for you to try out. +

+ +

+ Back to / +

+
+
+
+{{/layout}} \ No newline at end of file diff --git a/server/templates/team.hbs b/server/templates/team.hbs new file mode 100644 index 0000000000000000000000000000000000000000..e02f284c0c3980e9bf9be79c1f5214019831857c --- /dev/null +++ b/server/templates/team.hbs @@ -0,0 +1,62 @@ +{{#> layout }} + +
+
+
+ +
+ + NATHAN SOBO + +
+ Nathan joined GitHub in late 2011 to build the Atom text editor, and + he led the Atom team until 2018. He also co-led development of Teletype for Atom, pioneering one of the first production + uses of conflict-free replicated data types for collaborative text editing. He's been dreaming about + building the world’s best text editor since he graduated from college, and is excited to finally + have + the knowledge, tools, and resources to achieve this vision. +
+
+
+
+ +
+ + ANTONIO SCANDURRA + +
+ Antonio joined the Atom team in 2014 while still in university after his outstanding open source + contributions caught the attention of the team. He later joined Nathan in architecting Teletype for + Atom and researching the foundations of what has turned into Zed. For the last two years, + he’s + become an expert in distributed systems and conflict-free replicated data types through the + development of a real-time, distributed, conflict-free database implemented in Rust for Ditto. +
+
+
+
+ +
+ + MAX BRUNSFELD + +
+ Max joined the Atom team in 2013 after working at Pivotal Labs. While driving Atom towards its 1.0 + launch during the day, Max spent nights and weekends building Tree-sitter, a blazing-fast and + expressive incremental parsing framework that currently powers all code analysis at GitHub. Before + leaving to start Zed, Max helped GitHub's semantic analysis team integrate Tree-sitter to support + syntax highlighting and code navigation on github.com. +
+
+
+
+
+ +{{/layout}} diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 3a1eb99215f080f025eb4e91a53270b8a215ba9e..7e4d4949d6732de853f942fae8b6e234dc9bc677 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -14,20 +14,20 @@ name = "Zed" path = "src/main.rs" [features] -test-support = ["tempdir", "serde_json", "zed-rpc/test-support"] +test-support = ["tempdir", "serde_json", "zrpc/test-support"] [dependencies] anyhow = "1.0.38" arrayvec = "0.5.2" async-trait = "0.1" -async-tungstenite = { version="0.14", features=["async-tls"] } +async-tungstenite = { version = "0.14", features = ["async-tls"] } crossbeam-channel = "0.5.0" ctor = "0.1.20" dirs = "3.0" easy-parallel = "3.1.0" -fsevent = { path="../fsevent" } +fsevent = { path = "../fsevent" } futures = "0.3" -gpui = { path="../gpui" } +gpui = { path = "../gpui" } http-auth-basic = "0.1.3" ignore = "0.4" lazy_static = "1.4.0" @@ -35,31 +35,33 @@ libc = "0.2" log = "0.4" num_cpus = "1.13.0" parking_lot = "0.11.1" -postage = { version="0.4.1", features=["futures-traits"] } +postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" rsa = "0.4" rust-embed = "5.9.0" seahash = "4.1" -serde = { version="1", features=["derive"] } -serde_json = { version="1.0.64", features=["preserve_order"], optional=true } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1.0.64", features = [ + "preserve_order", +], optional = true } similar = "1.3" simplelog = "0.9" -smallvec = { version="1.6", features=["union"] } +smallvec = { version = "1.6", features = ["union"] } smol = "1.2.5" surf = "2.2" -tempdir = { version="0.3.7", optional=true } +tempdir = { version = "0.3.7", optional = true } tiny_http = "0.8" toml = "0.5" tree-sitter = "0.19.5" tree-sitter-rust = "0.19.0" url = "2.2" -zed-rpc = { path="../zed-rpc" } +zrpc = { path = "../zrpc" } [dev-dependencies] cargo-bundle = "0.5.0" env_logger = "0.8" -serde_json = { version="1.0.64", features=["preserve_order"] } -tempdir = { version="0.3.7" } +serde_json = { version = "1.0.64", features = ["preserve_order"] } +tempdir = { version = "0.3.7" } unindent = "0.1.7" [package.metadata.bundle] diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 304d9e3f4074221787daf5f47170407d5ced914b..6c1a0af436aa8d78fed14272031cc498db863f71 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -11,7 +11,7 @@ use seahash::SeaHasher; pub use selection::*; use similar::{ChangeTag, TextDiff}; use tree_sitter::{InputEdit, Parser, QueryCursor}; -use zed_rpc::proto; +use zrpc::proto; use crate::{ language::{Language, Tree}, @@ -2734,7 +2734,7 @@ mod tests { use crate::{ test::{build_app_state, temp_tree}, util::RandomCharIter, - worktree::{Worktree, WorktreeHandle}, + worktree::{RealFs, Worktree, WorktreeHandle}, }; use gpui::ModelHandle; use rand::prelude::*; @@ -3209,7 +3209,14 @@ mod tests { "file2": "def", "file3": "ghi", })); - let tree = cx.add_model(|cx| Worktree::local(dir.path(), Default::default(), cx)); + let tree = Worktree::open_local( + dir.path(), + Default::default(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); tree.flush_fs_events(&cx).await; cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; @@ -3321,7 +3328,14 @@ mod tests { async fn test_file_changes_on_disk(mut cx: gpui::TestAppContext) { let initial_contents = "aaa\nbbbbb\nc\n"; let dir = temp_tree(json!({ "the-file": initial_contents })); - let tree = cx.add_model(|cx| Worktree::local(dir.path(), Default::default(), cx)); + let tree = Worktree::open_local( + dir.path(), + Default::default(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 9ea1a9b1a2a84d2fa993535c12d9b74f7d43eaec..02ea62eaf63030ce2b6c077408f939ca7059d823 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -399,11 +399,11 @@ impl FileFinder { .map(|tree| tree.read(cx).snapshot()) .collect::>(); let search_id = util::post_inc(&mut self.search_count); - let pool = cx.as_ref().thread_pool().clone(); + let background = cx.as_ref().background().clone(); self.cancel_flag.store(true, atomic::Ordering::Relaxed); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); - let background_task = cx.background_executor().spawn(async move { + Some(cx.spawn(|this, mut cx| async move { let include_root_name = snapshots.len() > 1; let matches = match_paths( snapshots.iter(), @@ -413,15 +413,13 @@ impl FileFinder { false, 100, cancel_flag.clone(), - pool, - ); + background, + ) + .await; let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); - (search_id, did_cancel, query, matches) - }); - - Some(cx.spawn(|this, mut cx| async move { - let matches = background_task.await; - this.update(&mut cx, |this, cx| this.update_matches(matches, cx)); + this.update(&mut cx, |this, cx| { + this.update_matches((search_id, did_cancel, query, matches), cx) + }); })) } @@ -461,6 +459,7 @@ mod tests { editor, test::{build_app_state, temp_tree}, workspace::Workspace, + worktree::FakeFs, }; use serde_json::json; use std::fs; @@ -478,16 +477,13 @@ mod tests { }); let app_state = cx.read(build_app_state); - let (window_id, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(tmp_dir.path(), cx); - workspace - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(tmp_dir.path(), cx) + }) + .await + .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; cx.dispatch_action( @@ -540,26 +536,31 @@ mod tests { #[gpui::test] async fn test_matching_cancellation(mut cx: gpui::TestAppContext) { - let tmp_dir = temp_tree(json!({ - "hello": "", - "goodbye": "", - "halogen-light": "", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - })); - let app_state = cx.read(build_app_state); - let (_, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(tmp_dir.path(), cx); - workspace - }); + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/dir", + json!({ + "hello": "", + "goodbye": "", + "halogen-light": "", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }), + ) + .await; + + let mut app_state = cx.read(build_app_state); + Arc::get_mut(&mut app_state).unwrap().fs = fs; + + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree("/dir".as_ref(), cx) + }) + .await + .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; let (_, finder) = @@ -613,16 +614,13 @@ mod tests { fs::write(&file_path, "").unwrap(); let app_state = cx.read(build_app_state); - let (_, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(&file_path, cx); - workspace - }); + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(&file_path, cx) + }) + .await + .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; let (_, finder) = @@ -663,15 +661,7 @@ mod tests { })); let app_state = cx.read(build_app_state); - - let (_, workspace) = cx.add_window(|cx| { - Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ) - }); + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); workspace .update(&mut cx, |workspace, cx| { diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 4b1c3571d69ed0701d16b811cb76700dceae85c0..d550c0ee49ef7850321e1d4d3c84e48d6fd81270 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,4 +1,4 @@ -use zed_rpc::ForegroundRouter; +use zrpc::ForegroundRouter; pub mod assets; pub mod editor; @@ -21,6 +21,7 @@ pub struct AppState { pub languages: std::sync::Arc, pub rpc_router: std::sync::Arc, pub rpc: rpc::Client, + pub fs: std::sync::Arc, } pub fn init(cx: &mut gpui::MutableAppContext) { diff --git a/zed/src/main.rs b/zed/src/main.rs index ea11ff0c69ee51aec99898f92a9f7ba94f079f03..4630f215cbf6918c6b0821b756634b9202be0e8b 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -8,9 +8,10 @@ use std::{fs, path::PathBuf, sync::Arc}; use zed::{ self, assets, editor, file_finder, language, menus, rpc, settings, workspace::{self, OpenParams}, - worktree, AppState, + worktree::{self, RealFs}, + AppState, }; -use zed_rpc::ForegroundRouter; +use zrpc::ForegroundRouter; fn main() { init_logger(); @@ -26,6 +27,7 @@ fn main() { settings, rpc_router: Arc::new(ForegroundRouter::new()), rpc: rpc::Client::new(languages), + fs: Arc::new(RealFs), }; app.run(move |cx| { diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index a64ac43084dfc6a3022377b4ba20f95de3bc2485..f455376f2092b99d314089eb6df0f6f2e850352e 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -9,8 +9,8 @@ use std::collections::HashMap; use std::time::Duration; use std::{convert::TryFrom, future::Future, sync::Arc}; use surf::Url; -pub use zed_rpc::{proto, ConnectionId, PeerId, TypedEnvelope}; -use zed_rpc::{ +pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope}; +use zrpc::{ proto::{EnvelopedMessage, RequestMessage}, ForegroundRouter, Peer, Receipt, }; @@ -158,7 +158,7 @@ impl Client { // zed server to encrypt the user's access token, so that it can'be intercepted by // any other app running on the user's device. let (public_key, private_key) = - zed_rpc::auth::keypair().expect("failed to generate keypair for auth"); + zrpc::auth::keypair().expect("failed to generate keypair for auth"); let public_key_string = String::try_from(public_key).expect("failed to serialize public key for auth"); diff --git a/zed/src/test.rs b/zed/src/test.rs index 2fb5b6dd2ef816c4a308334c661a77249f421120..c08f65937cb298a5aa81638fdedf116315d200cc 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -1,14 +1,16 @@ -use crate::{language::LanguageRegistry, rpc, settings, time::ReplicaId, AppState}; +use crate::{ + language::LanguageRegistry, rpc, settings, time::ReplicaId, worktree::RealFs, AppState, +}; use gpui::AppContext; use std::{ path::{Path, PathBuf}, sync::Arc, }; use tempdir::TempDir; -use zed_rpc::ForegroundRouter; +use zrpc::ForegroundRouter; #[cfg(feature = "test-support")] -pub use zed_rpc::test::Channel; +pub use zrpc::test::Channel; #[cfg(test)] #[ctor::ctor] @@ -152,5 +154,6 @@ pub fn build_app_state(cx: &AppContext) -> Arc { languages: languages.clone(), rpc_router: Arc::new(ForegroundRouter::new()), rpc: rpc::Client::new(languages), + fs: Arc::new(RealFs), }) } diff --git a/zed/src/time.rs b/zed/src/time.rs index 8b665e8814d4763c6ddb984597be0fbabf661feb..96418131f2a96410b989367dae0c1ccd613e3be7 100644 --- a/zed/src/time.rs +++ b/zed/src/time.rs @@ -61,8 +61,8 @@ impl<'a> AddAssign<&'a Local> for Local { #[derive(Clone, Default, Hash, Eq, PartialEq)] pub struct Global(SmallVec<[Local; 3]>); -impl From> for Global { - fn from(message: Vec) -> Self { +impl From> for Global { + fn from(message: Vec) -> Self { let mut version = Self::new(); for entry in message { version.observe(Local { @@ -74,11 +74,11 @@ impl From> for Global { } } -impl<'a> From<&'a Global> for Vec { +impl<'a> From<&'a Global> for Vec { fn from(version: &'a Global) -> Self { version .iter() - .map(|entry| zed_rpc::proto::VectorClockEntry { + .map(|entry| zrpc::proto::VectorClockEntry { replica_id: entry.replica_id as u32, timestamp: entry.value, }) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 0de8d3e5bf7ddb7d6954466f439e3e021458443c..28aea860dca4e891f8a4b77741cc6b69764c2682 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -6,7 +6,7 @@ use crate::{ language::LanguageRegistry, rpc, settings::Settings, - worktree::{File, Worktree}, + worktree::{File, Fs, Worktree}, AppState, }; use anyhow::{anyhow, Result}; @@ -29,7 +29,10 @@ use std::{ pub fn init(cx: &mut MutableAppContext) { cx.add_global_action("workspace:open", open); - cx.add_global_action("workspace:open_paths", open_paths); + cx.add_global_action( + "workspace:open_paths", + |params: &OpenParams, cx: &mut MutableAppContext| open_paths(params, cx).detach(), + ); cx.add_global_action("workspace:new_file", open_new); cx.add_global_action("workspace:join_worktree", join_worktree); cx.add_action("workspace:save", Workspace::save_active_item); @@ -65,23 +68,23 @@ fn open(app_state: &Arc, cx: &mut MutableAppContext) { ); } -fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) { +fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) -> Task<()> { log::info!("open paths {:?}", params.paths); // Open paths in existing workspace if possible for window_id in cx.window_ids().collect::>() { if let Some(handle) = cx.root_view::(window_id) { - if handle.update(cx, |view, cx| { + let task = handle.update(cx, |view, cx| { if view.contains_paths(¶ms.paths, cx.as_ref()) { - let open_paths = view.open_paths(¶ms.paths, cx); - cx.foreground().spawn(open_paths).detach(); log::info!("open paths on existing workspace"); - true + Some(view.open_paths(¶ms.paths, cx)) } else { - false + None } - }) { - return; + }); + + if let Some(task) = task { + return task; } } } @@ -89,27 +92,13 @@ fn open_paths(params: &OpenParams, cx: &mut MutableAppContext) { log::info!("open new workspace"); // Add a new workspace if necessary - cx.add_window(|cx| { - let mut view = Workspace::new( - params.app_state.settings.clone(), - params.app_state.languages.clone(), - params.app_state.rpc.clone(), - cx, - ); - let open_paths = view.open_paths(¶ms.paths, cx); - cx.foreground().spawn(open_paths).detach(); - view - }); + let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms.app_state, cx)); + workspace.update(cx, |workspace, cx| workspace.open_paths(¶ms.paths, cx)) } fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { cx.add_window(|cx| { - let mut view = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); + let mut view = Workspace::new(app_state.as_ref(), cx); view.open_new_file(&app_state, cx); view }); @@ -117,12 +106,7 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { fn join_worktree(app_state: &Arc, cx: &mut MutableAppContext) { cx.add_window(|cx| { - let mut view = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); + let mut view = Workspace::new(app_state.as_ref(), cx); view.join_worktree(&app_state, cx); view }); @@ -328,6 +312,7 @@ pub struct Workspace { pub settings: watch::Receiver, languages: Arc, rpc: rpc::Client, + fs: Arc, modal: Option, center: PaneGroup, panes: Vec>, @@ -341,13 +326,8 @@ pub struct Workspace { } impl Workspace { - pub fn new( - settings: watch::Receiver, - languages: Arc, - rpc: rpc::Client, - cx: &mut ViewContext, - ) -> Self { - let pane = cx.add_view(|_| Pane::new(settings.clone())); + pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { + let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); cx.subscribe_to_view(&pane, move |me, _, event, cx| { me.handle_pane_event(pane_id, event, cx) @@ -359,9 +339,10 @@ impl Workspace { center: PaneGroup::new(pane.id()), panes: vec![pane.clone()], active_pane: pane.clone(), - settings, - languages: languages, - rpc, + settings: app_state.settings.clone(), + languages: app_state.languages.clone(), + rpc: app_state.rpc.clone(), + fs: app_state.fs.clone(), worktrees: Default::default(), items: Default::default(), loading_items: Default::default(), @@ -400,87 +381,106 @@ impl Workspace { } } - pub fn open_paths( - &mut self, - abs_paths: &[PathBuf], - cx: &mut ViewContext, - ) -> impl Future { + pub fn open_paths(&mut self, abs_paths: &[PathBuf], cx: &mut ViewContext) -> Task<()> { let entries = abs_paths .iter() .cloned() .map(|path| self.entry_id_for_path(&path, cx)) .collect::>(); - let bg = cx.background_executor().clone(); + let fs = self.fs.clone(); let tasks = abs_paths .iter() .cloned() .zip(entries.into_iter()) .map(|(abs_path, entry_id)| { - let is_file = bg.spawn(async move { abs_path.is_file() }); - cx.spawn(|this, mut cx| async move { - if is_file.await { - return this.update(&mut cx, |this, cx| this.open_entry(entry_id, cx)); - } else { - None + cx.spawn(|this, mut cx| { + let fs = fs.clone(); + async move { + let entry_id = entry_id.await?; + if fs.is_file(&abs_path).await { + if let Some(entry) = + this.update(&mut cx, |this, cx| this.open_entry(entry_id, cx)) + { + entry.await; + } + } + Ok(()) } }) }) - .collect::>(); - async move { + .collect::>>>(); + + cx.foreground().spawn(async move { for task in tasks { - if let Some(task) = task.await { - task.await; + if let Err(error) = task.await { + log::error!("error opening paths {}", error); } } - } + }) } fn worktree_for_abs_path( - &mut self, + &self, abs_path: &Path, cx: &mut ViewContext, - ) -> (ModelHandle, PathBuf) { - for tree in self.worktrees.iter() { - if let Some(path) = tree - .read(cx) - .as_local() - .and_then(|tree| abs_path.strip_prefix(&tree.abs_path()).ok()) - { - return (tree.clone(), path.to_path_buf()); + ) -> Task, PathBuf)>> { + let abs_path: Arc = Arc::from(abs_path); + cx.spawn(|this, mut cx| async move { + let mut entry_id = None; + this.read_with(&cx, |this, cx| { + for tree in this.worktrees.iter() { + if let Some(relative_path) = tree + .read(cx) + .as_local() + .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok()) + { + entry_id = Some((tree.clone(), relative_path.into())); + break; + } + } + }); + + if let Some(entry_id) = entry_id { + Ok(entry_id) + } else { + let worktree = this + .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx)) + .await?; + Ok((worktree, PathBuf::new())) } - } - (self.add_worktree(abs_path, cx), PathBuf::new()) + }) } fn entry_id_for_path( - &mut self, + &self, abs_path: &Path, cx: &mut ViewContext, - ) -> (usize, Arc) { - for tree in self.worktrees.iter() { - if let Some(relative_path) = tree - .read(cx) - .as_local() - .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok()) - { - return (tree.id(), relative_path.into()); - } - } - let worktree = self.add_worktree(&abs_path, cx); - (worktree.id(), Path::new("").into()) + ) -> Task)>> { + let entry = self.worktree_for_abs_path(abs_path, cx); + cx.spawn(|_, _| async move { + let (worktree, path) = entry.await?; + Ok((worktree.id(), path.into())) + }) } pub fn add_worktree( - &mut self, + &self, path: &Path, cx: &mut ViewContext, - ) -> ModelHandle { - let worktree = cx.add_model(|cx| Worktree::local(path, self.languages.clone(), cx)); - cx.observe_model(&worktree, |_, _, cx| cx.notify()); - self.worktrees.insert(worktree.clone()); - cx.notify(); - worktree + ) -> Task>> { + let languages = self.languages.clone(); + let fs = self.fs.clone(); + let path = Arc::from(path); + cx.spawn(|this, mut cx| async move { + let worktree = Worktree::open_local(path, languages, fs, &mut cx).await?; + this.update(&mut cx, |this, cx| { + cx.observe_model(&worktree, |_, _, cx| cx.notify()); + this.worktrees.insert(worktree.clone()); + cx.notify(); + }); + Ok(worktree) + }) } pub fn toggle_modal(&mut self, cx: &mut ViewContext, add_view: F) @@ -655,12 +655,22 @@ impl Workspace { cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| { if let Some(abs_path) = abs_path { cx.spawn(|mut cx| async move { - let result = handle - .update(&mut cx, |me, cx| { - let (worktree, path) = me.worktree_for_abs_path(&abs_path, cx); - item.save_as(&worktree, &path, cx.as_mut()) + let result = match handle + .update(&mut cx, |this, cx| { + this.worktree_for_abs_path(&abs_path, cx) }) - .await; + .await + { + Ok((worktree, path)) => { + handle + .update(&mut cx, |_, cx| { + item.save_as(&worktree, &path, cx.as_mut()) + }) + .await + } + Err(error) => Err(error), + }; + if let Err(error) = result { error!("failed to save item: {:?}, ", error); } @@ -912,18 +922,15 @@ mod tests { use crate::{ editor::Editor, test::{build_app_state, temp_tree}, - worktree::WorktreeHandle, + worktree::{FakeFs, WorktreeHandle}, }; use serde_json::json; use std::{collections::HashSet, fs}; use tempdir::TempDir; #[gpui::test] - fn test_open_paths_action(cx: &mut gpui::MutableAppContext) { - let app_state = build_app_state(cx.as_ref()); - - init(cx); - + async fn test_open_paths_action(mut cx: gpui::TestAppContext) { + let app_state = cx.read(build_app_state); let dir = temp_tree(json!({ "a": { "aa": null, @@ -939,42 +946,51 @@ mod tests { }, })); - cx.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![ - dir.path().join("a").to_path_buf(), - dir.path().join("b").to_path_buf(), - ], - app_state: app_state.clone(), - }, - ); - assert_eq!(cx.window_ids().count(), 1); - - cx.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![dir.path().join("a").to_path_buf()], - app_state: app_state.clone(), - }, - ); - assert_eq!(cx.window_ids().count(), 1); - let workspace_view_1 = cx - .root_view::(cx.window_ids().next().unwrap()) - .unwrap(); - assert_eq!(workspace_view_1.read(cx).worktrees().len(), 2); - - cx.dispatch_global_action( - "workspace:open_paths", - OpenParams { - paths: vec![ - dir.path().join("b").to_path_buf(), - dir.path().join("c").to_path_buf(), - ], - app_state: app_state.clone(), - }, - ); - assert_eq!(cx.window_ids().count(), 2); + cx.update(|cx| { + open_paths( + &OpenParams { + paths: vec![ + dir.path().join("a").to_path_buf(), + dir.path().join("b").to_path_buf(), + ], + app_state: app_state.clone(), + }, + cx, + ) + }) + .await; + assert_eq!(cx.window_ids().len(), 1); + + cx.update(|cx| { + open_paths( + &OpenParams { + paths: vec![dir.path().join("a").to_path_buf()], + app_state: app_state.clone(), + }, + cx, + ) + }) + .await; + assert_eq!(cx.window_ids().len(), 1); + let workspace_view_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); + workspace_view_1.read_with(&cx, |workspace, _| { + assert_eq!(workspace.worktrees().len(), 2) + }); + + cx.update(|cx| { + open_paths( + &OpenParams { + paths: vec![ + dir.path().join("b").to_path_buf(), + dir.path().join("c").to_path_buf(), + ], + app_state: app_state.clone(), + }, + cx, + ) + }) + .await; + assert_eq!(cx.window_ids().len(), 2); } #[gpui::test] @@ -989,16 +1005,13 @@ mod tests { let app_state = cx.read(build_app_state); - let (_, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(dir.path(), cx); - workspace - }); + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(dir.path(), cx) + }) + .await + .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; @@ -1088,32 +1101,28 @@ mod tests { #[gpui::test] async fn test_open_paths(mut cx: gpui::TestAppContext) { - let dir1 = temp_tree(json!({ - "a.txt": "", - })); - let dir2 = temp_tree(json!({ - "b.txt": "", - })); + let fs = FakeFs::new(); + fs.insert_dir("/dir1").await.unwrap(); + fs.insert_dir("/dir2").await.unwrap(); + fs.insert_file("/dir1/a.txt", "".into()).await.unwrap(); + fs.insert_file("/dir2/b.txt", "".into()).await.unwrap(); - let app_state = cx.read(build_app_state); - let (_, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(dir1.path(), cx); - workspace - }); + let mut app_state = cx.read(build_app_state); + Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(fs); + + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree("/dir1".as_ref(), cx) + }) + .await + .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; // Open a file within an existing worktree. cx.update(|cx| { - workspace.update(cx, |view, cx| { - view.open_paths(&[dir1.path().join("a.txt")], cx) - }) + workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx)) }) .await; cx.read(|cx| { @@ -1131,9 +1140,7 @@ mod tests { // Open a file outside of any existing worktree. cx.update(|cx| { - workspace.update(cx, |view, cx| { - view.open_paths(&[dir2.path().join("b.txt")], cx) - }) + workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx)) }) .await; cx.read(|cx| { @@ -1145,8 +1152,9 @@ mod tests { .collect::>(); assert_eq!( worktree_roots, - vec![dir1.path(), &dir2.path().join("b.txt")] + vec!["/dir1", "/dir2/b.txt"] .into_iter() + .map(Path::new) .collect(), ); assert_eq!( @@ -1169,16 +1177,13 @@ mod tests { })); let app_state = cx.read(build_app_state); - let (window_id, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(dir.path(), cx); - workspace - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(dir.path(), cx) + }) + .await + .unwrap(); let tree = cx.read(|cx| { let mut trees = workspace.read(cx).worktrees().iter(); trees.next().unwrap().clone() @@ -1217,16 +1222,13 @@ mod tests { async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) { let dir = TempDir::new("test-new-file").unwrap(); let app_state = cx.read(build_app_state); - let (_, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(dir.path(), cx); - workspace - }); + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(dir.path(), cx) + }) + .await + .unwrap(); let tree = cx.read(|cx| { workspace .read(cx) @@ -1342,16 +1344,13 @@ mod tests { })); let app_state = cx.read(build_app_state); - let (window_id, workspace) = cx.add_window(|cx| { - let mut workspace = Workspace::new( - app_state.settings.clone(), - app_state.languages.clone(), - app_state.rpc.clone(), - cx, - ); - workspace.add_worktree(dir.path(), cx); - workspace - }); + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(dir.path(), cx) + }) + .await + .unwrap(); cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) .await; let entries = cx.read(|cx| workspace.file_entries(cx)); diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 1cc226b78b0ac7f56f09eeef3d1bcd0a57c557ca..55808941105800b584f5227cbbe27c73535d7042 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,4 +1,5 @@ mod char_bag; +mod fs; mod fuzzy; mod ignore; @@ -12,8 +13,8 @@ use crate::{ util::Bias, }; use ::ignore::gitignore::Gitignore; -use anyhow::{anyhow, Context, Result}; -use atomic::Ordering::SeqCst; +use anyhow::{anyhow, Result}; +pub use fs::*; use futures::{Stream, StreamExt}; pub use fuzzy::{match_paths, PathMatch}; use gpui::{ @@ -26,10 +27,7 @@ use postage::{ prelude::{Sink as _, Stream as _}, watch, }; -use smol::{ - channel::{self, Sender}, - io::{AsyncReadExt, AsyncWriteExt}, -}; +use smol::channel::{self, Sender}; use std::{ cmp::{self, Ordering}, collections::HashMap, @@ -37,18 +35,12 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - io, ops::Deref, - os::unix::fs::MetadataExt, path::{Path, PathBuf}, - pin::Pin, - sync::{ - atomic::{self, AtomicUsize}, - Arc, - }, + sync::{atomic::AtomicUsize, Arc}, time::{Duration, SystemTime}, }; -use zed_rpc::{ForegroundRouter, PeerId, TypedEnvelope}; +use zrpc::{ForegroundRouter, PeerId, TypedEnvelope}; lazy_static! { static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); @@ -65,370 +57,6 @@ pub fn init(cx: &mut MutableAppContext, rpc: &rpc::Client, router: &mut Foregrou rpc.on_message(router, remote::save_buffer, cx); } -#[async_trait::async_trait] -pub trait Fs: Send + Sync { - async fn entry( - &self, - root_char_bag: CharBag, - next_entry_id: &AtomicUsize, - path: Arc, - abs_path: &Path, - ) -> Result>; - async fn child_entries<'a>( - &self, - root_char_bag: CharBag, - next_entry_id: &'a AtomicUsize, - path: &'a Path, - abs_path: &'a Path, - ) -> Result> + Send>>>; - async fn load(&self, path: &Path) -> Result; - async fn save(&self, path: &Path, text: &Rope) -> Result<()>; - async fn canonicalize(&self, path: &Path) -> Result; -} - -struct ProductionFs; - -#[async_trait::async_trait] -impl Fs for ProductionFs { - async fn entry( - &self, - root_char_bag: CharBag, - next_entry_id: &AtomicUsize, - path: Arc, - abs_path: &Path, - ) -> Result> { - let metadata = match smol::fs::metadata(&abs_path).await { - Err(err) => { - return match (err.kind(), err.raw_os_error()) { - (io::ErrorKind::NotFound, _) => Ok(None), - (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None), - _ => Err(anyhow::Error::new(err)), - } - } - Ok(metadata) => metadata, - }; - let inode = metadata.ino(); - let mtime = metadata.modified()?; - let is_symlink = smol::fs::symlink_metadata(&abs_path) - .await - .context("failed to read symlink metadata")? - .file_type() - .is_symlink(); - - let entry = Entry { - id: next_entry_id.fetch_add(1, SeqCst), - kind: if metadata.file_type().is_dir() { - EntryKind::PendingDir - } else { - EntryKind::File(char_bag_for_path(root_char_bag, &path)) - }, - path: Arc::from(path), - inode, - mtime, - is_symlink, - is_ignored: false, - }; - - Ok(Some(entry)) - } - - async fn child_entries<'a>( - &self, - root_char_bag: CharBag, - next_entry_id: &'a AtomicUsize, - path: &'a Path, - abs_path: &'a Path, - ) -> Result> + Send>>> { - let entries = smol::fs::read_dir(abs_path).await?; - Ok(entries - .then(move |entry| async move { - let child_entry = entry?; - let child_name = child_entry.file_name(); - let child_path: Arc = path.join(&child_name).into(); - let child_abs_path = abs_path.join(&child_name); - let child_is_symlink = child_entry.metadata().await?.file_type().is_symlink(); - let child_metadata = smol::fs::metadata(child_abs_path).await?; - let child_inode = child_metadata.ino(); - let child_mtime = child_metadata.modified()?; - Ok(Entry { - id: next_entry_id.fetch_add(1, SeqCst), - kind: if child_metadata.file_type().is_dir() { - EntryKind::PendingDir - } else { - EntryKind::File(char_bag_for_path(root_char_bag, &child_path)) - }, - path: child_path, - inode: child_inode, - mtime: child_mtime, - is_symlink: child_is_symlink, - is_ignored: false, - }) - }) - .boxed()) - } - - async fn load(&self, path: &Path) -> Result { - let mut file = smol::fs::File::open(path).await?; - let mut text = String::new(); - file.read_to_string(&mut text).await?; - Ok(text) - } - - async fn save(&self, path: &Path, text: &Rope) -> Result<()> { - let buffer_size = text.summary().bytes.min(10 * 1024); - let file = smol::fs::File::create(path).await?; - let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); - for chunk in text.chunks() { - writer.write_all(chunk.as_bytes()).await?; - } - writer.flush().await?; - Ok(()) - } - - async fn canonicalize(&self, path: &Path) -> Result { - Ok(smol::fs::canonicalize(path).await?) - } -} - -#[derive(Clone, Debug)] -struct InMemoryEntry { - inode: u64, - mtime: SystemTime, - is_dir: bool, - is_symlink: bool, - content: Option, -} - -#[cfg(any(test, feature = "test-support"))] -struct InMemoryFsState { - entries: std::collections::BTreeMap, - next_inode: u64, - events_tx: postage::broadcast::Sender, -} - -#[cfg(any(test, feature = "test-support"))] -impl InMemoryFsState { - fn validate_path(&self, path: &Path) -> Result<()> { - if path.is_absolute() - && path - .parent() - .and_then(|path| self.entries.get(path)) - .map_or(false, |e| e.is_dir) - { - Ok(()) - } else { - Err(anyhow!("invalid path {:?}", path)) - } - } - - async fn emit_event(&mut self, path: &Path) { - let _ = self - .events_tx - .send(fsevent::Event { - event_id: 0, - flags: fsevent::StreamFlags::empty(), - path: path.to_path_buf(), - }) - .await; - } -} - -#[cfg(any(test, feature = "test-support"))] -pub struct InMemoryFs { - state: smol::lock::RwLock, -} - -#[cfg(any(test, feature = "test-support"))] -impl InMemoryFs { - pub fn new() -> Self { - let (events_tx, _) = postage::broadcast::channel(2048); - let mut entries = std::collections::BTreeMap::new(); - entries.insert( - Path::new("/").to_path_buf(), - InMemoryEntry { - inode: 0, - mtime: SystemTime::now(), - is_dir: true, - is_symlink: false, - content: None, - }, - ); - Self { - state: smol::lock::RwLock::new(InMemoryFsState { - entries, - next_inode: 1, - events_tx, - }), - } - } - - pub async fn insert_dir(&self, path: &Path) -> Result<()> { - let mut state = self.state.write().await; - state.validate_path(path)?; - - let inode = state.next_inode; - state.next_inode += 1; - state.entries.insert( - path.to_path_buf(), - InMemoryEntry { - inode, - mtime: SystemTime::now(), - is_dir: true, - is_symlink: false, - content: None, - }, - ); - state.emit_event(path).await; - Ok(()) - } - - pub async fn remove(&self, path: &Path) -> Result<()> { - let mut state = self.state.write().await; - state.validate_path(path)?; - state.entries.retain(|path, _| !path.starts_with(path)); - state.emit_event(&path).await; - Ok(()) - } - - pub async fn rename(&self, source: &Path, target: &Path) -> Result<()> { - let mut state = self.state.write().await; - state.validate_path(source)?; - state.validate_path(target)?; - if state.entries.contains_key(target) { - Err(anyhow!("target path already exists")) - } else { - let mut removed = Vec::new(); - state.entries.retain(|path, entry| { - if let Ok(relative_path) = path.strip_prefix(source) { - removed.push((relative_path.to_path_buf(), entry.clone())); - false - } else { - true - } - }); - - for (relative_path, entry) in removed { - let new_path = target.join(relative_path); - state.entries.insert(new_path, entry); - } - - state.emit_event(source).await; - state.emit_event(target).await; - - Ok(()) - } - } - - pub async fn events(&self) -> postage::broadcast::Receiver { - self.state.read().await.events_tx.subscribe() - } -} - -#[cfg(any(test, feature = "test-support"))] -#[async_trait::async_trait] -impl Fs for InMemoryFs { - async fn entry( - &self, - root_char_bag: CharBag, - next_entry_id: &AtomicUsize, - path: Arc, - abs_path: &Path, - ) -> Result> { - let state = self.state.read().await; - if let Some(entry) = state.entries.get(abs_path) { - Ok(Some(Entry { - id: next_entry_id.fetch_add(1, SeqCst), - kind: if entry.is_dir { - EntryKind::PendingDir - } else { - EntryKind::File(char_bag_for_path(root_char_bag, &path)) - }, - path: Arc::from(path), - inode: entry.inode, - mtime: entry.mtime, - is_symlink: entry.is_symlink, - is_ignored: false, - })) - } else { - Ok(None) - } - } - - async fn child_entries<'a>( - &self, - root_char_bag: CharBag, - next_entry_id: &'a AtomicUsize, - path: &'a Path, - abs_path: &'a Path, - ) -> Result> + Send>>> { - use futures::{future, stream}; - - let state = self.state.read().await; - Ok(stream::iter(state.entries.clone()) - .filter(move |(child_path, _)| future::ready(child_path.parent() == Some(abs_path))) - .then(move |(child_abs_path, child_entry)| async move { - smol::future::yield_now().await; - let child_path = Arc::from(path.join(child_abs_path.file_name().unwrap())); - Ok(Entry { - id: next_entry_id.fetch_add(1, SeqCst), - kind: if child_entry.is_dir { - EntryKind::PendingDir - } else { - EntryKind::File(char_bag_for_path(root_char_bag, &child_path)) - }, - path: child_path, - inode: child_entry.inode, - mtime: child_entry.mtime, - is_symlink: child_entry.is_symlink, - is_ignored: false, - }) - }) - .boxed()) - } - - async fn load(&self, path: &Path) -> Result { - let state = self.state.read().await; - let text = state - .entries - .get(path) - .and_then(|e| e.content.as_ref()) - .ok_or_else(|| anyhow!("file {:?} does not exist", path))?; - Ok(text.clone()) - } - - async fn save(&self, path: &Path, text: &Rope) -> Result<()> { - let mut state = self.state.write().await; - state.validate_path(path)?; - if let Some(entry) = state.entries.get_mut(path) { - if entry.is_dir { - Err(anyhow!("cannot overwrite a directory with a file")) - } else { - entry.content = Some(text.chunks().collect()); - entry.mtime = SystemTime::now(); - state.emit_event(path).await; - Ok(()) - } - } else { - let inode = state.next_inode; - state.next_inode += 1; - let entry = InMemoryEntry { - inode, - mtime: SystemTime::now(), - is_dir: false, - is_symlink: false, - content: Some(text.chunks().collect()), - }; - state.entries.insert(path.to_path_buf(), entry); - state.emit_event(path).await; - Ok(()) - } - } - - async fn canonicalize(&self, path: &Path) -> Result { - Ok(path.to_path_buf()) - } -} - #[derive(Clone, Debug)] enum ScanState { Idle, @@ -467,53 +95,26 @@ impl Entity for Worktree { } impl Worktree { - pub fn local( + pub async fn open_local( path: impl Into>, languages: Arc, - cx: &mut ModelContext, - ) -> Self { - let fs = Arc::new(ProductionFs); - let (mut tree, scan_states_tx) = - LocalWorktree::new(path, languages, fs.clone(), Duration::from_millis(100), cx); - let (event_stream, event_stream_handle) = fsevent::EventStream::new( - &[tree.snapshot.abs_path.as_ref()], - Duration::from_millis(100), - ); - let background_snapshot = tree.background_snapshot.clone(); - std::thread::spawn(move || { - let scanner = BackgroundScanner::new( - background_snapshot, - scan_states_tx, - fs, - Arc::new(executor::Background::new()), - ); - scanner.run(event_stream); - }); - tree._event_stream_handle = Some(event_stream_handle); - Worktree::Local(tree) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test( - path: impl Into>, - languages: Arc, - fs: Arc, - cx: &mut ModelContext, - ) -> Self { - let (tree, scan_states_tx) = - LocalWorktree::new(path, languages, fs.clone(), Duration::ZERO, cx); - let background_snapshot = tree.background_snapshot.clone(); - let fs = fs.clone(); - let background = cx.background().clone(); - cx.background() - .spawn(async move { - let events_rx = fs.events().await; + fs: Arc, + cx: &mut AsyncAppContext, + ) -> Result> { + let (tree, scan_states_tx) = LocalWorktree::new(path, languages, fs.clone(), cx).await?; + tree.update(cx, |tree, cx| { + let tree = tree.as_local_mut().unwrap(); + let abs_path = tree.snapshot.abs_path.clone(); + let background_snapshot = tree.background_snapshot.clone(); + let background = cx.background().clone(); + tree._background_scanner_task = Some(cx.background().spawn(async move { + let events = fs.watch(&abs_path, Duration::from_millis(100)).await; let scanner = BackgroundScanner::new(background_snapshot, scan_states_tx, fs, background); - scanner.run_test(events_rx).await; - }) - .detach(); - Worktree::Local(tree) + scanner.run(events).await; + })); + }); + Ok(tree) } pub async fn open_remote( @@ -831,25 +432,24 @@ impl Worktree { fn poll_snapshot(&mut self, cx: &mut ModelContext) { match self { Self::Local(worktree) => { - let poll_interval = worktree.poll_interval; + let is_fake_fs = worktree.fs.is_fake(); worktree.snapshot = worktree.background_snapshot.lock().clone(); if worktree.is_scanning() { - if !worktree.poll_scheduled { - cx.spawn(|this, mut cx| async move { - if poll_interval.is_zero() { + if worktree.poll_task.is_none() { + worktree.poll_task = Some(cx.spawn(|this, mut cx| async move { + if is_fake_fs { smol::future::yield_now().await; } else { - smol::Timer::after(poll_interval).await; + smol::Timer::after(Duration::from_millis(100)).await; } this.update(&mut cx, |this, cx| { - this.as_local_mut().unwrap().poll_scheduled = false; + this.as_local_mut().unwrap().poll_task = None; this.poll_snapshot(cx); }) - }) - .detach(); - worktree.poll_scheduled = true; + })); } } else { + worktree.poll_task.take(); self.update_open_buffers(cx); } } @@ -961,83 +561,110 @@ pub struct LocalWorktree { background_snapshot: Arc>, snapshots_to_send_tx: Option>, last_scan_state_rx: watch::Receiver, - _event_stream_handle: Option, - poll_scheduled: bool, + _background_scanner_task: Option>, + poll_task: Option>, rpc: Option<(rpc::Client, u64)>, open_buffers: HashMap>, shared_buffers: HashMap>>, peers: HashMap, languages: Arc, fs: Arc, - poll_interval: Duration, } impl LocalWorktree { - fn new( + async fn new( path: impl Into>, languages: Arc, fs: Arc, - poll_interval: Duration, - cx: &mut ModelContext, - ) -> (Self, Sender) { + cx: &mut AsyncAppContext, + ) -> Result<(ModelHandle, Sender)> { let abs_path = path.into(); + let path: Arc = Arc::from(Path::new("")); + let next_entry_id = AtomicUsize::new(0); + + // After determining whether the root entry is a file or a directory, populate the + // snapshot's "root name", which will be used for the purpose of fuzzy matching. + let mut root_name = abs_path + .file_name() + .map_or(String::new(), |f| f.to_string_lossy().to_string()); + let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect(); + let entry = fs + .entry(root_char_bag, &next_entry_id, path.clone(), &abs_path) + .await? + .ok_or_else(|| anyhow!("root entry does not exist"))?; + let is_dir = entry.is_dir(); + if is_dir { + root_name.push('/'); + } + let (scan_states_tx, scan_states_rx) = smol::channel::unbounded(); let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning); - let id = cx.model_id(); - let snapshot = Snapshot { - id, - scan_id: 0, - abs_path, - root_name: Default::default(), - root_char_bag: Default::default(), - ignores: Default::default(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - removed_entry_ids: Default::default(), - next_entry_id: Default::default(), - }; - - let tree = Self { - snapshot: snapshot.clone(), - background_snapshot: Arc::new(Mutex::new(snapshot)), - snapshots_to_send_tx: None, - last_scan_state_rx, - _event_stream_handle: None, - poll_scheduled: false, - open_buffers: Default::default(), - shared_buffers: Default::default(), - peers: Default::default(), - rpc: None, - languages, - fs, - poll_interval, - }; + let tree = cx.add_model(move |cx: &mut ModelContext| { + let mut snapshot = Snapshot { + id: cx.model_id(), + scan_id: 0, + abs_path, + root_name, + root_char_bag, + ignores: Default::default(), + entries_by_path: Default::default(), + entries_by_id: Default::default(), + removed_entry_ids: Default::default(), + next_entry_id: Arc::new(next_entry_id), + }; + snapshot.insert_entry(entry); + + let tree = Self { + snapshot: snapshot.clone(), + background_snapshot: Arc::new(Mutex::new(snapshot)), + snapshots_to_send_tx: None, + last_scan_state_rx, + _background_scanner_task: None, + poll_task: None, + open_buffers: Default::default(), + shared_buffers: Default::default(), + peers: Default::default(), + rpc: None, + languages, + fs, + }; - cx.spawn_weak(|this, mut cx| async move { - while let Ok(scan_state) = scan_states_rx.recv().await { - if let Some(handle) = cx.read(|cx| this.upgrade(&cx)) { - handle.update(&mut cx, |this, cx| { - last_scan_state_tx.blocking_send(scan_state).ok(); - this.poll_snapshot(cx); - let tree = this.as_local_mut().unwrap(); - if !tree.is_scanning() { - if let Some(snapshots_to_send_tx) = tree.snapshots_to_send_tx.clone() { - if let Err(err) = - smol::block_on(snapshots_to_send_tx.send(tree.snapshot())) + cx.spawn_weak(|this, mut cx| async move { + while let Ok(scan_state) = scan_states_rx.recv().await { + if let Some(handle) = cx.read(|cx| this.upgrade(&cx)) { + let to_send = handle.update(&mut cx, |this, cx| { + last_scan_state_tx.blocking_send(scan_state).ok(); + this.poll_snapshot(cx); + let tree = this.as_local_mut().unwrap(); + if !tree.is_scanning() { + if let Some(snapshots_to_send_tx) = + tree.snapshots_to_send_tx.clone() { - log::error!("error submitting snapshot to send {}", err); + Some((tree.snapshot(), snapshots_to_send_tx)) + } else { + None } + } else { + None + } + }); + + if let Some((snapshot, snapshots_to_send_tx)) = to_send { + if let Err(err) = snapshots_to_send_tx.send(snapshot).await { + log::error!("error submitting snapshot to send {}", err); } } - }); - } else { - break; + } else { + break; + } } - } - }) - .detach(); + }) + .detach(); + + Worktree::Local(tree) + }); - (tree, scan_states_tx) + Ok((tree, scan_states_tx)) } pub fn open_buffer( @@ -2158,40 +1785,7 @@ impl BackgroundScanner { self.snapshot.lock().clone() } - fn run(mut self, event_stream: fsevent::EventStream) { - if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { - return; - } - - if let Err(err) = smol::block_on(self.scan_dirs()) { - if smol::block_on(self.notify.send(ScanState::Err(Arc::new(err)))).is_err() { - return; - } - } - - if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { - return; - } - - event_stream.run(move |events| { - if smol::block_on(self.notify.send(ScanState::Scanning)).is_err() { - return false; - } - - if !smol::block_on(self.process_events(events)) { - return false; - } - - if smol::block_on(self.notify.send(ScanState::Idle)).is_err() { - return false; - } - - true - }); - } - - #[cfg(any(test, feature = "test-support"))] - async fn run_test(mut self, mut events_rx: postage::broadcast::Receiver) { + async fn run(mut self, events_rx: impl Stream>) { if self.notify.send(ScanState::Scanning).await.is_err() { return; } @@ -2211,12 +1805,8 @@ impl BackgroundScanner { return; } - while let Some(event) = events_rx.recv().await { - let mut events = vec![event]; - while let Ok(event) = events_rx.try_recv() { - events.push(event); - } - + futures::pin_mut!(events_rx); + while let Some(events) = events_rx.next().await { if self.notify.send(ScanState::Scanning).await.is_err() { break; } @@ -2232,40 +1822,19 @@ impl BackgroundScanner { } async fn scan_dirs(&mut self) -> Result<()> { + let root_char_bag; let next_entry_id; + let is_dir; { - let mut snapshot = self.snapshot.lock(); - snapshot.scan_id += 1; + let snapshot = self.snapshot.lock(); + root_char_bag = snapshot.root_char_bag; next_entry_id = snapshot.next_entry_id.clone(); + is_dir = snapshot.root_entry().is_dir(); } - let path: Arc = Arc::from(Path::new("")); - let abs_path = self.abs_path(); - - // After determining whether the root entry is a file or a directory, populate the - // snapshot's "root name", which will be used for the purpose of fuzzy matching. - let mut root_name = abs_path - .file_name() - .map_or(String::new(), |f| f.to_string_lossy().to_string()); - let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect(); - let entry = self - .fs - .entry(root_char_bag, &next_entry_id, path.clone(), &abs_path) - .await? - .ok_or_else(|| anyhow!("root entry does not exist"))?; - let is_dir = entry.is_dir(); - if is_dir { - root_name.push('/'); - } - - { - let mut snapshot = self.snapshot.lock(); - snapshot.root_name = root_name; - snapshot.root_char_bag = root_char_bag; - } - - self.snapshot.lock().insert_entry(entry); if is_dir { + let path: Arc = Arc::from(Path::new("")); + let abs_path = self.abs_path(); let (tx, rx) = channel::unbounded(); tx.send(ScanJob { abs_path: abs_path.to_path_buf(), @@ -2279,7 +1848,7 @@ impl BackgroundScanner { self.executor .scoped(|scope| { - for _ in 0..self.executor.threads() { + for _ in 0..self.executor.num_cpus() { scope.spawn(async { while let Ok(job) = rx.recv().await { if let Err(err) = self @@ -2471,7 +2040,7 @@ impl BackgroundScanner { drop(scan_queue_tx); self.executor .scoped(|scope| { - for _ in 0..self.executor.threads() { + for _ in 0..self.executor.num_cpus() { scope.spawn(async { while let Ok(job) = scan_queue_rx.recv().await { if let Err(err) = self @@ -2539,7 +2108,7 @@ impl BackgroundScanner { self.executor .scoped(|scope| { - for _ in 0..self.executor.threads() { + for _ in 0..self.executor.num_cpus() { scope.spawn(async { while let Ok(job) = ignore_queue_rx.recv().await { self.update_ignore_status(job, &snapshot).await; @@ -2949,8 +2518,6 @@ mod remote { rpc: &rpc::Client, cx: &mut AsyncAppContext, ) -> anyhow::Result<()> { - eprintln!("got buffer_saved {:?}", envelope.payload); - rpc.state .read() .await @@ -2973,7 +2540,7 @@ mod tests { use std::{env, fmt::Write, os::unix, time::SystemTime}; #[gpui::test] - async fn test_populate_and_search(mut cx: gpui::TestAppContext) { + async fn test_populate_and_search(cx: gpui::TestAppContext) { let dir = temp_tree(json!({ "root": { "apple": "", @@ -2997,40 +2564,51 @@ mod tests { ) .unwrap(); - let tree = cx.add_model(|cx| Worktree::local(root_link_path, Default::default(), cx)); + let tree = Worktree::open_local( + root_link_path, + Default::default(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; - cx.read(|cx| { + let snapshot = cx.read(|cx| { let tree = tree.read(cx); assert_eq!(tree.file_count(), 5); - assert_eq!( tree.inode_for_path("fennel/grape"), tree.inode_for_path("finnochio/grape") ); - let results = match_paths( - Some(tree.snapshot()).iter(), - "bna", - false, - false, - false, - 10, - Default::default(), - cx.thread_pool().clone(), - ) - .into_iter() - .map(|result| result.path) - .collect::>>(); - assert_eq!( - results, - vec![ - PathBuf::from("banana/carrot/date").into(), - PathBuf::from("banana/carrot/endive").into(), - ] - ); - }) + tree.snapshot() + }); + let results = cx + .read(|cx| { + match_paths( + Some(&snapshot).into_iter(), + "bna", + false, + false, + false, + 10, + Default::default(), + cx.background().clone(), + ) + }) + .await; + assert_eq!( + results + .into_iter() + .map(|result| result.path) + .collect::>>(), + vec![ + PathBuf::from("banana/carrot/date").into(), + PathBuf::from("banana/carrot/endive").into(), + ] + ); } #[gpui::test] @@ -3039,7 +2617,14 @@ mod tests { let dir = temp_tree(json!({ "file1": "the old contents", })); - let tree = cx.add_model(|cx| Worktree::local(dir.path(), app_state.languages.clone(), cx)); + let tree = Worktree::open_local( + dir.path(), + app_state.languages.clone(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); let buffer = tree .update(&mut cx, |tree, cx| tree.open_buffer("file1", cx)) .await @@ -3062,8 +2647,14 @@ mod tests { })); let file_path = dir.path().join("file1"); - let tree = - cx.add_model(|cx| Worktree::local(file_path.clone(), app_state.languages.clone(), cx)); + let tree = Worktree::open_local( + file_path.clone(), + app_state.languages.clone(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; cx.read(|cx| assert_eq!(tree.read(cx).file_count(), 1)); @@ -3098,7 +2689,14 @@ mod tests { } })); - let tree = cx.add_model(|cx| Worktree::local(dir.path(), Default::default(), cx)); + let tree = Worktree::open_local( + dir.path(), + Default::default(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { let buffer = tree.update(cx, |tree, cx| tree.open_buffer(path, cx)); @@ -3233,7 +2831,7 @@ mod tests { } #[gpui::test] - async fn test_rescan_with_gitignore(mut cx: gpui::TestAppContext) { + async fn test_rescan_with_gitignore(cx: gpui::TestAppContext) { let dir = temp_tree(json!({ ".git": {}, ".gitignore": "ignored-dir\n", @@ -3245,7 +2843,14 @@ mod tests { } })); - let tree = cx.add_model(|cx| Worktree::local(dir.path(), Default::default(), cx)); + let tree = Worktree::open_local( + dir.path(), + Default::default(), + Arc::new(RealFs), + &mut cx.to_async(), + ) + .await + .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.flush_fs_events(&cx).await; @@ -3299,21 +2904,34 @@ mod tests { log::info!("Generated initial tree"); let (notify_tx, _notify_rx) = smol::channel::unbounded(); + let fs = Arc::new(RealFs); + let next_entry_id = Arc::new(AtomicUsize::new(0)); + let mut initial_snapshot = Snapshot { + id: 0, + scan_id: 0, + abs_path: root_dir.path().into(), + entries_by_path: Default::default(), + entries_by_id: Default::default(), + removed_entry_ids: Default::default(), + ignores: Default::default(), + root_name: Default::default(), + root_char_bag: Default::default(), + next_entry_id: next_entry_id.clone(), + }; + initial_snapshot.insert_entry( + smol::block_on(fs.entry( + Default::default(), + &next_entry_id, + Path::new("").into(), + root_dir.path().into(), + )) + .unwrap() + .unwrap(), + ); let mut scanner = BackgroundScanner::new( - Arc::new(Mutex::new(Snapshot { - id: 0, - scan_id: 0, - abs_path: root_dir.path().into(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - removed_entry_ids: Default::default(), - ignores: Default::default(), - root_name: Default::default(), - root_char_bag: Default::default(), - next_entry_id: Default::default(), - })), + Arc::new(Mutex::new(initial_snapshot.clone())), notify_tx, - Arc::new(ProductionFs), + fs.clone(), Arc::new(gpui::executor::Background::new()), ); smol::block_on(scanner.scan_dirs()).unwrap(); @@ -3339,18 +2957,7 @@ mod tests { let (notify_tx, _notify_rx) = smol::channel::unbounded(); let mut new_scanner = BackgroundScanner::new( - Arc::new(Mutex::new(Snapshot { - id: 0, - scan_id: 0, - abs_path: root_dir.path().into(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - removed_entry_ids: Default::default(), - ignores: Default::default(), - root_name: Default::default(), - root_char_bag: Default::default(), - next_entry_id: Default::default(), - })), + Arc::new(Mutex::new(initial_snapshot)), notify_tx, scanner.fs.clone(), scanner.executor.clone(), diff --git a/zed/src/worktree/fs.rs b/zed/src/worktree/fs.rs new file mode 100644 index 0000000000000000000000000000000000000000..405580ab97eea8cd5694fb5710b2aa9c6df01e49 --- /dev/null +++ b/zed/src/worktree/fs.rs @@ -0,0 +1,490 @@ +use super::{char_bag::CharBag, char_bag_for_path, Entry, EntryKind, Rope}; +use anyhow::{anyhow, Context, Result}; +use atomic::Ordering::SeqCst; +use fsevent::EventStream; +use futures::{future::BoxFuture, Stream, StreamExt}; +use postage::prelude::Sink as _; +use smol::io::{AsyncReadExt, AsyncWriteExt}; +use std::{ + io, + os::unix::fs::MetadataExt, + path::{Path, PathBuf}, + pin::Pin, + sync::{ + atomic::{self, AtomicUsize}, + Arc, + }, + time::{Duration, SystemTime}, +}; + +#[async_trait::async_trait] +pub trait Fs: Send + Sync { + async fn entry( + &self, + root_char_bag: CharBag, + next_entry_id: &AtomicUsize, + path: Arc, + abs_path: &Path, + ) -> Result>; + async fn child_entries<'a>( + &self, + root_char_bag: CharBag, + next_entry_id: &'a AtomicUsize, + path: &'a Path, + abs_path: &'a Path, + ) -> Result> + Send>>>; + async fn load(&self, path: &Path) -> Result; + async fn save(&self, path: &Path, text: &Rope) -> Result<()>; + async fn canonicalize(&self, path: &Path) -> Result; + async fn is_file(&self, path: &Path) -> bool; + async fn watch( + &self, + path: &Path, + latency: Duration, + ) -> Pin>>>; + fn is_fake(&self) -> bool; +} + +pub struct RealFs; + +#[async_trait::async_trait] +impl Fs for RealFs { + async fn entry( + &self, + root_char_bag: CharBag, + next_entry_id: &AtomicUsize, + path: Arc, + abs_path: &Path, + ) -> Result> { + let metadata = match smol::fs::metadata(&abs_path).await { + Err(err) => { + return match (err.kind(), err.raw_os_error()) { + (io::ErrorKind::NotFound, _) => Ok(None), + (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None), + _ => Err(anyhow::Error::new(err)), + } + } + Ok(metadata) => metadata, + }; + let inode = metadata.ino(); + let mtime = metadata.modified()?; + let is_symlink = smol::fs::symlink_metadata(&abs_path) + .await + .context("failed to read symlink metadata")? + .file_type() + .is_symlink(); + + let entry = Entry { + id: next_entry_id.fetch_add(1, SeqCst), + kind: if metadata.file_type().is_dir() { + EntryKind::PendingDir + } else { + EntryKind::File(char_bag_for_path(root_char_bag, &path)) + }, + path: Arc::from(path), + inode, + mtime, + is_symlink, + is_ignored: false, + }; + + Ok(Some(entry)) + } + + async fn child_entries<'a>( + &self, + root_char_bag: CharBag, + next_entry_id: &'a AtomicUsize, + path: &'a Path, + abs_path: &'a Path, + ) -> Result> + Send>>> { + let entries = smol::fs::read_dir(abs_path).await?; + Ok(entries + .then(move |entry| async move { + let child_entry = entry?; + let child_name = child_entry.file_name(); + let child_path: Arc = path.join(&child_name).into(); + let child_abs_path = abs_path.join(&child_name); + let child_is_symlink = child_entry.metadata().await?.file_type().is_symlink(); + let child_metadata = smol::fs::metadata(child_abs_path).await?; + let child_inode = child_metadata.ino(); + let child_mtime = child_metadata.modified()?; + Ok(Entry { + id: next_entry_id.fetch_add(1, SeqCst), + kind: if child_metadata.file_type().is_dir() { + EntryKind::PendingDir + } else { + EntryKind::File(char_bag_for_path(root_char_bag, &child_path)) + }, + path: child_path, + inode: child_inode, + mtime: child_mtime, + is_symlink: child_is_symlink, + is_ignored: false, + }) + }) + .boxed()) + } + + async fn load(&self, path: &Path) -> Result { + let mut file = smol::fs::File::open(path).await?; + let mut text = String::new(); + file.read_to_string(&mut text).await?; + Ok(text) + } + + async fn save(&self, path: &Path, text: &Rope) -> Result<()> { + let buffer_size = text.summary().bytes.min(10 * 1024); + let file = smol::fs::File::create(path).await?; + let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); + for chunk in text.chunks() { + writer.write_all(chunk.as_bytes()).await?; + } + writer.flush().await?; + Ok(()) + } + + async fn canonicalize(&self, path: &Path) -> Result { + Ok(smol::fs::canonicalize(path).await?) + } + + async fn is_file(&self, path: &Path) -> bool { + smol::fs::metadata(path) + .await + .map_or(false, |metadata| metadata.is_file()) + } + + async fn watch( + &self, + path: &Path, + latency: Duration, + ) -> Pin>>> { + let (mut tx, rx) = postage::mpsc::channel(64); + let (stream, handle) = EventStream::new(&[path], latency); + std::mem::forget(handle); + std::thread::spawn(move || { + stream.run(move |events| smol::block_on(tx.send(events)).is_ok()); + }); + Box::pin(rx) + } + + fn is_fake(&self) -> bool { + false + } +} + +#[derive(Clone, Debug)] +struct FakeFsEntry { + inode: u64, + mtime: SystemTime, + is_dir: bool, + is_symlink: bool, + content: Option, +} + +#[cfg(any(test, feature = "test-support"))] +struct FakeFsState { + entries: std::collections::BTreeMap, + next_inode: u64, + events_tx: postage::broadcast::Sender>, +} + +#[cfg(any(test, feature = "test-support"))] +impl FakeFsState { + fn validate_path(&self, path: &Path) -> Result<()> { + if path.is_absolute() + && path + .parent() + .and_then(|path| self.entries.get(path)) + .map_or(false, |e| e.is_dir) + { + Ok(()) + } else { + Err(anyhow!("invalid path {:?}", path)) + } + } + + async fn emit_event(&mut self, paths: &[&Path]) { + let events = paths + .iter() + .map(|path| fsevent::Event { + event_id: 0, + flags: fsevent::StreamFlags::empty(), + path: path.to_path_buf(), + }) + .collect(); + + let _ = self.events_tx.send(events).await; + } +} + +#[cfg(any(test, feature = "test-support"))] +pub struct FakeFs { + // Use an unfair lock to ensure tests are deterministic. + state: futures::lock::Mutex, +} + +#[cfg(any(test, feature = "test-support"))] +impl FakeFs { + pub fn new() -> Self { + let (events_tx, _) = postage::broadcast::channel(2048); + let mut entries = std::collections::BTreeMap::new(); + entries.insert( + Path::new("/").to_path_buf(), + FakeFsEntry { + inode: 0, + mtime: SystemTime::now(), + is_dir: true, + is_symlink: false, + content: None, + }, + ); + Self { + state: futures::lock::Mutex::new(FakeFsState { + entries, + next_inode: 1, + events_tx, + }), + } + } + + pub async fn insert_dir(&self, path: impl AsRef) -> Result<()> { + let mut state = self.state.lock().await; + let path = path.as_ref(); + state.validate_path(path)?; + + let inode = state.next_inode; + state.next_inode += 1; + state.entries.insert( + path.to_path_buf(), + FakeFsEntry { + inode, + mtime: SystemTime::now(), + is_dir: true, + is_symlink: false, + content: None, + }, + ); + state.emit_event(&[path]).await; + Ok(()) + } + + pub async fn insert_file(&self, path: impl AsRef, content: String) -> Result<()> { + let mut state = self.state.lock().await; + let path = path.as_ref(); + state.validate_path(path)?; + + let inode = state.next_inode; + state.next_inode += 1; + state.entries.insert( + path.to_path_buf(), + FakeFsEntry { + inode, + mtime: SystemTime::now(), + is_dir: false, + is_symlink: false, + content: Some(content), + }, + ); + state.emit_event(&[path]).await; + Ok(()) + } + + #[must_use] + pub fn insert_tree<'a>( + &'a self, + path: impl 'a + AsRef + Send, + tree: serde_json::Value, + ) -> BoxFuture<'a, ()> { + use futures::FutureExt as _; + use serde_json::Value::*; + + async move { + let path = path.as_ref(); + + match tree { + Object(map) => { + self.insert_dir(path).await.unwrap(); + for (name, contents) in map { + let mut path = PathBuf::from(path); + path.push(name); + self.insert_tree(&path, contents).await; + } + } + Null => { + self.insert_dir(&path).await.unwrap(); + } + String(contents) => { + self.insert_file(&path, contents).await.unwrap(); + } + _ => { + panic!("JSON object must contain only objects, strings, or null"); + } + } + } + .boxed() + } + + pub async fn remove(&self, path: &Path) -> Result<()> { + let mut state = self.state.lock().await; + state.validate_path(path)?; + state.entries.retain(|path, _| !path.starts_with(path)); + state.emit_event(&[path]).await; + Ok(()) + } + + pub async fn rename(&self, source: &Path, target: &Path) -> Result<()> { + let mut state = self.state.lock().await; + state.validate_path(source)?; + state.validate_path(target)?; + if state.entries.contains_key(target) { + Err(anyhow!("target path already exists")) + } else { + let mut removed = Vec::new(); + state.entries.retain(|path, entry| { + if let Ok(relative_path) = path.strip_prefix(source) { + removed.push((relative_path.to_path_buf(), entry.clone())); + false + } else { + true + } + }); + + for (relative_path, entry) in removed { + let new_path = target.join(relative_path); + state.entries.insert(new_path, entry); + } + + state.emit_event(&[source, target]).await; + Ok(()) + } + } +} + +#[cfg(any(test, feature = "test-support"))] +#[async_trait::async_trait] +impl Fs for FakeFs { + async fn entry( + &self, + root_char_bag: CharBag, + next_entry_id: &AtomicUsize, + path: Arc, + abs_path: &Path, + ) -> Result> { + let state = self.state.lock().await; + if let Some(entry) = state.entries.get(abs_path) { + Ok(Some(Entry { + id: next_entry_id.fetch_add(1, SeqCst), + kind: if entry.is_dir { + EntryKind::PendingDir + } else { + EntryKind::File(char_bag_for_path(root_char_bag, &path)) + }, + path: Arc::from(path), + inode: entry.inode, + mtime: entry.mtime, + is_symlink: entry.is_symlink, + is_ignored: false, + })) + } else { + Ok(None) + } + } + + async fn child_entries<'a>( + &self, + root_char_bag: CharBag, + next_entry_id: &'a AtomicUsize, + path: &'a Path, + abs_path: &'a Path, + ) -> Result> + Send>>> { + use futures::{future, stream}; + + let state = self.state.lock().await; + Ok(stream::iter(state.entries.clone()) + .filter(move |(child_path, _)| future::ready(child_path.parent() == Some(abs_path))) + .then(move |(child_abs_path, child_entry)| async move { + smol::future::yield_now().await; + let child_path = Arc::from(path.join(child_abs_path.file_name().unwrap())); + Ok(Entry { + id: next_entry_id.fetch_add(1, SeqCst), + kind: if child_entry.is_dir { + EntryKind::PendingDir + } else { + EntryKind::File(char_bag_for_path(root_char_bag, &child_path)) + }, + path: child_path, + inode: child_entry.inode, + mtime: child_entry.mtime, + is_symlink: child_entry.is_symlink, + is_ignored: false, + }) + }) + .boxed()) + } + + async fn load(&self, path: &Path) -> Result { + let state = self.state.lock().await; + let text = state + .entries + .get(path) + .and_then(|e| e.content.as_ref()) + .ok_or_else(|| anyhow!("file {:?} does not exist", path))?; + Ok(text.clone()) + } + + async fn save(&self, path: &Path, text: &Rope) -> Result<()> { + let mut state = self.state.lock().await; + state.validate_path(path)?; + if let Some(entry) = state.entries.get_mut(path) { + if entry.is_dir { + Err(anyhow!("cannot overwrite a directory with a file")) + } else { + entry.content = Some(text.chunks().collect()); + entry.mtime = SystemTime::now(); + state.emit_event(&[path]).await; + Ok(()) + } + } else { + let inode = state.next_inode; + state.next_inode += 1; + let entry = FakeFsEntry { + inode, + mtime: SystemTime::now(), + is_dir: false, + is_symlink: false, + content: Some(text.chunks().collect()), + }; + state.entries.insert(path.to_path_buf(), entry); + state.emit_event(&[path]).await; + Ok(()) + } + } + + async fn canonicalize(&self, path: &Path) -> Result { + Ok(path.to_path_buf()) + } + + async fn is_file(&self, path: &Path) -> bool { + let state = self.state.lock().await; + state.entries.get(path).map_or(false, |entry| !entry.is_dir) + } + + async fn watch( + &self, + path: &Path, + _: Duration, + ) -> Pin>>> { + let state = self.state.lock().await; + let rx = state.events_tx.subscribe(); + let path = path.to_path_buf(); + Box::pin(futures::StreamExt::filter(rx, move |events| { + let result = events.iter().any(|event| event.path.starts_with(&path)); + async move { result } + })) + } + + fn is_fake(&self) -> bool { + true + } +} diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index baa2748de787e69b1295badc33b5999670551d3e..776fd2177db59710a64c0ddfd226b0f7846a84bb 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -1,6 +1,6 @@ use super::{char_bag::CharBag, EntryKind, Snapshot}; use crate::util; -use gpui::scoped_pool; +use gpui::executor; use std::{ cmp::{max, min, Ordering}, path::Path, @@ -51,7 +51,7 @@ impl Ord for PathMatch { } } -pub fn match_paths<'a, T>( +pub async fn match_paths<'a, T>( snapshots: T, query: &str, include_root_name: bool, @@ -59,7 +59,7 @@ pub fn match_paths<'a, T>( smart_case: bool, max_results: usize, cancel_flag: Arc, - pool: scoped_pool::Pool, + background: Arc, ) -> Vec where T: Clone + Send + Iterator + 'a, @@ -71,88 +71,91 @@ where let query = &query; let query_chars = CharBag::from(&lowercase_query[..]); - let cpus = num_cpus::get(); let path_count: usize = if include_ignored { snapshots.clone().map(Snapshot::file_count).sum() } else { snapshots.clone().map(Snapshot::visible_file_count).sum() }; - let segment_size = (path_count + cpus - 1) / cpus; - let mut segment_results = (0..cpus) + let num_cpus = background.num_cpus().min(path_count); + let segment_size = (path_count + num_cpus - 1) / num_cpus; + let mut segment_results = (0..num_cpus) .map(|_| Vec::with_capacity(max_results)) .collect::>(); - pool.scoped(|scope| { - for (segment_idx, results) in segment_results.iter_mut().enumerate() { - let snapshots = snapshots.clone(); - let cancel_flag = &cancel_flag; - scope.execute(move || { - let segment_start = segment_idx * segment_size; - let segment_end = segment_start + segment_size; - - let mut min_score = 0.0; - let mut last_positions = Vec::new(); - last_positions.resize(query.len(), 0); - let mut match_positions = Vec::new(); - match_positions.resize(query.len(), 0); - let mut score_matrix = Vec::new(); - let mut best_position_matrix = Vec::new(); - - let mut tree_start = 0; - for snapshot in snapshots { - let tree_end = if include_ignored { - tree_start + snapshot.file_count() - } else { - tree_start + snapshot.visible_file_count() - }; - - let include_root_name = include_root_name || snapshot.root_entry().is_file(); - if tree_start < segment_end && segment_start < tree_end { - let start = max(tree_start, segment_start) - tree_start; - let end = min(tree_end, segment_end) - tree_start; - let entries = if include_ignored { - snapshot.files(start).take(end - start) + background + .scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + let snapshots = snapshots.clone(); + let cancel_flag = &cancel_flag; + scope.spawn(async move { + let segment_start = segment_idx * segment_size; + let segment_end = segment_start + segment_size; + + let mut min_score = 0.0; + let mut last_positions = Vec::new(); + last_positions.resize(query.len(), 0); + let mut match_positions = Vec::new(); + match_positions.resize(query.len(), 0); + let mut score_matrix = Vec::new(); + let mut best_position_matrix = Vec::new(); + + let mut tree_start = 0; + for snapshot in snapshots { + let tree_end = if include_ignored { + tree_start + snapshot.file_count() } else { - snapshot.visible_files(start).take(end - start) + tree_start + snapshot.visible_file_count() }; - let paths = entries.map(|entry| { - if let EntryKind::File(char_bag) = entry.kind { - MatchCandidate { - path: &entry.path, - char_bag, - } + + let include_root_name = + include_root_name || snapshot.root_entry().is_file(); + if tree_start < segment_end && segment_start < tree_end { + let start = max(tree_start, segment_start) - tree_start; + let end = min(tree_end, segment_end) - tree_start; + let entries = if include_ignored { + snapshot.files(start).take(end - start) } else { - unreachable!() - } - }); - - match_single_tree_paths( - snapshot, - include_root_name, - paths, - query, - lowercase_query, - query_chars, - smart_case, - results, - max_results, - &mut min_score, - &mut match_positions, - &mut last_positions, - &mut score_matrix, - &mut best_position_matrix, - &cancel_flag, - ); - } - if tree_end >= segment_end { - break; + snapshot.visible_files(start).take(end - start) + }; + let paths = entries.map(|entry| { + if let EntryKind::File(char_bag) = entry.kind { + MatchCandidate { + path: &entry.path, + char_bag, + } + } else { + unreachable!() + } + }); + + match_single_tree_paths( + snapshot, + include_root_name, + paths, + query, + lowercase_query, + query_chars, + smart_case, + results, + max_results, + &mut min_score, + &mut match_positions, + &mut last_positions, + &mut score_matrix, + &mut best_position_matrix, + &cancel_flag, + ); + } + if tree_end >= segment_end { + break; + } + tree_start = tree_end; } - tree_start = tree_end; - } - }) - } - }); + }) + } + }) + .await; let mut results = Vec::new(); for segment_result in segment_results { diff --git a/zed-rpc/Cargo.toml b/zrpc/Cargo.toml similarity index 64% rename from zed-rpc/Cargo.toml rename to zrpc/Cargo.toml index c1e3136bd8646e2c9ba3a4093d3f80c819304ec0..5d78f46bb13591ccde8ea93633ea386026596c62 100644 --- a/zed-rpc/Cargo.toml +++ b/zrpc/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Shared logic for communication between the Zed app and the zed.dev server" edition = "2018" -name = "zed-rpc" +name = "zrpc" version = "0.1.0" [features] @@ -15,14 +15,14 @@ base64 = "0.13" futures = "0.3" log = "0.4" parking_lot = "0.11.1" -postage = {version = "0.4.1", features = ["futures-traits"]} +postage = { version = "0.4.1", features = ["futures-traits"] } prost = "0.7" rand = "0.8" rsa = "0.4" -serde = {version = "1", features = ["derive"]} +serde = { version = "1", features = ["derive"] } [build-dependencies] -prost-build = {git = "https://github.com/tokio-rs/prost", rev = "6cf97ea422b09d98de34643c4dda2d4f8b7e23e6"} +prost-build = { git = "https://github.com/tokio-rs/prost", rev = "6cf97ea422b09d98de34643c4dda2d4f8b7e23e6" } [dev-dependencies] smol = "1.2.5" diff --git a/zed-rpc/build.rs b/zrpc/build.rs similarity index 100% rename from zed-rpc/build.rs rename to zrpc/build.rs diff --git a/zed-rpc/proto/zed.proto b/zrpc/proto/zed.proto similarity index 100% rename from zed-rpc/proto/zed.proto rename to zrpc/proto/zed.proto diff --git a/zed-rpc/src/auth.rs b/zrpc/src/auth.rs similarity index 100% rename from zed-rpc/src/auth.rs rename to zrpc/src/auth.rs diff --git a/zed-rpc/src/lib.rs b/zrpc/src/lib.rs similarity index 100% rename from zed-rpc/src/lib.rs rename to zrpc/src/lib.rs diff --git a/zed-rpc/src/peer.rs b/zrpc/src/peer.rs similarity index 99% rename from zed-rpc/src/peer.rs rename to zrpc/src/peer.rs index 5580fc628a1c645e2564a340b05de90bcb5df8ea..2093d6a73215a5463502b28935a258ddb3de1751 100644 --- a/zed-rpc/src/peer.rs +++ b/zrpc/src/peer.rs @@ -227,7 +227,8 @@ impl Peer { connection .outgoing_tx .send(request.into_envelope(message_id, None, original_sender_id.map(|id| id.0))) - .await?; + .await + .map_err(|_| anyhow!("connection was closed"))?; let response = rx .recv() .await diff --git a/zed-rpc/src/proto.rs b/zrpc/src/proto.rs similarity index 100% rename from zed-rpc/src/proto.rs rename to zrpc/src/proto.rs diff --git a/zed-rpc/src/test.rs b/zrpc/src/test.rs similarity index 100% rename from zed-rpc/src/test.rs rename to zrpc/src/test.rs