diff --git a/.github/workflows/publish_collab_image.yml b/.github/workflows/publish_collab_image.yml index 3421409287dfdf146e745d9c34873d6f4a4e045e..b012e65841a1ab5f2e45ff0be05394f16247a64f 100644 --- a/.github/workflows/publish_collab_image.yml +++ b/.github/workflows/publish_collab_image.yml @@ -11,7 +11,7 @@ env: jobs: publish: - name: Publish collab server image + name: Publish collab server image runs-on: - self-hosted - deploy @@ -22,6 +22,9 @@ jobs: - name: Sign into DigitalOcean docker registry run: doctl registry login + - name: Prune Docker system + run: docker system prune + - name: Checkout repo uses: actions/checkout@v3 with: @@ -41,6 +44,6 @@ jobs: - name: Build docker image run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} - + - name: Publish docker image run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} diff --git a/Cargo.lock b/Cargo.lock index e74068b2d2a0085dcea482788beac5de90fe2f93..c232afa08176ec6251b2263db27f3dd167f237d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,11 +36,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli 0.27.3", + "gimli 0.28.0", ] [[package]] @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -146,7 +146,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -157,7 +157,7 @@ dependencies = [ "alacritty_config", "alacritty_config_derive", "base64 0.13.1", - "bitflags 2.3.3", + "bitflags 2.4.0", "home", "libc", "log", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -274,9 +274,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -343,7 +343,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -357,7 +357,7 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -417,15 +417,15 @@ dependencies = [ "polling", "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -488,7 +488,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -511,7 +511,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "wasm-bindgen-futures", @@ -525,7 +525,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -536,7 +536,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -573,13 +573,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -592,7 +592,7 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tungstenite 0.16.0", ] @@ -687,12 +687,12 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.9", + "itoa", "matchit", "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -733,7 +733,7 @@ dependencies = [ "futures-util", "http", "mime", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "tokio", @@ -745,16 +745,16 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ - "addr2line 0.20.0", + "addr2line 0.21.0", "cc", "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", - "object 0.31.1", + "object 0.32.0", "rustc-demangle", ] @@ -837,7 +837,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.28", + "syn 2.0.29", "which", ] @@ -864,9 +864,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -1002,7 +1002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "serde", ] @@ -1069,6 +1069,7 @@ dependencies = [ "anyhow", "async-broadcast", "audio", + "channel", "client", "collections", "fs", @@ -1162,11 +1163,12 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -1196,6 +1198,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "channel" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "db", + "futures 0.3.28", + "gpui", + "image", + "language", + "lazy_static", + "log", + "parking_lot 0.11.2", + "postage", + "rand 0.8.5", + "rpc", + "schemars", + "serde", + "serde_derive", + "settings", + "smol", + "staff_mode", + "sum_tree", + "tempfile", + "text", + "thiserror", + "time 0.3.27", + "tiny_http", + "url", + "util", + "uuid 1.4.1", +] + [[package]] name = "chrono" version = "0.4.26" @@ -1257,9 +1294,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive 4.3.12", @@ -1268,9 +1305,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", @@ -1300,7 +1337,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1318,12 +1355,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - [[package]] name = "cli" version = "0.1.0" @@ -1366,8 +1397,9 @@ dependencies = [ "staff_mode", "sum_tree", "tempfile", + "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", @@ -1421,7 +1453,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "async-tungstenite", @@ -1430,8 +1462,11 @@ dependencies = [ "axum-extra", "base64 0.13.1", "call", + "channel", "clap 3.2.25", "client", + "clock", + "collab_ui", "collections", "ctor", "dashmap", @@ -1456,6 +1491,7 @@ dependencies = [ "pretty_assertions", "project", "prometheus", + "prost 0.8.0", "rand 0.8.5", "reqwest", "rpc", @@ -1468,8 +1504,9 @@ dependencies = [ "settings", "sha-1 0.9.8", "sqlx", + "text", "theme", - "time 0.3.24", + "time 0.3.27", "tokio", "tokio-tungstenite", "toml 0.5.11", @@ -1490,6 +1527,7 @@ dependencies = [ "anyhow", "auto_update", "call", + "channel", "client", "clock", "collections", @@ -1500,6 +1538,7 @@ dependencies = [ "futures 0.3.28", "fuzzy", "gpui", + "language", "log", "menu", "picker", @@ -1568,6 +1607,19 @@ dependencies = [ "workspace", ] +[[package]] +name = "component_test" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "project", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -1996,7 +2048,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi 0.3.9", ] @@ -2017,9 +2069,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" dependencies = [ "cfg-if 1.0.0", "hashbrown 0.14.0", @@ -2077,9 +2129,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ "serde", ] @@ -2097,6 +2149,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_refineable" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "dhat" version = "0.3.2" @@ -2258,9 +2319,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "editor" @@ -2323,9 +2384,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if 1.0.0", ] @@ -2373,9 +2434,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" dependencies = [ "serde", ] @@ -2538,13 +2599,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "windows-sys", ] @@ -2556,9 +2617,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide 0.7.1", @@ -2699,7 +2760,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "time 0.3.24", + "time 0.3.27", "util", ] @@ -2837,7 +2898,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "waker-fn", ] @@ -2849,7 +2910,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2878,7 +2939,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "tokio-io", @@ -2956,9 +3017,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "git" @@ -3001,11 +3062,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "bstr", "fnv", "log", @@ -3079,6 +3140,7 @@ dependencies = [ "png", "postage", "rand 0.8.5", + "refineable", "resvg", "schemars", "seahash", @@ -3090,7 +3152,8 @@ dependencies = [ "smol", "sqlez", "sum_tree", - "time 0.3.24", + "taffy", + "time 0.3.27", "tiny-skia", "usvg", "util", @@ -3102,16 +3165,23 @@ dependencies = [ name = "gpui_macros" version = "0.1.0" dependencies = [ + "lazy_static", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "grid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" + [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes 1.4.0", "fnv", @@ -3305,7 +3375,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.9", + "itoa", ] [[package]] @@ -3316,7 +3386,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.4.0", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -3333,9 +3403,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human_bytes" @@ -3364,9 +3434,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.9", - "pin-project-lite 0.2.10", - "socket2", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -3380,7 +3450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tokio-io-timeout", ] @@ -3598,7 +3668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -3638,12 +3708,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.9" @@ -3896,17 +3960,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libc" version = "0.2.147" @@ -4081,9 +4134,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "serde", "value-bag", @@ -4114,9 +4167,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.94.0" +version = "0.94.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" dependencies = [ "bitflags 1.3.2", "serde", @@ -4572,9 +4625,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -4730,9 +4783,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -4760,15 +4813,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" version = "1.18.0" @@ -4783,9 +4827,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -4804,7 +4848,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4815,9 +4859,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -4952,7 +4996,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -5044,12 +5088,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -5077,22 +5121,22 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5103,9 +5147,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -5119,6 +5163,33 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "playground" +version = "0.1.0" +dependencies = [ + "anyhow", + "derive_more", + "gpui", + "log", + "parking_lot 0.11.2", + "playground_macros", + "refineable", + "serde", + "simplelog", + "smallvec", + "taffy", + "util", +] + +[[package]] +name = "playground_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "plist" version = "1.5.0" @@ -5130,7 +5201,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time 0.3.24", + "time 0.3.27", ] [[package]] @@ -5195,7 +5266,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "windows-sys", ] @@ -5245,7 +5316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5585,9 +5656,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -5796,6 +5867,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "refineable" +version = "0.1.0" +dependencies = [ + "derive_refineable", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "regalloc2" version = "0.2.3" @@ -5810,13 +5891,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -5831,11 +5912,11 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", "regex-syntax 0.7.4", ] @@ -5884,9 +5965,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "20b9b67e2ca7dd9e9f9285b759de30ff538aab981abaaf7bc9bd90b84a0126c3" dependencies = [ "base64 0.21.2", "bytes 1.4.0", @@ -5905,7 +5986,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -6014,11 +6095,8 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" dependencies = [ - "claxon", "cpal", "hound", - "lewton", - "symphonia", ] [[package]] @@ -6110,9 +6188,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -6121,22 +6199,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.28", + "syn 2.0.29", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "7.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" dependencies = [ "globset", "sha2 0.10.7", @@ -6145,13 +6223,12 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.31.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ "arrayvec 0.7.4", "borsh", - "byteorder", "bytes 1.4.0", "num-traits", "rand 0.8.5", @@ -6199,7 +6276,7 @@ dependencies = [ "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes 0.5.3", - "itoa 1.0.9", + "itoa", "libc", "linux-raw-sys 0.0.42", "once_cell", @@ -6222,11 +6299,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.2", "libc", "linux-raw-sys 0.4.5", @@ -6428,7 +6505,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.24", + "time 0.3.27", "tracing", "url", "uuid 1.4.1", @@ -6456,7 +6533,7 @@ dependencies = [ "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6471,7 +6548,7 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6599,10 +6676,11 @@ dependencies = [ "serde", "serde_json", "settings", + "sha1", "smol", "tempdir", "theme", - "tiktoken-rs 0.5.0", + "tiktoken-rs 0.5.1", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6650,22 +6728,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6690,24 +6768,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] [[package]] name = "serde_json_lenient" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" +checksum = "29591aaa3a13f5ad0f2dd1a8a21bcddab11eaae7c3522b20ade2e85e9df52206" dependencies = [ - "indexmap 1.9.3", - "itoa 0.4.8", + "indexmap 2.0.0", + "itoa", "ryu", "serde", ] @@ -6720,7 +6798,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6739,7 +6817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] @@ -6951,9 +7029,9 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -6964,6 +7042,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "sluice" version = "0.5.5" @@ -7026,6 +7113,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -7125,7 +7222,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "indexmap 1.9.3", - "itoa 1.0.9", + "itoa", "libc", "libsqlite3-sys", "log", @@ -7148,7 +7245,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.24", + "time 0.3.27", "tokio-stream", "url", "uuid 1.4.1", @@ -7271,7 +7368,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7282,7 +7379,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7334,56 +7431,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "symphonia" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" -dependencies = [ - "lazy_static", - "symphonia-bundle-mp3", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" -dependencies = [ - "bitflags 1.3.2", - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-core" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" -dependencies = [ - "arrayvec 0.7.4", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - [[package]] name = "syn" version = "1.0.109" @@ -7397,9 +7444,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -7453,6 +7500,17 @@ dependencies = [ "winx", ] +[[package]] +name = "taffy" +version = "0.3.11" +source = "git+https://github.com/DioxusLabs/taffy?rev=dab541d6104d58e2e10ce90c4a1dad0b703160cd#dab541d6104d58e2e10ce90c4a1dad0b703160cd" +dependencies = [ + "arrayvec 0.7.4", + "grid", + "num-traits", + "slotmap", +] + [[package]] name = "take-until" version = "0.2.0" @@ -7483,14 +7541,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -7637,22 +7695,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7699,9 +7757,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a99d843674a3468b4a9200a565bbe909a0152f95e82a52feae71e6bf2d4b49d" +checksum = "2bf14cb08d8fda6e484c75ec2bfb6bcef48347d47abcd011fa9d56ee995a3da0" dependencies = [ "anyhow", "base64 0.21.2", @@ -7725,12 +7783,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.24" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" +checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" dependencies = [ "deranged", - "itoa 1.0.9", + "itoa", "serde", "time-core", "time-macros", @@ -7744,9 +7802,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" dependencies = [ "time-core", ] @@ -7795,20 +7853,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes 1.4.0", "libc", "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -7830,7 +7887,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7842,7 +7899,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7873,7 +7930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7899,7 +7956,7 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7913,7 +7970,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tracing", ] @@ -8002,7 +8059,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "rand 0.8.5", "slab", "tokio", @@ -8025,7 +8082,7 @@ dependencies = [ "http", "http-body", "http-range-header", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tower", "tower-layer", "tower-service", @@ -8051,7 +8108,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tracing-attributes", "tracing-core", ] @@ -8064,7 +8121,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -8132,7 +8189,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=1c65ca24bc9a734ab70115188f465e12eecf224e#1c65ca24bc9a734ab70115188f465e12eecf224e" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" dependencies = [ "cc", "regex", @@ -8149,9 +8206,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1bb73a4101c88775e4fefcd0543ee25e192034484a5bd45cb99eefb997dca9" +checksum = "30b03bdf218020057abee831581a74bff8c298323d6c6cd1a70556430ded9f4b" dependencies = [ "cc", "tree-sitter", @@ -8298,9 +8355,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47ebd9cac632764b2f4389b08517bf2ef895431dd163eb562e3d2062cc23a14" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" dependencies = [ "cc", "tree-sitter", @@ -8327,9 +8384,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" +checksum = "b0832309b0b2b6d33760ce5c0e818cb47e1d72b468516bfe4134408926fa7594" dependencies = [ "cc", "tree-sitter", @@ -8450,9 +8507,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -8858,7 +8915,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -8892,7 +8949,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9129,9 +9186,9 @@ dependencies = [ [[package]] name = "wast" -version = "62.0.1" +version = "63.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f" +checksum = "2560471f60a48b77fccefaf40796fda61c97ce1e790b59dfcec9dc3995c9f63a" dependencies = [ "leb128", "memchr", @@ -9141,11 +9198,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637" +checksum = "3bdc306c2c4c2f2bf2ba69e083731d0d2a77437fc6a350a19db139636e7e416c" dependencies = [ - "wast 62.0.1", + "wast 63.0.0", ] [[package]] @@ -9347,7 +9404,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9356,7 +9413,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9376,17 +9433,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -9397,9 +9454,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -9409,9 +9466,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -9421,9 +9478,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -9433,9 +9490,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -9445,9 +9502,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -9457,9 +9514,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -9469,26 +9526,27 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.2" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi 0.3.9", + "cfg-if 1.0.0", + "windows-sys", ] [[package]] @@ -9531,6 +9589,7 @@ dependencies = [ "async-recursion 1.0.4", "bincode", "call", + "channel", "client", "collections", "context_menu", @@ -9607,7 +9666,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.19", + "clap 4.3.24", "schemars", "serde_json", "theme", @@ -9642,7 +9701,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.101.0" +version = "0.102.0" dependencies = [ "activity_indicator", "ai", @@ -9656,6 +9715,7 @@ dependencies = [ "backtrace", "breadcrumbs", "call", + "channel", "chrono", "cli", "client", @@ -9663,6 +9723,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", + "component_test", "context_menu", "copilot", "copilot_button", @@ -9788,7 +9849,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1938e832e9cdb74c26a96abfa150f09b23a97d75..0fb8f0b6b718013b65a999cd8620282fd6979a6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/auto_update", "crates/breadcrumbs", "crates/call", + "crates/channel", "crates/cli", "crates/client", "crates/clock", @@ -13,10 +14,13 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/component_test", "crates/context_menu", "crates/copilot", "crates/copilot_button", "crates/db", + "crates/refineable", + "crates/refineable/derive_refineable", "crates/diagnostics", "crates/drag_and_drop", "crates/editor", @@ -28,6 +32,8 @@ members = [ "crates/git", "crates/go_to_line", "crates/gpui", + "crates/gpui/playground", + "crates/gpui/playground_macros", "crates/gpui_macros", "crates/install_cli", "crates/journal", @@ -91,9 +97,11 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" } parking_lot = { version = "0.11.1" } postage = { version = "0.5", features = ["futures-traits"] } +prost = { version = "0.8" } rand = { version = "0.8.5" } +refineable = { path = "./crates/refineable" } regex = { version = "1.5" } -rust-embed = { version = "6.3", features = ["include-exclude"] } +rust-embed = { version = "8.0", features = ["include-exclude"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } @@ -135,7 +143,7 @@ tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1c65ca24bc9a734ab70115188f465e12eecf224e" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 diff --git a/assets/icons/file_icons/elixir.svg b/assets/icons/file_icons/elixir.svg new file mode 100644 index 0000000000000000000000000000000000000000..15a10043287bd52ed9297d649b433ce009669077 --- /dev/null +++ b/assets/icons/file_icons/elixir.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 9ea75d07309d853f37fed0192ca83bfd7d1099d8..a65155d4ebb461f8faad4e54936ed98eedb3df67 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -21,23 +21,27 @@ "dll": "storage", "doc": "document", "docx": "document", + "eex": "elixir", "eslintrc": "eslint", "eslintrc.js": "eslint", "eslintrc.json": "eslint", + "ex": "elixir", + "exs": "elixir", + "fish": "terminal", + "flac": "audio", "fmp": "storage", "fp7": "storage", - "flac": "audio", - "fish": "terminal", "frm": "storage", "gdb": "storage", + "gif": "image", "gitattributes": "vcs", "gitignore": "vcs", "gitmodules": "vcs", - "gif": "image", "go": "code", "h": "code", "handlebars": "code", "hbs": "template", + "heex": "elixir", "htm": "template", "html": "template", "ib": "storage", @@ -51,16 +55,16 @@ "ldf": "storage", "lock": "lock", "log": "log", - "mdb": "storage", "md": "document", + "mdb": "storage", "mdf": "storage", "mdx": "document", "mp3": "audio", "mp4": "video", "myd": "storage", "myi": "storage", - "ods": "document", "odp": "document", + "ods": "document", "odt": "document", "ogg": "video", "pdb": "storage", @@ -74,24 +78,24 @@ "profile": "terminal", "ps1": "terminal", "psd": "image", - "py": "code", + "py": "python", "rb": "code", "rkt": "code", "rs": "rust", "rtf": "document", "sav": "storage", "scm": "code", + "sdf": "storage", "sh": "terminal", "sqlite": "storage", - "sdf": "storage", "svelte": "template", "svg": "image", "swift": "code", - "ts": "typescript", - "tsx": "code", "tiff": "image", "toml": "toml", + "ts": "typescript", "tsv": "storage", + "tsx": "code", "txt": "document", "wav": "audio", "webm": "video", @@ -103,9 +107,9 @@ "zlogin": "terminal", "zsh": "terminal", "zsh_aliases": "terminal", - "zshenv": "terminal", "zsh_histfile": "terminal", "zsh_profile": "terminal", + "zshenv": "terminal", "zshrc": "terminal" }, "types": { @@ -127,6 +131,9 @@ "document": { "icon": "icons/file_icons/book.svg" }, + "elixir": { + "icon": "icons/file_icons/elixir.svg" + }, "eslint": { "icon": "icons/file_icons/eslint.svg" }, @@ -145,9 +152,15 @@ "log": { "icon": "icons/file_icons/info.svg" }, + "phoenix": { + "icon": "icons/file_icons/phoenix.svg" + }, "prettier": { "icon": "icons/file_icons/prettier.svg" }, + "python": { + "icon": "icons/file_icons/python.svg" + }, "rust": { "icon": "icons/file_icons/rust.svg" }, diff --git a/assets/icons/file_icons/phoenix.svg b/assets/icons/file_icons/phoenix.svg new file mode 100644 index 0000000000000000000000000000000000000000..424b75380669acd4ac1a109d2bd6d586c09feb83 --- /dev/null +++ b/assets/icons/file_icons/phoenix.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/python.svg b/assets/icons/file_icons/python.svg new file mode 100644 index 0000000000000000000000000000000000000000..dbc8565f9055e6222769bd7471f219d960dd1270 --- /dev/null +++ b/assets/icons/file_icons/python.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a81c18f7082b884c74de65d4be79fb12f236916a..9ec26d46aa48dcdf047563f03e1a3ef3b5f871c5 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -173,6 +173,7 @@ "context": "Editor && mode == full", "bindings": { "enter": "editor::Newline", + "shift-enter": "editor::Newline", "cmd-shift-enter": "editor::NewlineAbove", "cmd-enter": "editor::NewlineBelow", "alt-z": "editor::ToggleSoftWrap", @@ -543,6 +544,8 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", + "cmd-n": "project_panel::NewFile", + "alt-cmd-n": "project_panel::NewDirectory", "cmd-x": "project_panel::Cut", "cmd-c": "project_panel::Copy", "cmd-v": "project_panel::Paste", diff --git a/assets/keymaps/textmate.json b/assets/keymaps/textmate.json index 90eb090211a753c12c64d2fbd2782681e0e36ae6..dd3e217ae9cb3d2c856163b0df7767a3ffc3b325 100644 --- a/assets/keymaps/textmate.json +++ b/assets/keymaps/textmate.json @@ -2,7 +2,6 @@ { "bindings": { "cmd-shift-o": "projects::OpenRecent", - "cmd-shift-b": "branches::OpenRecent", "cmd-alt-tab": "project_panel::ToggleFocus" } }, @@ -12,8 +11,9 @@ "cmd-l": "go_to_line::Toggle", "ctrl-shift-d": "editor::DuplicateLine", "cmd-b": "editor::GoToDefinition", - "alt-cmd-b": "editor::GoToDefinition", "cmd-j": "editor::ScrollCursorCenter", + "cmd-enter": "editor::NewlineBelow", + "cmd-alt-enter": "editor::NewLineAbove", "cmd-shift-l": "editor::SelectLine", "cmd-shift-t": "outline::Toggle", "alt-backspace": "editor::DeleteToPreviousWordStart", @@ -51,14 +51,17 @@ } ], "ctrl-shift-left": "editor::SelectToPreviousSubwordStart", - "ctrl-shift-right": "editor::SelectToNextSubwordEnd" + "ctrl-shift-right": "editor::SelectToNextSubwordEnd", + "ctrl-w": "editor::SelectNext", + "ctrl-u": "editor::ConvertToUpperCase", + "ctrl-shift-u": "editor::ConvertToLowerCase", + "ctrl-alt-u": "editor::ConvertToUpperCamelCase", + "ctrl-_": "editor::ConvertToSnakeCase" } }, { "context": "Editor && mode == full", - "bindings": { - "cmd-alt-enter": "editor::NewlineAbove" - } + "bindings": {} }, { "context": "BufferSearchBar", @@ -85,5 +88,9 @@ { "context": "ProjectPanel", "bindings": {} + }, + { + "context": "Dock", + "bindings": {} } ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 02c09b33af1e2aeb90dea1625e9b8419d8331cd2..c0de3420f222566220d5f7e487554b3ee5bc33d5 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -103,9 +103,19 @@ ], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", + "ctrl-v": "vim::ToggleVisualBlock", + "ctrl-q": "vim::ToggleVisualBlock", "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion + "ctrl-f": "vim::PageDown", + "pagedown": "vim::PageDown", + "ctrl-b": "vim::PageUp", + "pageup": "vim::PageUp", + "ctrl-d": "vim::ScrollDown", + "ctrl-u": "vim::ScrollUp", + "ctrl-e": "vim::LineDown", + "ctrl-y": "vim::LineUp", // "g" commands "g g": "vim::StartOfDocument", "g h": "editor::Hover", @@ -277,6 +287,12 @@ "shift-o": "vim::InsertLineAbove", "~": "vim::ChangeCase", "p": "vim::Paste", + "shift-p": [ + "vim::Paste", + { + "before": true + } + ], "u": "editor::Undo", "ctrl-r": "editor::Redo", "/": "vim::Search", @@ -293,14 +309,6 @@ "backwards": true } ], - "ctrl-f": "vim::PageDown", - "pagedown": "vim::PageDown", - "ctrl-b": "vim::PageUp", - "pageup": "vim::PageUp", - "ctrl-d": "vim::ScrollDown", - "ctrl-u": "vim::ScrollUp", - "ctrl-e": "vim::LineDown", - "ctrl-y": "vim::LineUp", "r": [ "vim::PushOperator", "Replace" @@ -365,7 +373,7 @@ } }, { - "context": "Editor && vim_mode == visual && !VimWaiting", + "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject", "bindings": { "u": "editor::Undo", "o": "vim::OtherEnd", @@ -373,10 +381,21 @@ "d": "vim::VisualDelete", "x": "vim::VisualDelete", "y": "vim::VisualYank", - "p": "vim::VisualPaste", + "p": "vim::Paste", + "shift-p": [ + "vim::Paste", + { + "preserveClipboard": true + } + ], "s": "vim::Substitute", "c": "vim::Substitute", "~": "vim::ChangeCase", + "shift-i": [ + "vim::SwitchMode", + "Insert" + ], + "shift-a": "vim::InsertAfter", "r": [ "vim::PushOperator", "Replace" @@ -394,11 +413,27 @@ "Normal" ], ">": "editor::Indent", - "<": "editor::Outdent" + "<": "editor::Outdent", + "i": [ + "vim::PushOperator", + { + "Object": { + "around": false + } + } + ], + "a": [ + "vim::PushOperator", + { + "Object": { + "around": true + } + } + ], } }, { - "context": "Editor && vim_mode == insert", + "context": "Editor && vim_mode == insert && !menu", "bindings": { "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", diff --git a/assets/settings/default.json b/assets/settings/default.json index 2ddf4a137fbbc9d7df35fe520923725399bf430f..24412b883bf0be12cb2639dd54dec7f70adf6882 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -126,7 +126,7 @@ // Whether to show the collaboration panel button in the status bar. "button": true, // Where to dock channels panel. Can be 'left' or 'right'. - "dock": "right", + "dock": "left", // Default width of the channels panel. "default_width": 240 }, @@ -138,7 +138,13 @@ // Default width when the assistant is docked to the left or right. "default_width": 640, // Default height when the assistant is docked to the bottom. - "default_height": 320 + "default_height": 320, + // The default OpenAI model to use when starting new conversations. This + // setting can take two values: + // + // 1. "gpt-3.5-turbo-0613"" + // 2. "gpt-4-0613"" + "default_open_ai_model": "gpt-4-0613" }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 0b56fedb110ae5696c2a4525f9e8405b9f9b9350..2c2d7e774e120980d212cdcbd887289ebee0e768 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -4,6 +4,7 @@ mod streaming_diff; use anyhow::{anyhow, Result}; pub use assistant::AssistantPanel; +use assistant_settings::OpenAIModel; use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; @@ -65,7 +66,7 @@ struct SavedConversation { messages: Vec, message_metadata: HashMap, summary: String, - model: String, + model: OpenAIModel, } impl SavedConversation { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index b8458bb9ac84a6cdd32ea5a24820de1250759c1b..cfe3da22b58374b033328bb5c0f7f667d4fb540f 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,5 +1,5 @@ use crate::{ - assistant_settings::{AssistantDockPosition, AssistantSettings}, + assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, stream_completion, streaming_diff::{Hunk, StreamingDiff}, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role, @@ -1314,7 +1314,7 @@ struct Conversation { pending_summary: Task>, completion_count: usize, pending_completions: Vec, - model: String, + model: OpenAIModel, token_count: Option, max_token_count: usize, pending_token_count: Task>, @@ -1334,7 +1334,6 @@ impl Conversation { language_registry: Arc, cx: &mut ModelContext, ) -> Self { - let model = "gpt-3.5-turbo-0613"; let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); @@ -1353,6 +1352,9 @@ impl Conversation { buffer }); + let settings = settings::get::(cx); + let model = settings.default_open_ai_model.clone(); + let mut this = Self { message_anchors: Default::default(), messages_metadata: Default::default(), @@ -1362,9 +1364,9 @@ impl Conversation { completion_count: Default::default(), pending_completions: Default::default(), token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(model), + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), pending_token_count: Task::ready(None), - model: model.into(), + model: model.clone(), _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], pending_save: Task::ready(Ok(())), path: None, @@ -1458,7 +1460,7 @@ impl Conversation { completion_count: Default::default(), pending_completions: Default::default(), token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(&model), + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), pending_token_count: Task::ready(None), model, _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], @@ -1512,13 +1514,16 @@ impl Conversation { cx.background().timer(Duration::from_millis(200)).await; let token_count = cx .background() - .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) }) + .spawn(async move { + tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) + }) .await?; this.upgrade(&cx) .ok_or_else(|| anyhow!("conversation was dropped"))? .update(&mut cx, |this, cx| { - this.max_token_count = tiktoken_rs::model::get_context_size(&this.model); + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); this.token_count = Some(token_count); cx.notify() }); @@ -1532,7 +1537,7 @@ impl Conversation { Some(self.max_token_count as isize - self.token_count? as isize) } - fn set_model(&mut self, model: String, cx: &mut ModelContext) { + fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext) { self.model = model; self.count_remaining_tokens(cx); cx.notify(); @@ -1574,7 +1579,7 @@ impl Conversation { } } else { let request = OpenAIRequest { - model: self.model.clone(), + model: self.model.full_name().to_string(), messages: self .messages(cx) .filter(|message| matches!(message.status, MessageStatus::Done)) @@ -1900,7 +1905,7 @@ impl Conversation { .into(), })); let request = OpenAIRequest { - model: self.model.clone(), + model: self.model.full_name().to_string(), messages: messages.collect(), stream: true, }; @@ -2504,11 +2509,8 @@ impl ConversationEditor { fn cycle_model(&mut self, cx: &mut ViewContext) { self.conversation.update(cx, |conversation, cx| { - let new_model = match conversation.model.as_str() { - "gpt-4-0613" => "gpt-3.5-turbo-0613", - _ => "gpt-4-0613", - }; - conversation.set_model(new_model.into(), cx); + let new_model = conversation.model.cycle(); + conversation.set_model(new_model, cx); }); } @@ -2530,7 +2532,8 @@ impl ConversationEditor { MouseEventHandler::new::(0, cx, |state, cx| { let style = style.model.style_for(state); - Label::new(self.conversation.read(cx).model.clone(), style.text.clone()) + let model_display_name = self.conversation.read(cx).model.short_name(); + Label::new(model_display_name, style.text.clone()) .contained() .with_style(style.container) }) @@ -2712,6 +2715,8 @@ mod tests { #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = conversation.read(cx).buffer.clone(); @@ -2838,6 +2843,8 @@ mod tests { #[gpui::test] fn test_message_splitting(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = conversation.read(cx).buffer.clone(); @@ -2932,6 +2939,8 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = conversation.read(cx).buffer.clone(); @@ -3012,6 +3021,8 @@ mod tests { #[gpui::test] fn test_serialization(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry.clone(), cx)); diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs index 04ba8fb946eb7f450fe95bc7565bb304f8b4a1d7..05d8d9ffebe485204108969bc4ec308eedbd2d1d 100644 --- a/crates/ai/src/assistant_settings.rs +++ b/crates/ai/src/assistant_settings.rs @@ -3,6 +3,37 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Setting; +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub enum OpenAIModel { + #[serde(rename = "gpt-3.5-turbo-0613")] + ThreePointFiveTurbo, + #[serde(rename = "gpt-4-0613")] + Four, +} + +impl OpenAIModel { + pub fn full_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", + OpenAIModel::Four => "gpt-4-0613", + } + } + + pub fn short_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", + OpenAIModel::Four => "gpt-4", + } + } + + pub fn cycle(&self) -> Self { + match self { + OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, + OpenAIModel::Four => OpenAIModel::ThreePointFiveTurbo, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AssistantDockPosition { @@ -17,6 +48,7 @@ pub struct AssistantSettings { pub dock: AssistantDockPosition, pub default_width: f32, pub default_height: f32, + pub default_open_ai_model: OpenAIModel, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] @@ -25,6 +57,7 @@ pub struct AssistantSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, + pub default_open_ai_model: Option, } impl Setting for AssistantSettings { diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 182e421eb88c96ac2792763c829d2bd02300843e..36135a1e76b8a98119c64d0f4359d5fa0815cf9d 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -13,7 +13,7 @@ gpui = { path = "../gpui" } collections = { path = "../collections" } util = { path = "../util" } -rodio = "0.17.1" +rodio ={version = "0.17.1", default-features=false, features = ["wav"]} log.workspace = true diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index eb448d8d8d089369c724f49e5911a8946598f8a4..b4e94fe56c3b12533d232eacf30c5edd633d5a03 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ [dependencies] audio = { path = "../audio" } +channel = { path = "../channel" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 3ac29bfc85af9c47d790168aa2c32f49c14977be..5af094df05977c4e72cd36b98445588686f4e896 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -7,9 +7,8 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use client::{ - proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, -}; +use channel::ChannelId; +use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use postage::watch; @@ -274,7 +273,7 @@ impl ActiveCall { .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - Self::report_call_event_for_room("decline incoming", call.room_id, &self.client, cx); + Self::report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -406,19 +405,31 @@ impl ActiveCall { fn report_call_event(&self, operation: &'static str, cx: &AppContext) { if let Some(room) = self.room() { - Self::report_call_event_for_room(operation, room.read(cx).id(), &self.client, cx) + let room = room.read(cx); + Self::report_call_event_for_room( + operation, + room.id(), + room.channel_id(), + &self.client, + cx, + ) } } pub fn report_call_event_for_room( operation: &'static str, room_id: u64, + channel_id: Option, client: &Arc, cx: &AppContext, ) { let telemetry = client.telemetry(); let telemetry_settings = *settings::get::(cx); - let event = ClickhouseEvent::Call { operation, room_id }; + let event = ClickhouseEvent::Call { + operation, + room_id, + channel_id, + }; telemetry.report_clickhouse_event(event, telemetry_settings); } } diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..0978462a1a8a8a66760992edc4967b5b451603bc --- /dev/null +++ b/crates/channel/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "channel" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/channel.rs" +doctest = false + +[features] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] + +[dependencies] +client = { path = "../client" } +collections = { path = "../collections" } +db = { path = "../db" } +gpui = { path = "../gpui" } +util = { path = "../util" } +rpc = { path = "../rpc" } +text = { path = "../text" } +language = { path = "../language" } +settings = { path = "../settings" } +staff_mode = { path = "../staff_mode" } +sum_tree = { path = "../sum_tree" } + +anyhow.workspace = true +futures.workspace = true +image = "0.23" +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +schemars.workspace = true +smol.workspace = true +thiserror.workspace = true +time.workspace = true +tiny_http = "0.8" +uuid = { version = "1.1.2", features = ["v4"] } +url = "2.2" +serde.workspace = true +serde_derive.workspace = true +tempfile = "3" + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs new file mode 100644 index 0000000000000000000000000000000000000000..15631b7dd312f36126ec1e13b2413fc01e5ca8af --- /dev/null +++ b/crates/channel/src/channel.rs @@ -0,0 +1,14 @@ +mod channel_store; + +pub mod channel_buffer; +use std::sync::Arc; + +pub use channel_store::*; +use client::Client; + +#[cfg(test)] +mod channel_store_tests; + +pub fn init(client: &Arc) { + channel_buffer::init(client); +} diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..29f4d3493c6d0fe9e2fc041695f40fe48225c76f --- /dev/null +++ b/crates/channel/src/channel_buffer.rs @@ -0,0 +1,197 @@ +use crate::Channel; +use anyhow::Result; +use client::Client; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; +use rpc::{proto, TypedEnvelope}; +use std::sync::Arc; +use util::ResultExt; + +pub(crate) fn init(client: &Arc) { + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); + client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator); +} + +pub struct ChannelBuffer { + pub(crate) channel: Arc, + connected: bool, + collaborators: Vec, + buffer: ModelHandle, + client: Arc, + subscription: Option, +} + +pub enum Event { + CollaboratorsChanged, + Disconnected, +} + +impl Entity for ChannelBuffer { + type Event = Event; + + fn release(&mut self, _: &mut AppContext) { + if self.connected { + self.client + .send(proto::LeaveChannelBuffer { + channel_id: self.channel.id, + }) + .log_err(); + } + } +} + +impl ChannelBuffer { + pub(crate) async fn new( + channel: Arc, + client: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let response = client + .request(proto::JoinChannelBuffer { + channel_id: channel.id, + }) + .await?; + + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + let collaborators = response.collaborators; + + let buffer = cx.add_model(|_| { + language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + }); + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + + let subscription = client.subscribe_to_entity(channel.id)?; + + anyhow::Ok(cx.add_model(|cx| { + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + + Self { + buffer, + client, + connected: true, + collaborators, + channel, + subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), + } + })) + } + + async fn handle_update_channel_buffer( + this: ModelHandle, + update_channel_buffer: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let ops = update_channel_buffer + .payload + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + this.update(&mut cx, |this, cx| { + cx.notify(); + this.buffer + .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) + })?; + + Ok(()) + } + + async fn handle_add_channel_buffer_collaborator( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let collaborator = envelope.payload.collaborator.ok_or_else(|| { + anyhow::anyhow!( + "Should have gotten a collaborator in the AddChannelBufferCollaborator message" + ) + })?; + + this.update(&mut cx, |this, cx| { + this.collaborators.push(collaborator); + cx.emit(Event::CollaboratorsChanged); + cx.notify(); + }); + + Ok(()) + } + + async fn handle_remove_channel_buffer_collaborator( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.collaborators.retain(|collaborator| { + if collaborator.peer_id == message.payload.peer_id { + this.buffer.update(cx, |buffer, cx| { + buffer.remove_peer(collaborator.replica_id as u16, cx) + }); + false + } else { + true + } + }); + cx.emit(Event::CollaboratorsChanged); + cx.notify(); + }); + + Ok(()) + } + + fn on_buffer_update( + &mut self, + _: ModelHandle, + event: &language::Event, + _: &mut ModelContext, + ) { + if let language::Event::Operation(operation) = event { + let operation = language::proto::serialize_operation(operation); + self.client + .send(proto::UpdateChannelBuffer { + channel_id: self.channel.id, + operations: vec![operation], + }) + .log_err(); + } + } + + pub fn buffer(&self) -> ModelHandle { + self.buffer.clone() + } + + pub fn collaborators(&self) -> &[proto::Collaborator] { + &self.collaborators + } + + pub fn channel(&self) -> Arc { + self.channel.clone() + } + + pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { + if self.connected { + self.connected = false; + self.subscription.take(); + cx.emit(Event::Disconnected); + cx.notify() + } + } + + pub fn is_connected(&self) -> bool { + self.connected + } + + pub fn replica_id(&self, cx: &AppContext) -> u16 { + self.buffer.read(cx).replica_id() + } +} diff --git a/crates/client/src/channel_store.rs b/crates/channel/src/channel_store.rs similarity index 71% rename from crates/client/src/channel_store.rs rename to crates/channel/src/channel_store.rs index 03d334a9defcc05255874c75349f894d1b9bc1f7..861f731331ca6337ed7d798162ceb3321ad170fe 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,19 +1,14 @@ -use crate::Status; -use crate::{Client, Subscription, User, UserStore}; -use anyhow::anyhow; -use anyhow::Result; -use collections::HashMap; -use collections::HashSet; -use futures::channel::mpsc; -use futures::Future; -use futures::StreamExt; -use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use crate::channel_buffer::ChannelBuffer; +use anyhow::{anyhow, Result}; +use client::{Client, Status, Subscription, User, UserId, UserStore}; +use collections::{hash_map, HashMap, HashSet}; +use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; +use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; pub type ChannelId = u64; -pub type UserId = u64; pub struct ChannelStore { channels_by_id: HashMap>, @@ -23,6 +18,7 @@ pub struct ChannelStore { channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, + opened_buffers: HashMap, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -57,6 +53,11 @@ pub enum ChannelMemberStatus { NotMember, } +enum OpenedChannelBuffer { + Open(WeakModelHandle), + Loading(Shared, Arc>>>), +} + impl ChannelStore { pub fn new( client: Arc, @@ -70,16 +71,14 @@ impl ChannelStore { let mut connection_status = client.status(); let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { while let Some(status) = connection_status.next().await { - if matches!(status, Status::ConnectionLost | Status::SignedOut) { + if !status.is_connected() { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.channels_by_id.clear(); - this.channel_invitations.clear(); - this.channel_participants.clear(); - this.channels_with_admin_privileges.clear(); - this.channel_paths.clear(); - this.outgoing_invites.clear(); - cx.notify(); + if matches!(status, Status::ConnectionLost | Status::SignedOut) { + this.handle_disconnect(cx); + } else { + this.disconnect_buffers(cx); + } }); } else { break; @@ -87,6 +86,7 @@ impl ChannelStore { } } }); + Self { channels_by_id: HashMap::default(), channel_invitations: Vec::default(), @@ -94,6 +94,7 @@ impl ChannelStore { channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), + opened_buffers: Default::default(), update_channels_tx, client, user_store, @@ -114,6 +115,16 @@ impl ChannelStore { } } + pub fn has_children(&self, channel_id: ChannelId) -> bool { + self.channel_paths.iter().any(|path| { + if let Some(ix) = path.iter().position(|id| *id == channel_id) { + path.len() > ix + 1 + } else { + false + } + }) + } + pub fn channel_count(&self) -> usize { self.channel_paths.len() } @@ -141,6 +152,74 @@ impl ChannelStore { self.channels_by_id.get(&channel_id) } + pub fn open_channel_buffer( + &mut self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + // Make sure that a given channel buffer is only opened once per + // app instance, even if this method is called multiple times + // with the same channel id while the first task is still running. + let task = loop { + match self.opened_buffers.entry(channel_id) { + hash_map::Entry::Occupied(e) => match e.get() { + OpenedChannelBuffer::Open(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + break Task::ready(Ok(buffer)).shared(); + } else { + self.opened_buffers.remove(&channel_id); + continue; + } + } + OpenedChannelBuffer::Loading(task) => break task.clone(), + }, + hash_map::Entry::Vacant(e) => { + let client = self.client.clone(); + let task = cx + .spawn(|this, cx| async move { + let channel = this.read_with(&cx, |this, _| { + this.channel_for_id(channel_id).cloned().ok_or_else(|| { + Arc::new(anyhow!("no channel for id: {}", channel_id)) + }) + })?; + + ChannelBuffer::new(channel, client, cx) + .await + .map_err(Arc::new) + }) + .shared(); + e.insert(OpenedChannelBuffer::Loading(task.clone())); + cx.spawn({ + let task = task.clone(); + |this, mut cx| async move { + let result = task.await; + this.update(&mut cx, |this, cx| match result { + Ok(buffer) => { + cx.observe_release(&buffer, move |this, _, _| { + this.opened_buffers.remove(&channel_id); + }) + .detach(); + this.opened_buffers.insert( + channel_id, + OpenedChannelBuffer::Open(buffer.downgrade()), + ); + } + Err(error) => { + log::error!("failed to open channel buffer {error:?}"); + this.opened_buffers.remove(&channel_id); + } + }); + } + }) + .detach(); + break task; + } + } + }; + cx.foreground() + .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) + } + pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { self.channel_paths.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { @@ -403,6 +482,27 @@ impl ChannelStore { Ok(()) } + fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) { + self.disconnect_buffers(cx); + self.channels_by_id.clear(); + self.channel_invitations.clear(); + self.channel_participants.clear(); + self.channels_with_admin_privileges.clear(); + self.channel_paths.clear(); + self.outgoing_invites.clear(); + cx.notify(); + } + + fn disconnect_buffers(&mut self, cx: &mut ModelContext) { + for (_, buffer) in self.opened_buffers.drain() { + if let OpenedChannelBuffer::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); + } + } + } + } + pub(crate) fn update_channels( &mut self, payload: proto::UpdateChannels, @@ -437,38 +537,44 @@ impl ChannelStore { .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); self.channels_with_admin_privileges .retain(|channel_id| !payload.remove_channels.contains(channel_id)); - } - for channel in payload.channels { - if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) { - // FIXME: We may be missing a path for this existing channel in certain cases - let existing_channel = Arc::make_mut(existing_channel); - existing_channel.name = channel.name; - continue; + for channel_id in &payload.remove_channels { + let channel_id = *channel_id; + if let Some(OpenedChannelBuffer::Open(buffer)) = + self.opened_buffers.remove(&channel_id) + { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, ChannelBuffer::disconnect); + } + } } + } - self.channels_by_id.insert( - channel.id, - Arc::new(Channel { - id: channel.id, - name: channel.name, - }), - ); - - if let Some(parent_id) = channel.parent_id { - let mut ix = 0; - while ix < self.channel_paths.len() { - let path = &self.channel_paths[ix]; - if path.ends_with(&[parent_id]) { - let mut new_path = path.clone(); - new_path.push(channel.id); - self.channel_paths.insert(ix + 1, new_path); + for channel_proto in payload.channels { + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + Arc::make_mut(existing_channel).name = channel_proto.name; + } else { + let channel = Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + }); + self.channels_by_id.insert(channel.id, channel.clone()); + + if let Some(parent_id) = channel_proto.parent_id { + let mut ix = 0; + while ix < self.channel_paths.len() { + let path = &self.channel_paths[ix]; + if path.ends_with(&[parent_id]) { + let mut new_path = path.clone(); + new_path.push(channel.id); + self.channel_paths.insert(ix + 1, new_path); + ix += 1; + } ix += 1; } - ix += 1; + } else { + self.channel_paths.push(vec![channel.id]); } - } else { - self.channel_paths.push(vec![channel.id]); } } diff --git a/crates/client/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs similarity index 98% rename from crates/client/src/channel_store_tests.rs rename to crates/channel/src/channel_store_tests.rs index 51e819349e7c665976d4ff0af5f20c7bb32eaff2..18894b1f472f907d3b54ad35df57d78e5e974565 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -1,4 +1,7 @@ use super::*; +use client::{Client, UserStore}; +use gpui::{AppContext, ModelHandle}; +use rpc::proto; use util::http::FakeHttpClient; #[gpui::test] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 3ecc51598696cd9ec5965c35d346bda069418086..64d8f02c8ae1eba2525abca8a4847edb30a458e8 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -17,6 +17,7 @@ db = { path = "../db" } gpui = { path = "../gpui" } util = { path = "../util" } rpc = { path = "../rpc" } +text = { path = "../text" } settings = { path = "../settings" } staff_mode = { path = "../staff_mode" } sum_tree = { path = "../sum_tree" } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 8ef3e32ea8f98b47a744e148f881289934fae215..a32c415f7e9d1b9699b51582f05bee3af06792e2 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,10 +1,6 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -#[cfg(test)] -mod channel_store_tests; - -pub mod channel_store; pub mod telemetry; pub mod user; @@ -48,7 +44,6 @@ use util::channel::ReleaseChannel; use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; -pub use channel_store::*; pub use rpc::*; pub use telemetry::ClickhouseEvent; pub use user::*; diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index dc5154d96f96f9657947da92af6dae1ac873012a..48886377ba56ad046027da5cf7a754bdc22cea72 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -74,6 +74,7 @@ pub enum ClickhouseEvent { Call { operation: &'static str, room_id: u64, + channel_id: Option, }, } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index be11d1fb442b8d69228f867156b3b7e6c08b0b66..1dc384da1725c6d58b92aff7477ed516ef69590f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -10,9 +10,11 @@ use std::sync::{Arc, Weak}; use util::http::HttpClient; use util::TryFutureExt as _; +pub type UserId = u64; + #[derive(Default, Debug)] pub struct User { - pub id: u64, + pub id: UserId, pub github_login: String, pub avatar: Option>, } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fc8c1644cd7d127876f168ab2a32c17f5e2b114c..8adc38615c3cab87d36a348323e0c3674f555d6c 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.17.0" +version = "0.18.0" publish = false [[bin]] @@ -14,8 +14,10 @@ name = "seed" required-features = ["seed-support"] [dependencies] +clock = { path = "../clock" } collections = { path = "../collections" } live_kit_server = { path = "../live_kit_server" } +text = { path = "../text" } rpc = { path = "../rpc" } util = { path = "../util" } @@ -35,6 +37,7 @@ log.workspace = true nanoid = "0.4" parking_lot.workspace = true prometheus = "0.13" +prost.workspace = true rand.workspace = true reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" @@ -62,6 +65,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } +channel = { path = "../channel" } editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } @@ -74,6 +78,7 @@ rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } +collab_ui = { path = "../collab_ui", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 3dceaecef4e15a3fcbc221102110ee441b876832..7a4cd9fd23cbc80bb38e3b2e7446ae53a902066a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -208,3 +208,44 @@ CREATE TABLE "channel_members" ( ); CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); + +CREATE TABLE "buffers" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "value" BLOB NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); + +CREATE TABLE "channel_buffer_collaborators" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT false, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql new file mode 100644 index 0000000000000000000000000000000000000000..5e6e7ce3393a628c86cbcdabf2349ebfa6667bd6 --- /dev/null +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -0,0 +1,40 @@ +CREATE TABLE "buffers" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "value" BYTEA NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); + +CREATE TABLE "channel_buffer_collaborators" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT FALSE, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b457c4c1161399f1c5495479079f71f0b9d261c3..9c759f79a8dd47f4cc950809c7816f0204273372 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,3850 +1,117 @@ -mod access_token; -mod channel; -mod channel_member; -mod channel_path; -mod contact; -mod follower; -mod language_server; -mod project; -mod project_collaborator; -mod room; -mod room_participant; -mod server; -mod signup; #[cfg(test)] -mod tests; -mod user; -mod worktree; -mod worktree_diagnostic_summary; -mod worktree_entry; -mod worktree_repository; -mod worktree_repository_statuses; -mod worktree_settings_file; +pub mod tests; -use crate::executor::Executor; -use crate::{Error, Result}; -use anyhow::anyhow; -use collections::{BTreeMap, HashMap, HashSet}; -pub use contact::Contact; -use dashmap::DashMap; -use futures::StreamExt; -use hyper::StatusCode; -use rand::prelude::StdRng; -use rand::{Rng, SeedableRng}; -use rpc::{proto, ConnectionId}; -use sea_orm::Condition; -pub use sea_orm::ConnectOptions; -use sea_orm::{ - entity::prelude::*, ActiveValue, ConnectionTrait, DatabaseConnection, DatabaseTransaction, - DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, - Statement, TransactionTrait, -}; -use sea_query::{Alias, Expr, OnConflict, Query}; -use serde::{Deserialize, Serialize}; -pub use signup::{Invite, NewSignup, WaitlistSummary}; -use sqlx::migrate::{Migrate, Migration, MigrationSource}; -use sqlx::Connection; -use std::fmt::Write as _; -use std::ops::{Deref, DerefMut}; -use std::path::Path; -use std::time::Duration; -use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc}; -use tokio::sync::{Mutex, OwnedMutexGuard}; -pub use user::Model as User; - -pub struct Database { - options: ConnectOptions, - pool: DatabaseConnection, - rooms: DashMap>>, - rng: Mutex, - executor: Executor, - #[cfg(test)] - runtime: Option, -} - -impl Database { - pub async fn new(options: ConnectOptions, executor: Executor) -> Result { - Ok(Self { - options: options.clone(), - pool: sea_orm::Database::connect(options).await?, - rooms: DashMap::with_capacity(16384), - rng: Mutex::new(StdRng::seed_from_u64(0)), - executor, - #[cfg(test)] - runtime: None, - }) - } - - #[cfg(test)] - pub fn reset(&self) { - self.rooms.clear(); - } - - pub async fn migrate( - &self, - migrations_path: &Path, - ignore_checksum_mismatch: bool, - ) -> anyhow::Result> { - let migrations = MigrationSource::resolve(migrations_path) - .await - .map_err(|err| anyhow!("failed to load migrations: {err:?}"))?; - - let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?; - - connection.ensure_migrations_table().await?; - let applied_migrations: HashMap<_, _> = connection - .list_applied_migrations() - .await? - .into_iter() - .map(|m| (m.version, m)) - .collect(); - - let mut new_migrations = Vec::new(); - for migration in migrations { - match applied_migrations.get(&migration.version) { - Some(applied_migration) => { - if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch - { - Err(anyhow!( - "checksum mismatch for applied migration {}", - migration.description - ))?; - } - } - None => { - let elapsed = connection.apply(&migration).await?; - new_migrations.push((migration, elapsed)); - } - } - } - - Ok(new_migrations) - } - - pub async fn create_server(&self, environment: &str) -> Result { - self.transaction(|tx| async move { - let server = server::ActiveModel { - environment: ActiveValue::set(environment.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - Ok(server.id) - }) - .await - } - - pub async fn stale_room_ids( - &self, - environment: &str, - new_server_id: ServerId, - ) -> Result> { - self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - RoomId, - } - - let stale_server_epochs = self - .stale_server_ids(environment, new_server_id, &tx) - .await?; - Ok(room_participant::Entity::find() - .select_only() - .column(room_participant::Column::RoomId) - .distinct() - .filter( - room_participant::Column::AnsweringConnectionServerId - .is_in(stale_server_epochs), - ) - .into_values::<_, QueryAs>() - .all(&*tx) - .await?) - }) - .await - } - - pub async fn refresh_room( - &self, - room_id: RoomId, - new_server_id: ServerId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let stale_participant_filter = Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::AnsweringConnectionId.is_not_null()) - .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id)); - - let stale_participant_user_ids = room_participant::Entity::find() - .filter(stale_participant_filter.clone()) - .all(&*tx) - .await? - .into_iter() - .map(|participant| participant.user_id) - .collect::>(); - - // Delete participants who failed to reconnect and cancel their calls. - let mut canceled_calls_to_user_ids = Vec::new(); - room_participant::Entity::delete_many() - .filter(stale_participant_filter) - .exec(&*tx) - .await?; - let called_participants = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::CallingUserId - .is_in(stale_participant_user_ids.iter().copied()), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .all(&*tx) - .await?; - room_participant::Entity::delete_many() - .filter( - room_participant::Column::Id - .is_in(called_participants.iter().map(|participant| participant.id)), - ) - .exec(&*tx) - .await?; - canceled_calls_to_user_ids.extend( - called_participants - .into_iter() - .map(|participant| participant.user_id), - ); - - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; - let channel_members; - if let Some(channel_id) = channel_id { - channel_members = self.get_channel_members_internal(channel_id, &tx).await?; - } else { - channel_members = Vec::new(); - - // Delete the room if it becomes empty. - if room.participants.is_empty() { - project::Entity::delete_many() - .filter(project::Column::RoomId.eq(room_id)) - .exec(&*tx) - .await?; - room::Entity::delete_by_id(room_id).exec(&*tx).await?; - } - }; - - Ok(RefreshedRoom { - room, - channel_id, - channel_members, - stale_participant_user_ids, - canceled_calls_to_user_ids, - }) - }) - .await - } - - pub async fn delete_stale_servers( - &self, - environment: &str, - new_server_id: ServerId, - ) -> Result<()> { - self.transaction(|tx| async move { - server::Entity::delete_many() - .filter( - Condition::all() - .add(server::Column::Environment.eq(environment)) - .add(server::Column::Id.ne(new_server_id)), - ) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - async fn stale_server_ids( - &self, - environment: &str, - new_server_id: ServerId, - tx: &DatabaseTransaction, - ) -> Result> { - let stale_servers = server::Entity::find() - .filter( - Condition::all() - .add(server::Column::Environment.eq(environment)) - .add(server::Column::Id.ne(new_server_id)), - ) - .all(&*tx) - .await?; - Ok(stale_servers.into_iter().map(|server| server.id).collect()) - } - - // users - - pub async fn create_user( - &self, - email_address: &str, - admin: bool, - params: NewUserParams, - ) -> Result { - self.transaction(|tx| async { - let tx = tx; - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(Some(email_address.into())), - github_login: ActiveValue::set(params.github_login.clone()), - github_user_id: ActiveValue::set(Some(params.github_user_id)), - admin: ActiveValue::set(admin), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .on_conflict( - OnConflict::column(user::Column::GithubLogin) - .update_column(user::Column::GithubLogin) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - Ok(NewUserResult { - user_id: user.id, - metrics_id: user.metrics_id.to_string(), - signup_device_id: None, - inviting_user_id: None, - }) - }) - .await - } - - pub async fn get_user_by_id(&self, id: UserId) -> Result> { - self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) }) - .await - } - - pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { - self.transaction(|tx| async { - let tx = tx; - Ok(user::Entity::find() - .filter(user::Column::Id.is_in(ids.iter().copied())) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(&*tx) - .await?) - }) - .await - } - - pub async fn get_or_create_user_by_github_account( - &self, - github_login: &str, - github_user_id: Option, - github_email: Option<&str>, - ) -> Result> { - self.transaction(|tx| async move { - let tx = &*tx; - if let Some(github_user_id) = github_user_id { - if let Some(user_by_github_user_id) = user::Entity::find() - .filter(user::Column::GithubUserId.eq(github_user_id)) - .one(tx) - .await? - { - let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); - user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); - Ok(Some(user_by_github_user_id.update(tx).await?)) - } else if let Some(user_by_github_login) = user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await? - { - let mut user_by_github_login = user_by_github_login.into_active_model(); - user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id)); - Ok(Some(user_by_github_login.update(tx).await?)) - } else { - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(github_email.map(|email| email.into())), - github_login: ActiveValue::set(github_login.into()), - github_user_id: ActiveValue::set(Some(github_user_id)), - admin: ActiveValue::set(false), - invite_count: ActiveValue::set(0), - invite_code: ActiveValue::set(None), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .exec_with_returning(&*tx) - .await?; - Ok(Some(user)) - } - } else { - Ok(user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await?) - } - }) - .await - } - - pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .order_by_asc(user::Column::GithubLogin) - .limit(limit as u64) - .offset(page as u64 * limit as u64) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_users_with_no_invites( - &self, - invited_by_another_user: bool, - ) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .filter( - user::Column::InviteCount - .eq(0) - .and(if invited_by_another_user { - user::Column::InviterId.is_not_null() - } else { - user::Column::InviterId.is_null() - }), - ) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_user_metrics_id(&self, id: UserId) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - MetricsId, - } - - self.transaction(|tx| async move { - let metrics_id: Uuid = user::Entity::find_by_id(id) - .select_only() - .column(user::Column::MetricsId) - .into_values::<_, QueryAs>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("could not find user"))?; - Ok(metrics_id.to_string()) - }) - .await - } - - pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { - self.transaction(|tx| async move { - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - admin: ActiveValue::set(is_admin), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { - self.transaction(|tx| async move { - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - connected_once: ActiveValue::set(connected_once), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn destroy_user(&self, id: UserId) -> Result<()> { - self.transaction(|tx| async move { - access_token::Entity::delete_many() - .filter(access_token::Column::UserId.eq(id)) - .exec(&*tx) - .await?; - user::Entity::delete_by_id(id).exec(&*tx).await?; - Ok(()) - }) - .await - } - - // contacts - - pub async fn get_contacts(&self, user_id: UserId) -> Result> { - #[derive(Debug, FromQueryResult)] - struct ContactWithUserBusyStatuses { - user_id_a: UserId, - user_id_b: UserId, - a_to_b: bool, - accepted: bool, - should_notify: bool, - user_a_busy: bool, - user_b_busy: bool, - } - - self.transaction(|tx| async move { - let user_a_participant = Alias::new("user_a_participant"); - let user_b_participant = Alias::new("user_b_participant"); - let mut db_contacts = contact::Entity::find() - .column_as( - Expr::tbl(user_a_participant.clone(), room_participant::Column::Id) - .is_not_null(), - "user_a_busy", - ) - .column_as( - Expr::tbl(user_b_participant.clone(), room_participant::Column::Id) - .is_not_null(), - "user_b_busy", - ) - .filter( - contact::Column::UserIdA - .eq(user_id) - .or(contact::Column::UserIdB.eq(user_id)), - ) - .join_as( - JoinType::LeftJoin, - contact::Relation::UserARoomParticipant.def(), - user_a_participant, - ) - .join_as( - JoinType::LeftJoin, - contact::Relation::UserBRoomParticipant.def(), - user_b_participant, - ) - .into_model::() - .stream(&*tx) - .await?; - - let mut contacts = Vec::new(); - while let Some(db_contact) = db_contacts.next().await { - let db_contact = db_contact?; - if db_contact.user_id_a == user_id { - if db_contact.accepted { - contacts.push(Contact::Accepted { - user_id: db_contact.user_id_b, - should_notify: db_contact.should_notify && db_contact.a_to_b, - busy: db_contact.user_b_busy, - }); - } else if db_contact.a_to_b { - contacts.push(Contact::Outgoing { - user_id: db_contact.user_id_b, - }) - } else { - contacts.push(Contact::Incoming { - user_id: db_contact.user_id_b, - should_notify: db_contact.should_notify, - }); - } - } else if db_contact.accepted { - contacts.push(Contact::Accepted { - user_id: db_contact.user_id_a, - should_notify: db_contact.should_notify && !db_contact.a_to_b, - busy: db_contact.user_a_busy, - }); - } else if db_contact.a_to_b { - contacts.push(Contact::Incoming { - user_id: db_contact.user_id_a, - should_notify: db_contact.should_notify, - }); - } else { - contacts.push(Contact::Outgoing { - user_id: db_contact.user_id_a, - }); - } - } - - contacts.sort_unstable_by_key(|contact| contact.user_id()); - - Ok(contacts) - }) - .await - } - - pub async fn is_user_busy(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let participant = room_participant::Entity::find() - .filter(room_participant::Column::UserId.eq(user_id)) - .one(&*tx) - .await?; - Ok(participant.is_some()) - }) - .await - } - - pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { - self.transaction(|tx| async move { - let (id_a, id_b) = if user_id_1 < user_id_2 { - (user_id_1, user_id_2) - } else { - (user_id_2, user_id_1) - }; - - Ok(contact::Entity::find() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::Accepted.eq(true)), - ) - .one(&*tx) - .await? - .is_some()) - }) - .await - } - - pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if sender_id < receiver_id { - (sender_id, receiver_id, true) - } else { - (receiver_id, sender_id, false) - }; - - let rows_affected = contact::Entity::insert(contact::ActiveModel { - user_id_a: ActiveValue::set(id_a), - user_id_b: ActiveValue::set(id_b), - a_to_b: ActiveValue::set(a_to_b), - accepted: ActiveValue::set(false), - should_notify: ActiveValue::set(true), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB]) - .values([ - (contact::Column::Accepted, true.into()), - (contact::Column::ShouldNotify, false.into()), - ]) - .action_and_where( - contact::Column::Accepted.eq(false).and( - contact::Column::AToB - .eq(a_to_b) - .and(contact::Column::UserIdA.eq(id_b)) - .or(contact::Column::AToB - .ne(a_to_b) - .and(contact::Column::UserIdA.eq(id_a))), - ), - ) - .to_owned(), - ) - .exec_without_returning(&*tx) - .await?; - - if rows_affected == 1 { - Ok(()) - } else { - Err(anyhow!("contact already requested"))? - } - }) - .await - } - - /// Returns a bool indicating whether the removed contact had originally accepted or not - /// - /// Deletes the contact identified by the requester and responder ids, and then returns - /// whether the deleted contact had originally accepted or was a pending contact request. - /// - /// # Arguments - /// - /// * `requester_id` - The user that initiates this request - /// * `responder_id` - The user that will be removed - pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result { - self.transaction(|tx| async move { - let (id_a, id_b) = if responder_id < requester_id { - (responder_id, requester_id) - } else { - (requester_id, responder_id) - }; - - let contact = contact::Entity::find() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such contact"))?; - - contact::Entity::delete_by_id(contact.id).exec(&*tx).await?; - Ok(contact.accepted) - }) - .await - } - - pub async fn dismiss_contact_notification( - &self, - user_id: UserId, - contact_user_id: UserId, - ) -> Result<()> { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if user_id < contact_user_id { - (user_id, contact_user_id, true) - } else { - (contact_user_id, user_id, false) - }; - - let result = contact::Entity::update_many() - .set(contact::ActiveModel { - should_notify: ActiveValue::set(false), - ..Default::default() - }) - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and( - contact::Column::AToB - .eq(a_to_b) - .and(contact::Column::Accepted.eq(true)) - .or(contact::Column::AToB - .ne(a_to_b) - .and(contact::Column::Accepted.eq(false))), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("no such contact request"))? - } else { - Ok(()) - } - }) - .await - } - - pub async fn respond_to_contact_request( - &self, - responder_id: UserId, - requester_id: UserId, - accept: bool, - ) -> Result<()> { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if responder_id < requester_id { - (responder_id, requester_id, false) - } else { - (requester_id, responder_id, true) - }; - let rows_affected = if accept { - let result = contact::Entity::update_many() - .set(contact::ActiveModel { - accepted: ActiveValue::set(true), - should_notify: ActiveValue::set(true), - ..Default::default() - }) - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::AToB.eq(a_to_b)), - ) - .exec(&*tx) - .await?; - result.rows_affected - } else { - let result = contact::Entity::delete_many() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::AToB.eq(a_to_b)) - .and(contact::Column::Accepted.eq(false)), - ) - .exec(&*tx) - .await?; - - result.rows_affected - }; - - if rows_affected == 1 { - Ok(()) - } else { - Err(anyhow!("no such contact request"))? - } - }) - .await - } - - pub fn fuzzy_like_string(string: &str) -> String { - let mut result = String::with_capacity(string.len() * 2 + 1); - for c in string.chars() { - if c.is_alphanumeric() { - result.push('%'); - result.push(c); - } - } - result.push('%'); - result - } - - pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { - self.transaction(|tx| async { - let tx = tx; - let like_string = Self::fuzzy_like_string(name_query); - let query = " - SELECT users.* - FROM users - WHERE github_login ILIKE $1 - ORDER BY github_login <-> $2 - LIMIT $3 - "; - - Ok(user::Entity::find() - .from_raw_sql(Statement::from_sql_and_values( - self.pool.get_database_backend(), - query.into(), - vec![like_string.into(), name_query.into(), limit.into()], - )) - .all(&*tx) - .await?) - }) - .await - } - - // signups - - pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> { - self.transaction(|tx| async move { - signup::Entity::insert(signup::ActiveModel { - email_address: ActiveValue::set(signup.email_address.clone()), - email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), - email_confirmation_sent: ActiveValue::set(false), - platform_mac: ActiveValue::set(signup.platform_mac), - platform_windows: ActiveValue::set(signup.platform_windows), - platform_linux: ActiveValue::set(signup.platform_linux), - platform_unknown: ActiveValue::set(false), - editor_features: ActiveValue::set(Some(signup.editor_features.clone())), - programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())), - device_id: ActiveValue::set(signup.device_id.clone()), - added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list), - ..Default::default() - }) - .on_conflict( - OnConflict::column(signup::Column::EmailAddress) - .update_columns([ - signup::Column::PlatformMac, - signup::Column::PlatformWindows, - signup::Column::PlatformLinux, - signup::Column::EditorFeatures, - signup::Column::ProgrammingLanguages, - signup::Column::DeviceId, - signup::Column::AddedToMailingList, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_signup(&self, email_address: &str) -> Result { - self.transaction(|tx| async move { - let signup = signup::Entity::find() - .filter(signup::Column::EmailAddress.eq(email_address)) - .one(&*tx) - .await? - .ok_or_else(|| { - anyhow!("signup with email address {} doesn't exist", email_address) - })?; - - Ok(signup) - }) - .await - } - - pub async fn get_waitlist_summary(&self) -> Result { - self.transaction(|tx| async move { - let query = " - SELECT - COUNT(*) as count, - COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count, - COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count, - COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count, - COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count - FROM ( - SELECT * - FROM signups - WHERE - NOT email_confirmation_sent - ) AS unsent - "; - Ok( - WaitlistSummary::find_by_statement(Statement::from_sql_and_values( - self.pool.get_database_backend(), - query.into(), - vec![], - )) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid result"))?, - ) - }) - .await - } - - pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> { - let emails = invites - .iter() - .map(|s| s.email_address.as_str()) - .collect::>(); - self.transaction(|tx| async { - let tx = tx; - signup::Entity::update_many() - .filter(signup::Column::EmailAddress.is_in(emails.iter().copied())) - .set(signup::ActiveModel { - email_confirmation_sent: ActiveValue::set(true), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_unsent_invites(&self, count: usize) -> Result> { - self.transaction(|tx| async move { - Ok(signup::Entity::find() - .select_only() - .column(signup::Column::EmailAddress) - .column(signup::Column::EmailConfirmationCode) - .filter( - signup::Column::EmailConfirmationSent.eq(false).and( - signup::Column::PlatformMac - .eq(true) - .or(signup::Column::PlatformUnknown.eq(true)), - ), - ) - .order_by_asc(signup::Column::CreatedAt) - .limit(count as u64) - .into_model() - .all(&*tx) - .await?) - }) - .await - } - - // invite codes - - pub async fn create_invite_from_code( - &self, - code: &str, - email_address: &str, - device_id: Option<&str>, - added_to_mailing_list: bool, - ) -> Result { - self.transaction(|tx| async move { - let existing_user = user::Entity::find() - .filter(user::Column::EmailAddress.eq(email_address)) - .one(&*tx) - .await?; - - if existing_user.is_some() { - Err(anyhow!("email address is already in use"))?; - } - - let inviting_user_with_invites = match user::Entity::find() - .filter( - user::Column::InviteCode - .eq(code) - .and(user::Column::InviteCount.gt(0)), - ) - .one(&*tx) - .await? - { - Some(inviting_user) => inviting_user, - None => { - return Err(Error::Http( - StatusCode::UNAUTHORIZED, - "unable to find an invite code with invites remaining".to_string(), - ))? - } - }; - user::Entity::update_many() - .filter( - user::Column::Id - .eq(inviting_user_with_invites.id) - .and(user::Column::InviteCount.gt(0)), - ) - .col_expr( - user::Column::InviteCount, - Expr::col(user::Column::InviteCount).sub(1), - ) - .exec(&*tx) - .await?; - - let signup = signup::Entity::insert(signup::ActiveModel { - email_address: ActiveValue::set(email_address.into()), - email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), - email_confirmation_sent: ActiveValue::set(false), - inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)), - platform_linux: ActiveValue::set(false), - platform_mac: ActiveValue::set(false), - platform_windows: ActiveValue::set(false), - platform_unknown: ActiveValue::set(true), - device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())), - added_to_mailing_list: ActiveValue::set(added_to_mailing_list), - ..Default::default() - }) - .on_conflict( - OnConflict::column(signup::Column::EmailAddress) - .update_column(signup::Column::InvitingUserId) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - Ok(Invite { - email_address: signup.email_address, - email_confirmation_code: signup.email_confirmation_code, - }) - }) - .await - } - - pub async fn create_user_from_invite( - &self, - invite: &Invite, - user: NewUserParams, - ) -> Result> { - self.transaction(|tx| async { - let tx = tx; - let signup = signup::Entity::find() - .filter( - signup::Column::EmailAddress - .eq(invite.email_address.as_str()) - .and( - signup::Column::EmailConfirmationCode - .eq(invite.email_confirmation_code.as_str()), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?; - - if signup.user_id.is_some() { - return Ok(None); - } - - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(Some(invite.email_address.clone())), - github_login: ActiveValue::set(user.github_login.clone()), - github_user_id: ActiveValue::set(Some(user.github_user_id)), - admin: ActiveValue::set(false), - invite_count: ActiveValue::set(user.invite_count), - invite_code: ActiveValue::set(Some(random_invite_code())), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .on_conflict( - OnConflict::column(user::Column::GithubLogin) - .update_columns([ - user::Column::EmailAddress, - user::Column::GithubUserId, - user::Column::Admin, - ]) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - let mut signup = signup.into_active_model(); - signup.user_id = ActiveValue::set(Some(user.id)); - let signup = signup.update(&*tx).await?; - - if let Some(inviting_user_id) = signup.inviting_user_id { - let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id { - (inviting_user_id, user.id, true) - } else { - (user.id, inviting_user_id, false) - }; - - contact::Entity::insert(contact::ActiveModel { - user_id_a: ActiveValue::set(user_id_a), - user_id_b: ActiveValue::set(user_id_b), - a_to_b: ActiveValue::set(a_to_b), - should_notify: ActiveValue::set(true), - accepted: ActiveValue::set(true), - ..Default::default() - }) - .on_conflict(OnConflict::new().do_nothing().to_owned()) - .exec_without_returning(&*tx) - .await?; - } - - Ok(Some(NewUserResult { - user_id: user.id, - metrics_id: user.metrics_id.to_string(), - inviting_user_id: signup.inviting_user_id, - signup_device_id: signup.device_id, - })) - }) - .await - } - - pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> { - self.transaction(|tx| async move { - if count > 0 { - user::Entity::update_many() - .filter( - user::Column::Id - .eq(id) - .and(user::Column::InviteCode.is_null()), - ) - .set(user::ActiveModel { - invite_code: ActiveValue::set(Some(random_invite_code())), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - invite_count: ActiveValue::set(count), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_invite_code_for_user(&self, id: UserId) -> Result> { - self.transaction(|tx| async move { - match user::Entity::find_by_id(id).one(&*tx).await? { - Some(user) if user.invite_code.is_some() => { - Ok(Some((user.invite_code.unwrap(), user.invite_count))) - } - _ => Ok(None), - } - }) - .await - } - - pub async fn get_user_for_invite_code(&self, code: &str) -> Result { - self.transaction(|tx| async move { - user::Entity::find() - .filter(user::Column::InviteCode.eq(code)) - .one(&*tx) - .await? - .ok_or_else(|| { - Error::Http( - StatusCode::NOT_FOUND, - "that invite code does not exist".to_string(), - ) - }) - }) - .await - } - - // rooms - - pub async fn incoming_call_for_user( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - let pending_participant = room_participant::Entity::find() - .filter( - room_participant::Column::UserId - .eq(user_id) - .and(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .one(&*tx) - .await?; - - if let Some(pending_participant) = pending_participant { - let room = self.get_room(pending_participant.room_id, &tx).await?; - Ok(Self::build_incoming_call(&room, user_id)) - } else { - Ok(None) - } - }) - .await - } - - pub async fn create_room( - &self, - user_id: UserId, - connection: ConnectionId, - live_kit_room: &str, - ) -> Result { - self.transaction(|tx| async move { - let room = room::ActiveModel { - live_kit_room: ActiveValue::set(live_kit_room.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - room_participant::ActiveModel { - room_id: ActiveValue::set(room.id), - user_id: ActiveValue::set(user_id), - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(user_id), - calling_connection_id: ActiveValue::set(connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room.id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn call( - &self, - room_id: RoomId, - calling_user_id: UserId, - calling_connection: ConnectionId, - called_user_id: UserId, - initial_project_id: Option, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - room_participant::ActiveModel { - room_id: ActiveValue::set(room_id), - user_id: ActiveValue::set(called_user_id), - answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(calling_user_id), - calling_connection_id: ActiveValue::set(calling_connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - calling_connection.owner_id as i32, - ))), - initial_project_id: ActiveValue::set(initial_project_id), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - let incoming_call = Self::build_incoming_call(&room, called_user_id) - .ok_or_else(|| anyhow!("failed to build incoming call"))?; - Ok((room, incoming_call)) - }) - .await - } - - pub async fn call_failed( - &self, - room_id: RoomId, - called_user_id: UserId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - room_participant::Entity::delete_many() - .filter( - room_participant::Column::RoomId - .eq(room_id) - .and(room_participant::Column::UserId.eq(called_user_id)), - ) - .exec(&*tx) - .await?; - let room = self.get_room(room_id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn decline_call( - &self, - expected_room_id: Option, - user_id: UserId, - ) -> Result>> { - self.optional_room_transaction(|tx| async move { - let mut filter = Condition::all() - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_null()); - if let Some(room_id) = expected_room_id { - filter = filter.add(room_participant::Column::RoomId.eq(room_id)); - } - let participant = room_participant::Entity::find() - .filter(filter) - .one(&*tx) - .await?; - - let participant = if let Some(participant) = participant { - participant - } else if expected_room_id.is_some() { - return Err(anyhow!("could not find call to decline"))?; - } else { - return Ok(None); - }; - - let room_id = participant.room_id; - room_participant::Entity::delete(participant.into_active_model()) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok(Some((room_id, room))) - }) - .await - } - - pub async fn cancel_call( - &self, - room_id: RoomId, - calling_connection: ConnectionId, - called_user_id: UserId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add(room_participant::Column::UserId.eq(called_user_id)) - .add(room_participant::Column::RoomId.eq(room_id)) - .add( - room_participant::Column::CallingConnectionId - .eq(calling_connection.id as i32), - ) - .add( - room_participant::Column::CallingConnectionServerId - .eq(calling_connection.owner_id as i32), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no call to cancel"))?; - - room_participant::Entity::delete(participant.into_active_model()) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn is_current_room_different_channel( - &self, - user_id: UserId, - channel_id: ChannelId, - ) -> Result { - self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - ChannelId, - } - - let channel_id_model: Option = room_participant::Entity::find() - .select_only() - .column_as(room::Column::ChannelId, QueryAs::ChannelId) - .inner_join(room::Entity) - .filter(room_participant::Column::UserId.eq(user_id)) - .into_values::<_, QueryAs>() - .one(&*tx) - .await?; - - let result = channel_id_model - .map(|channel_id_model| channel_id_model != channel_id) - .unwrap_or(false); - - Ok(result) - }) - .await - } - - pub async fn join_room( - &self, - room_id: RoomId, - user_id: UserId, - connection: ConnectionId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelId { - ChannelId, - } - let channel_id: Option = room::Entity::find() - .select_only() - .column(room::Column::ChannelId) - .filter(room::Column::Id.eq(room_id)) - .into_values::<_, QueryChannelId>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such room"))?; - - if let Some(channel_id) = channel_id { - self.check_user_is_channel_member(channel_id, user_id, &*tx) - .await?; - - room_participant::Entity::insert_many([room_participant::ActiveModel { - room_id: ActiveValue::set(room_id), - user_id: ActiveValue::set(user_id), - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(user_id), - calling_connection_id: ActiveValue::set(connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..Default::default() - }]) - .on_conflict( - OnConflict::columns([room_participant::Column::UserId]) - .update_columns([ - room_participant::Column::AnsweringConnectionId, - room_participant::Column::AnsweringConnectionServerId, - room_participant::Column::AnsweringConnectionLost, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } else { - let result = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .set(room_participant::ActiveModel { - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - ..Default::default() - }) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("room does not exist or was already joined"))?; - } - } - - let room = self.get_room(room_id, &tx).await?; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? - } else { - Vec::new() - }; - Ok(JoinRoom { - room, - channel_id, - channel_members, - }) - }) - .await - } - - pub async fn rejoin_room( - &self, - rejoin_room: proto::RejoinRoom, - user_id: UserId, - connection: ConnectionId, - ) -> Result> { - let room_id = RoomId::from_proto(rejoin_room.id); - self.room_transaction(room_id, |tx| async { - let tx = tx; - let participant_update = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_not_null()) - .add( - Condition::any() - .add(room_participant::Column::AnsweringConnectionLost.eq(true)) - .add( - room_participant::Column::AnsweringConnectionServerId - .ne(connection.owner_id as i32), - ), - ), - ) - .set(room_participant::ActiveModel { - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - ..Default::default() - }) - .exec(&*tx) - .await?; - if participant_update.rows_affected == 0 { - return Err(anyhow!("room does not exist or was already joined"))?; - } - - let mut reshared_projects = Vec::new(); - for reshared_project in &rejoin_room.reshared_projects { - let project_id = ProjectId::from_proto(reshared_project.project_id); - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project does not exist"))?; - if project.host_user_id != user_id { - return Err(anyhow!("no such project"))?; - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let host_ix = collaborators - .iter() - .position(|collaborator| { - collaborator.user_id == user_id && collaborator.is_host - }) - .ok_or_else(|| anyhow!("host not found among collaborators"))?; - let host = collaborators.swap_remove(host_ix); - let old_connection_id = host.connection(); - - project::Entity::update(project::ActiveModel { - host_connection_id: ActiveValue::set(Some(connection.id as i32)), - host_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..project.into_active_model() - }) - .exec(&*tx) - .await?; - project_collaborator::Entity::update(project_collaborator::ActiveModel { - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - ..host.into_active_model() - }) - .exec(&*tx) - .await?; - - self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx) - .await?; - - reshared_projects.push(ResharedProject { - id: project_id, - old_connection_id, - collaborators: collaborators - .iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees: reshared_project.worktrees.clone(), - }); - } - - project::Entity::delete_many() - .filter( - Condition::all() - .add(project::Column::RoomId.eq(room_id)) - .add(project::Column::HostUserId.eq(user_id)) - .add( - project::Column::Id - .is_not_in(reshared_projects.iter().map(|project| project.id)), - ), - ) - .exec(&*tx) - .await?; - - let mut rejoined_projects = Vec::new(); - for rejoined_project in &rejoin_room.rejoined_projects { - let project_id = ProjectId::from_proto(rejoined_project.id); - let Some(project) = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? else { continue }; - - let mut worktrees = Vec::new(); - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - for db_worktree in db_worktrees { - let mut worktree = RejoinedWorktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - updated_entries: Default::default(), - removed_entries: Default::default(), - updated_repositories: Default::default(), - removed_repositories: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }; - - let rejoined_worktree = rejoined_project - .worktrees - .iter() - .find(|worktree| worktree.id == db_worktree.id as u64); - - // File entries - { - let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { - worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_entry::Column::IsDeleted.eq(false) - }; - - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project.id)) - .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) - .add(entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if db_entry.is_deleted { - worktree.removed_entries.push(db_entry.id as u64); - } else { - worktree.updated_entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Repository Entries - { - let repository_entry_filter = - if let Some(rejoined_worktree) = rejoined_worktree { - worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_repository::Column::IsDeleted.eq(false) - }; - - let mut db_repositories = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project.id)) - .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) - .add(repository_entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_repository) = db_repositories.next().await { - let db_repository = db_repository?; - if db_repository.is_deleted { - worktree - .removed_repositories - .push(db_repository.work_directory_id as u64); - } else { - worktree.updated_repositories.push(proto::RepositoryEntry { - work_directory_id: db_repository.work_directory_id as u64, - branch: db_repository.branch, - }); - } - } - } - - worktrees.push(worktree); - } - - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await? - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect::>(); - - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = worktrees - .iter_mut() - .find(|w| w.id == db_settings_file.worktree_id as u64) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let self_collaborator = if let Some(self_collaborator_ix) = collaborators - .iter() - .position(|collaborator| collaborator.user_id == user_id) - { - collaborators.swap_remove(self_collaborator_ix) - } else { - continue; - }; - let old_connection_id = self_collaborator.connection(); - project_collaborator::Entity::update(project_collaborator::ActiveModel { - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - ..self_collaborator.into_active_model() - }) - .exec(&*tx) - .await?; - - let collaborators = collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect::>(); - - rejoined_projects.push(RejoinedProject { - id: project_id, - old_connection_id, - collaborators, - worktrees, - language_servers, - }); - } - - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? - } else { - Vec::new() - }; - - Ok(RejoinedRoom { - room, - channel_id, - channel_members, - rejoined_projects, - reshared_projects, - }) - }) - .await - } - - pub async fn leave_room( - &self, - connection: ConnectionId, - ) -> Result>> { - self.optional_room_transaction(|tx| async move { - let leaving_participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await?; - - if let Some(leaving_participant) = leaving_participant { - // Leave room. - let room_id = leaving_participant.room_id; - room_participant::Entity::delete_by_id(leaving_participant.id) - .exec(&*tx) - .await?; - - // Cancel pending calls initiated by the leaving user. - let called_participants = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::CallingUserId - .eq(leaving_participant.user_id), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .all(&*tx) - .await?; - room_participant::Entity::delete_many() - .filter( - room_participant::Column::Id - .is_in(called_participants.iter().map(|participant| participant.id)), - ) - .exec(&*tx) - .await?; - let canceled_calls_to_user_ids = called_participants - .into_iter() - .map(|participant| participant.user_id) - .collect(); - - // Detect left projects. - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryProjectIds { - ProjectId, - } - let project_ids: Vec = project_collaborator::Entity::find() - .select_only() - .column_as( - project_collaborator::Column::ProjectId, - QueryProjectIds::ProjectId, - ) - .filter( - Condition::all() - .add( - project_collaborator::Column::ConnectionId.eq(connection.id as i32), - ) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .into_values::<_, QueryProjectIds>() - .all(&*tx) - .await?; - let mut left_projects = HashMap::default(); - let mut collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.is_in(project_ids)) - .stream(&*tx) - .await?; - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - let left_project = - left_projects - .entry(collaborator.project_id) - .or_insert(LeftProject { - id: collaborator.project_id, - host_user_id: Default::default(), - connection_ids: Default::default(), - host_connection_id: Default::default(), - }); - - let collaborator_connection_id = collaborator.connection(); - if collaborator_connection_id != connection { - left_project.connection_ids.push(collaborator_connection_id); - } - - if collaborator.is_host { - left_project.host_user_id = collaborator.user_id; - left_project.host_connection_id = collaborator_connection_id; - } - } - drop(collaborators); - - // Leave projects. - project_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add( - project_collaborator::Column::ConnectionId.eq(connection.id as i32), - ) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - - // Unshare projects. - project::Entity::delete_many() - .filter( - Condition::all() - .add(project::Column::RoomId.eq(room_id)) - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; - let deleted = if room.participants.is_empty() { - let result = room::Entity::delete_by_id(room_id) - .filter(room::Column::ChannelId.is_null()) - .exec(&*tx) - .await?; - result.rows_affected > 0 - } else { - false - }; - - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? - } else { - Vec::new() - }; - let left_room = LeftRoom { - room, - channel_id, - channel_members, - left_projects, - canceled_calls_to_user_ids, - deleted, - }; - - if left_room.room.participants.is_empty() { - self.rooms.remove(&room_id); - } - - Ok(Some((room_id, left_room))) - } else { - Ok(None) - } - }) - .await - } - - pub async fn follow( - &self, - project_id: ProjectId, - leader_connection: ConnectionId, - follower_connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - follower::ActiveModel { - room_id: ActiveValue::set(room_id), - project_id: ActiveValue::set(project_id), - leader_connection_server_id: ActiveValue::set(ServerId( - leader_connection.owner_id as i32, - )), - leader_connection_id: ActiveValue::set(leader_connection.id as i32), - follower_connection_server_id: ActiveValue::set(ServerId( - follower_connection.owner_id as i32, - )), - follower_connection_id: ActiveValue::set(follower_connection.id as i32), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &*tx).await?; - Ok(room) - }) - .await - } - - pub async fn unfollow( - &self, - project_id: ProjectId, - leader_connection: ConnectionId, - follower_connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - follower::Entity::delete_many() - .filter( - Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) - .add( - follower::Column::LeaderConnectionServerId - .eq(leader_connection.owner_id), - ) - .add(follower::Column::LeaderConnectionId.eq(leader_connection.id)) - .add( - follower::Column::FollowerConnectionServerId - .eq(follower_connection.owner_id), - ) - .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)), - ) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &*tx).await?; - Ok(room) - }) - .await - } - - pub async fn update_room_participant_location( - &self, - room_id: RoomId, - connection: ConnectionId, - location: proto::ParticipantLocation, - ) -> Result> { - self.room_transaction(room_id, |tx| async { - let tx = tx; - let location_kind; - let location_project_id; - match location - .variant - .as_ref() - .ok_or_else(|| anyhow!("invalid location"))? - { - proto::participant_location::Variant::SharedProject(project) => { - location_kind = 0; - location_project_id = Some(ProjectId::from_proto(project.id)); - } - proto::participant_location::Variant::UnsharedProject(_) => { - location_kind = 1; - location_project_id = None; - } - proto::participant_location::Variant::External(_) => { - location_kind = 2; - location_project_id = None; - } - } - - let result = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .set(room_participant::ActiveModel { - location_kind: ActiveValue::set(Some(location_kind)), - location_project_id: ActiveValue::set(location_project_id), - ..Default::default() - }) - .exec(&*tx) - .await?; - - if result.rows_affected == 1 { - let room = self.get_room(room_id, &tx).await?; - Ok(room) - } else { - Err(anyhow!("could not update room participant location"))? - } - }) - .await - } - - pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { - self.transaction(|tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("not a participant in any room"))?; - - room_participant::Entity::update(room_participant::ActiveModel { - answering_connection_lost: ActiveValue::set(true), - ..participant.into_active_model() - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - fn build_incoming_call( - room: &proto::Room, - called_user_id: UserId, - ) -> Option { - let pending_participant = room - .pending_participants - .iter() - .find(|participant| participant.user_id == called_user_id.to_proto())?; - - Some(proto::IncomingCall { - room_id: room.id, - calling_user_id: pending_participant.calling_user_id, - participant_user_ids: room - .participants - .iter() - .map(|participant| participant.user_id) - .collect(), - initial_project: room.participants.iter().find_map(|participant| { - let initial_project_id = pending_participant.initial_project_id?; - participant - .projects - .iter() - .find(|project| project.id == initial_project_id) - .cloned() - }), - }) - } - async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { - let (_, room) = self.get_channel_room(room_id, tx).await?; - Ok(room) - } - - async fn get_channel_room( - &self, - room_id: RoomId, - tx: &DatabaseTransaction, - ) -> Result<(Option, proto::Room)> { - let db_room = room::Entity::find_by_id(room_id) - .one(tx) - .await? - .ok_or_else(|| anyhow!("could not find room"))?; - - let mut db_participants = db_room - .find_related(room_participant::Entity) - .stream(tx) - .await?; - let mut participants = HashMap::default(); - let mut pending_participants = Vec::new(); - while let Some(db_participant) = db_participants.next().await { - let db_participant = db_participant?; - if let Some((answering_connection_id, answering_connection_server_id)) = db_participant - .answering_connection_id - .zip(db_participant.answering_connection_server_id) - { - let location = match ( - db_participant.location_kind, - db_participant.location_project_id, - ) { - (Some(0), Some(project_id)) => { - Some(proto::participant_location::Variant::SharedProject( - proto::participant_location::SharedProject { - id: project_id.to_proto(), - }, - )) - } - (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject( - Default::default(), - )), - _ => Some(proto::participant_location::Variant::External( - Default::default(), - )), - }; - - let answering_connection = ConnectionId { - owner_id: answering_connection_server_id.0 as u32, - id: answering_connection_id as u32, - }; - participants.insert( - answering_connection, - proto::Participant { - user_id: db_participant.user_id.to_proto(), - peer_id: Some(answering_connection.into()), - projects: Default::default(), - location: Some(proto::ParticipantLocation { variant: location }), - }, - ); - } else { - pending_participants.push(proto::PendingParticipant { - user_id: db_participant.user_id.to_proto(), - calling_user_id: db_participant.calling_user_id.to_proto(), - initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()), - }); - } - } - drop(db_participants); - - let mut db_projects = db_room - .find_related(project::Entity) - .find_with_related(worktree::Entity) - .stream(tx) - .await?; - - while let Some(row) = db_projects.next().await { - let (db_project, db_worktree) = row?; - let host_connection = db_project.host_connection()?; - if let Some(participant) = participants.get_mut(&host_connection) { - let project = if let Some(project) = participant - .projects - .iter_mut() - .find(|project| project.id == db_project.id.to_proto()) - { - project - } else { - participant.projects.push(proto::ParticipantProject { - id: db_project.id.to_proto(), - worktree_root_names: Default::default(), - }); - participant.projects.last_mut().unwrap() - }; - - if let Some(db_worktree) = db_worktree { - if db_worktree.visible { - project.worktree_root_names.push(db_worktree.root_name); - } - } - } - } - drop(db_projects); - - let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; - let mut followers = Vec::new(); - while let Some(db_follower) = db_followers.next().await { - let db_follower = db_follower?; - followers.push(proto::Follower { - leader_id: Some(db_follower.leader_connection().into()), - follower_id: Some(db_follower.follower_connection().into()), - project_id: db_follower.project_id.to_proto(), - }); - } - - Ok(( - db_room.channel_id, - proto::Room { - id: db_room.id.to_proto(), - live_kit_room: db_room.live_kit_room, - participants: participants.into_values().collect(), - pending_participants, - followers, - }, - )) - } - - // projects - - pub async fn project_count_excluding_admins(&self) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - Count, - } - - self.transaction(|tx| async move { - Ok(project::Entity::find() - .select_only() - .column_as(project::Column::Id.count(), QueryAs::Count) - .inner_join(user::Entity) - .filter(user::Column::Admin.eq(false)) - .into_values::<_, QueryAs>() - .one(&*tx) - .await? - .unwrap_or(0i64) as usize) - }) - .await - } - - pub async fn share_project( - &self, - room_id: RoomId, - connection: ConnectionId, - worktrees: &[proto::WorktreeMetadata], - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("could not find participant"))?; - if participant.room_id != room_id { - return Err(anyhow!("shared project on unexpected room"))?; - } - - let project = project::ActiveModel { - room_id: ActiveValue::set(participant.room_id), - host_user_id: ActiveValue::set(participant.user_id), - host_connection_id: ActiveValue::set(Some(connection.id as i32)), - host_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..Default::default() - } - .insert(&*tx) - .await?; - - if !worktrees.is_empty() { - worktree::Entity::insert_many(worktrees.iter().map(|worktree| { - worktree::ActiveModel { - id: ActiveValue::set(worktree.id as i64), - project_id: ActiveValue::set(project.id), - abs_path: ActiveValue::set(worktree.abs_path.clone()), - root_name: ActiveValue::set(worktree.root_name.clone()), - visible: ActiveValue::set(worktree.visible), - scan_id: ActiveValue::set(0), - completed_scan_id: ActiveValue::set(0), - } - })) - .exec(&*tx) - .await?; - } - - project_collaborator::ActiveModel { - project_id: ActiveValue::set(project.id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(ReplicaId(0)), - is_host: ActiveValue::set(true), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok((project.id, room)) - }) - .await - } - - pub async fn unshare_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result)>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project not found"))?; - if project.host_connection()? == connection { - project::Entity::delete(project.into_active_model()) - .exec(&*tx) - .await?; - let room = self.get_room(room_id, &tx).await?; - Ok((room, guest_connection_ids)) - } else { - Err(anyhow!("cannot unshare a project hosted by another user"))? - } - }) - .await - } - - pub async fn update_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - worktrees: &[proto::WorktreeMetadata], - ) -> Result)>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let project = project::Entity::find_by_id(project_id) - .filter( - Condition::all() - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId.eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - - self.update_project_worktrees(project.id, worktrees, &tx) - .await?; - - let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; - let room = self.get_room(project.room_id, &tx).await?; - Ok((room, guest_connection_ids)) - }) - .await - } - - async fn update_project_worktrees( - &self, - project_id: ProjectId, - worktrees: &[proto::WorktreeMetadata], - tx: &DatabaseTransaction, - ) -> Result<()> { - if !worktrees.is_empty() { - worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel { - id: ActiveValue::set(worktree.id as i64), - project_id: ActiveValue::set(project_id), - abs_path: ActiveValue::set(worktree.abs_path.clone()), - root_name: ActiveValue::set(worktree.root_name.clone()), - visible: ActiveValue::set(worktree.visible), - scan_id: ActiveValue::set(0), - completed_scan_id: ActiveValue::set(0), - })) - .on_conflict( - OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id]) - .update_column(worktree::Column::RootName) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - worktree::Entity::delete_many() - .filter(worktree::Column::ProjectId.eq(project_id).and( - worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)), - )) - .exec(&*tx) - .await?; - - Ok(()) - } - - pub async fn update_worktree( - &self, - update: &proto::UpdateWorktree, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let worktree_id = update.worktree_id as i64; - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - // Ensure the update comes from the host. - let _project = project::Entity::find_by_id(project_id) - .filter( - Condition::all() - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId.eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - - // Update metadata. - worktree::Entity::update(worktree::ActiveModel { - id: ActiveValue::set(worktree_id), - project_id: ActiveValue::set(project_id), - root_name: ActiveValue::set(update.root_name.clone()), - scan_id: ActiveValue::set(update.scan_id as i64), - completed_scan_id: if update.is_last_update { - ActiveValue::set(update.scan_id as i64) - } else { - ActiveValue::default() - }, - abs_path: ActiveValue::set(update.abs_path.clone()), - ..Default::default() - }) - .exec(&*tx) - .await?; - - if !update.updated_entries.is_empty() { - worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| { - let mtime = entry.mtime.clone().unwrap_or_default(); - worktree_entry::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - id: ActiveValue::set(entry.id as i64), - is_dir: ActiveValue::set(entry.is_dir), - path: ActiveValue::set(entry.path.clone()), - inode: ActiveValue::set(entry.inode as i64), - mtime_seconds: ActiveValue::set(mtime.seconds as i64), - mtime_nanos: ActiveValue::set(mtime.nanos as i32), - is_symlink: ActiveValue::set(entry.is_symlink), - is_ignored: ActiveValue::set(entry.is_ignored), - is_external: ActiveValue::set(entry.is_external), - git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), - is_deleted: ActiveValue::set(false), - scan_id: ActiveValue::set(update.scan_id as i64), - } - })) - .on_conflict( - OnConflict::columns([ - worktree_entry::Column::ProjectId, - worktree_entry::Column::WorktreeId, - worktree_entry::Column::Id, - ]) - .update_columns([ - worktree_entry::Column::IsDir, - worktree_entry::Column::Path, - worktree_entry::Column::Inode, - worktree_entry::Column::MtimeSeconds, - worktree_entry::Column::MtimeNanos, - worktree_entry::Column::IsSymlink, - worktree_entry::Column::IsIgnored, - worktree_entry::Column::GitStatus, - worktree_entry::Column::ScanId, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - if !update.removed_entries.is_empty() { - worktree_entry::Entity::update_many() - .filter( - worktree_entry::Column::ProjectId - .eq(project_id) - .and(worktree_entry::Column::WorktreeId.eq(worktree_id)) - .and( - worktree_entry::Column::Id - .is_in(update.removed_entries.iter().map(|id| *id as i64)), - ), - ) - .set(worktree_entry::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - if !update.updated_repositories.is_empty() { - worktree_repository::Entity::insert_many(update.updated_repositories.iter().map( - |repository| worktree_repository::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - work_directory_id: ActiveValue::set(repository.work_directory_id as i64), - scan_id: ActiveValue::set(update.scan_id as i64), - branch: ActiveValue::set(repository.branch.clone()), - is_deleted: ActiveValue::set(false), - }, - )) - .on_conflict( - OnConflict::columns([ - worktree_repository::Column::ProjectId, - worktree_repository::Column::WorktreeId, - worktree_repository::Column::WorkDirectoryId, - ]) - .update_columns([ - worktree_repository::Column::ScanId, - worktree_repository::Column::Branch, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - if !update.removed_repositories.is_empty() { - worktree_repository::Entity::update_many() - .filter( - worktree_repository::Column::ProjectId - .eq(project_id) - .and(worktree_repository::Column::WorktreeId.eq(worktree_id)) - .and( - worktree_repository::Column::WorkDirectoryId - .is_in(update.removed_repositories.iter().map(|id| *id as i64)), - ), - ) - .set(worktree_repository::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn update_diagnostic_summary( - &self, - update: &proto::UpdateDiagnosticSummary, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let worktree_id = update.worktree_id as i64; - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let summary = update - .summary - .as_ref() - .ok_or_else(|| anyhow!("invalid summary"))?; - - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - // Update summary. - worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - path: ActiveValue::set(summary.path.clone()), - language_server_id: ActiveValue::set(summary.language_server_id as i64), - error_count: ActiveValue::set(summary.error_count as i32), - warning_count: ActiveValue::set(summary.warning_count as i32), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([ - worktree_diagnostic_summary::Column::ProjectId, - worktree_diagnostic_summary::Column::WorktreeId, - worktree_diagnostic_summary::Column::Path, - ]) - .update_columns([ - worktree_diagnostic_summary::Column::LanguageServerId, - worktree_diagnostic_summary::Column::ErrorCount, - worktree_diagnostic_summary::Column::WarningCount, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn start_language_server( - &self, - update: &proto::StartLanguageServer, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let server = update - .server - .as_ref() - .ok_or_else(|| anyhow!("invalid language server"))?; - - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - // Add the newly-started language server. - language_server::Entity::insert(language_server::ActiveModel { - project_id: ActiveValue::set(project_id), - id: ActiveValue::set(server.id as i64), - name: ActiveValue::set(server.name.clone()), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([ - language_server::Column::ProjectId, - language_server::Column::Id, - ]) - .update_column(language_server::Column::Name) - .to_owned(), - ) - .exec(&*tx) - .await?; - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn update_worktree_settings( - &self, - update: &proto::UpdateWorktreeSettings, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - if let Some(content) = &update.content { - worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { - project_id: ActiveValue::Set(project_id), - worktree_id: ActiveValue::Set(update.worktree_id as i64), - path: ActiveValue::Set(update.path.clone()), - content: ActiveValue::Set(content.clone()), - }) - .on_conflict( - OnConflict::columns([ - worktree_settings_file::Column::ProjectId, - worktree_settings_file::Column::WorktreeId, - worktree_settings_file::Column::Path, - ]) - .update_column(worktree_settings_file::Column::Content) - .to_owned(), - ) - .exec(&*tx) - .await?; - } else { - worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { - project_id: ActiveValue::Set(project_id), - worktree_id: ActiveValue::Set(update.worktree_id as i64), - path: ActiveValue::Set(update.path.clone()), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn join_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("must join a room first"))?; - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.room_id != participant.room_id { - return Err(anyhow!("no such project"))?; - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let replica_ids = collaborators - .iter() - .map(|c| c.replica_id) - .collect::>(); - let mut replica_id = ReplicaId(1); - while replica_ids.contains(&replica_id) { - replica_id.0 += 1; - } - let new_collaborator = project_collaborator::ActiveModel { - project_id: ActiveValue::set(project_id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(replica_id), - is_host: ActiveValue::set(false), - ..Default::default() - } - .insert(&*tx) - .await?; - collaborators.push(new_collaborator); - - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - let mut worktrees = db_worktrees - .into_iter() - .map(|db_worktree| { - ( - db_worktree.id as u64, - Worktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - entries: Default::default(), - repository_entries: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }, - ) - }) - .collect::>(); - - // Populate worktree entries. - { - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project_id)) - .add(worktree_entry::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { - worktree.entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Populate repository entries. - { - let mut db_repository_entries = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project_id)) - .add(worktree_repository::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_repository_entry) = db_repository_entries.next().await { - let db_repository_entry = db_repository_entry?; - if let Some(worktree) = - worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) - { - worktree.repository_entries.insert( - db_repository_entry.work_directory_id as u64, - proto::RepositoryEntry { - work_directory_id: db_repository_entry.work_directory_id as u64, - branch: db_repository_entry.branch, - }, - ); - } - } - } - - // Populate worktree diagnostic summaries. - { - let mut db_summaries = worktree_diagnostic_summary::Entity::find() - .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_summary) = db_summaries.next().await { - let db_summary = db_summary?; - if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { - worktree - .diagnostic_summaries - .push(proto::DiagnosticSummary { - path: db_summary.path, - language_server_id: db_summary.language_server_id as u64, - error_count: db_summary.error_count as u32, - warning_count: db_summary.warning_count as u32, - }); - } - } - } - - // Populate worktree settings files - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = - worktrees.get_mut(&(db_settings_file.worktree_id as u64)) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - // Populate language servers. - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await?; - - let project = Project { - collaborators: collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees, - language_servers: language_servers - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect(), - }; - Ok((project, replica_id as ReplicaId)) - }) - .await - } - - pub async fn leave_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let result = project_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add(project_collaborator::Column::ProjectId.eq(project_id)) - .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32)) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("not a collaborator on this project"))?; - } - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - let collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let connection_ids = collaborators - .into_iter() - .map(|collaborator| collaborator.connection()) - .collect(); - - follower::Entity::delete_many() - .filter( - Condition::any() - .add( - Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) - .add( - follower::Column::LeaderConnectionServerId - .eq(connection.owner_id), - ) - .add(follower::Column::LeaderConnectionId.eq(connection.id)), - ) - .add( - Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) - .add( - follower::Column::FollowerConnectionServerId - .eq(connection.owner_id), - ) - .add(follower::Column::FollowerConnectionId.eq(connection.id)), - ), - ) - .exec(&*tx) - .await?; - - let room = self.get_room(project.room_id, &tx).await?; - let left_project = LeftProject { - id: project_id, - host_user_id: project.host_user_id, - host_connection_id: project.host_connection()?, - connection_ids, - }; - Ok((room, left_project)) - }) - .await - } - - pub async fn project_collaborators( - &self, - project_id: ProjectId, - connection_id: ConnectionId, - ) -> Result>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .all(&*tx) - .await? - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect::>(); - - if collaborators - .iter() - .any(|collaborator| collaborator.connection_id == connection_id) - { - Ok(collaborators) - } else { - Err(anyhow!("no such project"))? - } - }) - .await - } - - pub async fn project_connection_ids( - &self, - project_id: ProjectId, - connection_id: ConnectionId, - ) -> Result>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let mut collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - - let mut connection_ids = HashSet::default(); - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - connection_ids.insert(collaborator.connection()); - } - - if connection_ids.contains(&connection_id) { - Ok(connection_ids) - } else { - Err(anyhow!("no such project"))? - } - }) - .await - } - - async fn project_guest_connection_ids( - &self, - project_id: ProjectId, - tx: &DatabaseTransaction, - ) -> Result> { - let mut collaborators = project_collaborator::Entity::find() - .filter( - project_collaborator::Column::ProjectId - .eq(project_id) - .and(project_collaborator::Column::IsHost.eq(false)), - ) - .stream(tx) - .await?; - - let mut guest_connection_ids = Vec::new(); - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - guest_connection_ids.push(collaborator.connection()); - } - Ok(guest_connection_ids) - } - - async fn room_id_for_project(&self, project_id: ProjectId) -> Result { - self.transaction(|tx| async move { - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project {} not found", project_id))?; - Ok(project.room_id) - }) - .await - } - - // access tokens - - pub async fn create_access_token( - &self, - user_id: UserId, - access_token_hash: &str, - max_access_token_count: usize, - ) -> Result { - self.transaction(|tx| async { - let tx = tx; - - let token = access_token::ActiveModel { - user_id: ActiveValue::set(user_id), - hash: ActiveValue::set(access_token_hash.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - - access_token::Entity::delete_many() - .filter( - access_token::Column::Id.in_subquery( - Query::select() - .column(access_token::Column::Id) - .from(access_token::Entity) - .and_where(access_token::Column::UserId.eq(user_id)) - .order_by(access_token::Column::Id, sea_orm::Order::Desc) - .limit(10000) - .offset(max_access_token_count as u64) - .to_owned(), - ), - ) - .exec(&*tx) - .await?; - Ok(token.id) - }) - .await - } - - pub async fn get_access_token( - &self, - access_token_id: AccessTokenId, - ) -> Result { - self.transaction(|tx| async move { - Ok(access_token::Entity::find_by_id(access_token_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such access token"))?) - }) - .await - } - - // channels - - pub async fn create_root_channel( - &self, - name: &str, - live_kit_room: &str, - creator_id: UserId, - ) -> Result { - self.create_channel(name, None, live_kit_room, creator_id) - .await - } - - pub async fn create_channel( - &self, - name: &str, - parent: Option, - live_kit_room: &str, - creator_id: UserId, - ) -> Result { - let name = Self::sanitize_channel_name(name)?; - self.transaction(move |tx| async move { - if let Some(parent) = parent { - self.check_user_is_channel_admin(parent, creator_id, &*tx) - .await?; - } - - let channel = channel::ActiveModel { - name: ActiveValue::Set(name.to_string()), - ..Default::default() - } - .insert(&*tx) - .await?; - - let channel_paths_stmt; - if let Some(parent) = parent { - let sql = r#" - INSERT INTO channel_paths - (id_path, channel_id) - SELECT - id_path || $1 || '/', $2 - FROM - channel_paths - WHERE - channel_id = $3 - "#; - channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [ - channel.id.to_proto().into(), - channel.id.to_proto().into(), - parent.to_proto().into(), - ], - ); - tx.execute(channel_paths_stmt).await?; - } else { - channel_path::Entity::insert(channel_path::ActiveModel { - channel_id: ActiveValue::Set(channel.id), - id_path: ActiveValue::Set(format!("/{}/", channel.id)), - }) - .exec(&*tx) - .await?; - } - - channel_member::ActiveModel { - channel_id: ActiveValue::Set(channel.id), - user_id: ActiveValue::Set(creator_id), - accepted: ActiveValue::Set(true), - admin: ActiveValue::Set(true), - ..Default::default() - } - .insert(&*tx) - .await?; - - room::ActiveModel { - channel_id: ActiveValue::Set(Some(channel.id)), - live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - ..Default::default() - } - .insert(&*tx) - .await?; - - Ok(channel.id) - }) - .await - } - - pub async fn remove_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result<(Vec, Vec)> { - self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) - .await?; - - // Don't remove descendant channels that have additional parents. - let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?; - { - let mut channels_to_keep = channel_path::Entity::find() - .filter( - channel_path::Column::ChannelId - .is_in( - channels_to_remove - .keys() - .copied() - .filter(|&id| id != channel_id), - ) - .and( - channel_path::Column::IdPath - .not_like(&format!("%/{}/%", channel_id)), - ), - ) - .stream(&*tx) - .await?; - while let Some(row) = channels_to_keep.next().await { - let row = row?; - channels_to_remove.remove(&row.channel_id); - } - } - - let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?; - let members_to_notify: Vec = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel_ancestors)) - .select_only() - .column(channel_member::Column::UserId) - .distinct() - .into_values::<_, QueryUserIds>() - .all(&*tx) - .await?; - - channel::Entity::delete_many() - .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied())) - .exec(&*tx) - .await?; - - Ok((channels_to_remove.into_keys().collect(), members_to_notify)) - }) - .await - } - - pub async fn invite_channel_member( - &self, - channel_id: ChannelId, - invitee_id: UserId, - inviter_id: UserId, - is_admin: bool, - ) -> Result<()> { - self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) - .await?; - - channel_member::ActiveModel { - channel_id: ActiveValue::Set(channel_id), - user_id: ActiveValue::Set(invitee_id), - accepted: ActiveValue::Set(false), - admin: ActiveValue::Set(is_admin), - ..Default::default() - } - .insert(&*tx) - .await?; - - Ok(()) - }) - .await - } - - fn sanitize_channel_name(name: &str) -> Result<&str> { - let new_name = name.trim().trim_start_matches('#'); - if new_name == "" { - Err(anyhow!("channel name can't be blank"))?; - } - Ok(new_name) - } - - pub async fn rename_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - new_name: &str, - ) -> Result { - self.transaction(move |tx| async move { - let new_name = Self::sanitize_channel_name(new_name)?.to_string(); - - self.check_user_is_channel_admin(channel_id, user_id, &*tx) - .await?; - - channel::ActiveModel { - id: ActiveValue::Unchanged(channel_id), - name: ActiveValue::Set(new_name.clone()), - ..Default::default() - } - .update(&*tx) - .await?; - - Ok(new_name) - }) - .await - } - - pub async fn respond_to_channel_invite( - &self, - channel_id: ChannelId, - user_id: UserId, - accept: bool, - ) -> Result<()> { - self.transaction(move |tx| async move { - let rows_affected = if accept { - channel_member::Entity::update_many() - .set(channel_member::ActiveModel { - accepted: ActiveValue::Set(accept), - ..Default::default() - }) - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Accepted.eq(false)), - ) - .exec(&*tx) - .await? - .rows_affected - } else { - channel_member::ActiveModel { - channel_id: ActiveValue::Unchanged(channel_id), - user_id: ActiveValue::Unchanged(user_id), - ..Default::default() - } - .delete(&*tx) - .await? - .rows_affected - }; - - if rows_affected == 0 { - Err(anyhow!("no such invitation"))?; - } - - Ok(()) - }) - .await - } - - pub async fn remove_channel_member( - &self, - channel_id: ChannelId, - member_id: UserId, - remover_id: UserId, - ) -> Result<()> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, remover_id, &*tx) - .await?; - - let result = channel_member::Entity::delete_many() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(member_id)), - ) - .exec(&*tx) - .await?; - - if result.rows_affected == 0 { - Err(anyhow!("no such member"))?; - } - - Ok(()) - }) - .await - } - - pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { - self.transaction(|tx| async move { - let channel_invites = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(false)), - ) - .all(&*tx) - .await?; - - let channels = channel::Entity::find() - .filter( - channel::Column::Id.is_in( - channel_invites - .into_iter() - .map(|channel_member| channel_member.channel_id), - ), - ) - .all(&*tx) - .await?; - - let channels = channels - .into_iter() - .map(|channel| Channel { - id: channel.id, - name: channel.name, - parent_id: None, - }) - .collect(); - - Ok(channels) - }) - .await - } - - pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let tx = tx; - - let channel_memberships = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(true)), - ) - .all(&*tx) - .await?; - - let parents_by_child_id = self - .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) - .await?; - - let channels_with_admin_privileges = channel_memberships - .iter() - .filter_map(|membership| membership.admin.then_some(membership.channel_id)) - .collect(); - - let mut channels = Vec::with_capacity(parents_by_child_id.len()); - { - let mut rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied())) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - channels.push(Channel { - id: row.id, - name: row.name, - parent_id: parents_by_child_id.get(&row.id).copied().flatten(), - }); - } - } - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryUserIdsAndChannelIds { - ChannelId, - UserId, - } - - let mut channel_participants: HashMap> = HashMap::default(); - { - let mut rows = room_participant::Entity::find() - .inner_join(room::Entity) - .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) - .select_only() - .column(room::Column::ChannelId) - .column(room_participant::Column::UserId) - .into_values::<_, QueryUserIdsAndChannelIds>() - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row: (ChannelId, UserId) = row?; - channel_participants.entry(row.0).or_default().push(row.1) - } - } - - Ok(ChannelsForUser { - channels, - channel_participants, - channels_with_admin_privileges, - }) - }) - .await - } +#[cfg(test)] +pub use tests::TestDb; - pub async fn get_channel_members(&self, id: ChannelId) -> Result> { - self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await }) - .await - } +mod ids; +mod queries; +mod tables; - pub async fn set_channel_member_admin( - &self, - channel_id: ChannelId, - from: UserId, - for_user: UserId, - admin: bool, - ) -> Result<()> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, from, &*tx) - .await?; +use crate::{executor::Executor, Error, Result}; +use anyhow::anyhow; +use collections::{BTreeMap, HashMap, HashSet}; +use dashmap::DashMap; +use futures::StreamExt; +use rand::{prelude::StdRng, Rng, SeedableRng}; +use rpc::{proto, ConnectionId}; +use sea_orm::{ + entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, + DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, + QueryOrder, QuerySelect, Statement, TransactionTrait, +}; +use sea_query::{Alias, Expr, OnConflict, Query}; +use serde::{Deserialize, Serialize}; +use sqlx::{ + migrate::{Migrate, Migration, MigrationSource}, + Connection, +}; +use std::{ + fmt::Write as _, + future::Future, + marker::PhantomData, + ops::{Deref, DerefMut}, + path::Path, + rc::Rc, + sync::Arc, + time::Duration, +}; +use tables::*; +use tokio::sync::{Mutex, OwnedMutexGuard}; - let result = channel_member::Entity::update_many() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(for_user)), - ) - .set(channel_member::ActiveModel { - admin: ActiveValue::set(admin), - ..Default::default() - }) - .exec(&*tx) - .await?; +pub use ids::*; +pub use sea_orm::ConnectOptions; +pub use tables::user::Model as User; - if result.rows_affected == 0 { - Err(anyhow!("no such member"))?; - } +pub struct Database { + options: ConnectOptions, + pool: DatabaseConnection, + rooms: DashMap>>, + rng: Mutex, + executor: Executor, + #[cfg(test)] + runtime: Option, +} - Ok(()) +// The `Database` type has so many methods that its impl blocks are split into +// separate files in the `queries` folder. +impl Database { + pub async fn new(options: ConnectOptions, executor: Executor) -> Result { + Ok(Self { + options: options.clone(), + pool: sea_orm::Database::connect(options).await?, + rooms: DashMap::with_capacity(16384), + rng: Mutex::new(StdRng::seed_from_u64(0)), + executor, + #[cfg(test)] + runtime: None, }) - .await } - pub async fn get_channel_member_details( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) - .await?; - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryMemberDetails { - UserId, - Admin, - IsDirectMember, - Accepted, - } - - let tx = tx; - let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; - let mut stream = channel_member::Entity::find() - .distinct() - .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) - .select_only() - .column(channel_member::Column::UserId) - .column(channel_member::Column::Admin) - .column_as( - channel_member::Column::ChannelId.eq(channel_id), - QueryMemberDetails::IsDirectMember, - ) - .column(channel_member::Column::Accepted) - .order_by_asc(channel_member::Column::UserId) - .into_values::<_, QueryMemberDetails>() - .stream(&*tx) - .await?; - - let mut rows = Vec::::new(); - while let Some(row) = stream.next().await { - let (user_id, is_admin, is_direct_member, is_invite_accepted): ( - UserId, - bool, - bool, - bool, - ) = row?; - let kind = match (is_direct_member, is_invite_accepted) { - (true, true) => proto::channel_member::Kind::Member, - (true, false) => proto::channel_member::Kind::Invitee, - (false, true) => proto::channel_member::Kind::AncestorMember, - (false, false) => continue, - }; - let user_id = user_id.to_proto(); - let kind = kind.into(); - if let Some(last_row) = rows.last_mut() { - if last_row.user_id == user_id { - if is_direct_member { - last_row.kind = kind; - last_row.admin = is_admin; - } - continue; - } - } - rows.push(proto::ChannelMember { - user_id, - kind, - admin: is_admin, - }); - } - - Ok(rows) - }) - .await + #[cfg(test)] + pub fn reset(&self) { + self.rooms.clear(); } - pub async fn get_channel_members_internal( + pub async fn migrate( &self, - id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let ancestor_ids = self.get_channel_ancestors(id, tx).await?; - let user_ids = channel_member::Entity::find() - .distinct() - .filter( - channel_member::Column::ChannelId - .is_in(ancestor_ids.iter().copied()) - .and(channel_member::Column::Accepted.eq(true)), - ) - .select_only() - .column(channel_member::Column::UserId) - .into_values::<_, QueryUserIds>() - .all(&*tx) - .await?; - Ok(user_ids) - } + migrations_path: &Path, + ignore_checksum_mismatch: bool, + ) -> anyhow::Result> { + let migrations = MigrationSource::resolve(migrations_path) + .await + .map_err(|err| anyhow!("failed to load migrations: {err:?}"))?; - async fn check_user_is_channel_member( - &self, - channel_id: ChannelId, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result<()> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .is_in(channel_ids) - .and(channel_member::Column::UserId.eq(user_id)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?; - Ok(()) - } + let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?; - async fn check_user_is_channel_admin( - &self, - channel_id: ChannelId, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result<()> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .is_in(channel_ids) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Admin.eq(true)), - ) - .one(&*tx) + connection.ensure_migrations_table().await?; + let applied_migrations: HashMap<_, _> = connection + .list_applied_migrations() .await? - .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?; - Ok(()) - } + .into_iter() + .map(|m| (m.version, m)) + .collect(); - async fn get_channel_ancestors( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let paths = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel_id)) - .all(tx) - .await?; - let mut channel_ids = Vec::new(); - for path in paths { - for id in path.id_path.trim_matches('/').split('/') { - if let Ok(id) = id.parse() { - let id = ChannelId::from_proto(id); - if let Err(ix) = channel_ids.binary_search(&id) { - channel_ids.insert(ix, id); + let mut new_migrations = Vec::new(); + for migration in migrations { + match applied_migrations.get(&migration.version) { + Some(applied_migration) => { + if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch + { + Err(anyhow!( + "checksum mismatch for applied migration {}", + migration.description + ))?; } } - } - } - Ok(channel_ids) - } - - async fn get_channel_descendants( - &self, - channel_ids: impl IntoIterator, - tx: &DatabaseTransaction, - ) -> Result>> { - let mut values = String::new(); - for id in channel_ids { - if !values.is_empty() { - values.push_str(", "); - } - write!(&mut values, "({})", id).unwrap(); - } - - if values.is_empty() { - return Ok(HashMap::default()); - } - - let sql = format!( - r#" - SELECT - descendant_paths.* - FROM - channel_paths parent_paths, channel_paths descendant_paths - WHERE - parent_paths.channel_id IN ({values}) AND - descendant_paths.id_path LIKE (parent_paths.id_path || '%') - "# - ); - - let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - - let mut parents_by_child_id = HashMap::default(); - let mut paths = channel_path::Entity::find() - .from_raw_sql(stmt) - .stream(tx) - .await?; - - while let Some(path) = paths.next().await { - let path = path?; - let ids = path.id_path.trim_matches('/').split('/'); - let mut parent_id = None; - for id in ids { - if let Ok(id) = id.parse() { - let id = ChannelId::from_proto(id); - if id == path.channel_id { - break; - } - parent_id = Some(id); + None => { + let elapsed = connection.apply(&migration).await?; + new_migrations.push((migration, elapsed)); } } - parents_by_child_id.insert(path.channel_id, parent_id); } - Ok(parents_by_child_id) - } - - /// Returns the channel with the given ID and: - /// - true if the user is a member - /// - false if the user hasn't accepted the invitation yet - pub async fn get_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - let tx = tx; - - let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; - - if let Some(channel) = channel { - if self - .check_user_is_channel_member(channel_id, user_id, &*tx) - .await - .is_err() - { - return Ok(None); - } - - let channel_membership = channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)), - ) - .one(&*tx) - .await?; - - let is_accepted = channel_membership - .map(|membership| membership.accepted) - .unwrap_or(false); - - Ok(Some(( - Channel { - id: channel.id, - name: channel.name, - parent_id: None, - }, - is_accepted, - ))) - } else { - Ok(None) - } - }) - .await - } - - pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result { - self.transaction(|tx| async move { - let tx = tx; - let room = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(room::Entity) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid channel"))?; - Ok(room.id) - }) - .await + Ok(new_migrations) } async fn transaction(&self, f: F) -> Result @@ -4083,6 +350,60 @@ impl RoomGuard { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Contact { + Accepted { + user_id: UserId, + should_notify: bool, + busy: bool, + }, + Outgoing { + user_id: UserId, + }, + Incoming { + user_id: UserId, + should_notify: bool, + }, +} + +impl Contact { + pub fn user_id(&self) -> UserId { + match self { + Contact::Accepted { user_id, .. } => *user_id, + Contact::Outgoing { user_id } => *user_id, + Contact::Incoming { user_id, .. } => *user_id, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] +pub struct Invite { + pub email_address: String, + pub email_confirmation_code: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct NewSignup { + pub email_address: String, + pub platform_mac: bool, + pub platform_windows: bool, + pub platform_linux: bool, + pub editor_features: Vec, + pub programming_languages: Vec, + pub device_id: Option, + pub added_to_mailing_list: bool, + pub created_at: Option, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)] +pub struct WaitlistSummary { + pub count: i64, + pub linux_count: i64, + pub mac_count: i64, + pub windows_count: i64, + pub unknown_count: i64, +} + #[derive(Debug, Serialize, Deserialize)] pub struct NewUserParams { pub github_login: String, @@ -4112,147 +433,6 @@ pub struct ChannelsForUser { pub channels_with_admin_privileges: HashSet, } -fn random_invite_code() -> String { - nanoid::nanoid!(16) -} - -fn random_email_confirmation_code() -> String { - nanoid::nanoid!(64) -} - -macro_rules! id_type { - ($name:ident) => { - #[derive( - Clone, - Copy, - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - )] - #[serde(transparent)] - pub struct $name(pub i32); - - impl $name { - #[allow(unused)] - pub const MAX: Self = Self(i32::MAX); - - #[allow(unused)] - pub fn from_proto(value: u64) -> Self { - Self(value as i32) - } - - #[allow(unused)] - pub fn to_proto(self) -> u64 { - self.0 as u64 - } - } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - self.0.fmt(f) - } - } - - impl From<$name> for sea_query::Value { - fn from(value: $name) -> Self { - sea_query::Value::Int(Some(value.0)) - } - } - - impl sea_orm::TryGetable for $name { - fn try_get( - res: &sea_orm::QueryResult, - pre: &str, - col: &str, - ) -> Result { - Ok(Self(i32::try_get(res, pre, col)?)) - } - } - - impl sea_query::ValueType for $name { - fn try_from(v: Value) -> Result { - match v { - Value::TinyInt(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::SmallInt(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::Int(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::BigInt(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::TinyUnsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::SmallUnsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::Unsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::BigUnsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - _ => Err(sea_query::ValueTypeErr), - } - } - - fn type_name() -> String { - stringify!($name).into() - } - - fn array_type() -> sea_query::ArrayType { - sea_query::ArrayType::Int - } - - fn column_type() -> sea_query::ColumnType { - sea_query::ColumnType::Integer(None) - } - } - - impl sea_orm::TryFromU64 for $name { - fn try_from_u64(n: u64) -> Result { - Ok(Self(n.try_into().map_err(|_| { - DbErr::ConvertFromU64(concat!( - "error converting ", - stringify!($name), - " to u64" - )) - })?)) - } - } - - impl sea_query::Nullable for $name { - fn null() -> Value { - Value::Int(None) - } - } - }; -} - -id_type!(AccessTokenId); -id_type!(ChannelId); -id_type!(ChannelMemberId); -id_type!(ContactId); -id_type!(FollowerId); -id_type!(RoomId); -id_type!(RoomParticipantId); -id_type!(ProjectId); -id_type!(ProjectCollaboratorId); -id_type!(ReplicaId); -id_type!(ServerId); -id_type!(SignupId); -id_type!(UserId); - #[derive(Clone)] pub struct JoinRoom { pub room: proto::Room, @@ -4365,135 +545,3 @@ pub struct WorktreeSettingsFile { pub path: String, pub content: String, } - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -enum QueryUserIds { - UserId, -} - -#[cfg(test)] -pub use test::*; - -#[cfg(test)] -mod test { - use super::*; - use gpui::executor::Background; - use parking_lot::Mutex; - use sea_orm::ConnectionTrait; - use sqlx::migrate::MigrateDatabase; - use std::sync::Arc; - - pub struct TestDb { - pub db: Option>, - pub connection: Option, - } - - impl TestDb { - pub fn sqlite(background: Arc) -> Self { - let url = format!("sqlite::memory:"); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - let mut db = runtime.block_on(async { - let mut options = ConnectOptions::new(url); - options.max_connections(5); - let db = Database::new(options, Executor::Deterministic(background)) - .await - .unwrap(); - let sql = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/migrations.sqlite/20221109000000_test_schema.sql" - )); - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - sql.into(), - )) - .await - .unwrap(); - db - }); - - db.runtime = Some(runtime); - - Self { - db: Some(Arc::new(db)), - connection: None, - } - } - - pub fn postgres(background: Arc) -> Self { - static LOCK: Mutex<()> = Mutex::new(()); - - let _guard = LOCK.lock(); - let mut rng = StdRng::from_entropy(); - let url = format!( - "postgres://postgres@localhost/zed-test-{}", - rng.gen::() - ); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - let mut db = runtime.block_on(async { - sqlx::Postgres::create_database(&url) - .await - .expect("failed to create test db"); - let mut options = ConnectOptions::new(url); - options - .max_connections(5) - .idle_timeout(Duration::from_secs(0)); - let db = Database::new(options, Executor::Deterministic(background)) - .await - .unwrap(); - let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"); - db.migrate(Path::new(migrations_path), false).await.unwrap(); - db - }); - - db.runtime = Some(runtime); - - Self { - db: Some(Arc::new(db)), - connection: None, - } - } - - pub fn db(&self) -> &Arc { - self.db.as_ref().unwrap() - } - } - - impl Drop for TestDb { - fn drop(&mut self) { - let db = self.db.take().unwrap(); - if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() { - db.runtime.as_ref().unwrap().block_on(async { - use util::ResultExt; - let query = " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE - pg_stat_activity.datname = current_database() AND - pid <> pg_backend_pid(); - "; - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - query.into(), - )) - .await - .log_err(); - sqlx::Postgres::drop_database(db.options.get_url()) - .await - .log_err(); - }) - } - } - } -} diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs new file mode 100644 index 0000000000000000000000000000000000000000..8501083f839940ed9723813b9aac8a029d706a0d --- /dev/null +++ b/crates/collab/src/db/ids.rs @@ -0,0 +1,127 @@ +use crate::Result; +use sea_orm::DbErr; +use sea_query::{Value, ValueTypeErr}; +use serde::{Deserialize, Serialize}; + +macro_rules! id_type { + ($name:ident) => { + #[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + )] + #[serde(transparent)] + pub struct $name(pub i32); + + impl $name { + #[allow(unused)] + pub const MAX: Self = Self(i32::MAX); + + #[allow(unused)] + pub fn from_proto(value: u64) -> Self { + Self(value as i32) + } + + #[allow(unused)] + pub fn to_proto(self) -> u64 { + self.0 as u64 + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl From<$name> for sea_query::Value { + fn from(value: $name) -> Self { + sea_query::Value::Int(Some(value.0)) + } + } + + impl sea_orm::TryGetable for $name { + fn try_get( + res: &sea_orm::QueryResult, + pre: &str, + col: &str, + ) -> Result { + Ok(Self(i32::try_get(res, pre, col)?)) + } + } + + impl sea_query::ValueType for $name { + fn try_from(v: Value) -> Result { + Ok(Self(value_to_integer(v)?)) + } + + fn type_name() -> String { + stringify!($name).into() + } + + fn array_type() -> sea_query::ArrayType { + sea_query::ArrayType::Int + } + + fn column_type() -> sea_query::ColumnType { + sea_query::ColumnType::Integer(None) + } + } + + impl sea_orm::TryFromU64 for $name { + fn try_from_u64(n: u64) -> Result { + Ok(Self(n.try_into().map_err(|_| { + DbErr::ConvertFromU64(concat!( + "error converting ", + stringify!($name), + " to u64" + )) + })?)) + } + } + + impl sea_query::Nullable for $name { + fn null() -> Value { + Value::Int(None) + } + } + }; +} + +fn value_to_integer(v: Value) -> Result { + match v { + Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + _ => Err(ValueTypeErr), + } +} + +id_type!(BufferId); +id_type!(AccessTokenId); +id_type!(ChannelId); +id_type!(ChannelMemberId); +id_type!(ContactId); +id_type!(FollowerId); +id_type!(RoomId); +id_type!(RoomParticipantId); +id_type!(ProjectId); +id_type!(ProjectCollaboratorId); +id_type!(ReplicaId); +id_type!(ServerId); +id_type!(SignupId); +id_type!(UserId); +id_type!(ChannelBufferCollaboratorId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs new file mode 100644 index 0000000000000000000000000000000000000000..09a8f073b469f72773a0220750f5d65cf85629af --- /dev/null +++ b/crates/collab/src/db/queries.rs @@ -0,0 +1,11 @@ +use super::*; + +pub mod access_tokens; +pub mod buffers; +pub mod channels; +pub mod contacts; +pub mod projects; +pub mod rooms; +pub mod servers; +pub mod signups; +pub mod users; diff --git a/crates/collab/src/db/queries/access_tokens.rs b/crates/collab/src/db/queries/access_tokens.rs new file mode 100644 index 0000000000000000000000000000000000000000..def9428a2bedc0a8635364bc48aeb6fe419a2f11 --- /dev/null +++ b/crates/collab/src/db/queries/access_tokens.rs @@ -0,0 +1,53 @@ +use super::*; + +impl Database { + pub async fn create_access_token( + &self, + user_id: UserId, + access_token_hash: &str, + max_access_token_count: usize, + ) -> Result { + self.transaction(|tx| async { + let tx = tx; + + let token = access_token::ActiveModel { + user_id: ActiveValue::set(user_id), + hash: ActiveValue::set(access_token_hash.into()), + ..Default::default() + } + .insert(&*tx) + .await?; + + access_token::Entity::delete_many() + .filter( + access_token::Column::Id.in_subquery( + Query::select() + .column(access_token::Column::Id) + .from(access_token::Entity) + .and_where(access_token::Column::UserId.eq(user_id)) + .order_by(access_token::Column::Id, sea_orm::Order::Desc) + .limit(10000) + .offset(max_access_token_count as u64) + .to_owned(), + ), + ) + .exec(&*tx) + .await?; + Ok(token.id) + }) + .await + } + + pub async fn get_access_token( + &self, + access_token_id: AccessTokenId, + ) -> Result { + self.transaction(|tx| async move { + Ok(access_token::Entity::find_by_id(access_token_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such access token"))?) + }) + .await + } +} diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs new file mode 100644 index 0000000000000000000000000000000000000000..354accc01a237057a6ea3bbeb9b4c1986b4ea391 --- /dev/null +++ b/crates/collab/src/db/queries/buffers.rs @@ -0,0 +1,588 @@ +use super::*; +use prost::Message; +use text::{EditOperation, InsertionTimestamp, UndoOperation}; + +impl Database { + pub async fn join_channel_buffer( + &self, + channel_id: ChannelId, + user_id: UserId, + connection: ConnectionId, + ) -> Result { + self.transaction(|tx| async move { + let tx = tx; + + self.check_user_is_channel_member(channel_id, user_id, &tx) + .await?; + + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await?; + + let buffer = if let Some(buffer) = buffer { + buffer + } else { + let buffer = buffer::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + ..Default::default() + } + .insert(&*tx) + .await?; + buffer_snapshot::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(0), + text: ActiveValue::Set(String::new()), + operation_serialization_version: ActiveValue::Set( + storage::SERIALIZATION_VERSION, + ), + } + .insert(&*tx) + .await?; + buffer + }; + + // Join the collaborators + let mut collaborators = channel_buffer_collaborator::Entity::find() + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(0); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + let collaborator = channel_buffer_collaborator::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + connection_id: ActiveValue::Set(connection.id as i32), + connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::Set(user_id), + replica_id: ActiveValue::Set(replica_id), + ..Default::default() + } + .insert(&*tx) + .await?; + collaborators.push(collaborator); + + // Assemble the buffer state + let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?; + + Ok(proto::JoinChannelBufferResponse { + buffer_id: buffer.id.to_proto(), + replica_id: replica_id.to_proto() as u32, + base_text, + operations, + collaborators: collaborators + .into_iter() + .map(|collaborator| proto::Collaborator { + peer_id: Some(collaborator.connection().into()), + user_id: collaborator.user_id.to_proto(), + replica_id: collaborator.replica_id.0 as u32, + }) + .collect(), + }) + }) + .await + } + + pub async fn leave_channel_buffer( + &self, + channel_id: ChannelId, + connection: ConnectionId, + ) -> Result> { + self.transaction(|tx| async move { + self.leave_channel_buffer_internal(channel_id, connection, &*tx) + .await + }) + .await + } + + pub async fn leave_channel_buffer_internal( + &self, + channel_id: ChannelId, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result> { + let result = channel_buffer_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + drop(rows); + + if connections.is_empty() { + self.snapshot_buffer(channel_id, &tx).await?; + } + + Ok(connections) + } + + pub async fn leave_channel_buffers( + &self, + connection: ConnectionId, + ) -> Result)>> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, + } + + let channel_ids: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::ChannelId) + .filter(Condition::all().add( + channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32), + )) + .into_values::<_, QueryChannelIds>() + .all(&*tx) + .await?; + + let mut result = Vec::new(); + for channel_id in channel_ids { + let collaborators = self + .leave_channel_buffer_internal(channel_id, connection, &*tx) + .await?; + result.push((channel_id, collaborators)); + } + + Ok(result) + }) + .await + } + + #[cfg(debug_assertions)] + pub async fn get_channel_buffer_collaborators( + &self, + channel_id: ChannelId, + ) -> Result> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryUserIds { + UserId, + } + + let users: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::UserId) + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + Ok(users) + }) + .await + } + + pub async fn update_channel_buffer( + &self, + channel_id: ChannelId, + user: UserId, + operations: &[proto::Operation], + ) -> Result> { + self.transaction(move |tx| async move { + self.check_user_is_channel_member(channel_id, user, &*tx) + .await?; + + let buffer = buffer::Entity::find() + .filter(buffer::Column::ChannelId.eq(channel_id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryVersion { + OperationSerializationVersion, + } + + let serialization_version: i32 = buffer + .find_related(buffer_snapshot::Entity) + .select_only() + .column(buffer_snapshot::Column::OperationSerializationVersion) + .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch)) + .into_values::<_, QueryVersion>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("missing buffer snapshot"))?; + + let operations = operations + .iter() + .filter_map(|op| operation_to_storage(op, &buffer, serialization_version)) + .collect::>(); + if !operations.is_empty() { + buffer_operation::Entity::insert_many(operations) + .exec(&*tx) + .await?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) + }) + .await + } + + async fn get_buffer_state( + &self, + buffer: &buffer::Model, + tx: &DatabaseTransaction, + ) -> Result<(String, Vec)> { + let id = buffer.id; + let (base_text, version) = if buffer.epoch > 0 { + let snapshot = buffer_snapshot::Entity::find() + .filter( + buffer_snapshot::Column::BufferId + .eq(id) + .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such snapshot"))?; + + let version = snapshot.operation_serialization_version; + (snapshot.text, version) + } else { + (String::new(), storage::SERIALIZATION_VERSION) + }; + + let mut rows = buffer_operation::Entity::find() + .filter( + buffer_operation::Column::BufferId + .eq(id) + .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), + ) + .stream(&*tx) + .await?; + let mut operations = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + + let operation = operation_from_storage(row, version)?; + operations.push(proto::Operation { + variant: Some(operation), + }) + } + + Ok((base_text, operations)) + } + + async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> { + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + if operations.is_empty() { + return Ok(()); + } + + let mut text_buffer = text::Buffer::new(0, 0, base_text); + text_buffer + .apply_ops(operations.into_iter().filter_map(operation_from_wire)) + .unwrap(); + + let base_text = text_buffer.text(); + let epoch = buffer.epoch + 1; + + buffer_snapshot::Model { + buffer_id: buffer.id, + epoch, + text: base_text, + operation_serialization_version: storage::SERIALIZATION_VERSION, + } + .into_active_model() + .insert(tx) + .await?; + + buffer::ActiveModel { + id: ActiveValue::Unchanged(buffer.id), + epoch: ActiveValue::Set(epoch), + ..Default::default() + } + .save(tx) + .await?; + + Ok(()) + } +} + +fn operation_to_storage( + operation: &proto::Operation, + buffer: &buffer::Model, + _format: i32, +) -> Option { + let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: false, + edit_ranges: operation + .ranges + .iter() + .map(|range| storage::Range { + start: range.start, + end: range.end, + }) + .collect(), + edit_texts: operation.new_text.clone(), + undo_counts: Vec::new(), + }, + ), + proto::operation::Variant::Undo(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: true, + edit_ranges: Vec::new(), + edit_texts: Vec::new(), + undo_counts: operation + .counts + .iter() + .map(|entry| storage::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + }, + ), + _ => None?, + }; + + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(replica_id as i32), + lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32), + value: ActiveValue::Set(value.encode_to_vec()), + }) +} + +fn operation_from_storage( + row: buffer_operation::Model, + _format_version: i32, +) -> Result { + let operation = + storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?; + let version = version_from_storage(&operation.version); + Ok(if operation.is_undo { + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: row.replica_id as u32, + local_timestamp: operation.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + counts: operation + .undo_counts + .iter() + .map(|entry| proto::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + }) + } else { + proto::operation::Variant::Edit(proto::operation::Edit { + replica_id: row.replica_id as u32, + local_timestamp: operation.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + ranges: operation + .edit_ranges + .into_iter() + .map(|range| proto::Range { + start: range.start, + end: range.end, + }) + .collect(), + new_text: operation.edit_texts, + }) + }) +} + +fn version_to_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| storage::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() +} + +fn version_from_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| proto::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() +} + +// This is currently a manual copy of the deserialization code in the client's langauge crate +pub fn operation_from_wire(operation: proto::Operation) -> Option { + match operation.variant? { + proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { + timestamp: InsertionTimestamp { + replica_id: edit.replica_id as text::ReplicaId, + local: edit.local_timestamp, + lamport: edit.lamport_timestamp, + }, + version: version_from_wire(&edit.version), + ranges: edit + .ranges + .into_iter() + .map(|range| { + text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize) + }) + .collect(), + new_text: edit.new_text.into_iter().map(Arc::from).collect(), + })), + proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo { + lamport_timestamp: clock::Lamport { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.lamport_timestamp, + }, + undo: UndoOperation { + id: clock::Local { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.local_timestamp, + }, + version: version_from_wire(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Local { + replica_id: c.replica_id as text::ReplicaId, + value: c.local_timestamp, + }, + c.count, + ) + }) + .collect(), + }, + }), + _ => None, + } +} + +fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global { + let mut version = clock::Global::new(); + for entry in message { + version.observe(clock::Local { + replica_id: entry.replica_id as text::ReplicaId, + value: entry.timestamp, + }); + } + version +} + +mod storage { + #![allow(non_snake_case)] + use prost::Message; + pub const SERIALIZATION_VERSION: i32 = 1; + + #[derive(Message)] + pub struct Operation { + #[prost(uint32, tag = "1")] + pub local_timestamp: u32, + #[prost(message, repeated, tag = "2")] + pub version: Vec, + #[prost(bool, tag = "3")] + pub is_undo: bool, + #[prost(message, repeated, tag = "4")] + pub edit_ranges: Vec, + #[prost(string, repeated, tag = "5")] + pub edit_texts: Vec, + #[prost(message, repeated, tag = "6")] + pub undo_counts: Vec, + } + + #[derive(Message)] + pub struct VectorClockEntry { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub timestamp: u32, + } + + #[derive(Message)] + pub struct Range { + #[prost(uint64, tag = "1")] + pub start: u64, + #[prost(uint64, tag = "2")] + pub end: u64, + } + + #[derive(Message)] + pub struct UndoCount { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub local_timestamp: u32, + #[prost(uint32, tag = "3")] + pub count: u32, + } +} diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs new file mode 100644 index 0000000000000000000000000000000000000000..e3d3643a61f3b1fb1b498db9ef6ae36d1a1aa5ce --- /dev/null +++ b/crates/collab/src/db/queries/channels.rs @@ -0,0 +1,697 @@ +use super::*; + +impl Database { + pub async fn create_root_channel( + &self, + name: &str, + live_kit_room: &str, + creator_id: UserId, + ) -> Result { + self.create_channel(name, None, live_kit_room, creator_id) + .await + } + + pub async fn create_channel( + &self, + name: &str, + parent: Option, + live_kit_room: &str, + creator_id: UserId, + ) -> Result { + let name = Self::sanitize_channel_name(name)?; + self.transaction(move |tx| async move { + if let Some(parent) = parent { + self.check_user_is_channel_admin(parent, creator_id, &*tx) + .await?; + } + + let channel = channel::ActiveModel { + name: ActiveValue::Set(name.to_string()), + ..Default::default() + } + .insert(&*tx) + .await?; + + let channel_paths_stmt; + if let Some(parent) = parent { + let sql = r#" + INSERT INTO channel_paths + (id_path, channel_id) + SELECT + id_path || $1 || '/', $2 + FROM + channel_paths + WHERE + channel_id = $3 + "#; + channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + channel.id.to_proto().into(), + channel.id.to_proto().into(), + parent.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + } else { + channel_path::Entity::insert(channel_path::ActiveModel { + channel_id: ActiveValue::Set(channel.id), + id_path: ActiveValue::Set(format!("/{}/", channel.id)), + }) + .exec(&*tx) + .await?; + } + + channel_member::ActiveModel { + channel_id: ActiveValue::Set(channel.id), + user_id: ActiveValue::Set(creator_id), + accepted: ActiveValue::Set(true), + admin: ActiveValue::Set(true), + ..Default::default() + } + .insert(&*tx) + .await?; + + room::ActiveModel { + channel_id: ActiveValue::Set(Some(channel.id)), + live_kit_room: ActiveValue::Set(live_kit_room.to_string()), + ..Default::default() + } + .insert(&*tx) + .await?; + + Ok(channel.id) + }) + .await + } + + pub async fn remove_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result<(Vec, Vec)> { + self.transaction(move |tx| async move { + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .await?; + + // Don't remove descendant channels that have additional parents. + let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?; + { + let mut channels_to_keep = channel_path::Entity::find() + .filter( + channel_path::Column::ChannelId + .is_in( + channels_to_remove + .keys() + .copied() + .filter(|&id| id != channel_id), + ) + .and( + channel_path::Column::IdPath + .not_like(&format!("%/{}/%", channel_id)), + ), + ) + .stream(&*tx) + .await?; + while let Some(row) = channels_to_keep.next().await { + let row = row?; + channels_to_remove.remove(&row.channel_id); + } + } + + let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?; + let members_to_notify: Vec = channel_member::Entity::find() + .filter(channel_member::Column::ChannelId.is_in(channel_ancestors)) + .select_only() + .column(channel_member::Column::UserId) + .distinct() + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + channel::Entity::delete_many() + .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied())) + .exec(&*tx) + .await?; + + Ok((channels_to_remove.into_keys().collect(), members_to_notify)) + }) + .await + } + + pub async fn invite_channel_member( + &self, + channel_id: ChannelId, + invitee_id: UserId, + inviter_id: UserId, + is_admin: bool, + ) -> Result<()> { + self.transaction(move |tx| async move { + self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) + .await?; + + channel_member::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(invitee_id), + accepted: ActiveValue::Set(false), + admin: ActiveValue::Set(is_admin), + ..Default::default() + } + .insert(&*tx) + .await?; + + Ok(()) + }) + .await + } + + fn sanitize_channel_name(name: &str) -> Result<&str> { + let new_name = name.trim().trim_start_matches('#'); + if new_name == "" { + Err(anyhow!("channel name can't be blank"))?; + } + Ok(new_name) + } + + pub async fn rename_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + new_name: &str, + ) -> Result { + self.transaction(move |tx| async move { + let new_name = Self::sanitize_channel_name(new_name)?.to_string(); + + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .await?; + + channel::ActiveModel { + id: ActiveValue::Unchanged(channel_id), + name: ActiveValue::Set(new_name.clone()), + ..Default::default() + } + .update(&*tx) + .await?; + + Ok(new_name) + }) + .await + } + + pub async fn respond_to_channel_invite( + &self, + channel_id: ChannelId, + user_id: UserId, + accept: bool, + ) -> Result<()> { + self.transaction(move |tx| async move { + let rows_affected = if accept { + channel_member::Entity::update_many() + .set(channel_member::ActiveModel { + accepted: ActiveValue::Set(accept), + ..Default::default() + }) + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Accepted.eq(false)), + ) + .exec(&*tx) + .await? + .rows_affected + } else { + channel_member::ActiveModel { + channel_id: ActiveValue::Unchanged(channel_id), + user_id: ActiveValue::Unchanged(user_id), + ..Default::default() + } + .delete(&*tx) + .await? + .rows_affected + }; + + if rows_affected == 0 { + Err(anyhow!("no such invitation"))?; + } + + Ok(()) + }) + .await + } + + pub async fn remove_channel_member( + &self, + channel_id: ChannelId, + member_id: UserId, + remover_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, remover_id, &*tx) + .await?; + + let result = channel_member::Entity::delete_many() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(member_id)), + ) + .exec(&*tx) + .await?; + + if result.rows_affected == 0 { + Err(anyhow!("no such member"))?; + } + + Ok(()) + }) + .await + } + + pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { + self.transaction(|tx| async move { + let channel_invites = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(false)), + ) + .all(&*tx) + .await?; + + let channels = channel::Entity::find() + .filter( + channel::Column::Id.is_in( + channel_invites + .into_iter() + .map(|channel_member| channel_member.channel_id), + ), + ) + .all(&*tx) + .await?; + + let channels = channels + .into_iter() + .map(|channel| Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }) + .collect(); + + Ok(channels) + }) + .await + } + + pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { + self.transaction(|tx| async move { + let tx = tx; + + let channel_memberships = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(true)), + ) + .all(&*tx) + .await?; + + let parents_by_child_id = self + .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) + .await?; + + let channels_with_admin_privileges = channel_memberships + .iter() + .filter_map(|membership| membership.admin.then_some(membership.channel_id)) + .collect(); + + let mut channels = Vec::with_capacity(parents_by_child_id.len()); + { + let mut rows = channel::Entity::find() + .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied())) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + channels.push(Channel { + id: row.id, + name: row.name, + parent_id: parents_by_child_id.get(&row.id).copied().flatten(), + }); + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryUserIdsAndChannelIds { + ChannelId, + UserId, + } + + let mut channel_participants: HashMap> = HashMap::default(); + { + let mut rows = room_participant::Entity::find() + .inner_join(room::Entity) + .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) + .select_only() + .column(room::Column::ChannelId) + .column(room_participant::Column::UserId) + .into_values::<_, QueryUserIdsAndChannelIds>() + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row: (ChannelId, UserId) = row?; + channel_participants.entry(row.0).or_default().push(row.1) + } + } + + Ok(ChannelsForUser { + channels, + channel_participants, + channels_with_admin_privileges, + }) + }) + .await + } + + pub async fn get_channel_members(&self, id: ChannelId) -> Result> { + self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await }) + .await + } + + pub async fn set_channel_member_admin( + &self, + channel_id: ChannelId, + from: UserId, + for_user: UserId, + admin: bool, + ) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, from, &*tx) + .await?; + + let result = channel_member::Entity::update_many() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(for_user)), + ) + .set(channel_member::ActiveModel { + admin: ActiveValue::set(admin), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if result.rows_affected == 0 { + Err(anyhow!("no such member"))?; + } + + Ok(()) + }) + .await + } + + pub async fn get_channel_member_details( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .await?; + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryMemberDetails { + UserId, + Admin, + IsDirectMember, + Accepted, + } + + let tx = tx; + let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; + let mut stream = channel_member::Entity::find() + .distinct() + .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .column(channel_member::Column::Admin) + .column_as( + channel_member::Column::ChannelId.eq(channel_id), + QueryMemberDetails::IsDirectMember, + ) + .column(channel_member::Column::Accepted) + .order_by_asc(channel_member::Column::UserId) + .into_values::<_, QueryMemberDetails>() + .stream(&*tx) + .await?; + + let mut rows = Vec::::new(); + while let Some(row) = stream.next().await { + let (user_id, is_admin, is_direct_member, is_invite_accepted): ( + UserId, + bool, + bool, + bool, + ) = row?; + let kind = match (is_direct_member, is_invite_accepted) { + (true, true) => proto::channel_member::Kind::Member, + (true, false) => proto::channel_member::Kind::Invitee, + (false, true) => proto::channel_member::Kind::AncestorMember, + (false, false) => continue, + }; + let user_id = user_id.to_proto(); + let kind = kind.into(); + if let Some(last_row) = rows.last_mut() { + if last_row.user_id == user_id { + if is_direct_member { + last_row.kind = kind; + last_row.admin = is_admin; + } + continue; + } + } + rows.push(proto::ChannelMember { + user_id, + kind, + admin: is_admin, + }); + } + + Ok(rows) + }) + .await + } + + pub async fn get_channel_members_internal( + &self, + id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let ancestor_ids = self.get_channel_ancestors(id, tx).await?; + let user_ids = channel_member::Entity::find() + .distinct() + .filter( + channel_member::Column::ChannelId + .is_in(ancestor_ids.iter().copied()) + .and(channel_member::Column::Accepted.eq(true)), + ) + .select_only() + .column(channel_member::Column::UserId) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + Ok(user_ids) + } + + pub async fn check_user_is_channel_member( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<()> { + let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; + channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .is_in(channel_ids) + .and(channel_member::Column::UserId.eq(user_id)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?; + Ok(()) + } + + pub async fn check_user_is_channel_admin( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<()> { + let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; + channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .is_in(channel_ids) + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Admin.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?; + Ok(()) + } + + pub async fn get_channel_ancestors( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let paths = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel_id)) + .all(tx) + .await?; + let mut channel_ids = Vec::new(); + for path in paths { + for id in path.id_path.trim_matches('/').split('/') { + if let Ok(id) = id.parse() { + let id = ChannelId::from_proto(id); + if let Err(ix) = channel_ids.binary_search(&id) { + channel_ids.insert(ix, id); + } + } + } + } + Ok(channel_ids) + } + + async fn get_channel_descendants( + &self, + channel_ids: impl IntoIterator, + tx: &DatabaseTransaction, + ) -> Result>> { + let mut values = String::new(); + for id in channel_ids { + if !values.is_empty() { + values.push_str(", "); + } + write!(&mut values, "({})", id).unwrap(); + } + + if values.is_empty() { + return Ok(HashMap::default()); + } + + let sql = format!( + r#" + SELECT + descendant_paths.* + FROM + channel_paths parent_paths, channel_paths descendant_paths + WHERE + parent_paths.channel_id IN ({values}) AND + descendant_paths.id_path LIKE (parent_paths.id_path || '%') + "# + ); + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + + let mut parents_by_child_id = HashMap::default(); + let mut paths = channel_path::Entity::find() + .from_raw_sql(stmt) + .stream(tx) + .await?; + + while let Some(path) = paths.next().await { + let path = path?; + let ids = path.id_path.trim_matches('/').split('/'); + let mut parent_id = None; + for id in ids { + if let Ok(id) = id.parse() { + let id = ChannelId::from_proto(id); + if id == path.channel_id { + break; + } + parent_id = Some(id); + } + } + parents_by_child_id.insert(path.channel_id, parent_id); + } + + Ok(parents_by_child_id) + } + + /// Returns the channel with the given ID and: + /// - true if the user is a member + /// - false if the user hasn't accepted the invitation yet + pub async fn get_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result> { + self.transaction(|tx| async move { + let tx = tx; + + let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + + if let Some(channel) = channel { + if self + .check_user_is_channel_member(channel_id, user_id, &*tx) + .await + .is_err() + { + return Ok(None); + } + + let channel_membership = channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(user_id)), + ) + .one(&*tx) + .await?; + + let is_accepted = channel_membership + .map(|membership| membership.accepted) + .unwrap_or(false); + + Ok(Some(( + Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }, + is_accepted, + ))) + } else { + Ok(None) + } + }) + .await + } + + pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result { + self.transaction(|tx| async move { + let tx = tx; + let room = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(room::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("invalid channel"))?; + Ok(room.id) + }) + .await + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +enum QueryUserIds { + UserId, +} diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs new file mode 100644 index 0000000000000000000000000000000000000000..a18958f035515450be2f9448b65c5f00dc6368fd --- /dev/null +++ b/crates/collab/src/db/queries/contacts.rs @@ -0,0 +1,298 @@ +use super::*; + +impl Database { + pub async fn get_contacts(&self, user_id: UserId) -> Result> { + #[derive(Debug, FromQueryResult)] + struct ContactWithUserBusyStatuses { + user_id_a: UserId, + user_id_b: UserId, + a_to_b: bool, + accepted: bool, + should_notify: bool, + user_a_busy: bool, + user_b_busy: bool, + } + + self.transaction(|tx| async move { + let user_a_participant = Alias::new("user_a_participant"); + let user_b_participant = Alias::new("user_b_participant"); + let mut db_contacts = contact::Entity::find() + .column_as( + Expr::tbl(user_a_participant.clone(), room_participant::Column::Id) + .is_not_null(), + "user_a_busy", + ) + .column_as( + Expr::tbl(user_b_participant.clone(), room_participant::Column::Id) + .is_not_null(), + "user_b_busy", + ) + .filter( + contact::Column::UserIdA + .eq(user_id) + .or(contact::Column::UserIdB.eq(user_id)), + ) + .join_as( + JoinType::LeftJoin, + contact::Relation::UserARoomParticipant.def(), + user_a_participant, + ) + .join_as( + JoinType::LeftJoin, + contact::Relation::UserBRoomParticipant.def(), + user_b_participant, + ) + .into_model::() + .stream(&*tx) + .await?; + + let mut contacts = Vec::new(); + while let Some(db_contact) = db_contacts.next().await { + let db_contact = db_contact?; + if db_contact.user_id_a == user_id { + if db_contact.accepted { + contacts.push(Contact::Accepted { + user_id: db_contact.user_id_b, + should_notify: db_contact.should_notify && db_contact.a_to_b, + busy: db_contact.user_b_busy, + }); + } else if db_contact.a_to_b { + contacts.push(Contact::Outgoing { + user_id: db_contact.user_id_b, + }) + } else { + contacts.push(Contact::Incoming { + user_id: db_contact.user_id_b, + should_notify: db_contact.should_notify, + }); + } + } else if db_contact.accepted { + contacts.push(Contact::Accepted { + user_id: db_contact.user_id_a, + should_notify: db_contact.should_notify && !db_contact.a_to_b, + busy: db_contact.user_a_busy, + }); + } else if db_contact.a_to_b { + contacts.push(Contact::Incoming { + user_id: db_contact.user_id_a, + should_notify: db_contact.should_notify, + }); + } else { + contacts.push(Contact::Outgoing { + user_id: db_contact.user_id_a, + }); + } + } + + contacts.sort_unstable_by_key(|contact| contact.user_id()); + + Ok(contacts) + }) + .await + } + + pub async fn is_user_busy(&self, user_id: UserId) -> Result { + self.transaction(|tx| async move { + let participant = room_participant::Entity::find() + .filter(room_participant::Column::UserId.eq(user_id)) + .one(&*tx) + .await?; + Ok(participant.is_some()) + }) + .await + } + + pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { + self.transaction(|tx| async move { + let (id_a, id_b) = if user_id_1 < user_id_2 { + (user_id_1, user_id_2) + } else { + (user_id_2, user_id_1) + }; + + Ok(contact::Entity::find() + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and(contact::Column::Accepted.eq(true)), + ) + .one(&*tx) + .await? + .is_some()) + }) + .await + } + + pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> { + self.transaction(|tx| async move { + let (id_a, id_b, a_to_b) = if sender_id < receiver_id { + (sender_id, receiver_id, true) + } else { + (receiver_id, sender_id, false) + }; + + let rows_affected = contact::Entity::insert(contact::ActiveModel { + user_id_a: ActiveValue::set(id_a), + user_id_b: ActiveValue::set(id_b), + a_to_b: ActiveValue::set(a_to_b), + accepted: ActiveValue::set(false), + should_notify: ActiveValue::set(true), + ..Default::default() + }) + .on_conflict( + OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB]) + .values([ + (contact::Column::Accepted, true.into()), + (contact::Column::ShouldNotify, false.into()), + ]) + .action_and_where( + contact::Column::Accepted.eq(false).and( + contact::Column::AToB + .eq(a_to_b) + .and(contact::Column::UserIdA.eq(id_b)) + .or(contact::Column::AToB + .ne(a_to_b) + .and(contact::Column::UserIdA.eq(id_a))), + ), + ) + .to_owned(), + ) + .exec_without_returning(&*tx) + .await?; + + if rows_affected == 1 { + Ok(()) + } else { + Err(anyhow!("contact already requested"))? + } + }) + .await + } + + /// Returns a bool indicating whether the removed contact had originally accepted or not + /// + /// Deletes the contact identified by the requester and responder ids, and then returns + /// whether the deleted contact had originally accepted or was a pending contact request. + /// + /// # Arguments + /// + /// * `requester_id` - The user that initiates this request + /// * `responder_id` - The user that will be removed + pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result { + self.transaction(|tx| async move { + let (id_a, id_b) = if responder_id < requester_id { + (responder_id, requester_id) + } else { + (requester_id, responder_id) + }; + + let contact = contact::Entity::find() + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such contact"))?; + + contact::Entity::delete_by_id(contact.id).exec(&*tx).await?; + Ok(contact.accepted) + }) + .await + } + + pub async fn dismiss_contact_notification( + &self, + user_id: UserId, + contact_user_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + let (id_a, id_b, a_to_b) = if user_id < contact_user_id { + (user_id, contact_user_id, true) + } else { + (contact_user_id, user_id, false) + }; + + let result = contact::Entity::update_many() + .set(contact::ActiveModel { + should_notify: ActiveValue::set(false), + ..Default::default() + }) + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and( + contact::Column::AToB + .eq(a_to_b) + .and(contact::Column::Accepted.eq(true)) + .or(contact::Column::AToB + .ne(a_to_b) + .and(contact::Column::Accepted.eq(false))), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("no such contact request"))? + } else { + Ok(()) + } + }) + .await + } + + pub async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()> { + self.transaction(|tx| async move { + let (id_a, id_b, a_to_b) = if responder_id < requester_id { + (responder_id, requester_id, false) + } else { + (requester_id, responder_id, true) + }; + let rows_affected = if accept { + let result = contact::Entity::update_many() + .set(contact::ActiveModel { + accepted: ActiveValue::set(true), + should_notify: ActiveValue::set(true), + ..Default::default() + }) + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and(contact::Column::AToB.eq(a_to_b)), + ) + .exec(&*tx) + .await?; + result.rows_affected + } else { + let result = contact::Entity::delete_many() + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and(contact::Column::AToB.eq(a_to_b)) + .and(contact::Column::Accepted.eq(false)), + ) + .exec(&*tx) + .await?; + + result.rows_affected + }; + + if rows_affected == 1 { + Ok(()) + } else { + Err(anyhow!("no such contact request"))? + } + }) + .await + } +} diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs new file mode 100644 index 0000000000000000000000000000000000000000..31c7cdae3e4837dd46487526fbf5c20a021d9b7d --- /dev/null +++ b/crates/collab/src/db/queries/projects.rs @@ -0,0 +1,926 @@ +use super::*; + +impl Database { + pub async fn project_count_excluding_admins(&self) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + Count, + } + + self.transaction(|tx| async move { + Ok(project::Entity::find() + .select_only() + .column_as(project::Column::Id.count(), QueryAs::Count) + .inner_join(user::Entity) + .filter(user::Column::Admin.eq(false)) + .into_values::<_, QueryAs>() + .one(&*tx) + .await? + .unwrap_or(0i64) as usize) + }) + .await + } + + pub async fn share_project( + &self, + room_id: RoomId, + connection: ConnectionId, + worktrees: &[proto::WorktreeMetadata], + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("could not find participant"))?; + if participant.room_id != room_id { + return Err(anyhow!("shared project on unexpected room"))?; + } + + let project = project::ActiveModel { + room_id: ActiveValue::set(participant.room_id), + host_user_id: ActiveValue::set(participant.user_id), + host_connection_id: ActiveValue::set(Some(connection.id as i32)), + host_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + ..Default::default() + } + .insert(&*tx) + .await?; + + if !worktrees.is_empty() { + worktree::Entity::insert_many(worktrees.iter().map(|worktree| { + worktree::ActiveModel { + id: ActiveValue::set(worktree.id as i64), + project_id: ActiveValue::set(project.id), + abs_path: ActiveValue::set(worktree.abs_path.clone()), + root_name: ActiveValue::set(worktree.root_name.clone()), + visible: ActiveValue::set(worktree.visible), + scan_id: ActiveValue::set(0), + completed_scan_id: ActiveValue::set(0), + } + })) + .exec(&*tx) + .await?; + } + + project_collaborator::ActiveModel { + project_id: ActiveValue::set(project.id), + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::set(participant.user_id), + replica_id: ActiveValue::set(ReplicaId(0)), + is_host: ActiveValue::set(true), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + Ok((project.id, room)) + }) + .await + } + + pub async fn unshare_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result)>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("project not found"))?; + if project.host_connection()? == connection { + project::Entity::delete(project.into_active_model()) + .exec(&*tx) + .await?; + let room = self.get_room(room_id, &tx).await?; + Ok((room, guest_connection_ids)) + } else { + Err(anyhow!("cannot unshare a project hosted by another user"))? + } + }) + .await + } + + pub async fn update_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + worktrees: &[proto::WorktreeMetadata], + ) -> Result)>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let project = project::Entity::find_by_id(project_id) + .filter( + Condition::all() + .add(project::Column::HostConnectionId.eq(connection.id as i32)) + .add( + project::Column::HostConnectionServerId.eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + + self.update_project_worktrees(project.id, worktrees, &tx) + .await?; + + let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; + let room = self.get_room(project.room_id, &tx).await?; + Ok((room, guest_connection_ids)) + }) + .await + } + + pub(in crate::db) async fn update_project_worktrees( + &self, + project_id: ProjectId, + worktrees: &[proto::WorktreeMetadata], + tx: &DatabaseTransaction, + ) -> Result<()> { + if !worktrees.is_empty() { + worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel { + id: ActiveValue::set(worktree.id as i64), + project_id: ActiveValue::set(project_id), + abs_path: ActiveValue::set(worktree.abs_path.clone()), + root_name: ActiveValue::set(worktree.root_name.clone()), + visible: ActiveValue::set(worktree.visible), + scan_id: ActiveValue::set(0), + completed_scan_id: ActiveValue::set(0), + })) + .on_conflict( + OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id]) + .update_column(worktree::Column::RootName) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + worktree::Entity::delete_many() + .filter(worktree::Column::ProjectId.eq(project_id).and( + worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)), + )) + .exec(&*tx) + .await?; + + Ok(()) + } + + pub async fn update_worktree( + &self, + update: &proto::UpdateWorktree, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let worktree_id = update.worktree_id as i64; + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + // Ensure the update comes from the host. + let _project = project::Entity::find_by_id(project_id) + .filter( + Condition::all() + .add(project::Column::HostConnectionId.eq(connection.id as i32)) + .add( + project::Column::HostConnectionServerId.eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + + // Update metadata. + worktree::Entity::update(worktree::ActiveModel { + id: ActiveValue::set(worktree_id), + project_id: ActiveValue::set(project_id), + root_name: ActiveValue::set(update.root_name.clone()), + scan_id: ActiveValue::set(update.scan_id as i64), + completed_scan_id: if update.is_last_update { + ActiveValue::set(update.scan_id as i64) + } else { + ActiveValue::default() + }, + abs_path: ActiveValue::set(update.abs_path.clone()), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if !update.updated_entries.is_empty() { + worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| { + let mtime = entry.mtime.clone().unwrap_or_default(); + worktree_entry::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + id: ActiveValue::set(entry.id as i64), + is_dir: ActiveValue::set(entry.is_dir), + path: ActiveValue::set(entry.path.clone()), + inode: ActiveValue::set(entry.inode as i64), + mtime_seconds: ActiveValue::set(mtime.seconds as i64), + mtime_nanos: ActiveValue::set(mtime.nanos as i32), + is_symlink: ActiveValue::set(entry.is_symlink), + is_ignored: ActiveValue::set(entry.is_ignored), + is_external: ActiveValue::set(entry.is_external), + git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), + is_deleted: ActiveValue::set(false), + scan_id: ActiveValue::set(update.scan_id as i64), + } + })) + .on_conflict( + OnConflict::columns([ + worktree_entry::Column::ProjectId, + worktree_entry::Column::WorktreeId, + worktree_entry::Column::Id, + ]) + .update_columns([ + worktree_entry::Column::IsDir, + worktree_entry::Column::Path, + worktree_entry::Column::Inode, + worktree_entry::Column::MtimeSeconds, + worktree_entry::Column::MtimeNanos, + worktree_entry::Column::IsSymlink, + worktree_entry::Column::IsIgnored, + worktree_entry::Column::GitStatus, + worktree_entry::Column::ScanId, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + if !update.removed_entries.is_empty() { + worktree_entry::Entity::update_many() + .filter( + worktree_entry::Column::ProjectId + .eq(project_id) + .and(worktree_entry::Column::WorktreeId.eq(worktree_id)) + .and( + worktree_entry::Column::Id + .is_in(update.removed_entries.iter().map(|id| *id as i64)), + ), + ) + .set(worktree_entry::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + if !update.updated_repositories.is_empty() { + worktree_repository::Entity::insert_many(update.updated_repositories.iter().map( + |repository| worktree_repository::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + work_directory_id: ActiveValue::set(repository.work_directory_id as i64), + scan_id: ActiveValue::set(update.scan_id as i64), + branch: ActiveValue::set(repository.branch.clone()), + is_deleted: ActiveValue::set(false), + }, + )) + .on_conflict( + OnConflict::columns([ + worktree_repository::Column::ProjectId, + worktree_repository::Column::WorktreeId, + worktree_repository::Column::WorkDirectoryId, + ]) + .update_columns([ + worktree_repository::Column::ScanId, + worktree_repository::Column::Branch, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + if !update.removed_repositories.is_empty() { + worktree_repository::Entity::update_many() + .filter( + worktree_repository::Column::ProjectId + .eq(project_id) + .and(worktree_repository::Column::WorktreeId.eq(worktree_id)) + .and( + worktree_repository::Column::WorkDirectoryId + .is_in(update.removed_repositories.iter().map(|id| *id as i64)), + ), + ) + .set(worktree_repository::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn update_diagnostic_summary( + &self, + update: &proto::UpdateDiagnosticSummary, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let worktree_id = update.worktree_id as i64; + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let summary = update + .summary + .as_ref() + .ok_or_else(|| anyhow!("invalid summary"))?; + + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + // Update summary. + worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + path: ActiveValue::set(summary.path.clone()), + language_server_id: ActiveValue::set(summary.language_server_id as i64), + error_count: ActiveValue::set(summary.error_count as i32), + warning_count: ActiveValue::set(summary.warning_count as i32), + ..Default::default() + }) + .on_conflict( + OnConflict::columns([ + worktree_diagnostic_summary::Column::ProjectId, + worktree_diagnostic_summary::Column::WorktreeId, + worktree_diagnostic_summary::Column::Path, + ]) + .update_columns([ + worktree_diagnostic_summary::Column::LanguageServerId, + worktree_diagnostic_summary::Column::ErrorCount, + worktree_diagnostic_summary::Column::WarningCount, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn start_language_server( + &self, + update: &proto::StartLanguageServer, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let server = update + .server + .as_ref() + .ok_or_else(|| anyhow!("invalid language server"))?; + + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + // Add the newly-started language server. + language_server::Entity::insert(language_server::ActiveModel { + project_id: ActiveValue::set(project_id), + id: ActiveValue::set(server.id as i64), + name: ActiveValue::set(server.name.clone()), + ..Default::default() + }) + .on_conflict( + OnConflict::columns([ + language_server::Column::ProjectId, + language_server::Column::Id, + ]) + .update_column(language_server::Column::Name) + .to_owned(), + ) + .exec(&*tx) + .await?; + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn update_worktree_settings( + &self, + update: &proto::UpdateWorktreeSettings, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + if let Some(content) = &update.content { + worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + content: ActiveValue::Set(content.clone()), + }) + .on_conflict( + OnConflict::columns([ + worktree_settings_file::Column::ProjectId, + worktree_settings_file::Column::WorktreeId, + worktree_settings_file::Column::Path, + ]) + .update_column(worktree_settings_file::Column::Content) + .to_owned(), + ) + .exec(&*tx) + .await?; + } else { + worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn join_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("must join a room first"))?; + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.room_id != participant.room_id { + return Err(anyhow!("no such project"))?; + } + + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(1); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + let new_collaborator = project_collaborator::ActiveModel { + project_id: ActiveValue::set(project_id), + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::set(participant.user_id), + replica_id: ActiveValue::set(replica_id), + is_host: ActiveValue::set(false), + ..Default::default() + } + .insert(&*tx) + .await?; + collaborators.push(new_collaborator); + + let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; + let mut worktrees = db_worktrees + .into_iter() + .map(|db_worktree| { + ( + db_worktree.id as u64, + Worktree { + id: db_worktree.id as u64, + abs_path: db_worktree.abs_path, + root_name: db_worktree.root_name, + visible: db_worktree.visible, + entries: Default::default(), + repository_entries: Default::default(), + diagnostic_summaries: Default::default(), + settings_files: Default::default(), + scan_id: db_worktree.scan_id as u64, + completed_scan_id: db_worktree.completed_scan_id as u64, + }, + ) + }) + .collect::>(); + + // Populate worktree entries. + { + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::ProjectId.eq(project_id)) + .add(worktree_entry::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { + worktree.entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, + git_status: db_entry.git_status.map(|status| status as i32), + }); + } + } + } + + // Populate repository entries. + { + let mut db_repository_entries = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project_id)) + .add(worktree_repository::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_repository_entry) = db_repository_entries.next().await { + let db_repository_entry = db_repository_entry?; + if let Some(worktree) = + worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) + { + worktree.repository_entries.insert( + db_repository_entry.work_directory_id as u64, + proto::RepositoryEntry { + work_directory_id: db_repository_entry.work_directory_id as u64, + branch: db_repository_entry.branch, + }, + ); + } + } + } + + // Populate worktree diagnostic summaries. + { + let mut db_summaries = worktree_diagnostic_summary::Entity::find() + .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_summary) = db_summaries.next().await { + let db_summary = db_summary?; + if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { + worktree + .diagnostic_summaries + .push(proto::DiagnosticSummary { + path: db_summary.path, + language_server_id: db_summary.language_server_id as u64, + error_count: db_summary.error_count as u32, + warning_count: db_summary.warning_count as u32, + }); + } + } + } + + // Populate worktree settings files + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = + worktrees.get_mut(&(db_settings_file.worktree_id as u64)) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + + // Populate language servers. + let language_servers = project + .find_related(language_server::Entity) + .all(&*tx) + .await?; + + let project = Project { + collaborators: collaborators + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect(), + worktrees, + language_servers: language_servers + .into_iter() + .map(|language_server| proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + }) + .collect(), + }; + Ok((project, replica_id as ReplicaId)) + }) + .await + } + + pub async fn leave_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let result = project_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(project_collaborator::Column::ProjectId.eq(project_id)) + .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + let collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let connection_ids = collaborators + .into_iter() + .map(|collaborator| collaborator.connection()) + .collect(); + + follower::Entity::delete_many() + .filter( + Condition::any() + .add( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::LeaderConnectionServerId + .eq(connection.owner_id), + ) + .add(follower::Column::LeaderConnectionId.eq(connection.id)), + ) + .add( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::FollowerConnectionServerId + .eq(connection.owner_id), + ) + .add(follower::Column::FollowerConnectionId.eq(connection.id)), + ), + ) + .exec(&*tx) + .await?; + + let room = self.get_room(project.room_id, &tx).await?; + let left_project = LeftProject { + id: project_id, + host_user_id: project.host_user_id, + host_connection_id: project.host_connection()?, + connection_ids, + }; + Ok((room, left_project)) + }) + .await + } + + pub async fn project_collaborators( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let collaborators = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.eq(project_id)) + .all(&*tx) + .await? + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect::>(); + + if collaborators + .iter() + .any(|collaborator| collaborator.connection_id == connection_id) + { + Ok(collaborators) + } else { + Err(anyhow!("no such project"))? + } + }) + .await + } + + pub async fn project_connection_ids( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let mut collaborators = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + + let mut connection_ids = HashSet::default(); + while let Some(collaborator) = collaborators.next().await { + let collaborator = collaborator?; + connection_ids.insert(collaborator.connection()); + } + + if connection_ids.contains(&connection_id) { + Ok(connection_ids) + } else { + Err(anyhow!("no such project"))? + } + }) + .await + } + + async fn project_guest_connection_ids( + &self, + project_id: ProjectId, + tx: &DatabaseTransaction, + ) -> Result> { + let mut collaborators = project_collaborator::Entity::find() + .filter( + project_collaborator::Column::ProjectId + .eq(project_id) + .and(project_collaborator::Column::IsHost.eq(false)), + ) + .stream(tx) + .await?; + + let mut guest_connection_ids = Vec::new(); + while let Some(collaborator) = collaborators.next().await { + let collaborator = collaborator?; + guest_connection_ids.push(collaborator.connection()); + } + Ok(guest_connection_ids) + } + + pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result { + self.transaction(|tx| async move { + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("project {} not found", project_id))?; + Ok(project.room_id) + }) + .await + } + + pub async fn follow( + &self, + project_id: ProjectId, + leader_connection: ConnectionId, + follower_connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + follower::ActiveModel { + room_id: ActiveValue::set(room_id), + project_id: ActiveValue::set(project_id), + leader_connection_server_id: ActiveValue::set(ServerId( + leader_connection.owner_id as i32, + )), + leader_connection_id: ActiveValue::set(leader_connection.id as i32), + follower_connection_server_id: ActiveValue::set(ServerId( + follower_connection.owner_id as i32, + )), + follower_connection_id: ActiveValue::set(follower_connection.id as i32), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room_id, &*tx).await?; + Ok(room) + }) + .await + } + + pub async fn unfollow( + &self, + project_id: ProjectId, + leader_connection: ConnectionId, + follower_connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + follower::Entity::delete_many() + .filter( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::LeaderConnectionServerId + .eq(leader_connection.owner_id), + ) + .add(follower::Column::LeaderConnectionId.eq(leader_connection.id)) + .add( + follower::Column::FollowerConnectionServerId + .eq(follower_connection.owner_id), + ) + .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)), + ) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &*tx).await?; + Ok(room) + }) + .await + } +} diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs new file mode 100644 index 0000000000000000000000000000000000000000..a85d257187c2207b56b934540ba34c566eb1c77d --- /dev/null +++ b/crates/collab/src/db/queries/rooms.rs @@ -0,0 +1,1093 @@ +use super::*; + +impl Database { + pub async fn refresh_room( + &self, + room_id: RoomId, + new_server_id: ServerId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + let stale_participant_filter = Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::AnsweringConnectionId.is_not_null()) + .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id)); + + let stale_participant_user_ids = room_participant::Entity::find() + .filter(stale_participant_filter.clone()) + .all(&*tx) + .await? + .into_iter() + .map(|participant| participant.user_id) + .collect::>(); + + // Delete participants who failed to reconnect and cancel their calls. + let mut canceled_calls_to_user_ids = Vec::new(); + room_participant::Entity::delete_many() + .filter(stale_participant_filter) + .exec(&*tx) + .await?; + let called_participants = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::CallingUserId + .is_in(stale_participant_user_ids.iter().copied()), + ) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .all(&*tx) + .await?; + room_participant::Entity::delete_many() + .filter( + room_participant::Column::Id + .is_in(called_participants.iter().map(|participant| participant.id)), + ) + .exec(&*tx) + .await?; + canceled_calls_to_user_ids.extend( + called_participants + .into_iter() + .map(|participant| participant.user_id), + ); + + let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let channel_members; + if let Some(channel_id) = channel_id { + channel_members = self.get_channel_members_internal(channel_id, &tx).await?; + } else { + channel_members = Vec::new(); + + // Delete the room if it becomes empty. + if room.participants.is_empty() { + project::Entity::delete_many() + .filter(project::Column::RoomId.eq(room_id)) + .exec(&*tx) + .await?; + room::Entity::delete_by_id(room_id).exec(&*tx).await?; + } + }; + + Ok(RefreshedRoom { + room, + channel_id, + channel_members, + stale_participant_user_ids, + canceled_calls_to_user_ids, + }) + }) + .await + } + + pub async fn incoming_call_for_user( + &self, + user_id: UserId, + ) -> Result> { + self.transaction(|tx| async move { + let pending_participant = room_participant::Entity::find() + .filter( + room_participant::Column::UserId + .eq(user_id) + .and(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .one(&*tx) + .await?; + + if let Some(pending_participant) = pending_participant { + let room = self.get_room(pending_participant.room_id, &tx).await?; + Ok(Self::build_incoming_call(&room, user_id)) + } else { + Ok(None) + } + }) + .await + } + + pub async fn create_room( + &self, + user_id: UserId, + connection: ConnectionId, + live_kit_room: &str, + ) -> Result { + self.transaction(|tx| async move { + let room = room::ActiveModel { + live_kit_room: ActiveValue::set(live_kit_room.into()), + ..Default::default() + } + .insert(&*tx) + .await?; + room_participant::ActiveModel { + room_id: ActiveValue::set(room.id), + user_id: ActiveValue::set(user_id), + answering_connection_id: ActiveValue::set(Some(connection.id as i32)), + answering_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + answering_connection_lost: ActiveValue::set(false), + calling_user_id: ActiveValue::set(user_id), + calling_connection_id: ActiveValue::set(connection.id as i32), + calling_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room.id, &tx).await?; + Ok(room) + }) + .await + } + + pub async fn call( + &self, + room_id: RoomId, + calling_user_id: UserId, + calling_connection: ConnectionId, + called_user_id: UserId, + initial_project_id: Option, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + room_participant::ActiveModel { + room_id: ActiveValue::set(room_id), + user_id: ActiveValue::set(called_user_id), + answering_connection_lost: ActiveValue::set(false), + calling_user_id: ActiveValue::set(calling_user_id), + calling_connection_id: ActiveValue::set(calling_connection.id as i32), + calling_connection_server_id: ActiveValue::set(Some(ServerId( + calling_connection.owner_id as i32, + ))), + initial_project_id: ActiveValue::set(initial_project_id), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + let incoming_call = Self::build_incoming_call(&room, called_user_id) + .ok_or_else(|| anyhow!("failed to build incoming call"))?; + Ok((room, incoming_call)) + }) + .await + } + + pub async fn call_failed( + &self, + room_id: RoomId, + called_user_id: UserId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + room_participant::Entity::delete_many() + .filter( + room_participant::Column::RoomId + .eq(room_id) + .and(room_participant::Column::UserId.eq(called_user_id)), + ) + .exec(&*tx) + .await?; + let room = self.get_room(room_id, &tx).await?; + Ok(room) + }) + .await + } + + pub async fn decline_call( + &self, + expected_room_id: Option, + user_id: UserId, + ) -> Result>> { + self.optional_room_transaction(|tx| async move { + let mut filter = Condition::all() + .add(room_participant::Column::UserId.eq(user_id)) + .add(room_participant::Column::AnsweringConnectionId.is_null()); + if let Some(room_id) = expected_room_id { + filter = filter.add(room_participant::Column::RoomId.eq(room_id)); + } + let participant = room_participant::Entity::find() + .filter(filter) + .one(&*tx) + .await?; + + let participant = if let Some(participant) = participant { + participant + } else if expected_room_id.is_some() { + return Err(anyhow!("could not find call to decline"))?; + } else { + return Ok(None); + }; + + let room_id = participant.room_id; + room_participant::Entity::delete(participant.into_active_model()) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + Ok(Some((room_id, room))) + }) + .await + } + + pub async fn cancel_call( + &self, + room_id: RoomId, + calling_connection: ConnectionId, + called_user_id: UserId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add(room_participant::Column::UserId.eq(called_user_id)) + .add(room_participant::Column::RoomId.eq(room_id)) + .add( + room_participant::Column::CallingConnectionId + .eq(calling_connection.id as i32), + ) + .add( + room_participant::Column::CallingConnectionServerId + .eq(calling_connection.owner_id as i32), + ) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no call to cancel"))?; + + room_participant::Entity::delete(participant.into_active_model()) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + Ok(room) + }) + .await + } + + pub async fn join_room( + &self, + room_id: RoomId, + user_id: UserId, + connection: ConnectionId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelId { + ChannelId, + } + let channel_id: Option = room::Entity::find() + .select_only() + .column(room::Column::ChannelId) + .filter(room::Column::Id.eq(room_id)) + .into_values::<_, QueryChannelId>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if let Some(channel_id) = channel_id { + self.check_user_is_channel_member(channel_id, user_id, &*tx) + .await?; + + room_participant::Entity::insert_many([room_participant::ActiveModel { + room_id: ActiveValue::set(room_id), + user_id: ActiveValue::set(user_id), + answering_connection_id: ActiveValue::set(Some(connection.id as i32)), + answering_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + answering_connection_lost: ActiveValue::set(false), + calling_user_id: ActiveValue::set(user_id), + calling_connection_id: ActiveValue::set(connection.id as i32), + calling_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + ..Default::default() + }]) + .on_conflict( + OnConflict::columns([room_participant::Column::UserId]) + .update_columns([ + room_participant::Column::AnsweringConnectionId, + room_participant::Column::AnsweringConnectionServerId, + room_participant::Column::AnsweringConnectionLost, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } else { + let result = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(user_id)) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .set(room_participant::ActiveModel { + answering_connection_id: ActiveValue::set(Some(connection.id as i32)), + answering_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + answering_connection_lost: ActiveValue::set(false), + ..Default::default() + }) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("room does not exist or was already joined"))?; + } + } + + let room = self.get_room(room_id, &tx).await?; + let channel_members = if let Some(channel_id) = channel_id { + self.get_channel_members_internal(channel_id, &tx).await? + } else { + Vec::new() + }; + Ok(JoinRoom { + room, + channel_id, + channel_members, + }) + }) + .await + } + + pub async fn rejoin_room( + &self, + rejoin_room: proto::RejoinRoom, + user_id: UserId, + connection: ConnectionId, + ) -> Result> { + let room_id = RoomId::from_proto(rejoin_room.id); + self.room_transaction(room_id, |tx| async { + let tx = tx; + let participant_update = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(user_id)) + .add(room_participant::Column::AnsweringConnectionId.is_not_null()) + .add( + Condition::any() + .add(room_participant::Column::AnsweringConnectionLost.eq(true)) + .add( + room_participant::Column::AnsweringConnectionServerId + .ne(connection.owner_id as i32), + ), + ), + ) + .set(room_participant::ActiveModel { + answering_connection_id: ActiveValue::set(Some(connection.id as i32)), + answering_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + answering_connection_lost: ActiveValue::set(false), + ..Default::default() + }) + .exec(&*tx) + .await?; + if participant_update.rows_affected == 0 { + return Err(anyhow!("room does not exist or was already joined"))?; + } + + let mut reshared_projects = Vec::new(); + for reshared_project in &rejoin_room.reshared_projects { + let project_id = ProjectId::from_proto(reshared_project.project_id); + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("project does not exist"))?; + if project.host_user_id != user_id { + return Err(anyhow!("no such project"))?; + } + + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let host_ix = collaborators + .iter() + .position(|collaborator| { + collaborator.user_id == user_id && collaborator.is_host + }) + .ok_or_else(|| anyhow!("host not found among collaborators"))?; + let host = collaborators.swap_remove(host_ix); + let old_connection_id = host.connection(); + + project::Entity::update(project::ActiveModel { + host_connection_id: ActiveValue::set(Some(connection.id as i32)), + host_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + ..project.into_active_model() + }) + .exec(&*tx) + .await?; + project_collaborator::Entity::update(project_collaborator::ActiveModel { + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + ..host.into_active_model() + }) + .exec(&*tx) + .await?; + + self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx) + .await?; + + reshared_projects.push(ResharedProject { + id: project_id, + old_connection_id, + collaborators: collaborators + .iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect(), + worktrees: reshared_project.worktrees.clone(), + }); + } + + project::Entity::delete_many() + .filter( + Condition::all() + .add(project::Column::RoomId.eq(room_id)) + .add(project::Column::HostUserId.eq(user_id)) + .add( + project::Column::Id + .is_not_in(reshared_projects.iter().map(|project| project.id)), + ), + ) + .exec(&*tx) + .await?; + + let mut rejoined_projects = Vec::new(); + for rejoined_project in &rejoin_room.rejoined_projects { + let project_id = ProjectId::from_proto(rejoined_project.id); + let Some(project) = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? else { continue }; + + let mut worktrees = Vec::new(); + let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; + for db_worktree in db_worktrees { + let mut worktree = RejoinedWorktree { + id: db_worktree.id as u64, + abs_path: db_worktree.abs_path, + root_name: db_worktree.root_name, + visible: db_worktree.visible, + updated_entries: Default::default(), + removed_entries: Default::default(), + updated_repositories: Default::default(), + removed_repositories: Default::default(), + diagnostic_summaries: Default::default(), + settings_files: Default::default(), + scan_id: db_worktree.scan_id as u64, + completed_scan_id: db_worktree.completed_scan_id as u64, + }; + + let rejoined_worktree = rejoined_project + .worktrees + .iter() + .find(|worktree| worktree.id == db_worktree.id as u64); + + // File entries + { + let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { + worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) + } else { + worktree_entry::Column::IsDeleted.eq(false) + }; + + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::ProjectId.eq(project.id)) + .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) + .add(entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if db_entry.is_deleted { + worktree.removed_entries.push(db_entry.id as u64); + } else { + worktree.updated_entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, + git_status: db_entry.git_status.map(|status| status as i32), + }); + } + } + } + + // Repository Entries + { + let repository_entry_filter = + if let Some(rejoined_worktree) = rejoined_worktree { + worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id) + } else { + worktree_repository::Column::IsDeleted.eq(false) + }; + + let mut db_repositories = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project.id)) + .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) + .add(repository_entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_repository) = db_repositories.next().await { + let db_repository = db_repository?; + if db_repository.is_deleted { + worktree + .removed_repositories + .push(db_repository.work_directory_id as u64); + } else { + worktree.updated_repositories.push(proto::RepositoryEntry { + work_directory_id: db_repository.work_directory_id as u64, + branch: db_repository.branch, + }); + } + } + } + + worktrees.push(worktree); + } + + let language_servers = project + .find_related(language_server::Entity) + .all(&*tx) + .await? + .into_iter() + .map(|language_server| proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + }) + .collect::>(); + + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = worktrees + .iter_mut() + .find(|w| w.id == db_settings_file.worktree_id as u64) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let self_collaborator = if let Some(self_collaborator_ix) = collaborators + .iter() + .position(|collaborator| collaborator.user_id == user_id) + { + collaborators.swap_remove(self_collaborator_ix) + } else { + continue; + }; + let old_connection_id = self_collaborator.connection(); + project_collaborator::Entity::update(project_collaborator::ActiveModel { + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + ..self_collaborator.into_active_model() + }) + .exec(&*tx) + .await?; + + let collaborators = collaborators + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect::>(); + + rejoined_projects.push(RejoinedProject { + id: project_id, + old_connection_id, + collaborators, + worktrees, + language_servers, + }); + } + + let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let channel_members = if let Some(channel_id) = channel_id { + self.get_channel_members_internal(channel_id, &tx).await? + } else { + Vec::new() + }; + + Ok(RejoinedRoom { + room, + channel_id, + channel_members, + rejoined_projects, + reshared_projects, + }) + }) + .await + } + + pub async fn leave_room( + &self, + connection: ConnectionId, + ) -> Result>> { + self.optional_room_transaction(|tx| async move { + let leaving_participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await?; + + if let Some(leaving_participant) = leaving_participant { + // Leave room. + let room_id = leaving_participant.room_id; + room_participant::Entity::delete_by_id(leaving_participant.id) + .exec(&*tx) + .await?; + + // Cancel pending calls initiated by the leaving user. + let called_participants = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::CallingUserId + .eq(leaving_participant.user_id), + ) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .all(&*tx) + .await?; + room_participant::Entity::delete_many() + .filter( + room_participant::Column::Id + .is_in(called_participants.iter().map(|participant| participant.id)), + ) + .exec(&*tx) + .await?; + let canceled_calls_to_user_ids = called_participants + .into_iter() + .map(|participant| participant.user_id) + .collect(); + + // Detect left projects. + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryProjectIds { + ProjectId, + } + let project_ids: Vec = project_collaborator::Entity::find() + .select_only() + .column_as( + project_collaborator::Column::ProjectId, + QueryProjectIds::ProjectId, + ) + .filter( + Condition::all() + .add( + project_collaborator::Column::ConnectionId.eq(connection.id as i32), + ) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .into_values::<_, QueryProjectIds>() + .all(&*tx) + .await?; + let mut left_projects = HashMap::default(); + let mut collaborators = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.is_in(project_ids)) + .stream(&*tx) + .await?; + while let Some(collaborator) = collaborators.next().await { + let collaborator = collaborator?; + let left_project = + left_projects + .entry(collaborator.project_id) + .or_insert(LeftProject { + id: collaborator.project_id, + host_user_id: Default::default(), + connection_ids: Default::default(), + host_connection_id: Default::default(), + }); + + let collaborator_connection_id = collaborator.connection(); + if collaborator_connection_id != connection { + left_project.connection_ids.push(collaborator_connection_id); + } + + if collaborator.is_host { + left_project.host_user_id = collaborator.user_id; + left_project.host_connection_id = collaborator_connection_id; + } + } + drop(collaborators); + + // Leave projects. + project_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add( + project_collaborator::Column::ConnectionId.eq(connection.id as i32), + ) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + + // Unshare projects. + project::Entity::delete_many() + .filter( + Condition::all() + .add(project::Column::RoomId.eq(room_id)) + .add(project::Column::HostConnectionId.eq(connection.id as i32)) + .add( + project::Column::HostConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + + let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let deleted = if room.participants.is_empty() { + let result = room::Entity::delete_by_id(room_id) + .filter(room::Column::ChannelId.is_null()) + .exec(&*tx) + .await?; + result.rows_affected > 0 + } else { + false + }; + + let channel_members = if let Some(channel_id) = channel_id { + self.get_channel_members_internal(channel_id, &tx).await? + } else { + Vec::new() + }; + let left_room = LeftRoom { + room, + channel_id, + channel_members, + left_projects, + canceled_calls_to_user_ids, + deleted, + }; + + if left_room.room.participants.is_empty() { + self.rooms.remove(&room_id); + } + + Ok(Some((room_id, left_room))) + } else { + Ok(None) + } + }) + .await + } + + pub async fn update_room_participant_location( + &self, + room_id: RoomId, + connection: ConnectionId, + location: proto::ParticipantLocation, + ) -> Result> { + self.room_transaction(room_id, |tx| async { + let tx = tx; + let location_kind; + let location_project_id; + match location + .variant + .as_ref() + .ok_or_else(|| anyhow!("invalid location"))? + { + proto::participant_location::Variant::SharedProject(project) => { + location_kind = 0; + location_project_id = Some(ProjectId::from_proto(project.id)); + } + proto::participant_location::Variant::UnsharedProject(_) => { + location_kind = 1; + location_project_id = None; + } + proto::participant_location::Variant::External(_) => { + location_kind = 2; + location_project_id = None; + } + } + + let result = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .set(room_participant::ActiveModel { + location_kind: ActiveValue::set(Some(location_kind)), + location_project_id: ActiveValue::set(location_project_id), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if result.rows_affected == 1 { + let room = self.get_room(room_id, &tx).await?; + Ok(room) + } else { + Err(anyhow!("could not update room participant location"))? + } + }) + .await + } + + pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { + self.transaction(|tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await?; + + if let Some(participant) = participant { + room_participant::Entity::update(room_participant::ActiveModel { + answering_connection_lost: ActiveValue::set(true), + ..participant.into_active_model() + }) + .exec(&*tx) + .await?; + } + + channel_buffer_collaborator::Entity::update_many() + .filter( + Condition::all() + .add( + channel_buffer_collaborator::Column::ConnectionId + .eq(connection.id as i32), + ) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .set(channel_buffer_collaborator::ActiveModel { + connection_lost: ActiveValue::set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + fn build_incoming_call( + room: &proto::Room, + called_user_id: UserId, + ) -> Option { + let pending_participant = room + .pending_participants + .iter() + .find(|participant| participant.user_id == called_user_id.to_proto())?; + + Some(proto::IncomingCall { + room_id: room.id, + calling_user_id: pending_participant.calling_user_id, + participant_user_ids: room + .participants + .iter() + .map(|participant| participant.user_id) + .collect(), + initial_project: room.participants.iter().find_map(|participant| { + let initial_project_id = pending_participant.initial_project_id?; + participant + .projects + .iter() + .find(|project| project.id == initial_project_id) + .cloned() + }), + }) + } + + pub async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { + let (_, room) = self.get_channel_room(room_id, tx).await?; + Ok(room) + } + + async fn get_channel_room( + &self, + room_id: RoomId, + tx: &DatabaseTransaction, + ) -> Result<(Option, proto::Room)> { + let db_room = room::Entity::find_by_id(room_id) + .one(tx) + .await? + .ok_or_else(|| anyhow!("could not find room"))?; + + let mut db_participants = db_room + .find_related(room_participant::Entity) + .stream(tx) + .await?; + let mut participants = HashMap::default(); + let mut pending_participants = Vec::new(); + while let Some(db_participant) = db_participants.next().await { + let db_participant = db_participant?; + if let Some((answering_connection_id, answering_connection_server_id)) = db_participant + .answering_connection_id + .zip(db_participant.answering_connection_server_id) + { + let location = match ( + db_participant.location_kind, + db_participant.location_project_id, + ) { + (Some(0), Some(project_id)) => { + Some(proto::participant_location::Variant::SharedProject( + proto::participant_location::SharedProject { + id: project_id.to_proto(), + }, + )) + } + (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject( + Default::default(), + )), + _ => Some(proto::participant_location::Variant::External( + Default::default(), + )), + }; + + let answering_connection = ConnectionId { + owner_id: answering_connection_server_id.0 as u32, + id: answering_connection_id as u32, + }; + participants.insert( + answering_connection, + proto::Participant { + user_id: db_participant.user_id.to_proto(), + peer_id: Some(answering_connection.into()), + projects: Default::default(), + location: Some(proto::ParticipantLocation { variant: location }), + }, + ); + } else { + pending_participants.push(proto::PendingParticipant { + user_id: db_participant.user_id.to_proto(), + calling_user_id: db_participant.calling_user_id.to_proto(), + initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()), + }); + } + } + drop(db_participants); + + let mut db_projects = db_room + .find_related(project::Entity) + .find_with_related(worktree::Entity) + .stream(tx) + .await?; + + while let Some(row) = db_projects.next().await { + let (db_project, db_worktree) = row?; + let host_connection = db_project.host_connection()?; + if let Some(participant) = participants.get_mut(&host_connection) { + let project = if let Some(project) = participant + .projects + .iter_mut() + .find(|project| project.id == db_project.id.to_proto()) + { + project + } else { + participant.projects.push(proto::ParticipantProject { + id: db_project.id.to_proto(), + worktree_root_names: Default::default(), + }); + participant.projects.last_mut().unwrap() + }; + + if let Some(db_worktree) = db_worktree { + if db_worktree.visible { + project.worktree_root_names.push(db_worktree.root_name); + } + } + } + } + drop(db_projects); + + let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; + let mut followers = Vec::new(); + while let Some(db_follower) = db_followers.next().await { + let db_follower = db_follower?; + followers.push(proto::Follower { + leader_id: Some(db_follower.leader_connection().into()), + follower_id: Some(db_follower.follower_connection().into()), + project_id: db_follower.project_id.to_proto(), + }); + } + + Ok(( + db_room.channel_id, + proto::Room { + id: db_room.id.to_proto(), + live_kit_room: db_room.live_kit_room, + participants: participants.into_values().collect(), + pending_participants, + followers, + }, + )) + } +} diff --git a/crates/collab/src/db/queries/servers.rs b/crates/collab/src/db/queries/servers.rs new file mode 100644 index 0000000000000000000000000000000000000000..08a2bda16a96179c58628239bea236d071161493 --- /dev/null +++ b/crates/collab/src/db/queries/servers.rs @@ -0,0 +1,81 @@ +use super::*; + +impl Database { + pub async fn create_server(&self, environment: &str) -> Result { + self.transaction(|tx| async move { + let server = server::ActiveModel { + environment: ActiveValue::set(environment.into()), + ..Default::default() + } + .insert(&*tx) + .await?; + Ok(server.id) + }) + .await + } + + pub async fn stale_room_ids( + &self, + environment: &str, + new_server_id: ServerId, + ) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + RoomId, + } + + let stale_server_epochs = self + .stale_server_ids(environment, new_server_id, &tx) + .await?; + Ok(room_participant::Entity::find() + .select_only() + .column(room_participant::Column::RoomId) + .distinct() + .filter( + room_participant::Column::AnsweringConnectionServerId + .is_in(stale_server_epochs), + ) + .into_values::<_, QueryAs>() + .all(&*tx) + .await?) + }) + .await + } + + pub async fn delete_stale_servers( + &self, + environment: &str, + new_server_id: ServerId, + ) -> Result<()> { + self.transaction(|tx| async move { + server::Entity::delete_many() + .filter( + Condition::all() + .add(server::Column::Environment.eq(environment)) + .add(server::Column::Id.ne(new_server_id)), + ) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + async fn stale_server_ids( + &self, + environment: &str, + new_server_id: ServerId, + tx: &DatabaseTransaction, + ) -> Result> { + let stale_servers = server::Entity::find() + .filter( + Condition::all() + .add(server::Column::Environment.eq(environment)) + .add(server::Column::Id.ne(new_server_id)), + ) + .all(&*tx) + .await?; + Ok(stale_servers.into_iter().map(|server| server.id).collect()) + } +} diff --git a/crates/collab/src/db/queries/signups.rs b/crates/collab/src/db/queries/signups.rs new file mode 100644 index 0000000000000000000000000000000000000000..8cb8d866fb401cf20f6b29aac1deac84ea584ec7 --- /dev/null +++ b/crates/collab/src/db/queries/signups.rs @@ -0,0 +1,349 @@ +use super::*; +use hyper::StatusCode; + +impl Database { + pub async fn create_invite_from_code( + &self, + code: &str, + email_address: &str, + device_id: Option<&str>, + added_to_mailing_list: bool, + ) -> Result { + self.transaction(|tx| async move { + let existing_user = user::Entity::find() + .filter(user::Column::EmailAddress.eq(email_address)) + .one(&*tx) + .await?; + + if existing_user.is_some() { + Err(anyhow!("email address is already in use"))?; + } + + let inviting_user_with_invites = match user::Entity::find() + .filter( + user::Column::InviteCode + .eq(code) + .and(user::Column::InviteCount.gt(0)), + ) + .one(&*tx) + .await? + { + Some(inviting_user) => inviting_user, + None => { + return Err(Error::Http( + StatusCode::UNAUTHORIZED, + "unable to find an invite code with invites remaining".to_string(), + ))? + } + }; + user::Entity::update_many() + .filter( + user::Column::Id + .eq(inviting_user_with_invites.id) + .and(user::Column::InviteCount.gt(0)), + ) + .col_expr( + user::Column::InviteCount, + Expr::col(user::Column::InviteCount).sub(1), + ) + .exec(&*tx) + .await?; + + let signup = signup::Entity::insert(signup::ActiveModel { + email_address: ActiveValue::set(email_address.into()), + email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), + email_confirmation_sent: ActiveValue::set(false), + inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)), + platform_linux: ActiveValue::set(false), + platform_mac: ActiveValue::set(false), + platform_windows: ActiveValue::set(false), + platform_unknown: ActiveValue::set(true), + device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())), + added_to_mailing_list: ActiveValue::set(added_to_mailing_list), + ..Default::default() + }) + .on_conflict( + OnConflict::column(signup::Column::EmailAddress) + .update_column(signup::Column::InvitingUserId) + .to_owned(), + ) + .exec_with_returning(&*tx) + .await?; + + Ok(Invite { + email_address: signup.email_address, + email_confirmation_code: signup.email_confirmation_code, + }) + }) + .await + } + + pub async fn create_user_from_invite( + &self, + invite: &Invite, + user: NewUserParams, + ) -> Result> { + self.transaction(|tx| async { + let tx = tx; + let signup = signup::Entity::find() + .filter( + signup::Column::EmailAddress + .eq(invite.email_address.as_str()) + .and( + signup::Column::EmailConfirmationCode + .eq(invite.email_confirmation_code.as_str()), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?; + + if signup.user_id.is_some() { + return Ok(None); + } + + let user = user::Entity::insert(user::ActiveModel { + email_address: ActiveValue::set(Some(invite.email_address.clone())), + github_login: ActiveValue::set(user.github_login.clone()), + github_user_id: ActiveValue::set(Some(user.github_user_id)), + admin: ActiveValue::set(false), + invite_count: ActiveValue::set(user.invite_count), + invite_code: ActiveValue::set(Some(random_invite_code())), + metrics_id: ActiveValue::set(Uuid::new_v4()), + ..Default::default() + }) + .on_conflict( + OnConflict::column(user::Column::GithubLogin) + .update_columns([ + user::Column::EmailAddress, + user::Column::GithubUserId, + user::Column::Admin, + ]) + .to_owned(), + ) + .exec_with_returning(&*tx) + .await?; + + let mut signup = signup.into_active_model(); + signup.user_id = ActiveValue::set(Some(user.id)); + let signup = signup.update(&*tx).await?; + + if let Some(inviting_user_id) = signup.inviting_user_id { + let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id { + (inviting_user_id, user.id, true) + } else { + (user.id, inviting_user_id, false) + }; + + contact::Entity::insert(contact::ActiveModel { + user_id_a: ActiveValue::set(user_id_a), + user_id_b: ActiveValue::set(user_id_b), + a_to_b: ActiveValue::set(a_to_b), + should_notify: ActiveValue::set(true), + accepted: ActiveValue::set(true), + ..Default::default() + }) + .on_conflict(OnConflict::new().do_nothing().to_owned()) + .exec_without_returning(&*tx) + .await?; + } + + Ok(Some(NewUserResult { + user_id: user.id, + metrics_id: user.metrics_id.to_string(), + inviting_user_id: signup.inviting_user_id, + signup_device_id: signup.device_id, + })) + }) + .await + } + + pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> { + self.transaction(|tx| async move { + if count > 0 { + user::Entity::update_many() + .filter( + user::Column::Id + .eq(id) + .and(user::Column::InviteCode.is_null()), + ) + .set(user::ActiveModel { + invite_code: ActiveValue::set(Some(random_invite_code())), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + user::Entity::update_many() + .filter(user::Column::Id.eq(id)) + .set(user::ActiveModel { + invite_count: ActiveValue::set(count), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn get_invite_code_for_user(&self, id: UserId) -> Result> { + self.transaction(|tx| async move { + match user::Entity::find_by_id(id).one(&*tx).await? { + Some(user) if user.invite_code.is_some() => { + Ok(Some((user.invite_code.unwrap(), user.invite_count))) + } + _ => Ok(None), + } + }) + .await + } + + pub async fn get_user_for_invite_code(&self, code: &str) -> Result { + self.transaction(|tx| async move { + user::Entity::find() + .filter(user::Column::InviteCode.eq(code)) + .one(&*tx) + .await? + .ok_or_else(|| { + Error::Http( + StatusCode::NOT_FOUND, + "that invite code does not exist".to_string(), + ) + }) + }) + .await + } + + pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> { + self.transaction(|tx| async move { + signup::Entity::insert(signup::ActiveModel { + email_address: ActiveValue::set(signup.email_address.clone()), + email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), + email_confirmation_sent: ActiveValue::set(false), + platform_mac: ActiveValue::set(signup.platform_mac), + platform_windows: ActiveValue::set(signup.platform_windows), + platform_linux: ActiveValue::set(signup.platform_linux), + platform_unknown: ActiveValue::set(false), + editor_features: ActiveValue::set(Some(signup.editor_features.clone())), + programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())), + device_id: ActiveValue::set(signup.device_id.clone()), + added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list), + ..Default::default() + }) + .on_conflict( + OnConflict::column(signup::Column::EmailAddress) + .update_columns([ + signup::Column::PlatformMac, + signup::Column::PlatformWindows, + signup::Column::PlatformLinux, + signup::Column::EditorFeatures, + signup::Column::ProgrammingLanguages, + signup::Column::DeviceId, + signup::Column::AddedToMailingList, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn get_signup(&self, email_address: &str) -> Result { + self.transaction(|tx| async move { + let signup = signup::Entity::find() + .filter(signup::Column::EmailAddress.eq(email_address)) + .one(&*tx) + .await? + .ok_or_else(|| { + anyhow!("signup with email address {} doesn't exist", email_address) + })?; + + Ok(signup) + }) + .await + } + + pub async fn get_waitlist_summary(&self) -> Result { + self.transaction(|tx| async move { + let query = " + SELECT + COUNT(*) as count, + COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count, + COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count, + COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count, + COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count + FROM ( + SELECT * + FROM signups + WHERE + NOT email_confirmation_sent + ) AS unsent + "; + Ok( + WaitlistSummary::find_by_statement(Statement::from_sql_and_values( + self.pool.get_database_backend(), + query.into(), + vec![], + )) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("invalid result"))?, + ) + }) + .await + } + + pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> { + let emails = invites + .iter() + .map(|s| s.email_address.as_str()) + .collect::>(); + self.transaction(|tx| async { + let tx = tx; + signup::Entity::update_many() + .filter(signup::Column::EmailAddress.is_in(emails.iter().copied())) + .set(signup::ActiveModel { + email_confirmation_sent: ActiveValue::set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn get_unsent_invites(&self, count: usize) -> Result> { + self.transaction(|tx| async move { + Ok(signup::Entity::find() + .select_only() + .column(signup::Column::EmailAddress) + .column(signup::Column::EmailConfirmationCode) + .filter( + signup::Column::EmailConfirmationSent.eq(false).and( + signup::Column::PlatformMac + .eq(true) + .or(signup::Column::PlatformUnknown.eq(true)), + ), + ) + .order_by_asc(signup::Column::CreatedAt) + .limit(count as u64) + .into_model() + .all(&*tx) + .await?) + }) + .await + } +} + +fn random_invite_code() -> String { + nanoid::nanoid!(16) +} + +fn random_email_confirmation_code() -> String { + nanoid::nanoid!(64) +} diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs new file mode 100644 index 0000000000000000000000000000000000000000..bac0f14f8324126fe4f403887aa4eb65e4241de2 --- /dev/null +++ b/crates/collab/src/db/queries/users.rs @@ -0,0 +1,243 @@ +use super::*; + +impl Database { + pub async fn create_user( + &self, + email_address: &str, + admin: bool, + params: NewUserParams, + ) -> Result { + self.transaction(|tx| async { + let tx = tx; + let user = user::Entity::insert(user::ActiveModel { + email_address: ActiveValue::set(Some(email_address.into())), + github_login: ActiveValue::set(params.github_login.clone()), + github_user_id: ActiveValue::set(Some(params.github_user_id)), + admin: ActiveValue::set(admin), + metrics_id: ActiveValue::set(Uuid::new_v4()), + ..Default::default() + }) + .on_conflict( + OnConflict::column(user::Column::GithubLogin) + .update_column(user::Column::GithubLogin) + .to_owned(), + ) + .exec_with_returning(&*tx) + .await?; + + Ok(NewUserResult { + user_id: user.id, + metrics_id: user.metrics_id.to_string(), + signup_device_id: None, + inviting_user_id: None, + }) + }) + .await + } + + pub async fn get_user_by_id(&self, id: UserId) -> Result> { + self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) }) + .await + } + + pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { + self.transaction(|tx| async { + let tx = tx; + Ok(user::Entity::find() + .filter(user::Column::Id.is_in(ids.iter().copied())) + .all(&*tx) + .await?) + }) + .await + } + + pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + self.transaction(|tx| async move { + Ok(user::Entity::find() + .filter(user::Column::GithubLogin.eq(github_login)) + .one(&*tx) + .await?) + }) + .await + } + + pub async fn get_or_create_user_by_github_account( + &self, + github_login: &str, + github_user_id: Option, + github_email: Option<&str>, + ) -> Result> { + self.transaction(|tx| async move { + let tx = &*tx; + if let Some(github_user_id) = github_user_id { + if let Some(user_by_github_user_id) = user::Entity::find() + .filter(user::Column::GithubUserId.eq(github_user_id)) + .one(tx) + .await? + { + let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); + user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); + Ok(Some(user_by_github_user_id.update(tx).await?)) + } else if let Some(user_by_github_login) = user::Entity::find() + .filter(user::Column::GithubLogin.eq(github_login)) + .one(tx) + .await? + { + let mut user_by_github_login = user_by_github_login.into_active_model(); + user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id)); + Ok(Some(user_by_github_login.update(tx).await?)) + } else { + let user = user::Entity::insert(user::ActiveModel { + email_address: ActiveValue::set(github_email.map(|email| email.into())), + github_login: ActiveValue::set(github_login.into()), + github_user_id: ActiveValue::set(Some(github_user_id)), + admin: ActiveValue::set(false), + invite_count: ActiveValue::set(0), + invite_code: ActiveValue::set(None), + metrics_id: ActiveValue::set(Uuid::new_v4()), + ..Default::default() + }) + .exec_with_returning(&*tx) + .await?; + Ok(Some(user)) + } + } else { + Ok(user::Entity::find() + .filter(user::Column::GithubLogin.eq(github_login)) + .one(tx) + .await?) + } + }) + .await + } + + pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { + self.transaction(|tx| async move { + Ok(user::Entity::find() + .order_by_asc(user::Column::GithubLogin) + .limit(limit as u64) + .offset(page as u64 * limit as u64) + .all(&*tx) + .await?) + }) + .await + } + + pub async fn get_users_with_no_invites( + &self, + invited_by_another_user: bool, + ) -> Result> { + self.transaction(|tx| async move { + Ok(user::Entity::find() + .filter( + user::Column::InviteCount + .eq(0) + .and(if invited_by_another_user { + user::Column::InviterId.is_not_null() + } else { + user::Column::InviterId.is_null() + }), + ) + .all(&*tx) + .await?) + }) + .await + } + + pub async fn get_user_metrics_id(&self, id: UserId) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + MetricsId, + } + + self.transaction(|tx| async move { + let metrics_id: Uuid = user::Entity::find_by_id(id) + .select_only() + .column(user::Column::MetricsId) + .into_values::<_, QueryAs>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("could not find user"))?; + Ok(metrics_id.to_string()) + }) + .await + } + + pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { + self.transaction(|tx| async move { + user::Entity::update_many() + .filter(user::Column::Id.eq(id)) + .set(user::ActiveModel { + admin: ActiveValue::set(is_admin), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { + self.transaction(|tx| async move { + user::Entity::update_many() + .filter(user::Column::Id.eq(id)) + .set(user::ActiveModel { + connected_once: ActiveValue::set(connected_once), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn destroy_user(&self, id: UserId) -> Result<()> { + self.transaction(|tx| async move { + access_token::Entity::delete_many() + .filter(access_token::Column::UserId.eq(id)) + .exec(&*tx) + .await?; + user::Entity::delete_by_id(id).exec(&*tx).await?; + Ok(()) + }) + .await + } + + pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { + self.transaction(|tx| async { + let tx = tx; + let like_string = Self::fuzzy_like_string(name_query); + let query = " + SELECT users.* + FROM users + WHERE github_login ILIKE $1 + ORDER BY github_login <-> $2 + LIMIT $3 + "; + + Ok(user::Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + self.pool.get_database_backend(), + query.into(), + vec![like_string.into(), name_query.into(), limit.into()], + )) + .all(&*tx) + .await?) + }) + .await + } + + pub fn fuzzy_like_string(string: &str) -> String { + let mut result = String::with_capacity(string.len() * 2 + 1); + for c in string.chars() { + if c.is_alphanumeric() { + result.push('%'); + result.push(c); + } + } + result.push('%'); + result + } +} diff --git a/crates/collab/src/db/signup.rs b/crates/collab/src/db/signup.rs deleted file mode 100644 index 6368482de936b53b92bc27cd45ef0be0603e1f8c..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/signup.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::{SignupId, UserId}; -use sea_orm::{entity::prelude::*, FromQueryResult}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "signups")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: SignupId, - pub email_address: String, - pub email_confirmation_code: String, - pub email_confirmation_sent: bool, - pub created_at: DateTime, - pub device_id: Option, - pub user_id: Option, - pub inviting_user_id: Option, - pub platform_mac: bool, - pub platform_linux: bool, - pub platform_windows: bool, - pub platform_unknown: bool, - pub editor_features: Option>, - pub programming_languages: Option>, - pub added_to_mailing_list: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] -pub struct Invite { - pub email_address: String, - pub email_confirmation_code: String, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct NewSignup { - pub email_address: String, - pub platform_mac: bool, - pub platform_windows: bool, - pub platform_linux: bool, - pub editor_features: Vec, - pub programming_languages: Vec, - pub device_id: Option, - pub added_to_mailing_list: bool, - pub created_at: Option, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)] -pub struct WaitlistSummary { - pub count: i64, - pub linux_count: i64, - pub mac_count: i64, - pub windows_count: i64, - pub unknown_count: i64, -} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs new file mode 100644 index 0000000000000000000000000000000000000000..fe747e0d27ec1cc5b67b0bbdb55a1c5992fa27b4 --- /dev/null +++ b/crates/collab/src/db/tables.rs @@ -0,0 +1,24 @@ +pub mod access_token; +pub mod buffer; +pub mod buffer_operation; +pub mod buffer_snapshot; +pub mod channel; +pub mod channel_buffer_collaborator; +pub mod channel_member; +pub mod channel_path; +pub mod contact; +pub mod follower; +pub mod language_server; +pub mod project; +pub mod project_collaborator; +pub mod room; +pub mod room_participant; +pub mod server; +pub mod signup; +pub mod user; +pub mod worktree; +pub mod worktree_diagnostic_summary; +pub mod worktree_entry; +pub mod worktree_repository; +pub mod worktree_repository_statuses; +pub mod worktree_settings_file; diff --git a/crates/collab/src/db/access_token.rs b/crates/collab/src/db/tables/access_token.rs similarity index 94% rename from crates/collab/src/db/access_token.rs rename to crates/collab/src/db/tables/access_token.rs index f5caa4843dd43bff501ac87870e367a960dd25ac..da7392b98c444f3d83fd549525f9af4a2eb125d3 100644 --- a/crates/collab/src/db/access_token.rs +++ b/crates/collab/src/db/tables/access_token.rs @@ -1,4 +1,4 @@ -use super::{AccessTokenId, UserId}; +use crate::db::{AccessTokenId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec2ffd4a68d1958370be7632c943c26432a7c902 --- /dev/null +++ b/crates/collab/src/db/tables/buffer.rs @@ -0,0 +1,45 @@ +use crate::db::{BufferId, ChannelId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffers")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: BufferId, + pub epoch: i32, + pub channel_id: ChannelId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::buffer_operation::Entity")] + Operations, + #[sea_orm(has_many = "super::buffer_snapshot::Entity")] + Snapshots, + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Operations.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Snapshots.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_operation.rs b/crates/collab/src/db/tables/buffer_operation.rs new file mode 100644 index 0000000000000000000000000000000000000000..37bd4bedfebf1d0090e27017d19bf22ea8fa38b5 --- /dev/null +++ b/crates/collab/src/db/tables/buffer_operation.rs @@ -0,0 +1,34 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_operations")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + #[sea_orm(primary_key)] + pub lamport_timestamp: i32, + #[sea_orm(primary_key)] + pub replica_id: i32, + pub value: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_snapshot.rs b/crates/collab/src/db/tables/buffer_snapshot.rs new file mode 100644 index 0000000000000000000000000000000000000000..c9de665e438ef373e43b02cc98e7b3ec61640267 --- /dev/null +++ b/crates/collab/src/db/tables/buffer_snapshot.rs @@ -0,0 +1,31 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_snapshots")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + pub text: String, + pub operation_serialization_version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/channel.rs b/crates/collab/src/db/tables/channel.rs similarity index 61% rename from crates/collab/src/db/channel.rs rename to crates/collab/src/db/tables/channel.rs index 8834190645b1f3b5125e8d47bbcf2e3f76495ddc..05895ede4cf6b5080889cf281a1ce3651aebd1c2 100644 --- a/crates/collab/src/db/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use super::ChannelId; +use crate::db::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -15,8 +15,12 @@ impl ActiveModelBehavior for ActiveModel {} pub enum Relation { #[sea_orm(has_one = "super::room::Entity")] Room, + #[sea_orm(has_one = "super::buffer::Entity")] + Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, + #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] + BufferCollaborators, } impl Related for Entity { @@ -31,8 +35,14 @@ impl Related for Entity { } } -// impl Related for Entity { -// fn to() -> RelationDef { -// Relation::Follower.def() -// } -// } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::BufferCollaborators.def() + } +} diff --git a/crates/collab/src/db/tables/channel_buffer_collaborator.rs b/crates/collab/src/db/tables/channel_buffer_collaborator.rs new file mode 100644 index 0000000000000000000000000000000000000000..ac2637b36e3129d1c4582c869db398088b9055d9 --- /dev/null +++ b/crates/collab/src/db/tables/channel_buffer_collaborator.rs @@ -0,0 +1,43 @@ +use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId}; +use rpc::ConnectionId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_buffer_collaborators")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelBufferCollaboratorId, + pub channel_id: ChannelId, + pub connection_id: i32, + pub connection_server_id: ServerId, + pub connection_lost: bool, + pub user_id: UserId, + pub replica_id: ReplicaId, +} + +impl Model { + pub fn connection(&self) -> ConnectionId { + ConnectionId { + owner_id: self.connection_server_id.0 as u32, + id: self.connection_id as u32, + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/channel_member.rs b/crates/collab/src/db/tables/channel_member.rs similarity index 94% rename from crates/collab/src/db/channel_member.rs rename to crates/collab/src/db/tables/channel_member.rs index f0f1a852cb6a797359ef516f72f76b0a2ec627b5..ba3db5a15504e60838439cce60e5bef9a02d83bb 100644 --- a/crates/collab/src/db/channel_member.rs +++ b/crates/collab/src/db/tables/channel_member.rs @@ -1,6 +1,4 @@ -use crate::db::channel_member; - -use super::{ChannelId, ChannelMemberId, UserId}; +use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/channel_path.rs b/crates/collab/src/db/tables/channel_path.rs similarity index 93% rename from crates/collab/src/db/channel_path.rs rename to crates/collab/src/db/tables/channel_path.rs index 08ecbddb56ec49cb0ab2c5d24ddec2e2e2bff129..323f116dae35624e8d33e9737c46e1e3a17a65c3 100644 --- a/crates/collab/src/db/channel_path.rs +++ b/crates/collab/src/db/tables/channel_path.rs @@ -1,4 +1,4 @@ -use super::ChannelId; +use crate::db::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/contact.rs b/crates/collab/src/db/tables/contact.rs similarity index 59% rename from crates/collab/src/db/contact.rs rename to crates/collab/src/db/tables/contact.rs index c39d6643b3a4066eb159e8dc87f692d1d5ca3c3c..38af8b782b59f340f2aef68659cf9f606c66f67a 100644 --- a/crates/collab/src/db/contact.rs +++ b/crates/collab/src/db/tables/contact.rs @@ -1,4 +1,4 @@ -use super::{ContactId, UserId}; +use crate::db::{ContactId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -30,29 +30,3 @@ pub enum Relation { } impl ActiveModelBehavior for ActiveModel {} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Contact { - Accepted { - user_id: UserId, - should_notify: bool, - busy: bool, - }, - Outgoing { - user_id: UserId, - }, - Incoming { - user_id: UserId, - should_notify: bool, - }, -} - -impl Contact { - pub fn user_id(&self) -> UserId { - match self { - Contact::Accepted { user_id, .. } => *user_id, - Contact::Outgoing { user_id } => *user_id, - Contact::Incoming { user_id, .. } => *user_id, - } - } -} diff --git a/crates/collab/src/db/follower.rs b/crates/collab/src/db/tables/follower.rs similarity index 93% rename from crates/collab/src/db/follower.rs rename to crates/collab/src/db/tables/follower.rs index f1243dc99eb34371bdd433c431b8a12eafeadbb6..ffd45434e9006d09939a369a3010cc85393f7dde 100644 --- a/crates/collab/src/db/follower.rs +++ b/crates/collab/src/db/tables/follower.rs @@ -1,9 +1,8 @@ -use super::{FollowerId, ProjectId, RoomId, ServerId}; +use crate::db::{FollowerId, ProjectId, RoomId, ServerId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; -use serde::Serialize; -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "followers")] pub struct Model { #[sea_orm(primary_key)] diff --git a/crates/collab/src/db/language_server.rs b/crates/collab/src/db/tables/language_server.rs similarity index 96% rename from crates/collab/src/db/language_server.rs rename to crates/collab/src/db/tables/language_server.rs index d2c045c12146e1c8797c4cbd7e1ae52c52829e98..9ff8c75fc686442ad1d0cb65af66ad881c5fb6b7 100644 --- a/crates/collab/src/db/language_server.rs +++ b/crates/collab/src/db/tables/language_server.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/project.rs b/crates/collab/src/db/tables/project.rs similarity index 97% rename from crates/collab/src/db/project.rs rename to crates/collab/src/db/tables/project.rs index 5b1f9f8467853344e1c57004e875ba434117195b..8c26836046a1d37f3c87ee044074dfd786273c90 100644 --- a/crates/collab/src/db/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -1,4 +1,4 @@ -use super::{ProjectId, Result, RoomId, ServerId, UserId}; +use crate::db::{ProjectId, Result, RoomId, ServerId, UserId}; use anyhow::anyhow; use rpc::ConnectionId; use sea_orm::entity::prelude::*; diff --git a/crates/collab/src/db/project_collaborator.rs b/crates/collab/src/db/tables/project_collaborator.rs similarity index 92% rename from crates/collab/src/db/project_collaborator.rs rename to crates/collab/src/db/tables/project_collaborator.rs index 60b5f284e96bae607802ebcba6fc527c9e50b443..ac57befa6361d667ee124b1ed85557fb73d3beaa 100644 --- a/crates/collab/src/db/project_collaborator.rs +++ b/crates/collab/src/db/tables/project_collaborator.rs @@ -1,4 +1,4 @@ -use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId}; +use crate::db::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; diff --git a/crates/collab/src/db/room.rs b/crates/collab/src/db/tables/room.rs similarity index 97% rename from crates/collab/src/db/room.rs rename to crates/collab/src/db/tables/room.rs index c1624f0f2aa98ec255533e1220a4363ea690de0a..f72f7000a783570d2cac4f6aebc3cf68846ab3a3 100644 --- a/crates/collab/src/db/room.rs +++ b/crates/collab/src/db/tables/room.rs @@ -1,4 +1,4 @@ -use super::{ChannelId, RoomId}; +use crate::db::{ChannelId, RoomId}; use sea_orm::entity::prelude::*; #[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs similarity index 94% rename from crates/collab/src/db/room_participant.rs rename to crates/collab/src/db/tables/room_participant.rs index f939a3bfb8709132b1aa138ea43e21a25b0f61af..537cac9f14fcbe39e19915571e2833086c115888 100644 --- a/crates/collab/src/db/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -1,4 +1,4 @@ -use super::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; +use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/server.rs b/crates/collab/src/db/tables/server.rs similarity index 93% rename from crates/collab/src/db/server.rs rename to crates/collab/src/db/tables/server.rs index e3905f244892e8befd97f52afc119341106cb566..ea847bdf74d2d177d9628a65c47284c176233c5e 100644 --- a/crates/collab/src/db/server.rs +++ b/crates/collab/src/db/tables/server.rs @@ -1,4 +1,4 @@ -use super::ServerId; +use crate::db::ServerId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/tables/signup.rs b/crates/collab/src/db/tables/signup.rs new file mode 100644 index 0000000000000000000000000000000000000000..79d9f0580c13f981daf30283b1a9e8902ea0b995 --- /dev/null +++ b/crates/collab/src/db/tables/signup.rs @@ -0,0 +1,28 @@ +use crate::db::{SignupId, UserId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "signups")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: SignupId, + pub email_address: String, + pub email_confirmation_code: String, + pub email_confirmation_sent: bool, + pub created_at: DateTime, + pub device_id: Option, + pub user_id: Option, + pub inviting_user_id: Option, + pub platform_mac: bool, + pub platform_linux: bool, + pub platform_windows: bool, + pub platform_unknown: bool, + pub editor_features: Option>, + pub programming_languages: Option>, + pub added_to_mailing_list: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/user.rs b/crates/collab/src/db/tables/user.rs similarity index 98% rename from crates/collab/src/db/user.rs rename to crates/collab/src/db/tables/user.rs index 2d0e2fdf0b2e2c87be734fe9bbce4c38d1e80b01..402b06c2a71a164c9f9314ec0d9e4aa5519156c7 100644 --- a/crates/collab/src/db/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -1,4 +1,4 @@ -use super::UserId; +use crate::db::UserId; use sea_orm::entity::prelude::*; use serde::Serialize; diff --git a/crates/collab/src/db/worktree.rs b/crates/collab/src/db/tables/worktree.rs similarity index 97% rename from crates/collab/src/db/worktree.rs rename to crates/collab/src/db/tables/worktree.rs index fce72722db3b76f8553f5bf964bcecb9791b0e2b..46d9877dff152cdc3b30531606febec65595fec1 100644 --- a/crates/collab/src/db/worktree.rs +++ b/crates/collab/src/db/tables/worktree.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_diagnostic_summary.rs b/crates/collab/src/db/tables/worktree_diagnostic_summary.rs similarity index 95% rename from crates/collab/src/db/worktree_diagnostic_summary.rs rename to crates/collab/src/db/tables/worktree_diagnostic_summary.rs index f3dd8083fb57d9e863ea51de4e4de26b2d594a61..5620ed255f9c3a0c5ae5481052a6a21f499e1340 100644 --- a/crates/collab/src/db/worktree_diagnostic_summary.rs +++ b/crates/collab/src/db/tables/worktree_diagnostic_summary.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_entry.rs b/crates/collab/src/db/tables/worktree_entry.rs similarity index 96% rename from crates/collab/src/db/worktree_entry.rs rename to crates/collab/src/db/tables/worktree_entry.rs index cf5090ab6d09ade92e0524770200724f5bdf2d7f..81bf6e2d530d975e3659541f37a707da279eb0f4 100644 --- a/crates/collab/src/db/worktree_entry.rs +++ b/crates/collab/src/db/tables/worktree_entry.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_repository.rs b/crates/collab/src/db/tables/worktree_repository.rs similarity index 95% rename from crates/collab/src/db/worktree_repository.rs rename to crates/collab/src/db/tables/worktree_repository.rs index 116d7b3ed9ed214ef8989171bfa610c3fcb08c37..6f86ff0c2dda3e4ce310b9da056857e6ada98686 100644 --- a/crates/collab/src/db/worktree_repository.rs +++ b/crates/collab/src/db/tables/worktree_repository.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_repository_statuses.rs b/crates/collab/src/db/tables/worktree_repository_statuses.rs similarity index 95% rename from crates/collab/src/db/worktree_repository_statuses.rs rename to crates/collab/src/db/tables/worktree_repository_statuses.rs index fc15efc8161b3c3a2097ad4affdbb174eb440e7c..cab016749d6b9af2de236a6e7494d6a24972168b 100644 --- a/crates/collab/src/db/worktree_repository_statuses.rs +++ b/crates/collab/src/db/tables/worktree_repository_statuses.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_settings_file.rs b/crates/collab/src/db/tables/worktree_settings_file.rs similarity index 94% rename from crates/collab/src/db/worktree_settings_file.rs rename to crates/collab/src/db/tables/worktree_settings_file.rs index f8e87f6e599cbaaeb698bdb28cb1316c7097dc77..92348c1ec9436639dd76f0b2161c7d78fabc4da2 100644 --- a/crates/collab/src/db/worktree_settings_file.rs +++ b/crates/collab/src/db/tables/worktree_settings_file.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index dbbf162d1270133a85d942462d02085b8370809d..36a0888a62ed243904598d1386f8567fe5b821fd 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,1570 +1,143 @@ +mod buffer_tests; +mod db_tests; + use super::*; -use gpui::executor::{Background, Deterministic}; +use gpui::executor::Background; +use parking_lot::Mutex; +use sea_orm::ConnectionTrait; +use sqlx::migrate::MigrateDatabase; use std::sync::Arc; -#[cfg(test)] -use pretty_assertions::{assert_eq, assert_ne}; - -macro_rules! test_both_dbs { - ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => { - #[gpui::test] - async fn $postgres_test_name() { - let test_db = TestDb::postgres(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } - - #[gpui::test] - async fn $sqlite_test_name() { - let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } - }; +pub struct TestDb { + pub db: Option>, + pub connection: Option, } -test_both_dbs!( - test_get_users_by_ids_postgres, - test_get_users_by_ids_sqlite, - db, - { - let mut user_ids = Vec::new(); - let mut user_metric_ids = Vec::new(); - for i in 1..=4 { - let user = db - .create_user( - &format!("user{i}@example.com"), - false, - NewUserParams { - github_login: format!("user{i}"), - github_user_id: i, - invite_count: 0, - }, - ) +impl TestDb { + pub fn sqlite(background: Arc) -> Self { + let url = format!("sqlite::memory:"); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + .unwrap(); + + let mut db = runtime.block_on(async { + let mut options = ConnectOptions::new(url); + options.max_connections(5); + let db = Database::new(options, Executor::Deterministic(background)) .await .unwrap(); - user_ids.push(user.user_id); - user_metric_ids.push(user.metrics_id); - } - - assert_eq!( - db.get_users_by_ids(user_ids.clone()).await.unwrap(), - vec![ - User { - id: user_ids[0], - github_login: "user1".to_string(), - github_user_id: Some(1), - email_address: Some("user1@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[0].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[1], - github_login: "user2".to_string(), - github_user_id: Some(2), - email_address: Some("user2@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[1].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[2], - github_login: "user3".to_string(), - github_user_id: Some(3), - email_address: Some("user3@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[2].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[3], - github_login: "user4".to_string(), - github_user_id: Some(4), - email_address: Some("user4@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[3].parse().unwrap(), - ..Default::default() - } - ] - ); - } -); - -test_both_dbs!( - test_get_or_create_user_by_github_account_postgres, - test_get_or_create_user_by_github_account_sqlite, - db, - { - let user_id1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "login1".into(), - github_user_id: 101, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_id2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "login2".into(), - github_user_id: 102, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user = db - .get_or_create_user_by_github_account("login1", None, None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id1); - assert_eq!(&user.github_login, "login1"); - assert_eq!(user.github_user_id, Some(101)); + let sql = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/migrations.sqlite/20221109000000_test_schema.sql" + )); + db.pool + .execute(sea_orm::Statement::from_string( + db.pool.get_database_backend(), + sql.into(), + )) + .await + .unwrap(); + db + }); - assert!(db - .get_or_create_user_by_github_account("non-existent-login", None, None) - .await - .unwrap() - .is_none()); + db.runtime = Some(runtime); - let user = db - .get_or_create_user_by_github_account("the-new-login2", Some(102), None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id2); - assert_eq!(&user.github_login, "the-new-login2"); - assert_eq!(user.github_user_id, Some(102)); - - let user = db - .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) - .await - .unwrap() - .unwrap(); - assert_eq!(&user.github_login, "login3"); - assert_eq!(user.github_user_id, Some(103)); - assert_eq!(user.email_address, Some("user3@example.com".into())); + Self { + db: Some(Arc::new(db)), + connection: None, + } } -); -test_both_dbs!( - test_create_access_tokens_postgres, - test_create_access_tokens_sqlite, - db, - { - let user = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); - let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_1).await.unwrap(), - access_token::Model { - id: token_1, - user_id: user, - hash: "h1".into(), - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - - let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - assert!(db.get_access_token(token_1).await.is_err()); - - let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_4).await.unwrap(), - access_token::Model { - id: token_4, - user_id: user, - hash: "h4".into() - } - ); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert!(db.get_access_token(token_2).await.is_err()); - assert!(db.get_access_token(token_1).await.is_err()); - } -); + pub fn postgres(background: Arc) -> Self { + static LOCK: Mutex<()> = Mutex::new(()); -test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, { - let mut user_ids = Vec::new(); - for i in 0..3 { - user_ids.push( - db.create_user( - &format!("user{i}@example.com"), - false, - NewUserParams { - github_login: format!("user{i}"), - github_user_id: i, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id, + let _guard = LOCK.lock(); + let mut rng = StdRng::from_entropy(); + let url = format!( + "postgres://postgres@localhost/zed-test-{}", + rng.gen::() ); - } - - let user_1 = user_ids[0]; - let user_2 = user_ids[1]; - let user_3 = user_ids[2]; - - // User starts with no contacts - assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]); - - // User requests a contact. Both users see the pending request. - db.send_contact_request(user_1, user_2).await.unwrap(); - assert!(!db.has_contact(user_1, user_2).await.unwrap()); - assert!(!db.has_contact(user_2, user_1).await.unwrap()); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Outgoing { user_id: user_2 }], - ); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Incoming { - user_id: user_1, - should_notify: true - }] - ); - - // User 2 dismisses the contact request notification without accepting or rejecting. - // We shouldn't notify them again. - db.dismiss_contact_notification(user_1, user_2) - .await - .unwrap_err(); - db.dismiss_contact_notification(user_2, user_1) - .await - .unwrap(); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Incoming { - user_id: user_1, - should_notify: false - }] - ); - - // User can't accept their own contact request - db.respond_to_contact_request(user_1, user_2, true) - .await - .unwrap_err(); - - // User accepts a contact request. Both users see the contact. - db.respond_to_contact_request(user_2, user_1, true) - .await - .unwrap(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Accepted { - user_id: user_2, - should_notify: true, - busy: false, - }], - ); - assert!(db.has_contact(user_1, user_2).await.unwrap()); - assert!(db.has_contact(user_2, user_1).await.unwrap()); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - should_notify: false, - busy: false, - }] - ); - - // Users cannot re-request existing contacts. - db.send_contact_request(user_1, user_2).await.unwrap_err(); - db.send_contact_request(user_2, user_1).await.unwrap_err(); - - // Users can't dismiss notifications of them accepting other users' requests. - db.dismiss_contact_notification(user_2, user_1) - .await - .unwrap_err(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Accepted { - user_id: user_2, - should_notify: true, - busy: false, - }] - ); - - // Users can dismiss notifications of other users accepting their requests. - db.dismiss_contact_notification(user_1, user_2) - .await - .unwrap(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Accepted { - user_id: user_2, - should_notify: false, - busy: false, - }] - ); - - // Users send each other concurrent contact requests and - // see that they are immediately accepted. - db.send_contact_request(user_1, user_3).await.unwrap(); - db.send_contact_request(user_3, user_1).await.unwrap(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_2, - should_notify: false, - busy: false, - }, - Contact::Accepted { - user_id: user_3, - should_notify: false, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user_3).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - should_notify: false, - busy: false, - }], - ); - - // User declines a contact request. Both users see that it is gone. - db.send_contact_request(user_2, user_3).await.unwrap(); - db.respond_to_contact_request(user_3, user_2, false) - .await - .unwrap(); - assert!(!db.has_contact(user_2, user_3).await.unwrap()); - assert!(!db.has_contact(user_3, user_2).await.unwrap()); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - should_notify: false, - busy: false, - }] - ); - assert_eq!( - db.get_contacts(user_3).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - should_notify: false, - busy: false, - }], - ); -}); - -test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, { - let NewUserResult { - user_id: user1, - metrics_id: metrics_id1, - .. - } = db - .create_user( - "person1@example.com", - false, - NewUserParams { - github_login: "person1".into(), - github_user_id: 101, - invite_count: 5, - }, - ) - .await - .unwrap(); - let NewUserResult { - user_id: user2, - metrics_id: metrics_id2, - .. - } = db - .create_user( - "person2@example.com", - false, - NewUserParams { - github_login: "person2".into(), - github_user_id: 102, - invite_count: 5, - }, - ) - .await - .unwrap(); - - assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1); - assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2); - assert_eq!(metrics_id1.len(), 36); - assert_eq!(metrics_id2.len(), 36); - assert_ne!(metrics_id1, metrics_id2); -}); - -test_both_dbs!( - test_project_count_postgres, - test_project_count_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; - - let user1 = db - .create_user( - &format!("admin@example.com"), - true, - NewUserParams { - github_login: "admin".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user2 = db - .create_user( - &format!("user@example.com"), - false, - NewUserParams { - github_login: "user".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() .unwrap(); - let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") + let mut db = runtime.block_on(async { + sqlx::Postgres::create_database(&url) .await - .unwrap() - .id, - ); - db.call( - room_id, - user1.user_id, - ConnectionId { owner_id, id: 0 }, - user2.user_id, - None, - ) - .await - .unwrap(); - db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); - - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); - - // Projects shared by admins aren't counted. - db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); - - db.leave_room(ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - } -); + .expect("failed to create test db"); + let mut options = ConnectOptions::new(url); + options + .max_connections(5) + .idle_timeout(Duration::from_secs(0)); + let db = Database::new(options, Executor::Deterministic(background)) + .await + .unwrap(); + let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"); + db.migrate(Path::new(migrations_path), false).await.unwrap(); + db + }); -#[test] -fn test_fuzzy_like_string() { - assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%"); - assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%"); - assert_eq!(Database::fuzzy_like_string(" z "), "%z%"); -} + db.runtime = Some(runtime); -#[gpui::test] -async fn test_fuzzy_search_users() { - let test_db = TestDb::postgres(build_background_executor()); - let db = test_db.db(); - for (i, github_login) in [ - "California", - "colorado", - "oregon", - "washington", - "florida", - "delaware", - "rhode-island", - ] - .into_iter() - .enumerate() - { - db.create_user( - &format!("{github_login}@example.com"), - false, - NewUserParams { - github_login: github_login.into(), - github_user_id: i as i32, - invite_count: 0, - }, - ) - .await - .unwrap(); + Self { + db: Some(Arc::new(db)), + connection: None, + } } - assert_eq!( - fuzzy_search_user_names(db, "clr").await, - &["colorado", "California"] - ); - assert_eq!( - fuzzy_search_user_names(db, "ro").await, - &["rhode-island", "colorado", "oregon"], - ); - - async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec { - db.fuzzy_search_users(query, 10) - .await - .unwrap() - .into_iter() - .map(|user| user.github_login) - .collect::>() + pub fn db(&self) -> &Arc { + self.db.as_ref().unwrap() } } -#[gpui::test] -async fn test_invite_codes() { - let test_db = TestDb::postgres(build_background_executor()); - let db = test_db.db(); - - let NewUserResult { user_id: user1, .. } = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - - // Initially, user 1 has no invite code - assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None); - - // Setting invite count to 0 when no code is assigned does not assign a new code - db.set_invite_count_for_user(user1, 0).await.unwrap(); - assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none()); - - // User 1 creates an invite code that can be used twice. - db.set_invite_count_for_user(user1, 2).await.unwrap(); - let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 2); - - // User 2 redeems the invite code and becomes a contact of user 1. - let user2_invite = db - .create_invite_from_code( - &invite_code, - "user2@example.com", - Some("user-2-device-id"), - true, - ) - .await - .unwrap(); - let NewUserResult { - user_id: user2, - inviting_user_id, - signup_device_id, - metrics_id, - } = db - .create_user_from_invite( - &user2_invite, - NewUserParams { - github_login: "user2".into(), - github_user_id: 2, - invite_count: 7, - }, - ) - .await - .unwrap() - .unwrap(); - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 1); - assert_eq!(inviting_user_id, Some(user1)); - assert_eq!(signup_device_id.unwrap(), "user-2-device-id"); - assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }] - ); - assert_eq!( - db.get_contacts(user2).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: false, - busy: false, - }] - ); - assert!(db.has_contact(user1, user2).await.unwrap()); - assert!(db.has_contact(user2, user1).await.unwrap()); - assert_eq!( - db.get_invite_code_for_user(user2).await.unwrap().unwrap().1, - 7 - ); - - // User 3 redeems the invite code and becomes a contact of user 1. - let user3_invite = db - .create_invite_from_code(&invite_code, "user3@example.com", None, true) - .await - .unwrap(); - let NewUserResult { - user_id: user3, - inviting_user_id, - signup_device_id, - .. - } = db - .create_user_from_invite( - &user3_invite, - NewUserParams { - github_login: "user-3".into(), - github_user_id: 3, - invite_count: 3, - }, - ) - .await - .unwrap() - .unwrap(); - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 0); - assert_eq!(inviting_user_id, Some(user1)); - assert!(signup_device_id.is_none()); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [ - Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user3, - should_notify: true, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user3).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: false, - busy: false, - }] - ); - assert!(db.has_contact(user1, user3).await.unwrap()); - assert!(db.has_contact(user3, user1).await.unwrap()); - assert_eq!( - db.get_invite_code_for_user(user3).await.unwrap().unwrap().1, - 3 - ); - - // Trying to reedem the code for the third time results in an error. - db.create_invite_from_code( - &invite_code, - "user4@example.com", - Some("user-4-device-id"), - true, - ) - .await - .unwrap_err(); - - // Invite count can be updated after the code has been created. - db.set_invite_count_for_user(user1, 2).await.unwrap(); - let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0 - assert_eq!(invite_count, 2); - - // User 4 can now redeem the invite code and becomes a contact of user 1. - let user4_invite = db - .create_invite_from_code( - &invite_code, - "user4@example.com", - Some("user-4-device-id"), - true, - ) - .await - .unwrap(); - let user4 = db - .create_user_from_invite( - &user4_invite, - NewUserParams { - github_login: "user-4".into(), - github_user_id: 4, - invite_count: 5, - }, - ) - .await - .unwrap() - .unwrap() - .user_id; - - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 1); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [ - Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user3, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user4, - should_notify: true, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user4).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: false, - busy: false, - }] - ); - assert!(db.has_contact(user1, user4).await.unwrap()); - assert!(db.has_contact(user4, user1).await.unwrap()); - assert_eq!( - db.get_invite_code_for_user(user4).await.unwrap().unwrap().1, - 5 - ); - - // An existing user cannot redeem invite codes. - db.create_invite_from_code( - &invite_code, - "user2@example.com", - Some("user-2-device-id"), - true, - ) - .await - .unwrap_err(); - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 1); - - // A newer user can invite an existing one via a different email address - // than the one they used to sign up. - let user5 = db - .create_user( - "user5@example.com", - false, - NewUserParams { - github_login: "user5".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - db.set_invite_count_for_user(user5, 5).await.unwrap(); - let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap(); - let user5_invite_to_user1 = db - .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true) - .await - .unwrap(); - let user1_2 = db - .create_user_from_invite( - &user5_invite_to_user1, - NewUserParams { - github_login: "user1".into(), - github_user_id: 1, - invite_count: 5, - }, - ) - .await - .unwrap() - .unwrap() - .user_id; - assert_eq!(user1_2, user1); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [ - Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user3, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user4, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user5, - should_notify: false, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user5).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: true, - busy: false, - }] - ); - assert!(db.has_contact(user1, user5).await.unwrap()); - assert!(db.has_contact(user5, user1).await.unwrap()); -} - -test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { - let a_id = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let b_id = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); - - // Make sure that people cannot read channels they haven't been invited to - assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); - - db.invite_channel_member(zed_id, b_id, a_id, false) - .await - .unwrap(); - - db.respond_to_channel_invite(zed_id, b_id, true) - .await - .unwrap(); - - let crdb_id = db - .create_channel("crdb", Some(zed_id), "2", a_id) - .await - .unwrap(); - let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id), "3", a_id) - .await - .unwrap(); - let replace_id = db - .create_channel("replace", Some(zed_id), "4", a_id) - .await - .unwrap(); - - let mut members = db.get_channel_members(replace_id).await.unwrap(); - members.sort(); - assert_eq!(members, &[a_id, b_id]); - - let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); - let cargo_id = db - .create_channel("cargo", Some(rust_id), "6", a_id) - .await - .unwrap(); - - let cargo_ra_id = db - .create_channel("cargo-ra", Some(cargo_id), "7", a_id) - .await - .unwrap(); - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: rust_id, - name: "rust".to_string(), - parent_id: None, - }, - Channel { - id: cargo_id, - name: "cargo".to_string(), - parent_id: Some(rust_id), - }, - Channel { - id: cargo_ra_id, - name: "cargo-ra".to_string(), - parent_id: Some(cargo_id), - } - ] - ); - - let result = db.get_channels_for_user(b_id).await.unwrap(); - assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - ] - ); - - // Update member permissions - let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await; - assert!(set_subchannel_admin.is_err()); - let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; - assert!(set_channel_admin.is_ok()); - - let result = db.get_channels_for_user(b_id).await.unwrap(); - assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - ] - ); - - // Remove a single channel - db.remove_channel(crdb_id, a_id).await.unwrap(); - assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none()); - - // Remove a channel tree - let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap(); - channel_ids.sort(); - assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]); - assert_eq!(user_ids, &[a_id]); - - assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); -}); - -test_both_dbs!( - test_joining_channels_postgres, - test_joining_channels_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let channel_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); - - // can join a room with membership to its channel - let joined_room = db - .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(joined_room.room.participants.len(), 1); - - drop(joined_room); - // cannot join a room without membership to its channel - assert!(db - .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) - .await - .is_err()); - } -); - -test_both_dbs!( - test_channel_invites_postgres, - test_channel_invites_sqlite, - db, - { - db.create_server("test").await.unwrap(); - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user_3 = db - .create_user( - "user3@example.com", - false, - NewUserParams { - github_login: "user3".into(), - github_user_id: 7, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let channel_1_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - - let channel_1_2 = db - .create_root_channel("channel_2", "2", user_1) - .await - .unwrap(); - - db.invite_channel_member(channel_1_1, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, true) - .await - .unwrap(); - - let user_2_invites = db - .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); - - assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); - - let user_3_invites = db - .get_channel_invites_for_user(user_3) // -> [channel_1_1] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); - - assert_eq!(user_3_invites, &[channel_1_1]); - - let members = db - .get_channel_member_details(channel_1_1, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: false, - }, - proto::ChannelMember { - user_id: user_3.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: true, - }, - ] - ); - - db.respond_to_channel_invite(channel_1_1, user_2, true) - .await - .unwrap(); - - let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), "1", user_1) - .await - .unwrap(); - - let members = db - .get_channel_member_details(channel_1_3, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - admin: false, - }, - ] - ); - } -); - -test_both_dbs!( - test_channel_renames_postgres, - test_channel_renames_sqlite, - db, - { - db.create_server("test").await.unwrap(); - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); - - db.rename_channel(zed_id, user_1, "#zed-archive") - .await - .unwrap(); - - let zed_archive_id = zed_id; - - let (channel, _) = db - .get_channel(zed_archive_id, user_1) - .await - .unwrap() - .unwrap(); - assert_eq!(channel.name, "zed-archive"); - - let non_permissioned_rename = db - .rename_channel(zed_archive_id, user_2, "hacked-lol") - .await; - assert!(non_permissioned_rename.is_err()); - - let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; - assert!(bad_name_rename.is_err()) - } -); - -#[gpui::test] -async fn test_multiple_signup_overwrite() { - let test_db = TestDb::postgres(build_background_executor()); - let db = test_db.db(); - - let email_address = "user_1@example.com".to_string(); - - let initial_signup_created_at_milliseconds = 0; - - let initial_signup = NewSignup { - email_address: email_address.clone(), - platform_mac: false, - platform_linux: true, - platform_windows: false, - editor_features: vec!["speed".into()], - programming_languages: vec!["rust".into(), "c".into()], - device_id: Some(format!("device_id")), - added_to_mailing_list: false, - created_at: Some( - DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(), - ), - }; - - db.create_signup(&initial_signup).await.unwrap(); - - let initial_signup_from_db = db.get_signup(&email_address).await.unwrap(); - - assert_eq!( - initial_signup_from_db.clone(), - signup::Model { - email_address: initial_signup.email_address, - platform_mac: initial_signup.platform_mac, - platform_linux: initial_signup.platform_linux, - platform_windows: initial_signup.platform_windows, - editor_features: Some(initial_signup.editor_features), - programming_languages: Some(initial_signup.programming_languages), - added_to_mailing_list: initial_signup.added_to_mailing_list, - ..initial_signup_from_db +#[macro_export] +macro_rules! test_both_dbs { + ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { + #[gpui::test] + async fn $postgres_test_name() { + let test_db = crate::db::TestDb::postgres( + gpui::executor::Deterministic::new(0).build_background(), + ); + $test_name(test_db.db()).await; } - ); - let subsequent_signup = NewSignup { - email_address: email_address.clone(), - platform_mac: true, - platform_linux: false, - platform_windows: true, - editor_features: vec!["git integration".into(), "clean design".into()], - programming_languages: vec!["d".into(), "elm".into()], - device_id: Some(format!("different_device_id")), - added_to_mailing_list: true, - // subsequent signup happens next day - created_at: Some( - DateTime::from_timestamp_millis( - initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24), - ) - .unwrap(), - ), - }; - - db.create_signup(&subsequent_signup).await.unwrap(); - - let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap(); - - assert_eq!( - subsequent_signup_from_db.clone(), - signup::Model { - platform_mac: subsequent_signup.platform_mac, - platform_linux: subsequent_signup.platform_linux, - platform_windows: subsequent_signup.platform_windows, - editor_features: Some(subsequent_signup.editor_features), - programming_languages: Some(subsequent_signup.programming_languages), - device_id: subsequent_signup.device_id, - added_to_mailing_list: subsequent_signup.added_to_mailing_list, - // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line - created_at: initial_signup_from_db.created_at, - ..subsequent_signup_from_db + #[gpui::test] + async fn $sqlite_test_name() { + let test_db = + crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background()); + $test_name(test_db.db()).await; } - ); + }; } -#[gpui::test] -async fn test_signups() { - let test_db = TestDb::postgres(build_background_executor()); - let db = test_db.db(); - - let usernames = (0..8).map(|i| format!("person-{i}")).collect::>(); - - let all_signups = usernames - .iter() - .enumerate() - .map(|(i, username)| NewSignup { - email_address: format!("{username}@example.com"), - platform_mac: true, - platform_linux: i % 2 == 0, - platform_windows: i % 4 == 0, - editor_features: vec!["speed".into()], - programming_languages: vec!["rust".into(), "c".into()], - device_id: Some(format!("device_id_{i}")), - added_to_mailing_list: i != 0, // One user failed to subscribe - created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive - }) - .collect::>(); - - // people sign up on the waitlist - for signup in &all_signups { - // users can sign up multiple times without issues - for _ in 0..2 { - db.create_signup(&signup).await.unwrap(); +impl Drop for TestDb { + fn drop(&mut self) { + let db = self.db.take().unwrap(); + if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() { + db.runtime.as_ref().unwrap().block_on(async { + use util::ResultExt; + let query = " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE + pg_stat_activity.datname = current_database() AND + pid <> pg_backend_pid(); + "; + db.pool + .execute(sea_orm::Statement::from_string( + db.pool.get_database_backend(), + query.into(), + )) + .await + .log_err(); + sqlx::Postgres::drop_database(db.options.get_url()) + .await + .log_err(); + }) } } - - assert_eq!( - db.get_waitlist_summary().await.unwrap(), - WaitlistSummary { - count: 8, - mac_count: 8, - linux_count: 4, - windows_count: 2, - unknown_count: 0, - } - ); - - // retrieve the next batch of signup emails to send - let signups_batch1 = db.get_unsent_invites(3).await.unwrap(); - let addresses = signups_batch1 - .iter() - .map(|s| &s.email_address) - .collect::>(); - assert_eq!( - addresses, - &[ - all_signups[0].email_address.as_str(), - all_signups[1].email_address.as_str(), - all_signups[2].email_address.as_str() - ] - ); - assert_ne!( - signups_batch1[0].email_confirmation_code, - signups_batch1[1].email_confirmation_code - ); - - // the waitlist isn't updated until we record that the emails - // were successfully sent. - let signups_batch = db.get_unsent_invites(3).await.unwrap(); - assert_eq!(signups_batch, signups_batch1); - - // once the emails go out, we can retrieve the next batch - // of signups. - db.record_sent_invites(&signups_batch1).await.unwrap(); - let signups_batch2 = db.get_unsent_invites(3).await.unwrap(); - let addresses = signups_batch2 - .iter() - .map(|s| &s.email_address) - .collect::>(); - assert_eq!( - addresses, - &[ - all_signups[3].email_address.as_str(), - all_signups[4].email_address.as_str(), - all_signups[5].email_address.as_str() - ] - ); - - // the sent invites are excluded from the summary. - assert_eq!( - db.get_waitlist_summary().await.unwrap(), - WaitlistSummary { - count: 5, - mac_count: 5, - linux_count: 2, - windows_count: 1, - unknown_count: 0, - } - ); - - // user completes the signup process by providing their - // github account. - let NewUserResult { - user_id, - inviting_user_id, - signup_device_id, - .. - } = db - .create_user_from_invite( - &Invite { - ..signups_batch1[0].clone() - }, - NewUserParams { - github_login: usernames[0].clone(), - github_user_id: 0, - invite_count: 5, - }, - ) - .await - .unwrap() - .unwrap(); - let user = db.get_user_by_id(user_id).await.unwrap().unwrap(); - assert!(inviting_user_id.is_none()); - assert_eq!(user.github_login, usernames[0]); - assert_eq!( - user.email_address, - Some(all_signups[0].email_address.clone()) - ); - assert_eq!(user.invite_count, 5); - assert_eq!(signup_device_id.unwrap(), "device_id_0"); - - // cannot redeem the same signup again. - assert!(db - .create_user_from_invite( - &Invite { - email_address: signups_batch1[0].email_address.clone(), - email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), - }, - NewUserParams { - github_login: "some-other-github_account".into(), - github_user_id: 1, - invite_count: 5, - }, - ) - .await - .unwrap() - .is_none()); - - // cannot redeem a signup with the wrong confirmation code. - db.create_user_from_invite( - &Invite { - email_address: signups_batch1[1].email_address.clone(), - email_confirmation_code: "the-wrong-code".to_string(), - }, - NewUserParams { - github_login: usernames[1].clone(), - github_user_id: 2, - invite_count: 5, - }, - ) - .await - .unwrap_err(); -} - -fn build_background_executor() -> Arc { - Deterministic::new(0).build_background() } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..e71748b88b0571c2ccd7840924b8fc325cd368b7 --- /dev/null +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -0,0 +1,165 @@ +use super::*; +use crate::test_both_dbs; +use language::proto; +use text::Buffer; + +test_both_dbs!( + test_channel_buffers, + test_channel_buffers_postgres, + test_channel_buffers_sqlite +); + +async fn test_channel_buffers(db: &Arc) { + let a_id = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let b_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + // This user will not be a part of the channel + let c_id = db + .create_user( + "user_c@example.com", + false, + NewUserParams { + github_login: "user_c".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let owner_id = db.create_server("production").await.unwrap().0 as u32; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + db.invite_channel_member(zed_id, b_id, a_id, false) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + + let connection_id_a = ConnectionId { owner_id, id: 1 }; + let _ = db + .join_channel_buffer(zed_id, a_id, connection_id_a) + .await + .unwrap(); + + let mut buffer_a = Buffer::new(0, 0, "".to_string()); + let mut operations = Vec::new(); + operations.push(buffer_a.edit([(0..0, "hello world")])); + operations.push(buffer_a.edit([(5..5, ", cruel")])); + operations.push(buffer_a.edit([(0..5, "goodbye")])); + operations.push(buffer_a.undo().unwrap().1); + assert_eq!(buffer_a.text(), "hello, cruel world"); + + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + + db.update_channel_buffer(zed_id, a_id, &operations) + .await + .unwrap(); + + let connection_id_b = ConnectionId { owner_id, id: 2 }; + let buffer_response_b = db + .join_channel_buffer(zed_id, b_id, connection_id_b) + .await + .unwrap(); + + let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text); + buffer_b + .apply_ops(buffer_response_b.operations.into_iter().map(|operation| { + let operation = proto::deserialize_operation(operation).unwrap(); + if let language::Operation::Buffer(operation) = operation { + operation + } else { + unreachable!() + } + })) + .unwrap(); + + assert_eq!(buffer_b.text(), "hello, cruel world"); + + // Ensure that C fails to open the buffer + assert!(db + .join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 }) + .await + .is_err()); + + // Ensure that both collaborators have shown up + assert_eq!( + buffer_response_b.collaborators, + &[ + rpc::proto::Collaborator { + user_id: a_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), + replica_id: 0, + }, + rpc::proto::Collaborator { + user_id: b_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), + replica_id: 1, + } + ] + ); + + // Ensure that get_channel_buffer_collaborators works + let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + assert_eq!(zed_collaborats, &[a_id, b_id]); + + let collaborators = db + .leave_channel_buffer(zed_id, connection_id_b) + .await + .unwrap(); + + assert_eq!(collaborators, &[connection_id_a],); + + let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); + let _ = db + .join_channel_buffer(cargo_id, a_id, connection_id_a) + .await + .unwrap(); + + db.leave_channel_buffers(connection_id_a).await.unwrap(); + + let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap(); + assert_eq!(zed_collaborators, &[]); + assert_eq!(cargo_collaborators, &[]); + + // When everyone has left the channel, the operations are collapsed into + // a new base text. + let buffer_response_b = db + .join_channel_buffer(zed_id, b_id, connection_id_b) + .await + .unwrap(); + assert_eq!(buffer_response_b.base_text, "hello, cruel world"); + assert_eq!(buffer_response_b.operations, &[]); +} diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..fc31ee7c4d4aee8dddc46bf6cc0e77fc89e4dd39 --- /dev/null +++ b/crates/collab/src/db/tests/db_tests.rs @@ -0,0 +1,1573 @@ +use super::*; +use crate::test_both_dbs; +use gpui::executor::{Background, Deterministic}; +use pretty_assertions::{assert_eq, assert_ne}; +use std::sync::Arc; +use tests::TestDb; + +test_both_dbs!( + test_get_users, + test_get_users_by_ids_postgres, + test_get_users_by_ids_sqlite +); + +async fn test_get_users(db: &Arc) { + let mut user_ids = Vec::new(); + let mut user_metric_ids = Vec::new(); + for i in 1..=4 { + let user = db + .create_user( + &format!("user{i}@example.com"), + false, + NewUserParams { + github_login: format!("user{i}"), + github_user_id: i, + invite_count: 0, + }, + ) + .await + .unwrap(); + user_ids.push(user.user_id); + user_metric_ids.push(user.metrics_id); + } + + assert_eq!( + db.get_users_by_ids(user_ids.clone()).await.unwrap(), + vec![ + User { + id: user_ids[0], + github_login: "user1".to_string(), + github_user_id: Some(1), + email_address: Some("user1@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[0].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[1], + github_login: "user2".to_string(), + github_user_id: Some(2), + email_address: Some("user2@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[1].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[2], + github_login: "user3".to_string(), + github_user_id: Some(3), + email_address: Some("user3@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[2].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[3], + github_login: "user4".to_string(), + github_user_id: Some(4), + email_address: Some("user4@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[3].parse().unwrap(), + ..Default::default() + } + ] + ); +} + +test_both_dbs!( + test_get_or_create_user_by_github_account, + test_get_or_create_user_by_github_account_postgres, + test_get_or_create_user_by_github_account_sqlite +); + +async fn test_get_or_create_user_by_github_account(db: &Arc) { + let user_id1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "login1".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_id2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "login2".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user = db + .get_or_create_user_by_github_account("login1", None, None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id1); + assert_eq!(&user.github_login, "login1"); + assert_eq!(user.github_user_id, Some(101)); + + assert!(db + .get_or_create_user_by_github_account("non-existent-login", None, None) + .await + .unwrap() + .is_none()); + + let user = db + .get_or_create_user_by_github_account("the-new-login2", Some(102), None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id2); + assert_eq!(&user.github_login, "the-new-login2"); + assert_eq!(user.github_user_id, Some(102)); + + let user = db + .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) + .await + .unwrap() + .unwrap(); + assert_eq!(&user.github_login, "login3"); + assert_eq!(user.github_user_id, Some(103)); + assert_eq!(user.email_address, Some("user3@example.com".into())); +} + +test_both_dbs!( + test_create_access_tokens, + test_create_access_tokens_postgres, + test_create_access_tokens_sqlite +); + +async fn test_create_access_tokens(db: &Arc) { + let user = db + .create_user( + "u1@example.com", + false, + NewUserParams { + github_login: "u1".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); + let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_1).await.unwrap(), + access_token::Model { + id: token_1, + user_id: user, + hash: "h1".into(), + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + + let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + assert!(db.get_access_token(token_1).await.is_err()); + + let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_4).await.unwrap(), + access_token::Model { + id: token_4, + user_id: user, + hash: "h4".into() + } + ); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert!(db.get_access_token(token_2).await.is_err()); + assert!(db.get_access_token(token_1).await.is_err()); +} + +test_both_dbs!( + test_add_contacts, + test_add_contacts_postgres, + test_add_contacts_sqlite +); + +async fn test_add_contacts(db: &Arc) { + let mut user_ids = Vec::new(); + for i in 0..3 { + user_ids.push( + db.create_user( + &format!("user{i}@example.com"), + false, + NewUserParams { + github_login: format!("user{i}"), + github_user_id: i, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id, + ); + } + + let user_1 = user_ids[0]; + let user_2 = user_ids[1]; + let user_3 = user_ids[2]; + + // User starts with no contacts + assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]); + + // User requests a contact. Both users see the pending request. + db.send_contact_request(user_1, user_2).await.unwrap(); + assert!(!db.has_contact(user_1, user_2).await.unwrap()); + assert!(!db.has_contact(user_2, user_1).await.unwrap()); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + &[Contact::Outgoing { user_id: user_2 }], + ); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + &[Contact::Incoming { + user_id: user_1, + should_notify: true + }] + ); + + // User 2 dismisses the contact request notification without accepting or rejecting. + // We shouldn't notify them again. + db.dismiss_contact_notification(user_1, user_2) + .await + .unwrap_err(); + db.dismiss_contact_notification(user_2, user_1) + .await + .unwrap(); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + &[Contact::Incoming { + user_id: user_1, + should_notify: false + }] + ); + + // User can't accept their own contact request + db.respond_to_contact_request(user_1, user_2, true) + .await + .unwrap_err(); + + // User accepts a contact request. Both users see the contact. + db.respond_to_contact_request(user_2, user_1, true) + .await + .unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + &[Contact::Accepted { + user_id: user_2, + should_notify: true, + busy: false, + }], + ); + assert!(db.has_contact(user_1, user_2).await.unwrap()); + assert!(db.has_contact(user_2, user_1).await.unwrap()); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + &[Contact::Accepted { + user_id: user_1, + should_notify: false, + busy: false, + }] + ); + + // Users cannot re-request existing contacts. + db.send_contact_request(user_1, user_2).await.unwrap_err(); + db.send_contact_request(user_2, user_1).await.unwrap_err(); + + // Users can't dismiss notifications of them accepting other users' requests. + db.dismiss_contact_notification(user_2, user_1) + .await + .unwrap_err(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + &[Contact::Accepted { + user_id: user_2, + should_notify: true, + busy: false, + }] + ); + + // Users can dismiss notifications of other users accepting their requests. + db.dismiss_contact_notification(user_1, user_2) + .await + .unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + &[Contact::Accepted { + user_id: user_2, + should_notify: false, + busy: false, + }] + ); + + // Users send each other concurrent contact requests and + // see that they are immediately accepted. + db.send_contact_request(user_1, user_3).await.unwrap(); + db.send_contact_request(user_3, user_1).await.unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + &[ + Contact::Accepted { + user_id: user_2, + should_notify: false, + busy: false, + }, + Contact::Accepted { + user_id: user_3, + should_notify: false, + busy: false, + } + ] + ); + assert_eq!( + db.get_contacts(user_3).await.unwrap(), + &[Contact::Accepted { + user_id: user_1, + should_notify: false, + busy: false, + }], + ); + + // User declines a contact request. Both users see that it is gone. + db.send_contact_request(user_2, user_3).await.unwrap(); + db.respond_to_contact_request(user_3, user_2, false) + .await + .unwrap(); + assert!(!db.has_contact(user_2, user_3).await.unwrap()); + assert!(!db.has_contact(user_3, user_2).await.unwrap()); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + &[Contact::Accepted { + user_id: user_1, + should_notify: false, + busy: false, + }] + ); + assert_eq!( + db.get_contacts(user_3).await.unwrap(), + &[Contact::Accepted { + user_id: user_1, + should_notify: false, + busy: false, + }], + ); +} + +test_both_dbs!( + test_metrics_id, + test_metrics_id_postgres, + test_metrics_id_sqlite +); + +async fn test_metrics_id(db: &Arc) { + let NewUserResult { + user_id: user1, + metrics_id: metrics_id1, + .. + } = db + .create_user( + "person1@example.com", + false, + NewUserParams { + github_login: "person1".into(), + github_user_id: 101, + invite_count: 5, + }, + ) + .await + .unwrap(); + let NewUserResult { + user_id: user2, + metrics_id: metrics_id2, + .. + } = db + .create_user( + "person2@example.com", + false, + NewUserParams { + github_login: "person2".into(), + github_user_id: 102, + invite_count: 5, + }, + ) + .await + .unwrap(); + + assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1); + assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2); + assert_eq!(metrics_id1.len(), 36); + assert_eq!(metrics_id2.len(), 36); + assert_ne!(metrics_id1, metrics_id2); +} + +test_both_dbs!( + test_project_count, + test_project_count_postgres, + test_project_count_sqlite +); + +async fn test_project_count(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + let user1 = db + .create_user( + &format!("admin@example.com"), + true, + NewUserParams { + github_login: "admin".into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .unwrap(); + let user2 = db + .create_user( + &format!("user@example.com"), + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap(); + + let room_id = RoomId::from_proto( + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") + .await + .unwrap() + .id, + ); + db.call( + room_id, + user1.user_id, + ConnectionId { owner_id, id: 0 }, + user2.user_id, + None, + ) + .await + .unwrap(); + db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); + + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); + + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + + // Projects shared by admins aren't counted. + db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + + db.leave_room(ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); +} + +#[test] +fn test_fuzzy_like_string() { + assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%"); + assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%"); + assert_eq!(Database::fuzzy_like_string(" z "), "%z%"); +} + +#[gpui::test] +async fn test_fuzzy_search_users() { + let test_db = TestDb::postgres(build_background_executor()); + let db = test_db.db(); + for (i, github_login) in [ + "California", + "colorado", + "oregon", + "washington", + "florida", + "delaware", + "rhode-island", + ] + .into_iter() + .enumerate() + { + db.create_user( + &format!("{github_login}@example.com"), + false, + NewUserParams { + github_login: github_login.into(), + github_user_id: i as i32, + invite_count: 0, + }, + ) + .await + .unwrap(); + } + + assert_eq!( + fuzzy_search_user_names(db, "clr").await, + &["colorado", "California"] + ); + assert_eq!( + fuzzy_search_user_names(db, "ro").await, + &["rhode-island", "colorado", "oregon"], + ); + + async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec { + db.fuzzy_search_users(query, 10) + .await + .unwrap() + .into_iter() + .map(|user| user.github_login) + .collect::>() + } +} + +#[gpui::test] +async fn test_invite_codes() { + let test_db = TestDb::postgres(build_background_executor()); + let db = test_db.db(); + + let NewUserResult { user_id: user1, .. } = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .unwrap(); + + // Initially, user 1 has no invite code + assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None); + + // Setting invite count to 0 when no code is assigned does not assign a new code + db.set_invite_count_for_user(user1, 0).await.unwrap(); + assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none()); + + // User 1 creates an invite code that can be used twice. + db.set_invite_count_for_user(user1, 2).await.unwrap(); + let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); + assert_eq!(invite_count, 2); + + // User 2 redeems the invite code and becomes a contact of user 1. + let user2_invite = db + .create_invite_from_code( + &invite_code, + "user2@example.com", + Some("user-2-device-id"), + true, + ) + .await + .unwrap(); + let NewUserResult { + user_id: user2, + inviting_user_id, + signup_device_id, + metrics_id, + } = db + .create_user_from_invite( + &user2_invite, + NewUserParams { + github_login: "user2".into(), + github_user_id: 2, + invite_count: 7, + }, + ) + .await + .unwrap() + .unwrap(); + let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); + assert_eq!(invite_count, 1); + assert_eq!(inviting_user_id, Some(user1)); + assert_eq!(signup_device_id.unwrap(), "user-2-device-id"); + assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id); + assert_eq!( + db.get_contacts(user1).await.unwrap(), + [Contact::Accepted { + user_id: user2, + should_notify: true, + busy: false, + }] + ); + assert_eq!( + db.get_contacts(user2).await.unwrap(), + [Contact::Accepted { + user_id: user1, + should_notify: false, + busy: false, + }] + ); + assert!(db.has_contact(user1, user2).await.unwrap()); + assert!(db.has_contact(user2, user1).await.unwrap()); + assert_eq!( + db.get_invite_code_for_user(user2).await.unwrap().unwrap().1, + 7 + ); + + // User 3 redeems the invite code and becomes a contact of user 1. + let user3_invite = db + .create_invite_from_code(&invite_code, "user3@example.com", None, true) + .await + .unwrap(); + let NewUserResult { + user_id: user3, + inviting_user_id, + signup_device_id, + .. + } = db + .create_user_from_invite( + &user3_invite, + NewUserParams { + github_login: "user-3".into(), + github_user_id: 3, + invite_count: 3, + }, + ) + .await + .unwrap() + .unwrap(); + let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); + assert_eq!(invite_count, 0); + assert_eq!(inviting_user_id, Some(user1)); + assert!(signup_device_id.is_none()); + assert_eq!( + db.get_contacts(user1).await.unwrap(), + [ + Contact::Accepted { + user_id: user2, + should_notify: true, + busy: false, + }, + Contact::Accepted { + user_id: user3, + should_notify: true, + busy: false, + } + ] + ); + assert_eq!( + db.get_contacts(user3).await.unwrap(), + [Contact::Accepted { + user_id: user1, + should_notify: false, + busy: false, + }] + ); + assert!(db.has_contact(user1, user3).await.unwrap()); + assert!(db.has_contact(user3, user1).await.unwrap()); + assert_eq!( + db.get_invite_code_for_user(user3).await.unwrap().unwrap().1, + 3 + ); + + // Trying to reedem the code for the third time results in an error. + db.create_invite_from_code( + &invite_code, + "user4@example.com", + Some("user-4-device-id"), + true, + ) + .await + .unwrap_err(); + + // Invite count can be updated after the code has been created. + db.set_invite_count_for_user(user1, 2).await.unwrap(); + let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); + assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0 + assert_eq!(invite_count, 2); + + // User 4 can now redeem the invite code and becomes a contact of user 1. + let user4_invite = db + .create_invite_from_code( + &invite_code, + "user4@example.com", + Some("user-4-device-id"), + true, + ) + .await + .unwrap(); + let user4 = db + .create_user_from_invite( + &user4_invite, + NewUserParams { + github_login: "user-4".into(), + github_user_id: 4, + invite_count: 5, + }, + ) + .await + .unwrap() + .unwrap() + .user_id; + + let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); + assert_eq!(invite_count, 1); + assert_eq!( + db.get_contacts(user1).await.unwrap(), + [ + Contact::Accepted { + user_id: user2, + should_notify: true, + busy: false, + }, + Contact::Accepted { + user_id: user3, + should_notify: true, + busy: false, + }, + Contact::Accepted { + user_id: user4, + should_notify: true, + busy: false, + } + ] + ); + assert_eq!( + db.get_contacts(user4).await.unwrap(), + [Contact::Accepted { + user_id: user1, + should_notify: false, + busy: false, + }] + ); + assert!(db.has_contact(user1, user4).await.unwrap()); + assert!(db.has_contact(user4, user1).await.unwrap()); + assert_eq!( + db.get_invite_code_for_user(user4).await.unwrap().unwrap().1, + 5 + ); + + // An existing user cannot redeem invite codes. + db.create_invite_from_code( + &invite_code, + "user2@example.com", + Some("user-2-device-id"), + true, + ) + .await + .unwrap_err(); + let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); + assert_eq!(invite_count, 1); + + // A newer user can invite an existing one via a different email address + // than the one they used to sign up. + let user5 = db + .create_user( + "user5@example.com", + false, + NewUserParams { + github_login: "user5".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + db.set_invite_count_for_user(user5, 5).await.unwrap(); + let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap(); + let user5_invite_to_user1 = db + .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true) + .await + .unwrap(); + let user1_2 = db + .create_user_from_invite( + &user5_invite_to_user1, + NewUserParams { + github_login: "user1".into(), + github_user_id: 1, + invite_count: 5, + }, + ) + .await + .unwrap() + .unwrap() + .user_id; + assert_eq!(user1_2, user1); + assert_eq!( + db.get_contacts(user1).await.unwrap(), + [ + Contact::Accepted { + user_id: user2, + should_notify: true, + busy: false, + }, + Contact::Accepted { + user_id: user3, + should_notify: true, + busy: false, + }, + Contact::Accepted { + user_id: user4, + should_notify: true, + busy: false, + }, + Contact::Accepted { + user_id: user5, + should_notify: false, + busy: false, + } + ] + ); + assert_eq!( + db.get_contacts(user5).await.unwrap(), + [Contact::Accepted { + user_id: user1, + should_notify: true, + busy: false, + }] + ); + assert!(db.has_contact(user1, user5).await.unwrap()); + assert!(db.has_contact(user5, user1).await.unwrap()); +} + +test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); + +async fn test_channels(db: &Arc) { + let a_id = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let b_id = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + // Make sure that people cannot read channels they haven't been invited to + assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); + + db.invite_channel_member(zed_id, b_id, a_id, false) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + + let crdb_id = db + .create_channel("crdb", Some(zed_id), "2", a_id) + .await + .unwrap(); + let livestreaming_id = db + .create_channel("livestreaming", Some(zed_id), "3", a_id) + .await + .unwrap(); + let replace_id = db + .create_channel("replace", Some(zed_id), "4", a_id) + .await + .unwrap(); + + let mut members = db.get_channel_members(replace_id).await.unwrap(); + members.sort(); + assert_eq!(members, &[a_id, b_id]); + + let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); + let cargo_id = db + .create_channel("cargo", Some(rust_id), "6", a_id) + .await + .unwrap(); + + let cargo_ra_id = db + .create_channel("cargo-ra", Some(cargo_id), "7", a_id) + .await + .unwrap(); + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: rust_id, + name: "rust".to_string(), + parent_id: None, + }, + Channel { + id: cargo_id, + name: "cargo".to_string(), + parent_id: Some(rust_id), + }, + Channel { + id: cargo_ra_id, + name: "cargo-ra".to_string(), + parent_id: Some(cargo_id), + } + ] + ); + + let result = db.get_channels_for_user(b_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + ] + ); + + // Update member permissions + let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await; + assert!(set_subchannel_admin.is_err()); + let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; + assert!(set_channel_admin.is_ok()); + + let result = db.get_channels_for_user(b_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + ] + ); + + // Remove a single channel + db.remove_channel(crdb_id, a_id).await.unwrap(); + assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none()); + + // Remove a channel tree + let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap(); + channel_ids.sort(); + assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]); + assert_eq!(user_ids, &[a_id]); + + assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); +} + +test_both_dbs!( + test_joining_channels, + test_joining_channels_postgres, + test_joining_channels_sqlite +); + +async fn test_joining_channels(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); + + // can join a room with membership to its channel + let joined_room = db + .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(joined_room.room.participants.len(), 1); + + drop(joined_room); + // cannot join a room without membership to its channel + assert!(db + .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) + .await + .is_err()); +} + +test_both_dbs!( + test_channel_invites, + test_channel_invites_postgres, + test_channel_invites_sqlite +); + +async fn test_channel_invites(db: &Arc) { + db.create_server("test").await.unwrap(); + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_3 = db + .create_user( + "user3@example.com", + false, + NewUserParams { + github_login: "user3".into(), + github_user_id: 7, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + + let channel_1_2 = db + .create_root_channel("channel_2", "2", user_1) + .await + .unwrap(); + + db.invite_channel_member(channel_1_1, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_2, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_1, user_3, user_1, true) + .await + .unwrap(); + + let user_2_invites = db + .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); + + let user_3_invites = db + .get_channel_invites_for_user(user_3) // -> [channel_1_1] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_3_invites, &[channel_1_1]); + + let members = db + .get_channel_member_details(channel_1_1, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: false, + }, + proto::ChannelMember { + user_id: user_3.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: true, + }, + ] + ); + + db.respond_to_channel_invite(channel_1_1, user_2, true) + .await + .unwrap(); + + let channel_1_3 = db + .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .await + .unwrap(); + + let members = db + .get_channel_member_details(channel_1_3, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + admin: false, + }, + ] + ); +} + +test_both_dbs!( + test_channel_renames, + test_channel_renames_postgres, + test_channel_renames_sqlite +); + +async fn test_channel_renames(db: &Arc) { + db.create_server("test").await.unwrap(); + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + + db.rename_channel(zed_id, user_1, "#zed-archive") + .await + .unwrap(); + + let zed_archive_id = zed_id; + + let (channel, _) = db + .get_channel(zed_archive_id, user_1) + .await + .unwrap() + .unwrap(); + assert_eq!(channel.name, "zed-archive"); + + let non_permissioned_rename = db + .rename_channel(zed_archive_id, user_2, "hacked-lol") + .await; + assert!(non_permissioned_rename.is_err()); + + let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; + assert!(bad_name_rename.is_err()) +} + +#[gpui::test] +async fn test_multiple_signup_overwrite() { + let test_db = TestDb::postgres(build_background_executor()); + let db = test_db.db(); + + let email_address = "user_1@example.com".to_string(); + + let initial_signup_created_at_milliseconds = 0; + + let initial_signup = NewSignup { + email_address: email_address.clone(), + platform_mac: false, + platform_linux: true, + platform_windows: false, + editor_features: vec!["speed".into()], + programming_languages: vec!["rust".into(), "c".into()], + device_id: Some(format!("device_id")), + added_to_mailing_list: false, + created_at: Some( + DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(), + ), + }; + + db.create_signup(&initial_signup).await.unwrap(); + + let initial_signup_from_db = db.get_signup(&email_address).await.unwrap(); + + assert_eq!( + initial_signup_from_db.clone(), + signup::Model { + email_address: initial_signup.email_address, + platform_mac: initial_signup.platform_mac, + platform_linux: initial_signup.platform_linux, + platform_windows: initial_signup.platform_windows, + editor_features: Some(initial_signup.editor_features), + programming_languages: Some(initial_signup.programming_languages), + added_to_mailing_list: initial_signup.added_to_mailing_list, + ..initial_signup_from_db + } + ); + + let subsequent_signup = NewSignup { + email_address: email_address.clone(), + platform_mac: true, + platform_linux: false, + platform_windows: true, + editor_features: vec!["git integration".into(), "clean design".into()], + programming_languages: vec!["d".into(), "elm".into()], + device_id: Some(format!("different_device_id")), + added_to_mailing_list: true, + // subsequent signup happens next day + created_at: Some( + DateTime::from_timestamp_millis( + initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24), + ) + .unwrap(), + ), + }; + + db.create_signup(&subsequent_signup).await.unwrap(); + + let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap(); + + assert_eq!( + subsequent_signup_from_db.clone(), + signup::Model { + platform_mac: subsequent_signup.platform_mac, + platform_linux: subsequent_signup.platform_linux, + platform_windows: subsequent_signup.platform_windows, + editor_features: Some(subsequent_signup.editor_features), + programming_languages: Some(subsequent_signup.programming_languages), + device_id: subsequent_signup.device_id, + added_to_mailing_list: subsequent_signup.added_to_mailing_list, + // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line + created_at: initial_signup_from_db.created_at, + ..subsequent_signup_from_db + } + ); +} + +#[gpui::test] +async fn test_signups() { + let test_db = TestDb::postgres(build_background_executor()); + let db = test_db.db(); + + let usernames = (0..8).map(|i| format!("person-{i}")).collect::>(); + + let all_signups = usernames + .iter() + .enumerate() + .map(|(i, username)| NewSignup { + email_address: format!("{username}@example.com"), + platform_mac: true, + platform_linux: i % 2 == 0, + platform_windows: i % 4 == 0, + editor_features: vec!["speed".into()], + programming_languages: vec!["rust".into(), "c".into()], + device_id: Some(format!("device_id_{i}")), + added_to_mailing_list: i != 0, // One user failed to subscribe + created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive + }) + .collect::>(); + + // people sign up on the waitlist + for signup in &all_signups { + // users can sign up multiple times without issues + for _ in 0..2 { + db.create_signup(&signup).await.unwrap(); + } + } + + assert_eq!( + db.get_waitlist_summary().await.unwrap(), + WaitlistSummary { + count: 8, + mac_count: 8, + linux_count: 4, + windows_count: 2, + unknown_count: 0, + } + ); + + // retrieve the next batch of signup emails to send + let signups_batch1 = db.get_unsent_invites(3).await.unwrap(); + let addresses = signups_batch1 + .iter() + .map(|s| &s.email_address) + .collect::>(); + assert_eq!( + addresses, + &[ + all_signups[0].email_address.as_str(), + all_signups[1].email_address.as_str(), + all_signups[2].email_address.as_str() + ] + ); + assert_ne!( + signups_batch1[0].email_confirmation_code, + signups_batch1[1].email_confirmation_code + ); + + // the waitlist isn't updated until we record that the emails + // were successfully sent. + let signups_batch = db.get_unsent_invites(3).await.unwrap(); + assert_eq!(signups_batch, signups_batch1); + + // once the emails go out, we can retrieve the next batch + // of signups. + db.record_sent_invites(&signups_batch1).await.unwrap(); + let signups_batch2 = db.get_unsent_invites(3).await.unwrap(); + let addresses = signups_batch2 + .iter() + .map(|s| &s.email_address) + .collect::>(); + assert_eq!( + addresses, + &[ + all_signups[3].email_address.as_str(), + all_signups[4].email_address.as_str(), + all_signups[5].email_address.as_str() + ] + ); + + // the sent invites are excluded from the summary. + assert_eq!( + db.get_waitlist_summary().await.unwrap(), + WaitlistSummary { + count: 5, + mac_count: 5, + linux_count: 2, + windows_count: 1, + unknown_count: 0, + } + ); + + // user completes the signup process by providing their + // github account. + let NewUserResult { + user_id, + inviting_user_id, + signup_device_id, + .. + } = db + .create_user_from_invite( + &Invite { + ..signups_batch1[0].clone() + }, + NewUserParams { + github_login: usernames[0].clone(), + github_user_id: 0, + invite_count: 5, + }, + ) + .await + .unwrap() + .unwrap(); + let user = db.get_user_by_id(user_id).await.unwrap().unwrap(); + assert!(inviting_user_id.is_none()); + assert_eq!(user.github_login, usernames[0]); + assert_eq!( + user.email_address, + Some(all_signups[0].email_address.clone()) + ); + assert_eq!(user.invite_count, 5); + assert_eq!(signup_device_id.unwrap(), "device_id_0"); + + // cannot redeem the same signup again. + assert!(db + .create_user_from_invite( + &Invite { + email_address: signups_batch1[0].email_address.clone(), + email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), + }, + NewUserParams { + github_login: "some-other-github_account".into(), + github_user_id: 1, + invite_count: 5, + }, + ) + .await + .unwrap() + .is_none()); + + // cannot redeem a signup with the wrong confirmation code. + db.create_user_from_invite( + &Invite { + email_address: signups_batch1[1].email_address.clone(), + email_confirmation_code: "the-wrong-code".to_string(), + }, + NewUserParams { + github_login: usernames[1].clone(), + github_user_id: 2, + invite_count: 5, + }, + ) + .await + .unwrap_err(); +} + +fn build_background_executor() -> Arc { + Deterministic::new(0).build_background() +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 521aa3e7b45b7be2683a4312395b8328df2892b0..18587c2ba8f590f3646a3a7de6d4121ffe35586d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -35,8 +35,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, + self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -248,6 +248,9 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) + .add_request_handler(join_channel_buffer) + .add_request_handler(leave_channel_buffer) + .add_message_handler(update_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -851,6 +854,10 @@ async fn connection_lost( .await .trace_err(); + leave_channel_buffers_for_session(&session) + .await + .trace_err(); + futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { leave_room_for_session(&session).await.trace_err(); @@ -866,6 +873,8 @@ async fn connection_lost( } } update_user_contacts(session.user_id, &session).await?; + + } _ = teardown.changed().fuse() => {} } @@ -2478,6 +2487,104 @@ async fn join_channel( Ok(()) } +async fn join_channel_buffer( + request: proto::JoinChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let open_response = db + .join_channel_buffer(channel_id, session.user_id, session.connection_id) + .await?; + + let replica_id = open_response.replica_id; + let collaborators = open_response.collaborators.clone(); + + response.send(open_response)?; + + let update = AddChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + collaborator: Some(proto::Collaborator { + user_id: session.user_id.to_proto(), + peer_id: Some(session.connection_id.into()), + replica_id, + }), + }; + channel_buffer_updated( + session.connection_id, + collaborators + .iter() + .filter_map(|collaborator| Some(collaborator.peer_id?.into())), + &update, + &session.peer, + ); + + Ok(()) +} + +async fn update_channel_buffer( + request: proto::UpdateChannelBuffer, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators = db + .update_channel_buffer(channel_id, session.user_id, &request.operations) + .await?; + + channel_buffer_updated( + session.connection_id, + collaborators, + &proto::UpdateChannelBuffer { + channel_id: channel_id.to_proto(), + operations: request.operations, + }, + &session.peer, + ); + Ok(()) +} + +async fn leave_channel_buffer( + request: proto::LeaveChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators_to_notify = db + .leave_channel_buffer(channel_id, session.connection_id) + .await?; + + response.send(Ack {})?; + + channel_buffer_updated( + session.connection_id, + collaborators_to_notify, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + + Ok(()) +} + +fn channel_buffer_updated( + sender_id: ConnectionId, + collaborators: impl IntoIterator, + message: &T, + peer: &Peer, +) { + broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| { + peer.send(peer_id.into(), message.clone()) + }); +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session @@ -2803,6 +2910,28 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { Ok(()) } +async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { + let left_channel_buffers = session + .db() + .await + .leave_channel_buffers(session.connection_id) + .await?; + + for (channel_id, connections) in left_channel_buffers { + channel_buffer_updated( + session.connection_id, + connections, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + } + + Ok(()) +} + fn project_left(project: &db::LeftProject, session: &Session) { for connection_id in &project.connection_ids { if project.host_user_id == session.user_id { diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 46cbcb0213af963446158b096b5051a84f7ab94b..25f059c0aa959fe20116dd7596682adf6a02f945 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,14 +1,14 @@ use crate::{ - db::{NewUserParams, TestDb, UserId}, + db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, rpc::{Server, CLEANUP_TIMEOUT}, AppState, }; use anyhow::anyhow; use call::{ActiveCall, Room}; +use channel::ChannelStore; use client::{ - self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError, - UserStore, + self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -31,6 +31,7 @@ use std::{ use util::http::FakeHttpClient; use workspace::Workspace; +mod channel_buffer_tests; mod channel_tests; mod integration_tests; mod randomized_integration_tests; @@ -210,6 +211,7 @@ impl TestServer { workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); + channel::init(&client); }); client diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ac4dbbd3f1c606b52fb445a1c08ca4f1e8c6883 --- /dev/null +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -0,0 +1,426 @@ +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use call::ActiveCall; +use channel::Channel; +use client::UserId; +use collab_ui::channel_view::ChannelView; +use collections::HashMap; +use futures::future; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use rpc::{proto, RECEIVE_TIMEOUT}; +use serde_json::json; +use std::sync::Arc; + +#[gpui::test] +async fn test_core_channel_buffers( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let zed_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + // Client A joins the channel buffer + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + + // Client A edits the buffer + let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); + + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "hello world")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(5..5, ", cruel")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..5, "goodbye")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); + deterministic.run_until_parked(); + + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + + // Client B joins the channel buffer + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client B sees the correct text, and then edits it + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), + buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) + ); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([(7..12, "beautiful")], None, cx) + }); + + // Both A and B see the new edit + deterministic.run_until_parked(); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + + // Client A closes the channel buffer. + cx_a.update(|_| drop(channel_buffer_a)); + deterministic.run_until_parked(); + + // Client B sees that client A is gone from the channel buffer. + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // Client A rejoins the channel buffer + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channels, cx| { + channels.open_channel_buffer(zed_id, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Sanity test, make sure we saw A rejoining + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + &buffer.collaborators(), + &[client_b.user_id(), client_a.user_id()], + ); + }); + + // Client A loses connection. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + // Client B observes A disconnect + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // TODO: + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open +} + +#[gpui::test] +async fn test_channel_buffer_replica_ids( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "zed", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + + // Clients A and B join a channel. + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + + // Clients A, B, and C join a channel buffer + // C first so that the replica IDs in the project and the channel buffer are different + let channel_buffer_c = client_c + .channel_store() + .update(cx_c, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + // Client B shares a project + client_b + .fs() + .insert_tree("/dir", json!({ "file.txt": "contents" })) + .await; + let (project_b, _) = client_b.build_local_project("/dir", cx_b).await; + let shared_project_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + // Client A joins the project + let project_a = client_a.build_remote_project(shared_project_id, cx_a).await; + deterministic.run_until_parked(); + + // Client C is in a separate project. + client_c.fs().insert_tree("/dir", json!({})).await; + let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await; + + // Note that each user has a different replica id in the projects vs the + // channel buffer. + channel_buffer_a.read_with(cx_a, |channel_buffer, cx| { + assert_eq!(project_a.read(cx).replica_id(), 1); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2); + }); + channel_buffer_b.read_with(cx_b, |channel_buffer, cx| { + assert_eq!(project_b.read(cx).replica_id(), 0); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1); + }); + channel_buffer_c.read_with(cx_c, |channel_buffer, cx| { + // C is not in the project + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); + }); + + let channel_window_a = + cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx)); + let channel_window_b = + cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx)); + let channel_window_c = cx_c.add_window(|cx| { + ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx) + }); + + let channel_view_a = channel_window_a.root(cx_a); + let channel_view_b = channel_window_b.root(cx_b); + let channel_view_c = channel_window_c.root(cx_c); + + // For clients A and B, the replica ids in the channel buffer are mapped + // so that they match the same users' replica ids in their shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>() + ); + }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>(), + ) + }); + + // Client C only sees themself, as they're not part of any shared project + channel_view_c.read_with(cx_c, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(0, 0)].into_iter().collect::>(), + ); + }); + + // Client C joins the project that clients A and B are in. + active_call_c + .update(cx_c, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + let project_c = client_c.build_remote_project(shared_project_id, cx_c).await; + deterministic.run_until_parked(); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.replica_id(), 2); + }); + + // For clients A and B, client C's replica id in the channel buffer is + // now mapped to their replica id in the shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>() + ); + }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>(), + ) + }); +} + +#[gpui::test] +async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mut TestAppContext) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + + let channel_buffer_1 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_2 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_3 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + + // All concurrent tasks for opening a channel buffer return the same model handle. + let (channel_buffer_1, channel_buffer_2, channel_buffer_3) = + future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) + .await + .unwrap(); + let model_id = channel_buffer_1.id(); + assert_eq!(channel_buffer_1, channel_buffer_2); + assert_eq!(channel_buffer_1, channel_buffer_3); + + channel_buffer_1.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "hello")], None, cx); + }) + }); + deterministic.run_until_parked(); + + cx_a.update(|_| { + drop(channel_buffer_1); + drop(channel_buffer_2); + drop(channel_buffer_3); + }); + deterministic.run_until_parked(); + + // The channel buffer can be reopened after dropping it. + let channel_buffer = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + assert_ne!(channel_buffer.id(), model_id); + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, _| { + assert_eq!(buffer.text(), "hello"); + }) + }); +} + +#[gpui::test] +async fn test_channel_buffer_disconnect( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + channel_buffer_a.update(cx_a, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); + + deterministic.run_until_parked(); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + deterministic.run_until_parked(); + + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_id) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Channel buffer observed the deletion + channel_buffer_b.update(cx_b, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); +} + +#[track_caller] +fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { + assert_eq!( + collaborators + .into_iter() + .map(|collaborator| collaborator.user_id) + .collect::>(), + ids.into_iter().map(|id| id.unwrap()).collect::>() + ); +} + +fn buffer_text(channel_buffer: &ModelHandle, cx: &mut TestAppContext) -> String { + channel_buffer.read_with(cx, |buffer, _| buffer.text()) +} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 06cf3607c0555a606b2409a6b419d5df12794121..b54b4d349ba54e5c23e048cd81b292a10566445d 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -3,7 +3,8 @@ use crate::{ tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; -use client::{ChannelId, ChannelMembership, ChannelStore, User}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::User; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; use std::sync::Arc; @@ -798,7 +799,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // Sanity check + // Sanity check, B has the invitation assert_channel_invitations( client_b.channel_store(), cx_b, @@ -810,6 +811,7 @@ async fn test_lost_channel_creation( }], ); + // A creates a subchannel while the invite is still pending. let subchannel_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { @@ -840,7 +842,7 @@ async fn test_lost_channel_creation( ], ); - // Accept the invite + // Client B accepts the invite client_b .channel_store() .update(cx_b, |channel_store, _| { @@ -851,7 +853,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // B should now see the channel + // Client B should now see the channel assert_channels( client_b.channel_store(), cx_b, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index a03e2ff16fc2fa325aeff79c058ded7c7dc5575c..9bee8d434cd9ccb6d0fa252e2badc49be99a54d4 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4163,6 +4163,7 @@ async fn test_collaborating_with_completion( capabilities: lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), ..Default::default() }), ..Default::default() diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 471608c43ec86a1afce15bc5552bdd58b7d0cd86..1ecb4b84227b066ab959997c128bfd96cec6055d 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -26,6 +26,7 @@ auto_update = { path = "../auto_update" } db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } +channel = { path = "../channel" } clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } @@ -33,6 +34,7 @@ editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +language = { path = "../language" } menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..bb1e840ffca40087519d324db5a2ae9a62a38222 --- /dev/null +++ b/crates/collab_ui/src/channel_view.rs @@ -0,0 +1,351 @@ +use anyhow::{anyhow, Result}; +use channel::{ + channel_buffer::{self, ChannelBuffer}, + ChannelId, +}; +use client::proto; +use clock::ReplicaId; +use collections::HashMap; +use editor::Editor; +use gpui::{ + actions, + elements::{ChildView, Label}, + geometry::vector::Vector2F, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, +}; +use project::Project; +use std::any::Any; +use workspace::{ + item::{FollowableItem, Item, ItemHandle}, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId, +}; + +actions!(channel_view, [Deploy]); + +pub(crate) fn init(cx: &mut AppContext) { + register_followable_item::(cx) +} + +pub struct ChannelView { + pub editor: ViewHandle, + project: ModelHandle, + channel_buffer: ModelHandle, + remote_id: Option, + _editor_event_subscription: Subscription, +} + +impl ChannelView { + pub fn open( + channel_id: ChannelId, + pane: ViewHandle, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = workspace.app_state().channel_store.clone(); + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await?; + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx); + }) + }); + + pane.update(&mut cx, |pane, cx| { + pane.items_of_type::() + .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer) + .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx))) + }) + .ok_or_else(|| anyhow!("pane was dropped")) + }) + } + + pub fn new( + project: ModelHandle, + channel_buffer: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); + let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + + cx.subscribe(&project, Self::handle_project_event).detach(); + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + + let this = Self { + editor, + project, + channel_buffer, + remote_id: None, + _editor_event_subscription, + }; + this.refresh_replica_id_map(cx); + this + } + + fn handle_project_event( + &mut self, + _: ModelHandle, + event: &project::Event, + cx: &mut ViewContext, + ) { + match event { + project::Event::RemoteIdChanged(_) => {} + project::Event::DisconnectedFromHost => {} + project::Event::Closed => {} + project::Event::CollaboratorUpdated { .. } => {} + project::Event::CollaboratorLeft(_) => {} + project::Event::CollaboratorJoined(_) => {} + _ => return, + } + self.refresh_replica_id_map(cx); + } + + fn handle_channel_buffer_event( + &mut self, + _: ModelHandle, + event: &channel_buffer::Event, + cx: &mut ViewContext, + ) { + match event { + channel_buffer::Event::CollaboratorsChanged => { + self.refresh_replica_id_map(cx); + } + channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + } + } + + /// Build a mapping of channel buffer replica ids to the corresponding + /// replica ids in the current project. + /// + /// Using this mapping, a given user can be displayed with the same color + /// in the channel buffer as in other files in the project. Users who are + /// in the channel buffer but not the project will not have a color. + fn refresh_replica_id_map(&self, cx: &mut ViewContext) { + let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default(); + let project = self.project.read(cx); + let channel_buffer = self.channel_buffer.read(cx); + project_replica_ids_by_channel_buffer_replica_id + .insert(channel_buffer.replica_id(cx), project.replica_id()); + project_replica_ids_by_channel_buffer_replica_id.extend( + channel_buffer + .collaborators() + .iter() + .filter_map(|channel_buffer_collaborator| { + project + .collaborators() + .values() + .find_map(|project_collaborator| { + (project_collaborator.user_id == channel_buffer_collaborator.user_id) + .then_some(( + channel_buffer_collaborator.replica_id as ReplicaId, + project_collaborator.replica_id, + )) + }) + }), + ); + + self.editor.update(cx, |editor, cx| { + editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx) + }); + } +} + +impl Entity for ChannelView { + type Event = editor::Event; +} + +impl View for ChannelView { + fn ui_name() -> &'static str { + "ChannelView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(self.editor.as_any(), cx).into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(self.editor.as_any()) + } + } +} + +impl Item for ChannelView { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let channel_name = &self.channel_buffer.read(cx).channel().name; + let label = if self.channel_buffer.read(cx).is_connected() { + format!("#{}", channel_name) + } else { + format!("#{} (disconnected)", channel_name) + }; + Label::new(label, style.label.to_owned()).into_any() + } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { + Some(Self::new( + self.project.clone(), + self.channel_buffer.clone(), + cx, + )) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + true + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &ViewHandle) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.editor.read(cx).pixel_position_of_cursor(cx) + } +} + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + let channel = self.channel_buffer.read(cx).channel(); + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { + channel_id: channel.id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, + }, + )) + } + + fn from_state_proto( + pane: ViewHandle, + workspace: ViewHandle, + remote_id: workspace::ViewId, + state: &mut Option, + cx: &mut AppContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; + + let open = ChannelView::open(state.channel_id, pane, workspace, cx); + + Some(cx.spawn(|mut cx| async move { + let this = open.await?; + + let task = this + .update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + }) + .ok_or_else(|| anyhow!("window was closed"))?; + + if let Some(task) = task { + task.await?; + } + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } + + fn apply_update_proto( + &mut self, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) + } + + fn set_leader_replica_id( + &mut self, + leader_replica_id: Option, + cx: &mut ViewContext, + ) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_replica_id(leader_replica_id, cx) + }) + } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + Editor::should_unfollow_on_event(event, cx) + } +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0e7bd5f929c8e0102ba8d78d9bcefac6c831c5e4..411a3a2598c052dfb92f4df438effa1c1e57270a 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -4,10 +4,8 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; -use client::{ - proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, -}; - +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; +use client::{proto::PeerId, Client, Contact, User, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -16,16 +14,18 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, + Stack, Svg, }, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, - serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, + serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; @@ -35,7 +35,7 @@ use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; -use theme::IconButton; +use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -43,7 +43,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + channel_view::{self, ChannelView}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -53,6 +56,11 @@ struct RemoveChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct ToggleCollapse { + channel_id: u64, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { channel_id: u64, @@ -73,7 +81,21 @@ struct RenameChannel { channel_id: u64, } -actions!(collab_panel, [ToggleFocus, Remove, Secondary]); +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct OpenChannelBuffer { + channel_id: u64, +} + +actions!( + collab_panel, + [ + ToggleFocus, + Remove, + Secondary, + CollapseSelectedChannel, + ExpandSelectedChannel + ] +); impl_actions!( collab_panel, @@ -82,16 +104,19 @@ impl_actions!( NewChannel, InviteMembers, ManageMembers, - RenameChannel + RenameChannel, + ToggleCollapse, + OpenChannelBuffer ] ); -const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; +const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); contact_finder::init(cx); channel_modal::init(cx); + channel_view::init(cx); cx.add_action(CollabPanel::cancel); cx.add_action(CollabPanel::select_next); @@ -105,6 +130,10 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::manage_members); cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); + cx.add_action(CollabPanel::toggle_channel_collapsed); + cx.add_action(CollabPanel::collapse_selected_channel); + cx.add_action(CollabPanel::expand_selected_channel); + cx.add_action(CollabPanel::open_channel_buffer); } #[derive(Debug)] @@ -147,6 +176,7 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, + collapsed_channels: Vec, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -154,6 +184,7 @@ pub struct CollabPanel { #[derive(Serialize, Deserialize)] struct SerializedChannelsPanel { width: Option, + collapsed_channels: Vec, } #[derive(Debug)] @@ -198,6 +229,9 @@ enum ListEntry { channel: Arc, depth: usize, }, + ChannelNotes { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -341,6 +375,12 @@ impl CollabPanel { return channel_row; } } + ListEntry::ChannelNotes { channel_id } => this.render_channel_notes( + *channel_id, + &theme.collab_panel, + is_selected, + cx, + ), ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), this.channel_store.clone(), @@ -398,6 +438,7 @@ impl CollabPanel { subscriptions: Vec::default(), match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], + collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, @@ -464,7 +505,7 @@ impl CollabPanel { cx.spawn(|mut cx| async move { let serialized_panel = if let Some(panel) = cx .background() - .spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) }) + .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) .await .log_err() .flatten() @@ -479,6 +520,7 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; + panel.collapsed_channels = serialized_panel.collapsed_channels; cx.notify(); }); } @@ -489,12 +531,16 @@ impl CollabPanel { fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; + let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background().spawn( async move { KEY_VALUE_STORE .write_kvp( - CHANNELS_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { width })?, + COLLABORATION_PANEL_KEY.into(), + serde_json::to_string(&SerializedChannelsPanel { + width, + collapsed_channels, + })?, ) .await?; anyhow::Ok(()) @@ -518,6 +564,10 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); + if let Some(channel_id) = room.channel_id() { + self.entries.push(ListEntry::ChannelNotes { channel_id }) + } + // Populate the active user. if let Some(user) = user_store.current_user() { self.match_candidates.clear(); @@ -657,10 +707,24 @@ impl CollabPanel { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } + let mut collapse_depth = None; for mat in matches { let (depth, channel) = channel_store.channel_at_index(mat.candidate_id).unwrap(); + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } + match &self.channel_editing_state { Some(ChannelEditingState::Create { parent_id, .. }) if *parent_id == Some(channel.id) => @@ -963,25 +1027,19 @@ impl CollabPanel { ) -> AnyElement { enum JoinProject {} - let font_cache = cx.font_cache(); - let host_avatar_height = theme + let host_avatar_width = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; let project_name = if worktree_root_names.is_empty() { "untitled".to_string() } else { worktree_root_names.join(", ") }; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -989,39 +1047,20 @@ impl CollabPanel { .style_for(mouse_state); Flex::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = - bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - })) + Svg::new("icons/file_icons/folder.svg") + .with_color(theme.channel_hash.color) .constrained() - .with_width(host_avatar_height), + .with_width(theme.channel_hash.width) + .aligned() + .left(), ) .with_child( Label::new(project_name, row.name.text.clone()) @@ -1196,7 +1235,7 @@ impl CollabPanel { }); if let Some(name) = channel_name { - Cow::Owned(format!("Current Call - #{}", name)) + Cow::Owned(format!("#{}", name)) } else { Cow::Borrowed("Current Call") } @@ -1332,7 +1371,7 @@ impl CollabPanel { .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { if can_collapse { - this.toggle_expanded(section, cx); + this.toggle_section_expanded(section, cx); } }) } @@ -1479,6 +1518,11 @@ impl CollabPanel { cx: &AppContext, ) -> AnyElement { Flex::row() + .with_child( + Empty::new() + .constrained() + .with_width(theme.collab_panel.disclosure.button_space()), + ) .with_child( Svg::new("icons/hash.svg") .with_color(theme.collab_panel.channel_hash.color) @@ -1537,6 +1581,10 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + let has_children = self.channel_store.read(cx).has_children(channel_id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); + let is_active = iife!({ let call_channel = ActiveCall::global(cx) .read(cx) @@ -1550,7 +1598,7 @@ impl CollabPanel { const FACEPILE_LIMIT: usize = 3; MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { - Flex::row() + Flex::::row() .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -1599,6 +1647,11 @@ impl CollabPanel { } }) .align_children_center() + .styleable_component() + .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) + .with_id(channel_id as usize) + .with_style(theme.disclosure.clone()) + .element() .constrained() .with_height(theme.row_height) .contained() @@ -1618,6 +1671,61 @@ impl CollabPanel { .into_any() } + fn render_channel_notes( + &self, + channel_id: ChannelId, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum ChannelNotes {} + let host_avatar_width = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + MouseEventHandler::new::(channel_id as usize, cx, |state, cx| { + let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + let row = theme.project_row.in_state(is_selected).style_for(state); + + Flex::::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + true, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/radix/file.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new("notes", theme.channel_name.text.clone()) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.channel_row.style_for(is_selected, state)) + .with_padding_left(theme.channel_row.default_style().padding.left) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .into_any() + } + fn render_channel_invite( channel: Arc, channel_store: ModelHandle, @@ -1815,39 +1923,52 @@ impl CollabPanel { channel_id: u64, cx: &mut ViewContext, ) { - if self.channel_store.read(cx).is_user_admin(channel_id) { - self.context_menu_on_selected = position.is_none(); - - self.context_menu.update(cx, |context_menu, cx| { - context_menu.set_position_mode(if self.context_menu_on_selected { - OverlayPositionMode::Local - } else { - OverlayPositionMode::Window - }); + self.context_menu_on_selected = position.is_none(); - context_menu.show( - position.unwrap_or_default(), - if self.context_menu_on_selected { - gpui::elements::AnchorCorner::TopRight - } else { - gpui::elements::AnchorCorner::BottomLeft - }, - vec![ - ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Rename", RenameChannel { channel_id }), - ContextMenuItem::action("Manage", ManageMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Delete", RemoveChannel { channel_id }), - ], - cx, - ); + self.context_menu.update(cx, |context_menu, cx| { + context_menu.set_position_mode(if self.context_menu_on_selected { + OverlayPositionMode::Local + } else { + OverlayPositionMode::Window }); - cx.notify(); - } + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + + let mut items = vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), + ]; + + if self.channel_store.read(cx).is_user_admin(channel_id) { + items.extend([ + ContextMenuItem::Separator, + ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), + ContextMenuItem::action("Rename", RenameChannel { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Invite Members", InviteMembers { channel_id }), + ContextMenuItem::action("Manage Members", ManageMembers { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Delete", RemoveChannel { channel_id }), + ]); + } + + context_menu.show( + position.unwrap_or_default(), + if self.context_menu_on_selected { + gpui::elements::AnchorCorner::TopRight + } else { + gpui::elements::AnchorCorner::BottomLeft + }, + items, + cx, + ); + }); + + cx.notify(); } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { @@ -1912,7 +2033,7 @@ impl CollabPanel { | Section::Online | Section::Offline | Section::ChannelInvites => { - self.toggle_expanded(*section, cx); + self.toggle_section_expanded(*section, cx); } }, ListEntry::Contact { contact, calling } => { @@ -2000,7 +2121,7 @@ impl CollabPanel { } } - fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { @@ -2009,6 +2130,55 @@ impl CollabPanel { self.update_entries(false, cx); } + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if !self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { + let channel_id = action.channel_id; + + match self.collapsed_channels.binary_search(&channel_id) { + Ok(ix) => { + self.collapsed_channels.remove(ix); + } + Err(ix) => { + self.collapsed_channels.insert(ix, channel_id); + } + }; + self.serialize(cx); + self.update_entries(true, cx); + cx.notify(); + cx.focus_self(); + } + + fn is_channel_collapsed(&self, channel: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel).is_ok() + } + fn leave_call(cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) @@ -2048,6 +2218,8 @@ impl CollabPanel { } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|&channel| channel != action.channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), pending_name: None, @@ -2103,6 +2275,21 @@ impl CollabPanel { } } + fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx); + cx.spawn(|_, mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + pane.add_item(Box::new(channel_view), true, true, None, cx) + }); + anyhow::Ok(()) + }) + .detach(); + } + } + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { let Some(channel) = self.selected_channel() else { return; @@ -2261,6 +2448,51 @@ impl CollabPanel { } } +fn render_tree_branch( + branch_style: theme::TreeBranch, + row_style: &TextStyle, + is_last: bool, + size: Vector2F, + font_cache: &FontCache, +) -> gpui::elements::ConstrainedBox { + let line_height = row_style.line_height(font_cache); + let cap_height = row_style.cap_height(font_cache); + let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.; + + Canvas::new(move |scene, bounds, _, _, _| { + scene.paint_layer(None, |scene| { + let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + branch_style.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + branch_style.width), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + }) + }) + .constrained() + .with_width(size.x()) +} + impl View for CollabPanel { fn ui_name() -> &'static str { "CollabPanel" @@ -2354,7 +2586,7 @@ impl View for CollabPanel { .into_any() }) .on_click(MouseButton::Left, |_, _, cx| cx.focus_self()) - .into_any_named("channels panel") + .into_any_named("collab panel") } } @@ -2404,7 +2636,10 @@ impl Panel for CollabPanel { } fn icon_tooltip(&self) -> (String, Option>) { - ("Channels Panel".to_string(), Some(Box::new(ToggleFocus))) + ( + "Collaboration Panel".to_string(), + Some(Box::new(ToggleFocus)), + ) } fn should_change_position_on_event(event: &Self::Event) -> bool { @@ -2467,6 +2702,14 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id && depth_1 == depth_2; } } + ListEntry::ChannelNotes { channel_id } => { + if let ListEntry::ChannelNotes { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } ListEntry::ChannelInvite(channel_1) => { if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 75ab40be85adb1e1df7678cf292c4c177237db0c..0adf2806d72dc5c440ee08ec80a1331cdccc62cc 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,4 +1,5 @@ -use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::{proto, User, UserId, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index e4faa3b9c9c004050f3a839d4f8b0b8363d163a0..684ddca08de742823dd0d3dac54bd4a89b256b70 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1096,7 +1096,7 @@ impl CollabTitlebarItem { style } - fn render_face( + fn render_face( avatar: Arc, avatar_style: AvatarStyle, background_color: Color, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 0fed2e0ef93476e1375d89f6824c8ba05b4dd9db..04644b62d985698dcebfdfea352cc3cb2e15f824 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +pub mod channel_view; pub mod collab_panel; mod collab_titlebar_item; mod contact_notification; @@ -49,6 +50,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { ActiveCall::report_call_event_for_room( "disable screen share", room.id(), + room.channel_id(), &client, cx, ); @@ -57,6 +59,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { ActiveCall::report_call_event_for_room( "enable screen share", room.id(), + room.channel_id(), &client, cx, ); @@ -73,11 +76,18 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { let client = call.client(); room.update(cx, |room, cx| { if room.is_muted(cx) { - ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx); + ActiveCall::report_call_event_for_room( + "enable microphone", + room.id(), + room.channel_id(), + &client, + cx, + ); } else { ActiveCall::report_call_event_for_room( "disable microphone", room.id(), + room.channel_id(), &client, cx, ); diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index 9258ad3ab121b87e043521c43004e5ba24f8a9c5..9aff3a35220b8afe2b78de271abd9a62bb7f86ab 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -2,14 +2,14 @@ use client::User; use gpui::{ elements::*, platform::{CursorStyle, MouseButton}, - AnyElement, Element, View, ViewContext, + AnyElement, Element, ViewContext, }; use std::sync::Arc; enum Dismiss {} enum Button {} -pub fn render_user_notification( +pub fn render_user_notification( user: Arc, title: &'static str, body: Option<&'static str>, @@ -19,7 +19,6 @@ pub fn render_user_notification( ) -> AnyElement where F: 'static + Fn(&mut V, &mut ViewContext), - V: View, { let theme = theme::current(cx).clone(); let theme = &theme.contact_notification; diff --git a/crates/component_test/Cargo.toml b/crates/component_test/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d714f6f72f62bf0c869439d07e937592a6ea644b --- /dev/null +++ b/crates/component_test/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "component_test" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/component_test.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +project = { path = "../project" } diff --git a/crates/component_test/src/component_test.rs b/crates/component_test/src/component_test.rs new file mode 100644 index 0000000000000000000000000000000000000000..9f6b4918b9e065f6627e0f9765e0827ccfb54c9b --- /dev/null +++ b/crates/component_test/src/component_test.rs @@ -0,0 +1,121 @@ +use gpui::{ + actions, + elements::{Component, Flex, ParentElement, SafeStylable}, + AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use project::Project; +use theme::components::{action_button::Button, label::Label, ComponentExt}; +use workspace::{ + item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId, +}; + +pub fn init(cx: &mut AppContext) { + cx.add_action(ComponentTest::toggle_disclosure); + cx.add_action(ComponentTest::toggle_toggle); + cx.add_action(ComponentTest::deploy); + register_deserializable_item::(cx); +} + +actions!( + test, + [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest] +); + +struct ComponentTest { + disclosed: bool, + toggled: bool, +} + +impl ComponentTest { + fn new() -> Self { + Self { + disclosed: false, + toggled: false, + } + } + + fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx); + } + + fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext) { + self.disclosed = !self.disclosed; + cx.notify(); + } + + fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext) { + self.toggled = !self.toggled; + cx.notify(); + } +} + +impl Entity for ComponentTest { + type Event = (); +} + +impl View for ComponentTest { + fn ui_name() -> &'static str { + "Component Test" + } + + fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let theme = theme::current(cx); + + PaneBackdrop::new( + cx.view_id(), + Flex::column() + .with_spacing(10.) + .with_child( + Button::action(NoAction) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Click me!")) + .with_style(theme.component_test.button.clone()) + .element(), + ) + .with_child( + Button::action(ToggleToggle) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Toggle me!")) + .toggleable(self.toggled) + .with_style(theme.component_test.toggle.clone()) + .element(), + ) + .with_child( + Label::new("A disclosure") + .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure)) + .with_style(theme.component_test.disclosure.clone()) + .element(), + ) + .constrained() + .with_width(200.) + .aligned() + .into_any(), + ) + .into_any() + } +} + +impl Item for ComponentTest { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new("Component test", style.label.clone()).into_any() + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("ComponentTest") + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: WorkspaceId, + _item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(cx.add_view(|_| Self::new()))) + } +} diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 16a7340fae485b1c1b329315700437e4470c3af9..0e5b714f097b820ad24f1c49d412d698fd7a3c4e 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -538,7 +538,7 @@ impl ProjectDiagnosticsEditor { } impl Item for ProjectDiagnosticsEditor { - fn tab_content( + fn tab_content( &self, _detail: Option, style: &theme::Tab, @@ -735,7 +735,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { }) } -pub(crate) fn render_summary( +pub(crate) fn render_summary( summary: &DiagnosticSummary, text_style: &TextStyle, theme: &theme::ProjectDiagnostics, diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index 59b0bc89e2ff9b9e43faff2b8ed1d364f1bf6897..197c9918f54176e038efbd6e93887cc129c3f764 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -11,7 +11,7 @@ use gpui::{ const DEAD_ZONE: f32 = 4.; -enum State { +enum State { Down { region_offset: Vector2F, region: RectF, @@ -31,7 +31,7 @@ enum State { Canceled, } -impl Clone for State { +impl Clone for State { fn clone(&self) -> Self { match self { &State::Down { @@ -68,12 +68,12 @@ impl Clone for State { } } -pub struct DragAndDrop { +pub struct DragAndDrop { containers: HashSet>, currently_dragged: Option>, } -impl Default for DragAndDrop { +impl Default for DragAndDrop { fn default() -> Self { Self { containers: Default::default(), @@ -82,7 +82,7 @@ impl Default for DragAndDrop { } } -impl DragAndDrop { +impl DragAndDrop { pub fn register_container(&mut self, handle: WeakViewHandle) { self.containers.insert(handle); } @@ -291,7 +291,7 @@ impl DragAndDrop { } } -pub trait Draggable { +pub trait Draggable { fn as_draggable( self, payload: P, @@ -301,7 +301,7 @@ pub trait Draggable { Self: Sized; } -impl Draggable for MouseEventHandler { +impl Draggable for MouseEventHandler { fn as_draggable( self, payload: P, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0283b396f3f8f29d846f63a907b5554008d2cb8f..b206f2ec8b4496e6935f1a2453c3dbce5ecf83ef 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -559,6 +559,7 @@ pub struct Editor { blink_manager: ModelHandle, show_local_selections: bool, mode: EditorMode, + replica_id_mapping: Option>, show_gutter: bool, show_wrap_guides: Option, placeholder_text: Option>, @@ -577,6 +578,7 @@ pub struct Editor { searchable: bool, cursor_shape: CursorShape, collapse_matches: bool, + autoindent_mode: Option, workspace: Option<(WeakViewHandle, i64)>, keymap_context_layers: BTreeMap, input_enabled: bool, @@ -1393,6 +1395,7 @@ impl Editor { blink_manager: blink_manager.clone(), show_local_selections: true, mode, + replica_id_mapping: None, show_gutter: mode == EditorMode::Full, show_wrap_guides: None, placeholder_text: None, @@ -1412,6 +1415,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, workspace: None, keymap_context_layers: Default::default(), @@ -1590,10 +1594,31 @@ impl Editor { self.input_enabled = input_enabled; } + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } + pub fn set_read_only(&mut self, read_only: bool) { self.read_only = read_only; } + pub fn replica_id_map(&self) -> Option<&HashMap> { + self.replica_id_mapping.as_ref() + } + + pub fn set_replica_id_map( + &mut self, + mapping: Option>, + cx: &mut ViewContext, + ) { + self.replica_id_mapping = mapping; + cx.notify(); + } + fn selections_did_change( &mut self, local: bool, @@ -1722,7 +1747,32 @@ impl Editor { } self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::EachLine), cx) + buffer.edit(edits, self.autoindent_mode.clone(), cx) + }); + } + + pub fn edit_with_block_indent( + &mut self, + edits: I, + original_indent_columns: Vec, + cx: &mut ViewContext, + ) where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only { + return; + } + + self.buffer.update(cx, |buffer, cx| { + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns, + }), + cx, + ) }); } @@ -2093,12 +2143,12 @@ impl Editor { for (selection, autoclose_region) in self.selections_with_autoclose_regions(selections, &snapshot) { - if let Some(language) = snapshot.language_scope_at(selection.head()) { + if let Some(scope) = snapshot.language_scope_at(selection.head()) { // Determine if the inserted text matches the opening or closing // bracket of any of this language's bracket pairs. let mut bracket_pair = None; let mut is_bracket_pair_start = false; - for (pair, enabled) in language.brackets() { + for (pair, enabled) in scope.brackets() { if enabled && pair.close && pair.start.ends_with(text.as_ref()) { bracket_pair = Some(pair.clone()); is_bracket_pair_start = true; @@ -2120,7 +2170,7 @@ impl Editor { let following_text_allows_autoclose = snapshot .chars_at(selection.start) .next() - .map_or(true, |c| language.should_autoclose_before(c)); + .map_or(true, |c| scope.should_autoclose_before(c)); let preceding_text_matches_prefix = prefix_len == 0 || (selection.start.column >= (prefix_len as u32) && snapshot.contains_str_at( @@ -2197,7 +2247,7 @@ impl Editor { drop(snapshot); self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + buffer.edit(edits, this.autoindent_mode.clone(), cx); }); let new_anchor_selections = new_selections.iter().map(|e| &e.0); @@ -2657,7 +2707,6 @@ impl Editor { false }); } - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { let offset = position.to_offset(buffer); let (word_range, kind) = buffer.surrounding_word(offset); @@ -3037,7 +3086,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.edit( ranges.iter().map(|range| (range.clone(), text)), - Some(AutoindentMode::EachLine), + this.autoindent_mode.clone(), cx, ); }); @@ -4732,6 +4781,7 @@ impl Editor { let mut clipboard_selections = Vec::with_capacity(selections.len()); { let max_point = buffer.max_point(); + let mut is_first = true; for selection in &mut selections { let is_entire_line = selection.is_empty() || self.selections.line_mode; if is_entire_line { @@ -4739,6 +4789,11 @@ impl Editor { selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); selection.goal = SelectionGoal::None; } + if is_first { + is_first = false; + } else { + text += "\n"; + } let mut len = 0; for chunk in buffer.text_for_range(selection.start..selection.end) { text.push_str(chunk); @@ -4769,6 +4824,7 @@ impl Editor { let mut clipboard_selections = Vec::with_capacity(selections.len()); { let max_point = buffer.max_point(); + let mut is_first = true; for selection in selections.iter() { let mut start = selection.start; let mut end = selection.end; @@ -4777,6 +4833,11 @@ impl Editor { start = Point::new(start.row, 0); end = cmp::min(max_point, Point::new(end.row + 1, 0)); } + if is_first { + is_first = false; + } else { + text += "\n"; + } let mut len = 0; for chunk in buffer.text_for_range(start..end) { text.push_str(chunk); @@ -4796,7 +4857,7 @@ impl Editor { pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { self.transact(cx, |this, cx| { if let Some(item) = cx.read_from_clipboard() { - let mut clipboard_text = Cow::Borrowed(item.text()); + let clipboard_text = Cow::Borrowed(item.text()); if let Some(mut clipboard_selections) = item.metadata::>() { let old_selections = this.selections.all::(cx); let all_selections_were_entire_line = @@ -4804,18 +4865,7 @@ impl Editor { let first_selection_indent_column = clipboard_selections.first().map(|s| s.first_line_indent); if clipboard_selections.len() != old_selections.len() { - let mut newline_separated_text = String::new(); - let mut clipboard_selections = clipboard_selections.drain(..).peekable(); - let mut ix = 0; - while let Some(clipboard_selection) = clipboard_selections.next() { - newline_separated_text - .push_str(&clipboard_text[ix..ix + clipboard_selection.len]); - ix += clipboard_selection.len; - if clipboard_selections.peek().is_some() { - newline_separated_text.push('\n'); - } - } - clipboard_text = Cow::Owned(newline_separated_text); + clipboard_selections.drain(..); } this.buffer.update(cx, |buffer, cx| { @@ -4831,8 +4881,9 @@ impl Editor { if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; + dbg!(start_offset, end_offset, &clipboard_text, &to_insert); entire_line = clipboard_selection.is_entire_line; - start_offset = end_offset; + start_offset = end_offset + 1; original_indent_column = Some(clipboard_selection.first_line_indent); } else { @@ -8527,6 +8578,7 @@ fn build_style( font_size, font_properties, underline: Default::default(), + soft_wrap: false, }, placeholder_text: None, line_height_scalar, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ec1cc1249895881485489b56474c61aafd78d31d..a2a561402f904be26d7a3a8d316519fb60387a19 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5237,6 +5237,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), ..Default::default() }), ..Default::default() @@ -6383,7 +6384,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, @@ -6478,7 +6479,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, @@ -7528,6 +7529,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), ..Default::default() }), ..Default::default() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 04f45921d79b0f3cc481a5b7fcae9d4dad101dd5..9f74eed790fa6b025bdd12125858ad02ec44d8ac 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -62,6 +62,7 @@ struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, is_newest: bool, + is_local: bool, range: Range, active_rows: Range, } @@ -73,6 +74,7 @@ impl SelectionLayout { cursor_shape: CursorShape, map: &DisplaySnapshot, is_newest: bool, + is_local: bool, ) -> Self { let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); let display_selection = point_selection.map(|p| p.to_display_point(map)); @@ -109,6 +111,7 @@ impl SelectionLayout { head, cursor_shape, is_newest, + is_local, range, active_rows, } @@ -605,7 +608,7 @@ impl EditorElement { visible_bounds: RectF, layout: &mut LayoutState, editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { let line_height = layout.position_map.line_height; @@ -760,10 +763,9 @@ impl EditorElement { visible_bounds: RectF, layout: &mut LayoutState, editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { let style = &self.style; - let local_replica_id = editor.replica_id(cx); let scroll_position = layout.position_map.snapshot.scroll_position(); let start_row = layout.visible_display_row_range.start; let scroll_top = scroll_position.y() * layout.position_map.line_height; @@ -852,15 +854,13 @@ impl EditorElement { for (replica_id, selections) in &layout.selections { let replica_id = *replica_id; - let selection_style = style.replica_selection_style(replica_id); + let selection_style = if let Some(replica_id) = replica_id { + style.replica_selection_style(replica_id) + } else { + &style.absent_selection + }; for selection in selections { - if !selection.range.is_empty() - && (replica_id == local_replica_id - || Some(replica_id) == editor.leader_replica_id) - { - invisible_display_ranges.push(selection.range.clone()); - } self.paint_highlighted_range( scene, selection.range.clone(), @@ -874,7 +874,10 @@ impl EditorElement { bounds, ); - if editor.show_local_cursors(cx) || replica_id != local_replica_id { + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + if !selection.is_local || editor.show_local_cursors(cx) { let cursor_position = selection.head; if layout .visible_display_row_range @@ -1337,7 +1340,7 @@ impl EditorElement { visible_bounds: RectF, layout: &mut LayoutState, editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_left = scroll_position.x() * layout.position_map.em_width; @@ -2124,7 +2127,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections: Vec<(ReplicaId, Vec)> = Vec::new(); + let mut selections: Vec<(Option, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut fold_ranges = Vec::new(); let is_singleton = editor.is_singleton(cx); @@ -2155,8 +2158,14 @@ impl Element for EditorElement { .buffer_snapshot .remote_selections_in_range(&(start_anchor..end_anchor)) { + let replica_id = if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied() + } else { + None + }; + // The local selections match the leader's selections. - if Some(replica_id) == editor.leader_replica_id { + if replica_id.is_some() && replica_id == editor.leader_replica_id { continue; } remote_selections @@ -2168,6 +2177,7 @@ impl Element for EditorElement { cursor_shape, &snapshot.display_snapshot, false, + false, )); } selections.extend(remote_selections); @@ -2191,6 +2201,7 @@ impl Element for EditorElement { editor.cursor_shape, &snapshot.display_snapshot, is_newest, + true, ); if is_newest { newest_selection_head = Some(layout.head); @@ -2206,11 +2217,18 @@ impl Element for EditorElement { } // Render the local selections in the leader's color when following. - let local_replica_id = editor - .leader_replica_id - .unwrap_or_else(|| editor.replica_id(cx)); + let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id { + leader_replica_id + } else { + let replica_id = editor.replica_id(cx); + if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied().unwrap_or(replica_id) + } else { + replica_id + } + }; - selections.push((local_replica_id, layouts)); + selections.push((Some(local_replica_id), layouts)); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2591,7 +2609,7 @@ pub struct LayoutState { blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, fold_ranges: Vec<(BufferRow, Range, Color)>, - selections: Vec<(ReplicaId, Vec)>, + selections: Vec<(Option, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, is_singleton: bool, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b99977a60eb45dc3a0a616169067063e6c4e691f..477eab41ac9cc7a0c7b38e0ec07f3eb41f46963e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -49,11 +49,12 @@ impl FollowableItem for Editor { fn from_state_proto( pane: ViewHandle, - project: ModelHandle, + workspace: ViewHandle, remote_id: ViewId, state: &mut Option, cx: &mut AppContext, ) -> Option>>> { + let project = workspace.read(cx).project().to_owned(); let Some(proto::view::Variant::Editor(_)) = state else { return None }; let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; @@ -561,7 +562,7 @@ impl Item for Editor { } } - fn tab_content( + fn tab_content( &self, detail: Option, style: &theme::Tab, @@ -753,7 +754,7 @@ impl Item for Editor { Some(Box::new(handle.clone())) } - fn pixel_position_of_cursor(&self) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { self.pixel_position_of_newest_cursor } @@ -1028,7 +1029,7 @@ impl SearchableItem for Editor { if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { ranges.extend( query - .search(excerpt_buffer.as_rope()) + .search(excerpt_buffer, None) .await .into_iter() .map(|range| { @@ -1038,17 +1039,22 @@ impl SearchableItem for Editor { } else { for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer); - let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone()); - ranges.extend(query.search(&rope).await.into_iter().map(|range| { - let start = excerpt - .buffer - .anchor_after(excerpt_range.start + range.start); - let end = excerpt - .buffer - .anchor_before(excerpt_range.start + range.end); - buffer.anchor_in_excerpt(excerpt.id.clone(), start) - ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) - })); + ranges.extend( + query + .search(&excerpt.buffer, Some(excerpt_range.clone())) + .await + .into_iter() + .map(|range| { + let start = excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(excerpt.id.clone(), start) + ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + }), + ); } } ranges diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index f70436abeb45f248ee8c306613a220655313797f..6b3032b2a35ba9fd46ec145953e626a0f4914f98 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -61,10 +61,10 @@ pub fn up_by_rows( goal: SelectionGoal, preserve_column_at_start: bool, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = if let SelectionGoal::Column(column) = goal { - column - } else { - map.column_to_chars(start.row(), start.column()) + let mut goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => map.column_to_chars(start.row(), start.column()), }; let prev_row = start.row().saturating_sub(row_count); @@ -95,10 +95,10 @@ pub fn down_by_rows( goal: SelectionGoal, preserve_column_at_end: bool, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = if let SelectionGoal::Column(column) = goal { - column - } else { - map.column_to_chars(start.row(), start.column()) + let mut goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => map.column_to_chars(start.row(), start.column()), }; let new_row = start.row() + row_count; @@ -176,14 +176,21 @@ pub fn line_end( } pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); + find_preceding_boundary(map, point, |left, right| { - (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n' + (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) + || left == '\n' }) } pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_preceding_boundary(map, point, |left, right| { - let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace(); + let is_word_start = + char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); let is_subword_start = left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start || left == '\n' @@ -191,14 +198,20 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis } pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_boundary(map, point, |left, right| { - (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n' + (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) + || right == '\n' }) } pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_boundary(map, point, |left, right| { - let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace(); + let is_word_end = + (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); let is_subword_end = left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_end || is_subword_end || right == '\n' @@ -385,10 +398,15 @@ pub fn find_boundary_in_line( } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; - let next_char_kind = text.chars_at(ix).next().map(char_kind); - let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind); + let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c)); + let prev_char_kind = text + .reversed_chars_at(ix) + .next() + .map(|c| char_kind(language, c)); prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index ac3b726b26817cefe6cf6b9750cec53b51b5b524..52fbcd99d9c0035206366486efdd33751342ecc8 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1394,10 +1394,7 @@ impl MultiBuffer { .map(|state| state.buffer.clone()) } - pub fn is_completion_trigger(&self, position: T, text: &str, cx: &AppContext) -> bool - where - T: ToOffset, - { + pub fn is_completion_trigger(&self, position: Anchor, text: &str, cx: &AppContext) -> bool { let mut chars = text.chars(); let char = if let Some(char) = chars.next() { char @@ -1408,7 +1405,9 @@ impl MultiBuffer { return false; } - if char.is_alphanumeric() || char == '_' { + let language = self.language_at(position.clone(), cx); + + if char_kind(language.as_ref(), char) == CharKind::Word { return true; } @@ -1913,13 +1912,16 @@ impl MultiBufferSnapshot { let mut end = start; let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); + + let language = self.language_at(start); + let kind = |c| char_kind(language, c); let word_kind = cmp::max( - prev_chars.peek().copied().map(char_kind), - next_chars.peek().copied().map(char_kind), + prev_chars.peek().copied().map(kind), + next_chars.peek().copied().map(kind), ); for ch in prev_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { start -= ch.len_utf8(); } else { break; @@ -1927,7 +1929,7 @@ impl MultiBufferSnapshot { } for ch in next_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { end += ch.len_utf8(); } else { break; diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index f5edb00d589f6e881b78e7327e53cdcba5ccde2f..d87bc0ae4fb7895ab24973d59c0b46815abaca79 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -29,6 +29,7 @@ use self::{ }; pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); +pub const VERTICAL_SCROLL_MARGIN: f32 = 3.; const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); #[derive(Default)] @@ -136,7 +137,7 @@ pub struct ScrollManager { impl ScrollManager { pub fn new() -> Self { ScrollManager { - vertical_scroll_margin: 3.0, + vertical_scroll_margin: VERTICAL_SCROLL_MARGIN, anchor: ScrollAnchor::new(), ongoing: OngoingScroll::new(), autoscroll_request: None, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 1921bc0738ec588c8b9218c717de497232a345ec..6a21c898ef3617fc37bfe159be220cfe4360f884 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -1,7 +1,7 @@ use std::{ cell::Ref, cmp, iter, mem, - ops::{Deref, Range, Sub}, + ops::{Deref, DerefMut, Range, Sub}, sync::Arc, }; @@ -53,7 +53,7 @@ impl SelectionsCollection { } } - fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot { + pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot { self.display_map.update(cx, |map, cx| map.snapshot(cx)) } @@ -250,6 +250,10 @@ impl SelectionsCollection { resolve(self.oldest_anchor(), &self.buffer(cx)) } + pub fn first_anchor(&self) -> Selection { + self.disjoint[0].clone() + } + pub fn first>( &self, cx: &AppContext, @@ -352,7 +356,7 @@ pub struct MutableSelectionsCollection<'a> { } impl<'a> MutableSelectionsCollection<'a> { - fn display_map(&mut self) -> DisplaySnapshot { + pub fn display_map(&mut self) -> DisplaySnapshot { self.collection.display_map(self.cx) } @@ -607,6 +611,10 @@ impl<'a> MutableSelectionsCollection<'a> { self.select_anchors(selections) } + pub fn new_selection_id(&mut self) -> usize { + post_inc(&mut self.next_selection_id) + } + pub fn select_display_ranges(&mut self, ranges: T) where T: IntoIterator>, @@ -831,6 +839,12 @@ impl<'a> Deref for MutableSelectionsCollection<'a> { } } +impl<'a> DerefMut for MutableSelectionsCollection<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.collection + } +} + // Panics if passed selections are not in order pub fn resolve_multiple<'a, D, I>( selections: I, diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 83aaa3b703491b5f5334d173ca8d90e731aeb5e8..668d6abf21c9bfae39c1f47065c3986a6a14f25a 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -6,6 +6,7 @@ use std::{ use anyhow::Result; +use collections::HashSet; use futures::Future; use gpui::{json, ViewContext, ViewHandle}; use indoc::indoc; @@ -154,10 +155,23 @@ impl<'a> EditorLspTestContext<'a> { capabilities: lsp::ServerCapabilities, cx: &'a mut gpui::TestAppContext, ) -> EditorLspTestContext<'a> { + let mut word_characters: HashSet = Default::default(); + word_characters.insert('$'); + word_characters.insert('#'); let language = Language::new( LanguageConfig { name: "Typescript".into(), path_suffixes: vec!["ts".to_string()], + brackets: language::BracketPairConfig { + pairs: vec![language::BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Default::default(), + }, + word_characters, ..Default::default() }, Some(tree_sitter_typescript::language_typescript()), @@ -169,6 +183,23 @@ impl<'a> EditorLspTestContext<'a> { ("{" @open "}" @close) ("<" @open ">" @close) ("\"" @open "\"" @close)"#})), + indents: Some(Cow::from(indoc! {r#" + [ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent + "#})), ..Default::default() }) .expect("Could not parse queries"); diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 47cb90875a4f83cfb5950d68cfaac1f5b38c2de6..a717223f6d7d3a5e3eb4dd2dd50f81154bee3072 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -268,7 +268,7 @@ impl Item for FeedbackEditor { Some("Send Feedback".into()) } - fn tab_content( + fn tab_content( &self, _: Option, style: &theme::Tab, diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 5bd7d0318439d2cbd46667f585be37ae25efa33e..24397f619357a5454f333c21ef96e4ac369c6397 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -39,6 +39,7 @@ pathfinder_color = "0.5" pathfinder_geometry = "0.5" postage.workspace = true rand.workspace = true +refineable.workspace = true resvg = "0.14" schemars = "0.8" seahash = "4.1" @@ -47,6 +48,7 @@ serde_derive.workspace = true serde_json.workspace = true smallvec.workspace = true smol.workspace = true +taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] } time.workspace = true tiny-skia = "0.5" usvg = { version = "0.14", features = [] } diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index cf695ea834b4424a243b40f6d49222f6bb8a54d2..d3ca0d1eccaef343058968856a0e8715849357a8 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -2,7 +2,7 @@ use button_component::Button; use gpui::{ color::Color, - elements::{Component, ContainerStyle, Flex, Label, ParentElement}, + elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent}, fonts::{self, TextStyle}, platform::WindowOptions, AnyElement, App, Element, Entity, View, ViewContext, @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .into_element(), + .element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .into_element(), + .element(), ) .expanded() .contained() @@ -114,7 +114,7 @@ mod theme { // Component creation: mod toggleable_button { use gpui::{ - elements::{Component, ContainerStyle, LabelStyle}, + elements::{ContainerStyle, LabelStyle, StatefulComponent}, scene::MouseClick, EventContext, View, }; @@ -156,7 +156,7 @@ mod toggleable_button { } } - impl Component for ToggleableButton { + impl StatefulComponent for ToggleableButton { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let button = if let Some(style) = self.style { self.button.with_style(*style.style_for(self.active)) @@ -171,7 +171,7 @@ mod toggleable_button { mod button_component { use gpui::{ - elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, + elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent}, platform::MouseButton, scene::MouseClick, AnyElement, Element, EventContext, TypeTag, View, ViewContext, @@ -212,7 +212,7 @@ mod button_component { } } - impl Component for Button { + impl StatefulComponent for Button { fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { let click_handler = self.click_handler; diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index a269706cc503e45ac56a737b405efc45bfe6702a..bda70a49dc00954c5872d580c4dbadb58ce86e42 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -58,6 +58,7 @@ impl gpui::View for TextView { font_family_id: family, underline: Default::default(), font_properties: Default::default(), + soft_wrap: false, }, ) .with_highlights(vec![(17..26, underline), (34..40, underline)]) diff --git a/crates/gpui/playground/Cargo.lock b/crates/gpui/playground/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..95e0b10c625ae380e0090c01e50d37ed2bd3f398 --- /dev/null +++ b/crates/gpui/playground/Cargo.lock @@ -0,0 +1,2919 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-net" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f" +dependencies = [ + "async-io", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "signal-hook", + "windows-sys", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.1", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.25", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[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.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "log", +] + +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading 0.7.4", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" +dependencies = [ + "bitflags", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "collections" +version = "0.1.0" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-cstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", + "uuid 0.5.1", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +dependencies = [ + "bitflags", + "core-foundation", + "libc", +] + +[[package]] +name = "core-text" +version = "19.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.63+curl-8.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi", +] + +[[package]] +name = "data-url" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" +dependencies = [ + "matches", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.0", +] + +[[package]] +name = "dwrote" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "etagere" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "float-cmp" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e" + +[[package]] +name = "float-ord" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "font-kit" +version = "0.11.0" +source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" +dependencies = [ + "bitflags", + "byteorder", + "core-foundation", + "core-graphics", + "core-text", + "dirs-next", + "dwrote", + "float-ord", + "freetype", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "fontdb" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58903f4f8d5b58c7d300908e4ebe5289c1bfdf5587964330f12023b8ff17fd1" +dependencies = [ + "log", + "memmap2", + "ttf-parser 0.12.3", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6" +dependencies = [ + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a" +dependencies = [ + "cmake", + "libc", + "pkg-config", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.25", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "gpui" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-task", + "bindgen", + "block", + "cc", + "cocoa", + "collections", + "core-foundation", + "core-graphics", + "core-text", + "ctor", + "etagere", + "font-kit", + "foreign-types", + "futures", + "gpui_macros", + "image", + "itertools", + "lazy_static", + "log", + "media", + "metal", + "num_cpus", + "objc", + "ordered-float", + "parking", + "parking_lot 0.11.2", + "pathfinder_color", + "pathfinder_geometry", + "postage", + "rand", + "resvg", + "schemars", + "seahash", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "smol", + "sqlez", + "sum_tree", + "time", + "tiny-skia", + "usvg", + "util", + "uuid 1.4.0", + "waker-fn", +] + +[[package]] +name = "gpui_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "isahc" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener", + "futures-lite", + "http", + "log", + "mime", + "once_cell", + "polling", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" + +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] +name = "kurbo" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" +dependencies = [ + "arrayvec 0.7.4", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +dependencies = [ + "serde", + "value-bag", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "media" +version = "0.1.0" +dependencies = [ + "anyhow", + "bindgen", + "block", + "bytes", + "core-foundation", + "foreign-types", + "metal", + "objc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4598d719460ade24c7d91f335daf055bf2a7eec030728ce751814c50cdd6a26c" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "foreign-types", + "log", + "objc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pathfinder_color" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69bdc0d277d559e35e1b374de56df9262a6b71e091ca04a8831a239f8c7f0c62" +dependencies = [ + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pico-args" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" + +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.25", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "playground" +version = "0.1.0" +dependencies = [ + "gpui", +] + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot 0.12.1", + "pin-project", + "pollster", + "static_assertions", + "thiserror", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387" +dependencies = [ + "proc-macro2", + "syn 2.0.25", +] + +[[package]] +name = "proc-macro2" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rctree" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "resvg" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09697862c5c3f940cbaffef91969c62188b5c8ed385b0aef43a5ff01ddc8000f" +dependencies = [ + "jpeg-decoder", + "log", + "pico-args", + "png", + "rgb", + "svgfilters", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.25", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustybuzz" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab463a295d00f3692e0974a0bfd83c7a9bcd119e27e07c2beecdb1b44a09d10" +dependencies = [ + "bitflags", + "bytemuck", + "smallvec", + "ttf-parser 0.9.0", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-general-category", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" + +[[package]] +name = "safe_arch" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "schemars" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[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 = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.25", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simplecss" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "smol" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "sqlez" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "indoc", + "lazy_static", + "libsqlite3-sys", + "parking_lot 0.11.2", + "smol", + "thread_local", + "uuid 1.4.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "sum_tree" +version = "0.1.0" +dependencies = [ + "arrayvec 0.7.4", + "log", +] + +[[package]] +name = "sval" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" + +[[package]] +name = "sval_buffer" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_ref" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" +dependencies = [ + "serde", + "sval", + "sval_buffer", + "sval_fmt", +] + +[[package]] +name = "svg_fmt" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" + +[[package]] +name = "svgfilters" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0dce2fee79ac40c21dafba48565ff7a5fa275e23ffe9ce047a40c9574ba34e" +dependencies = [ + "float-cmp", + "rgb", +] + +[[package]] +name = "svgtypes" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" +dependencies = [ + "float-cmp", + "siphasher", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "take-until" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" + +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.25", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +dependencies = [ + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "bytemuck", + "cfg-if", + "png", + "safe_arch", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.25", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "ttf-parser" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddb402ac6c2af6f7a2844243887631c4e94b51585b229fcfddb43958cd55ca" + +[[package]] +name = "ttf-parser" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-general-category" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f" + +[[package]] +name = "unicode-ident" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-script" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "usvg" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "kurbo", + "log", + "memmap2", + "pico-args", + "rctree", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "svgtypes", + "ttf-parser 0.12.3", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "util" +version = "0.1.0" +dependencies = [ + "anyhow", + "backtrace", + "dirs", + "futures", + "isahc", + "lazy_static", + "log", + "rand", + "rust-embed", + "serde", + "serde_json", + "smol", + "take-until", + "url", +] + +[[package]] +name = "uuid" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" + +[[package]] +name = "uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +dependencies = [ + "getrandom", +] + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "xmlparser" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386" +dependencies = [ + "const-cstr", + "dlib", + "once_cell", + "pkg-config", +] diff --git a/crates/gpui/playground/Cargo.toml b/crates/gpui/playground/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..3e5a5e5606a5e3351104a9587f881ab3caf2fccc --- /dev/null +++ b/crates/gpui/playground/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "playground" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "playground" +path = "src/playground.rs" + +[dependencies] +anyhow.workspace = true +derive_more.workspace = true +gpui = { path = ".." } +log.workspace = true +playground_macros = { path = "../playground_macros" } +parking_lot.workspace = true +refineable.workspace = true +serde.workspace = true +simplelog = "0.9" +smallvec.workspace = true +taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] } +util = { path = "../../util" } + +[dev-dependencies] +gpui = { path = "..", features = ["test-support"] } diff --git a/crates/gpui/playground/docs/thoughts.md b/crates/gpui/playground/docs/thoughts.md new file mode 100644 index 0000000000000000000000000000000000000000..9416f2593313b973ada409770b41b1f1f074ba09 --- /dev/null +++ b/crates/gpui/playground/docs/thoughts.md @@ -0,0 +1,72 @@ +Much of element styling is now handled by an external engine. + + +How do I make an element hover. + +There's a hover style. + +Hoverable needs to wrap another element. That element can be styled. + +```rs +struct Hoverable { + +} + +impl Element for Hoverable { + +} + +``` + + + +```rs +#[derive(Styled, Interactive)] +pub struct Div { + declared_style: StyleRefinement, + interactions: Interactions +} + +pub trait Styled { + fn declared_style(&mut self) -> &mut StyleRefinement; + fn compute_style(&mut self) -> Style { + Style::default().refine(self.declared_style()) + } + + // All the tailwind classes, modifying self.declared_style() +} + +impl Style { + pub fn paint_background(layout: Layout, cx: &mut PaintContext); + pub fn paint_foreground(layout: Layout, cx: &mut PaintContext); +} + +pub trait Interactive { + fn interactions(&mut self) -> &mut Interactions; + + fn on_click(self, ) +} + +struct Interactions { + click: SmallVec<[; 1]>, +} + + +``` + + +```rs + + +trait Stylable { + type Style; + + fn with_style(self, style: Self::Style) -> Self; +} + + + + + + +``` diff --git a/crates/gpui/playground/src/adapter.rs b/crates/gpui/playground/src/adapter.rs new file mode 100644 index 0000000000000000000000000000000000000000..7a5a0399a81224ac2a9032f791b55764d9d76f68 --- /dev/null +++ b/crates/gpui/playground/src/adapter.rs @@ -0,0 +1,78 @@ +use crate::{layout_context::LayoutContext, paint_context::PaintContext}; +use gpui::{geometry::rect::RectF, LayoutEngine, LayoutId}; +use util::ResultExt; + +/// Makes a new, playground-style element into a legacy element. +pub struct AdapterElement(pub(crate) crate::element::AnyElement); + +impl gpui::Element for AdapterElement { + type LayoutState = Option<(LayoutEngine, LayoutId)>; + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + view: &mut V, + cx: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + cx.push_layout_engine(LayoutEngine::new()); + + let size = constraint.max; + let mut cx = LayoutContext::new(cx); + let layout_id = self.0.layout(view, &mut cx).log_err(); + if let Some(layout_id) = layout_id { + cx.layout_engine() + .unwrap() + .compute_layout(layout_id, constraint.max) + .log_err(); + } + + let layout_engine = cx.pop_layout_engine(); + debug_assert!(layout_engine.is_some(), + "unexpected layout stack state. is there an unmatched pop_layout_engine in the called code?" + ); + + (constraint.max, layout_engine.zip(layout_id)) + } + + fn paint( + &mut self, + scene: &mut gpui::SceneBuilder, + bounds: RectF, + visible_bounds: RectF, + layout_data: &mut Option<(LayoutEngine, LayoutId)>, + view: &mut V, + legacy_cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + let (layout_engine, layout_id) = layout_data.take().unwrap(); + legacy_cx.push_layout_engine(layout_engine); + let mut cx = PaintContext::new(legacy_cx, scene); + self.0.paint(view, &mut cx); + *layout_data = legacy_cx.pop_layout_engine().zip(Some(layout_id)); + debug_assert!(layout_data.is_some()); + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + bounds: RectF, + visible_bounds: RectF, + layout: &Self::LayoutState, + paint: &Self::PaintState, + view: &V, + cx: &gpui::ViewContext, + ) -> Option { + todo!("implement before merging to main") + } + + fn debug( + &self, + bounds: RectF, + layout: &Self::LayoutState, + paint: &Self::PaintState, + view: &V, + cx: &gpui::ViewContext, + ) -> gpui::serde_json::Value { + todo!("implement before merging to main") + } +} diff --git a/crates/gpui/playground/src/color.rs b/crates/gpui/playground/src/color.rs new file mode 100644 index 0000000000000000000000000000000000000000..14c60248aade8fbd99f8a3105dd87c6e949ac689 --- /dev/null +++ b/crates/gpui/playground/src/color.rs @@ -0,0 +1,276 @@ +#![allow(dead_code)] + +use std::{num::ParseIntError, ops::Range}; + +use smallvec::SmallVec; + +pub fn rgb>(hex: u32) -> C { + let r = ((hex >> 16) & 0xFF) as f32 / 255.0; + let g = ((hex >> 8) & 0xFF) as f32 / 255.0; + let b = (hex & 0xFF) as f32 / 255.0; + Rgba { r, g, b, a: 1.0 }.into() +} + +#[derive(Clone, Copy, Default, Debug)] +pub struct Rgba { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +pub trait Lerp { + fn lerp(&self, level: f32) -> Hsla; +} + +impl Lerp for Range { + fn lerp(&self, level: f32) -> Hsla { + let level = level.clamp(0., 1.); + Hsla { + h: self.start.h + (level * (self.end.h - self.start.h)), + s: self.start.s + (level * (self.end.s - self.start.s)), + l: self.start.l + (level * (self.end.l - self.start.l)), + a: self.start.a + (level * (self.end.a - self.start.a)), + } + } +} + +impl From for Rgba { + fn from(value: gpui::color::Color) -> Self { + Self { + r: value.0.r as f32 / 255.0, + g: value.0.g as f32 / 255.0, + b: value.0.b as f32 / 255.0, + a: value.0.a as f32 / 255.0, + } + } +} + +impl From for Rgba { + fn from(color: Hsla) -> Self { + let h = color.h; + let s = color.s; + let l = color.l; + + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + let cm = c + m; + let xm = x + m; + + let (r, g, b) = match (h * 6.0).floor() as i32 { + 0 | 6 => (cm, xm, m), + 1 => (xm, cm, m), + 2 => (m, cm, xm), + 3 => (m, xm, cm), + 4 => (xm, m, cm), + _ => (cm, m, xm), + }; + + Rgba { + r, + g, + b, + a: color.a, + } + } +} + +impl TryFrom<&'_ str> for Rgba { + type Error = ParseIntError; + + fn try_from(value: &'_ str) -> Result { + let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0; + let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0; + let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0; + let a = if value.len() > 7 { + u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0 + } else { + 1.0 + }; + + Ok(Rgba { r, g, b, a }) + } +} + +impl Into for Rgba { + fn into(self) -> gpui::color::Color { + gpui::color::rgba(self.r, self.g, self.b, self.a) + } +} + +#[derive(Default, Copy, Clone, Debug, PartialEq)] +pub struct Hsla { + pub h: f32, + pub s: f32, + pub l: f32, + pub a: f32, +} + +pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { + Hsla { + h: h.clamp(0., 1.), + s: s.clamp(0., 1.), + l: l.clamp(0., 1.), + a: a.clamp(0., 1.), + } +} + +pub fn black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 1., + } +} + +impl From for Hsla { + fn from(color: Rgba) -> Self { + let r = color.r; + let g = color.g; + let b = color.b; + + let max = r.max(g.max(b)); + let min = r.min(g.min(b)); + let delta = max - min; + + let l = (max + min) / 2.0; + let s = if l == 0.0 || l == 1.0 { + 0.0 + } else if l < 0.5 { + delta / (2.0 * l) + } else { + delta / (2.0 - 2.0 * l) + }; + + let h = if delta == 0.0 { + 0.0 + } else if max == r { + ((g - b) / delta).rem_euclid(6.0) / 6.0 + } else if max == g { + ((b - r) / delta + 2.0) / 6.0 + } else { + ((r - g) / delta + 4.0) / 6.0 + }; + + Hsla { + h, + s, + l, + a: color.a, + } + } +} + +impl Hsla { + /// Scales the saturation and lightness by the given values, clamping at 1.0. + pub fn scale_sl(mut self, s: f32, l: f32) -> Self { + self.s = (self.s * s).clamp(0., 1.); + self.l = (self.l * l).clamp(0., 1.); + self + } + + /// Increases the saturation of the color by a certain amount, with a max + /// value of 1.0. + pub fn saturate(mut self, amount: f32) -> Self { + self.s += amount; + self.s = self.s.clamp(0.0, 1.0); + self + } + + /// Decreases the saturation of the color by a certain amount, with a min + /// value of 0.0. + pub fn desaturate(mut self, amount: f32) -> Self { + self.s -= amount; + self.s = self.s.max(0.0); + if self.s < 0.0 { + self.s = 0.0; + } + self + } + + /// Brightens the color by increasing the lightness by a certain amount, + /// with a max value of 1.0. + pub fn brighten(mut self, amount: f32) -> Self { + self.l += amount; + self.l = self.l.clamp(0.0, 1.0); + self + } + + /// Darkens the color by decreasing the lightness by a certain amount, + /// with a max value of 0.0. + pub fn darken(mut self, amount: f32) -> Self { + self.l -= amount; + self.l = self.l.clamp(0.0, 1.0); + self + } +} + +impl From for Hsla { + fn from(value: gpui::color::Color) -> Self { + Rgba::from(value).into() + } +} + +impl Into for Hsla { + fn into(self) -> gpui::color::Color { + Rgba::from(self).into() + } +} + +pub struct ColorScale { + colors: SmallVec<[Hsla; 2]>, + positions: SmallVec<[f32; 2]>, +} + +pub fn scale(colors: I) -> ColorScale +where + I: IntoIterator, + C: Into, +{ + let mut scale = ColorScale { + colors: colors.into_iter().map(Into::into).collect(), + positions: SmallVec::new(), + }; + let num_colors: f32 = scale.colors.len() as f32 - 1.0; + scale.positions = (0..scale.colors.len()) + .map(|i| i as f32 / num_colors) + .collect(); + scale +} + +impl ColorScale { + fn at(&self, t: f32) -> Hsla { + // Ensure that the input is within [0.0, 1.0] + debug_assert!( + 0.0 <= t && t <= 1.0, + "t value {} is out of range. Expected value in range 0.0 to 1.0", + t + ); + + let position = match self + .positions + .binary_search_by(|a| a.partial_cmp(&t).unwrap()) + { + Ok(index) | Err(index) => index, + }; + let lower_bound = position.saturating_sub(1); + let upper_bound = position.min(self.colors.len() - 1); + let lower_color = &self.colors[lower_bound]; + let upper_color = &self.colors[upper_bound]; + + match upper_bound.checked_sub(lower_bound) { + Some(0) | None => *lower_color, + Some(_) => { + let interval_t = (t - self.positions[lower_bound]) + / (self.positions[upper_bound] - self.positions[lower_bound]); + let h = lower_color.h + interval_t * (upper_color.h - lower_color.h); + let s = lower_color.s + interval_t * (upper_color.s - lower_color.s); + let l = lower_color.l + interval_t * (upper_color.l - lower_color.l); + let a = lower_color.a + interval_t * (upper_color.a - lower_color.a); + Hsla { h, s, l, a } + } + } + } +} diff --git a/crates/gpui/playground/src/components.rs b/crates/gpui/playground/src/components.rs new file mode 100644 index 0000000000000000000000000000000000000000..3901968bfcc0c1cd1f75f80419942a27b4180494 --- /dev/null +++ b/crates/gpui/playground/src/components.rs @@ -0,0 +1,100 @@ +use crate::{ + div::div, + element::{Element, ParentElement}, + style::StyleHelpers, + text::ArcCow, + themes::rose_pine, +}; +use gpui::ViewContext; +use playground_macros::Element; +use std::{marker::PhantomData, rc::Rc}; + +struct ButtonHandlers { + click: Option)>>, +} + +impl Default for ButtonHandlers { + fn default() -> Self { + Self { click: None } + } +} + +use crate as playground; +#[derive(Element)] +pub struct Button { + handlers: ButtonHandlers, + label: Option>, + icon: Option>, + data: Rc, + view_type: PhantomData, +} + +// Impl block for buttons without data. +// See below for an impl block for any button. +impl Button { + fn new() -> Self { + Self { + handlers: ButtonHandlers::default(), + label: None, + icon: None, + data: Rc::new(()), + view_type: PhantomData, + } + } + + pub fn data(self, data: D) -> Button { + Button { + handlers: ButtonHandlers::default(), + label: self.label, + icon: self.icon, + data: Rc::new(data), + view_type: PhantomData, + } + } +} + +// Impl block for *any* button. +impl Button { + pub fn label(mut self, label: impl Into>) -> Self { + self.label = Some(label.into()); + self + } + + pub fn icon(mut self, icon: impl Into>) -> Self { + self.icon = Some(icon.into()); + self + } + + // pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext) + 'static) -> Self { + // let data = self.data.clone(); + // Self::click(self, MouseButton::Left, move |view, _, cx| { + // handler(view, data.as_ref(), cx); + // }) + // } +} + +pub fn button() -> Button { + Button::new() +} + +impl Button { + fn render(&mut self, view: &mut V, cx: &mut ViewContext) -> impl Element { + // TODO: Drive theme from the context + let button = div() + .fill(rose_pine::dawn().error(0.5)) + .h_4() + .children(self.label.clone()); + + button + + // TODO: Event handling + // if let Some(handler) = self.handlers.click.clone() { + // let data = self.data.clone(); + // // button.mouse_down(MouseButton::Left, move |view, event, cx| { + // // handler(view, data.as_ref(), cx) + // // }) + // } else { + // button + // } + } +} diff --git a/crates/gpui/playground/src/div.rs b/crates/gpui/playground/src/div.rs new file mode 100644 index 0000000000000000000000000000000000000000..8efe3590258a4249abd9bb69a359f95692b6a6a0 --- /dev/null +++ b/crates/gpui/playground/src/div.rs @@ -0,0 +1,108 @@ +use crate::{ + element::{AnyElement, Element, Layout, ParentElement}, + interactive::{InteractionHandlers, Interactive}, + layout_context::LayoutContext, + paint_context::PaintContext, + style::{Style, StyleHelpers, StyleRefinement, Styleable}, +}; +use anyhow::Result; +use gpui::LayoutId; +use smallvec::SmallVec; + +pub struct Div { + style: StyleRefinement, + handlers: InteractionHandlers, + children: SmallVec<[AnyElement; 2]>, +} + +pub fn div() -> Div { + Div { + style: Default::default(), + handlers: Default::default(), + children: Default::default(), + } +} + +impl Element for Div { + type Layout = (); + + fn layout(&mut self, view: &mut V, cx: &mut LayoutContext) -> Result> + where + Self: Sized, + { + let children = self + .children + .iter_mut() + .map(|child| child.layout(view, cx)) + .collect::>>()?; + + cx.add_layout_node(self.style(), (), children) + } + + fn paint(&mut self, view: &mut V, layout: &mut Layout, cx: &mut PaintContext) + where + Self: Sized, + { + let style = self.style(); + + style.paint_background::(layout, cx); + for child in &mut self.children { + child.paint(view, cx); + } + } +} + +impl Styleable for Div { + type Style = Style; + + fn declared_style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl StyleHelpers for Div {} + +impl Interactive for Div { + fn interaction_handlers(&mut self) -> &mut InteractionHandlers { + &mut self.handlers + } +} + +impl ParentElement for Div { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +#[test] +fn test() { + // let elt = div().w_auto(); +} + +// trait Element { +// type Style; + +// fn layout() +// } + +// trait Stylable: Element { +// type Style; + +// fn with_style(self, style: Self::Style) -> Self; +// } + +// pub struct HoverStyle { +// default: S, +// hovered: S, +// } + +// struct Hover> { +// child: C, +// style: HoverStyle, +// } + +// impl> Hover { +// fn new(child: C, style: HoverStyle) -> Self { +// Self { child, style } +// } +// } diff --git a/crates/gpui/playground/src/element.rs b/crates/gpui/playground/src/element.rs new file mode 100644 index 0000000000000000000000000000000000000000..082f3b02b06522f3c12b435e6d02febd4a319390 --- /dev/null +++ b/crates/gpui/playground/src/element.rs @@ -0,0 +1,158 @@ +use anyhow::Result; +use derive_more::{Deref, DerefMut}; +use gpui::{geometry::rect::RectF, EngineLayout}; +use smallvec::SmallVec; +use std::marker::PhantomData; +use util::ResultExt; + +pub use crate::layout_context::LayoutContext; +pub use crate::paint_context::PaintContext; + +type LayoutId = gpui::LayoutId; + +pub trait Element: 'static { + type Layout; + + fn layout( + &mut self, + view: &mut V, + cx: &mut LayoutContext, + ) -> Result> + where + Self: Sized; + + fn paint( + &mut self, + view: &mut V, + layout: &mut Layout, + cx: &mut PaintContext, + ) where + Self: Sized; + + fn into_any(self) -> AnyElement + where + Self: 'static + Sized, + { + AnyElement(Box::new(ElementState { + element: self, + layout: None, + })) + } +} + +/// Used to make ElementState into a trait object, so we can wrap it in AnyElement. +trait ElementStateObject { + fn layout(&mut self, view: &mut V, cx: &mut LayoutContext) -> Result; + fn paint(&mut self, view: &mut V, cx: &mut PaintContext); +} + +/// A wrapper around an element that stores its layout state. +struct ElementState> { + element: E, + layout: Option>, +} + +/// We blanket-implement the object-safe ElementStateObject interface to make ElementStates into trait objects +impl> ElementStateObject for ElementState { + fn layout(&mut self, view: &mut V, cx: &mut LayoutContext) -> Result { + let layout = self.element.layout(view, cx)?; + let layout_id = layout.id; + self.layout = Some(layout); + Ok(layout_id) + } + + fn paint(&mut self, view: &mut V, cx: &mut PaintContext) { + let layout = self.layout.as_mut().expect("paint called before layout"); + if layout.engine_layout.is_none() { + layout.engine_layout = cx.computed_layout(layout.id).log_err() + } + self.element.paint(view, layout, cx) + } +} + +/// A dynamic element. +pub struct AnyElement(Box>); + +impl AnyElement { + pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext) -> Result { + self.0.layout(view, cx) + } + + pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext) { + self.0.paint(view, cx) + } +} + +#[derive(Deref, DerefMut)] +pub struct Layout { + id: LayoutId, + engine_layout: Option, + #[deref] + #[deref_mut] + element_data: D, + view_type: PhantomData, +} + +impl Layout { + pub fn new(id: LayoutId, element_data: D) -> Self { + Self { + id, + engine_layout: None, + element_data: element_data, + view_type: PhantomData, + } + } + + pub fn bounds(&mut self, cx: &mut PaintContext) -> RectF { + self.engine_layout(cx).bounds + } + + pub fn order(&mut self, cx: &mut PaintContext) -> u32 { + self.engine_layout(cx).order + } + + fn engine_layout(&mut self, cx: &mut PaintContext<'_, '_, '_, '_, V>) -> &mut EngineLayout { + self.engine_layout + .get_or_insert_with(|| cx.computed_layout(self.id).log_err().unwrap_or_default()) + } +} + +impl Layout>> { + pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext) { + let mut element = self.element_data.take().unwrap(); + element.paint(view, cx); + self.element_data = Some(element); + } +} + +pub trait ParentElement { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; + + fn child(mut self, child: impl IntoElement) -> Self + where + Self: Sized, + { + self.children_mut().push(child.into_element().into_any()); + self + } + + fn children(mut self, children: I) -> Self + where + I: IntoIterator, + E: IntoElement, + Self: Sized, + { + self.children_mut().extend( + children + .into_iter() + .map(|child| child.into_element().into_any()), + ); + self + } +} + +pub trait IntoElement { + type Element: Element; + + fn into_element(self) -> Self::Element; +} diff --git a/crates/gpui/playground/src/hoverable.rs b/crates/gpui/playground/src/hoverable.rs new file mode 100644 index 0000000000000000000000000000000000000000..5545155a60c4e55e15bc534843c76a0e6f543d36 --- /dev/null +++ b/crates/gpui/playground/src/hoverable.rs @@ -0,0 +1,76 @@ +use crate::{ + element::{Element, Layout}, + layout_context::LayoutContext, + paint_context::PaintContext, + style::{StyleRefinement, Styleable}, +}; +use anyhow::Result; +use gpui::platform::MouseMovedEvent; +use refineable::Refineable; +use std::{cell::Cell, marker::PhantomData}; + +pub struct Hoverable + Styleable> { + hovered: Cell, + child_style: StyleRefinement, + hovered_style: StyleRefinement, + child: E, + view_type: PhantomData, +} + +pub fn hoverable + Styleable>(mut child: E) -> Hoverable { + Hoverable { + hovered: Cell::new(false), + child_style: child.declared_style().clone(), + hovered_style: Default::default(), + child, + view_type: PhantomData, + } +} + +impl + Styleable> Styleable for Hoverable { + type Style = E::Style; + + fn declared_style(&mut self) -> &mut crate::style::StyleRefinement { + self.child.declared_style() + } +} + +impl + Styleable> Element for Hoverable { + type Layout = E::Layout; + + fn layout(&mut self, view: &mut V, cx: &mut LayoutContext) -> Result> + where + Self: Sized, + { + self.child.layout(view, cx) + } + + fn paint( + &mut self, + view: &mut V, + layout: &mut Layout, + cx: &mut PaintContext, + ) where + Self: Sized, + { + if self.hovered.get() { + // If hovered, refine the child's style with this element's style. + self.child.declared_style().refine(&self.hovered_style); + } else { + // Otherwise, set the child's style back to its original style. + *self.child.declared_style() = self.child_style.clone(); + } + + let bounds = layout.bounds(cx); + let order = layout.order(cx); + self.hovered.set(bounds.contains_point(cx.mouse_position())); + let was_hovered = self.hovered.clone(); + cx.on_event(order, move |view, event: &MouseMovedEvent, cx| { + let is_hovered = bounds.contains_point(event.position); + if is_hovered != was_hovered.get() { + was_hovered.set(is_hovered); + cx.repaint(); + } + }); + } +} diff --git a/crates/gpui/playground/src/interactive.rs b/crates/gpui/playground/src/interactive.rs new file mode 100644 index 0000000000000000000000000000000000000000..8debcb1692cc5bcd42a8e65e82aa27ad6a02d4a0 --- /dev/null +++ b/crates/gpui/playground/src/interactive.rs @@ -0,0 +1,34 @@ +use gpui::{platform::MouseMovedEvent, EventContext}; +use smallvec::SmallVec; +use std::rc::Rc; + +pub trait Interactive { + fn interaction_handlers(&mut self) -> &mut InteractionHandlers; + + fn on_mouse_move(mut self, handler: H) -> Self + where + H: 'static + Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext), + Self: Sized, + { + self.interaction_handlers() + .mouse_moved + .push(Rc::new(move |view, event, hit_test, cx| { + handler(view, event, hit_test, cx); + cx.bubble + })); + self + } +} + +pub struct InteractionHandlers { + mouse_moved: + SmallVec<[Rc) -> bool>; 2]>, +} + +impl Default for InteractionHandlers { + fn default() -> Self { + Self { + mouse_moved: Default::default(), + } + } +} diff --git a/crates/gpui/playground/src/layout_context.rs b/crates/gpui/playground/src/layout_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..7c9f13e7f08519c13a7ac78c2f90bd628e8fd3a8 --- /dev/null +++ b/crates/gpui/playground/src/layout_context.rs @@ -0,0 +1,54 @@ +use anyhow::{anyhow, Result}; +use derive_more::{Deref, DerefMut}; +pub use gpui::LayoutContext as LegacyLayoutContext; +use gpui::{RenderContext, ViewContext}; +pub use taffy::tree::NodeId; + +use crate::{element::Layout, style::Style}; + +#[derive(Deref, DerefMut)] +pub struct LayoutContext<'a, 'b, 'c, 'd, V> { + #[deref] + #[deref_mut] + pub(crate) legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>, +} + +impl<'a, 'b, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, '_, '_, V> { + fn text_style(&self) -> gpui::fonts::TextStyle { + self.legacy_cx.text_style() + } + + fn push_text_style(&mut self, style: gpui::fonts::TextStyle) { + self.legacy_cx.push_text_style(style) + } + + fn pop_text_style(&mut self) { + self.legacy_cx.pop_text_style() + } + + fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> { + &mut self.view_context + } +} + +impl<'a, 'b, 'c, 'd, V: 'static> LayoutContext<'a, 'b, 'c, 'd, V> { + pub fn new(legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>) -> Self { + Self { legacy_cx } + } + + pub fn add_layout_node( + &mut self, + style: Style, + element_data: D, + children: impl IntoIterator, + ) -> Result> { + let rem_size = self.rem_pixels(); + let id = self + .legacy_cx + .layout_engine() + .ok_or_else(|| anyhow!("no layout engine"))? + .add_node(style.to_taffy(rem_size), children)?; + + Ok(Layout::new(id, element_data)) + } +} diff --git a/crates/gpui/playground/src/paint_context.rs b/crates/gpui/playground/src/paint_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..d853aff7f85ff4b8b434dffb6e99caca9f91d0d2 --- /dev/null +++ b/crates/gpui/playground/src/paint_context.rs @@ -0,0 +1,71 @@ +use anyhow::{anyhow, Result}; +use derive_more::{Deref, DerefMut}; +use gpui::{scene::EventHandler, EngineLayout, EventContext, LayoutId, RenderContext, ViewContext}; +pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext}; +use std::{any::TypeId, rc::Rc}; +pub use taffy::tree::NodeId; + +#[derive(Deref, DerefMut)] +pub struct PaintContext<'a, 'b, 'c, 'd, V> { + #[deref] + #[deref_mut] + pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>, + pub(crate) scene: &'d mut gpui::SceneBuilder, +} + +impl<'a, 'b, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, '_, '_, V> { + fn text_style(&self) -> gpui::fonts::TextStyle { + self.legacy_cx.text_style() + } + + fn push_text_style(&mut self, style: gpui::fonts::TextStyle) { + self.legacy_cx.push_text_style(style) + } + + fn pop_text_style(&mut self) { + self.legacy_cx.pop_text_style() + } + + fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> { + &mut self.view_context + } +} + +impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> { + pub fn new( + legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>, + scene: &'d mut gpui::SceneBuilder, + ) -> Self { + Self { legacy_cx, scene } + } + + pub fn on_event( + &mut self, + order: u32, + handler: impl Fn(&mut V, &E, &mut ViewContext) + 'static, + ) { + let view = self.weak_handle(); + + self.scene.event_handlers.push(EventHandler { + order, + handler: Rc::new(move |event, window_cx| { + if let Some(view) = view.upgrade(window_cx) { + view.update(window_cx, |view, view_cx| { + let mut event_cx = EventContext::new(view_cx); + handler(view, event.downcast_ref().unwrap(), &mut event_cx); + event_cx.bubble + }) + } else { + true + } + }), + event_type: TypeId::of::(), + }) + } + + pub(crate) fn computed_layout(&mut self, layout_id: LayoutId) -> Result { + self.layout_engine() + .ok_or_else(|| anyhow!("no layout engine present"))? + .computed_layout(layout_id) + } +} diff --git a/crates/gpui/playground/src/playground.rs b/crates/gpui/playground/src/playground.rs new file mode 100644 index 0000000000000000000000000000000000000000..2462ac99f59e412cdbd16d3b616503dcb7810660 --- /dev/null +++ b/crates/gpui/playground/src/playground.rs @@ -0,0 +1,83 @@ +#![allow(dead_code, unused_variables)] +use crate::{color::black, style::StyleHelpers}; +use element::Element; +use gpui::{ + geometry::{rect::RectF, vector::vec2f}, + platform::WindowOptions, +}; +use log::LevelFilter; +use simplelog::SimpleLogger; +use themes::{rose_pine, ThemeColors}; +use view::view; + +mod adapter; +mod color; +mod components; +mod div; +mod element; +mod hoverable; +mod interactive; +mod layout_context; +mod paint_context; +mod style; +mod text; +mod themes; +mod view; + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + gpui::App::new(()).unwrap().run(|cx| { + cx.add_window( + WindowOptions { + bounds: gpui::platform::WindowBounds::Fixed(RectF::new( + vec2f(0., 0.), + vec2f(400., 300.), + )), + center: true, + ..Default::default() + }, + |_| view(|_| playground(&rose_pine::moon())), + ); + cx.platform().activate(true); + }); +} + +fn playground(theme: &ThemeColors) -> impl Element { + use div::div; + + div() + .text_color(black()) + .h_full() + .w_1_2() + .fill(theme.success(0.5)) + // .hover() + // .fill(theme.error(0.5)) + // .child(button().label("Hello").click(|_, _, _| println!("click!"))) +} + +// todo!() +// // column() +// // .size(auto()) +// // .fill(theme.base(0.5)) +// // .text_color(theme.text(0.5)) +// // .child(title_bar(theme)) +// // .child(stage(theme)) +// // .child(status_bar(theme)) +// } + +// fn title_bar(theme: &ThemeColors) -> impl Element { +// row() +// .fill(theme.base(0.2)) +// .justify(0.) +// .width(auto()) +// .child(text("Zed Playground")) +// } + +// fn stage(theme: &ThemeColors) -> impl Element { +// row().fill(theme.surface(0.9)) +// } + +// fn status_bar(theme: &ThemeColors) -> impl Element { +// row().fill(theme.surface(0.1)) +// } diff --git a/crates/gpui/playground/src/style.rs b/crates/gpui/playground/src/style.rs new file mode 100644 index 0000000000000000000000000000000000000000..9216702f7fab3a50cc4a27502ebd3a383db5134f --- /dev/null +++ b/crates/gpui/playground/src/style.rs @@ -0,0 +1,286 @@ +use crate::{ + color::Hsla, + element::{Element, Layout}, + paint_context::PaintContext, +}; +use gpui::{ + fonts::TextStyleRefinement, + geometry::{ + AbsoluteLength, DefiniteLength, Edges, EdgesRefinement, Length, Point, PointRefinement, + Size, SizeRefinement, + }, +}; +use playground_macros::styleable_helpers; +use refineable::Refineable; +pub use taffy::style::{ + AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent, + Overflow, Position, +}; + +#[derive(Clone, Refineable)] +pub struct Style { + /// What layout strategy should be used? + pub display: Display, + + // Overflow properties + /// How children overflowing their container should affect layout + #[refineable] + pub overflow: Point, + /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. + pub scrollbar_width: f32, + + // Position properties + /// What should the `position` value of this struct use as a base offset? + pub position: Position, + /// How should the position of this element be tweaked relative to the layout defined? + #[refineable] + pub inset: Edges, + + // Size properies + /// Sets the initial size of the item + #[refineable] + pub size: Size, + /// Controls the minimum size of the item + #[refineable] + pub min_size: Size, + /// Controls the maximum size of the item + #[refineable] + pub max_size: Size, + /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height. + pub aspect_ratio: Option, + + // Spacing Properties + /// How large should the margin be on each side? + #[refineable] + pub margin: Edges, + /// How large should the padding be on each side? + #[refineable] + pub padding: Edges, + /// How large should the border be on each side? + #[refineable] + pub border: Edges, + + // Alignment properties + /// How this node's children aligned in the cross/block axis? + pub align_items: Option, + /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set + pub align_self: Option, + /// How should content contained within this item be aligned in the cross/block axis + pub align_content: Option, + /// How should contained within this item be aligned in the main/inline axis + pub justify_content: Option, + /// How large should the gaps between items in a flex container be? + #[refineable] + pub gap: Size, + + // Flexbox properies + /// Which direction does the main axis flow in? + pub flex_direction: FlexDirection, + /// Should elements wrap, or stay in a single line? + pub flex_wrap: FlexWrap, + /// Sets the initial main axis size of the item + pub flex_basis: Length, + /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive. + pub flex_grow: f32, + /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive. + pub flex_shrink: f32, + + /// The fill color of this element + pub fill: Option, + /// The radius of the corners of this element + #[refineable] + pub corner_radii: CornerRadii, + /// The color of text within this element. Cascades to children unless overridden. + pub text_color: Option, +} + +impl Style { + pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style { + taffy::style::Style { + display: self.display, + overflow: self.overflow.clone().into(), + scrollbar_width: self.scrollbar_width, + position: self.position, + inset: self.inset.to_taffy(rem_size), + size: self.size.to_taffy(rem_size), + min_size: self.min_size.to_taffy(rem_size), + max_size: self.max_size.to_taffy(rem_size), + aspect_ratio: self.aspect_ratio, + margin: self.margin.to_taffy(rem_size), + padding: self.padding.to_taffy(rem_size), + border: self.border.to_taffy(rem_size), + align_items: self.align_items, + align_self: self.align_self, + align_content: self.align_content, + justify_content: self.justify_content, + gap: self.gap.to_taffy(rem_size), + flex_direction: self.flex_direction, + flex_wrap: self.flex_wrap, + flex_basis: self.flex_basis.to_taffy(rem_size).into(), + flex_grow: self.flex_grow, + flex_shrink: self.flex_shrink, + ..Default::default() // Ignore grid properties for now + } + } + + /// Paints the background of an element styled with this style. + /// Return the bounds in which to paint the content. + pub fn paint_background>( + &self, + layout: &mut Layout, + cx: &mut PaintContext, + ) { + let bounds = layout.bounds(cx); + let rem_size = cx.rem_pixels(); + if let Some(color) = self.fill.as_ref().and_then(Fill::color) { + cx.scene.push_quad(gpui::Quad { + bounds, + background: Some(color.into()), + corner_radii: self.corner_radii.to_gpui(rem_size), + border: Default::default(), + }); + } + } +} + +impl Default for Style { + fn default() -> Self { + Style { + display: Display::DEFAULT, + overflow: Point { + x: Overflow::Visible, + y: Overflow::Visible, + }, + scrollbar_width: 0.0, + position: Position::Relative, + inset: Edges::auto(), + margin: Edges::::zero(), + padding: Edges::::zero(), + border: Edges::::zero(), + size: Size::auto(), + min_size: Size::auto(), + max_size: Size::auto(), + aspect_ratio: None, + gap: Size::zero(), + // Aligment + align_items: None, + align_self: None, + align_content: None, + justify_content: None, + // Flexbox + flex_direction: FlexDirection::Row, + flex_wrap: FlexWrap::NoWrap, + flex_grow: 0.0, + flex_shrink: 1.0, + flex_basis: Length::Auto, + fill: None, + text_color: None, + corner_radii: CornerRadii::default(), + } + } +} + +impl StyleRefinement { + pub fn text_style(&self) -> Option { + self.text_color.map(|color| TextStyleRefinement { + color: Some(color.into()), + ..Default::default() + }) + } +} + +pub struct OptionalTextStyle { + color: Option, +} + +impl OptionalTextStyle { + pub fn apply(&self, style: &mut gpui::fonts::TextStyle) { + if let Some(color) = self.color { + style.color = color.into(); + } + } +} + +#[derive(Clone)] +pub enum Fill { + Color(Hsla), +} + +impl Fill { + pub fn color(&self) -> Option { + match self { + Fill::Color(color) => Some(*color), + } + } +} + +impl Default for Fill { + fn default() -> Self { + Self::Color(Hsla::default()) + } +} + +impl From for Fill { + fn from(color: Hsla) -> Self { + Self::Color(color) + } +} + +#[derive(Clone, Refineable, Default)] +pub struct CornerRadii { + top_left: AbsoluteLength, + top_right: AbsoluteLength, + bottom_left: AbsoluteLength, + bottom_right: AbsoluteLength, +} + +impl CornerRadii { + pub fn to_gpui(&self, rem_size: f32) -> gpui::scene::CornerRadii { + gpui::scene::CornerRadii { + top_left: self.top_left.to_pixels(rem_size), + top_right: self.top_right.to_pixels(rem_size), + bottom_left: self.bottom_left.to_pixels(rem_size), + bottom_right: self.bottom_right.to_pixels(rem_size), + } + } +} + +pub trait Styleable { + type Style: refineable::Refineable; + + fn declared_style(&mut self) -> &mut playground::style::StyleRefinement; + + fn style(&mut self) -> playground::style::Style { + let mut style = playground::style::Style::default(); + style.refine(self.declared_style()); + style + } +} + +// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc. +// +// Example: +// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind. +// fn p_2(mut self) -> Self where Self: Sized; +use crate as playground; // Macro invocation references this crate as playground. +pub trait StyleHelpers: Styleable