diff --git a/.cargo/config.toml b/.cargo/config.toml index 35049cbcb13c204648d1f7897162492f05123199..9da6b3be080072d89d16a199e2d60d527eeacd07 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,6 @@ [alias] xtask = "run --package xtask --" + +[build] +# v0 mangling scheme provides more detailed backtraces around closures +rustflags = ["-C", "symbol-mangling-version=v0"] diff --git a/.gitignore b/.gitignore index 15a0a9f5f2f02bee670d6b23dbfc4116ccd20448..2d8807a4b0559751ff341eacf7dfaf51c84c405c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea **/target **/cargo-target /zed.xcworkspace diff --git a/Cargo.lock b/Cargo.lock index 8062731144a9656c0fba03c045bcf7a41e90db87..b39571520dcbe22ab34826c0e1a447f7940207a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "activity_indicator" version = "0.1.0" @@ -73,24 +67,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if 1.0.0", + "getrandom 0.2.10", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "aho-corasick" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -100,33 +86,25 @@ name = "ai" version = "0.1.0" dependencies = [ "anyhow", - "chrono", - "collections", - "ctor", - "editor", - "env_logger 0.9.3", - "fs", + "async-trait", + "bincode", "futures 0.3.28", "gpui", - "indoc", "isahc", - "language", + "lazy_static", "log", - "menu", - "ordered-float", - "project", + "matrixmultiply", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "parse_duration", + "postage", "rand 0.8.5", "regex", - "schemars", - "search", + "rusqlite", "serde", "serde_json", - "settings", - "smol", - "theme", - "tiktoken-rs 0.4.5", + "tiktoken-rs 0.5.4", "util", - "workspace", ] [[package]] @@ -136,7 +114,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "log", "serde", - "toml 0.7.6", + "toml 0.7.8", ] [[package]] @@ -146,7 +124,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -165,14 +143,14 @@ dependencies = [ "mio-anonymous-pipes", "mio-extras", "miow 0.3.7", - "nix 0.26.2", + "nix 0.26.4", "parking_lot 0.12.1", "regex-automata 0.1.10", "serde", "serde_yaml", "signal-hook", "signal-hook-mio", - "toml 0.7.6", + "toml 0.7.8", "unicode-width", "vte", "windows-sys", @@ -235,24 +213,23 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal 0.4.9", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" [[package]] name = "anstyle-parse" @@ -274,9 +251,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", "windows-sys", @@ -312,6 +289,44 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "assistant" +version = "0.1.0" +dependencies = [ + "ai", + "anyhow", + "chrono", + "client", + "collections", + "ctor", + "editor", + "env_logger 0.9.3", + "fs", + "futures 0.3.28", + "gpui", + "indoc", + "isahc", + "language", + "log", + "menu", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "project", + "rand 0.8.5", + "regex", + "schemars", + "search", + "serde", + "serde_json", + "settings", + "smol", + "theme", + "tiktoken-rs 0.4.5", + "util", + "uuid 1.4.1", + "workspace", +] + [[package]] name = "async-broadcast" version = "0.4.1" @@ -343,7 +358,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tokio", ] @@ -357,7 +372,7 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", ] [[package]] @@ -482,13 +497,13 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -511,7 +526,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "pin-utils", "slab", "wasm-bindgen-futures", @@ -525,7 +540,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", ] [[package]] @@ -536,7 +551,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -567,7 +582,7 @@ dependencies = [ "futures-core", "futures-io", "rustls 0.19.1", - "webpki 0.21.4", + "webpki", "webpki-roots 0.21.1", ] @@ -579,7 +594,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -592,15 +607,15 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tungstenite 0.16.0", ] [[package]] name = "atoi" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ "num-traits", ] @@ -681,7 +696,7 @@ dependencies = [ "axum-core", "base64 0.13.1", "bitflags 1.3.2", - "bytes 1.4.0", + "bytes 1.5.0", "futures-util", "headers", "http", @@ -692,7 +707,7 @@ dependencies = [ "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "serde", "serde_json", "serde_urlencoded", @@ -713,7 +728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" dependencies = [ "async-trait", - "bytes 1.4.0", + "bytes 1.5.0", "futures-util", "http", "http-body", @@ -729,11 +744,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb" dependencies = [ "axum", - "bytes 1.4.0", + "bytes 1.5.0", "futures-util", "http", "mime", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "serde", "serde_json", "tokio", @@ -754,7 +769,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", - "object 0.32.0", + "object 0.32.1", "rustc-demangle", ] @@ -769,19 +784,6 @@ dependencies = [ "nix 0.23.2", ] -[[package]] -name = "bae" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "base64" version = "0.13.1" @@ -790,9 +792,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -800,6 +802,17 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint 0.4.4", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -848,7 +861,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.29", + "syn 2.0.37", "which", ] @@ -985,7 +998,7 @@ dependencies = [ "collections", "editor", "gpui", - "itertools", + "itertools 0.10.5", "language", "outline", "project", @@ -1008,20 +1021,20 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", - "regex-automata 0.3.6", + "regex-automata 0.3.8", "serde", ] [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecheck" @@ -1069,9 +1082,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "call" @@ -1234,6 +1247,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "clock", "collections", "db", "feature_flags", @@ -1251,12 +1265,13 @@ dependencies = [ "serde", "serde_derive", "settings", + "smallvec", "smol", "sum_tree", "tempfile", "text", "thiserror", - "time 0.3.27", + "time", "tiny_http", "url", "util", @@ -1265,18 +1280,17 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi 0.3.9", + "windows-targets 0.48.5", ] [[package]] @@ -1324,24 +1338,23 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.24" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", - "clap_derive 4.3.12", - "once_cell", + "clap_derive 4.4.2", ] [[package]] name = "clap_builder" -version = "4.3.24" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", - "clap_lex 0.5.0", + "clap_lex 0.5.1", "strsim", ] @@ -1360,14 +1373,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -1381,9 +1394,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "cli" @@ -1426,10 +1439,11 @@ dependencies = [ "settings", "smol", "sum_tree", + "sysinfo", "tempfile", "text", "thiserror", - "time 0.3.27", + "time", "tiny_http", "url", "util", @@ -1483,7 +1497,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.20.0" +version = "0.23.2" dependencies = [ "anyhow", "async-trait", @@ -1528,16 +1542,16 @@ dependencies = [ "rpc", "scrypt", "sea-orm", - "sea-query", "serde", "serde_derive", "serde_json", "settings", "sha-1 0.9.8", + "smallvec", "sqlx", "text", "theme", - "time 0.3.27", + "time", "tokio", "tokio-tungstenite", "toml 0.5.11", @@ -1548,6 +1562,7 @@ dependencies = [ "tracing-subscriber", "unindent", "util", + "uuid 1.4.1", "workspace", ] @@ -1564,6 +1579,7 @@ dependencies = [ "collections", "context_menu", "db", + "drag_and_drop", "editor", "feature_flags", "feedback", @@ -1577,12 +1593,14 @@ dependencies = [ "postage", "project", "recent_projects", + "rich_text", "schemars", "serde", "serde_derive", "settings", "theme", "theme_selector", + "time", "util", "vcs_menu", "workspace", @@ -1614,7 +1632,7 @@ version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "memchr", ] @@ -1666,6 +1684,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "context_menu" version = "0.1.0" @@ -1783,9 +1807,9 @@ dependencies = [ [[package]] name = "core-services" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b344b958cae90858bf6086f49599ecc5ec8698eacad0ea155509ba11fab347" +checksum = "92567e81db522550ebaf742c5d875624ec7820c2c7ee5f8c60e4ce7c2ae3c0fd" dependencies = [ "core-foundation", ] @@ -1954,7 +1978,7 @@ dependencies = [ "cranelift-codegen", "cranelift-entity", "cranelift-frontend", - "itertools", + "itertools 0.10.5", "log", "smallvec", "wasmparser", @@ -2085,9 +2109,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.65+curl-8.2.1" +version = "0.4.66+curl-8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986" +checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9" dependencies = [ "cc", "libc", @@ -2095,14 +2119,14 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "winapi 0.3.9", + "windows-sys", ] [[package]] name = "dashmap" -version = "5.5.1" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if 1.0.0", "hashbrown 0.14.0", @@ -2158,6 +2182,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.8" @@ -2167,6 +2202,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -2176,7 +2222,7 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn 1.0.109", ] @@ -2218,6 +2264,9 @@ dependencies = [ "lsp", "postage", "project", + "schemars", + "serde", + "serde_derive", "serde_json", "settings", "smallvec", @@ -2249,6 +2298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -2350,15 +2400,15 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "editor" version = "0.1.0" dependencies = [ - "aho-corasick 0.7.20", + "aho-corasick", "anyhow", "client", "clock", @@ -2375,17 +2425,18 @@ dependencies = [ "git", "gpui", "indoc", - "itertools", + "itertools 0.10.5", "language", "lazy_static", "log", "lsp", - "ordered-float", + "ordered-float 2.10.0", "parking_lot 0.11.2", "postage", "project", "pulldown-cmark", "rand 0.8.5", + "rich_text", "rpc", "schemars", "serde", @@ -2412,6 +2463,9 @@ name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" @@ -2465,9 +2519,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ "serde", ] @@ -2485,9 +2539,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -2514,6 +2568,17 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if 1.0.0", + "home", + "windows-sys", +] + [[package]] name = "euclid" version = "0.22.9" @@ -2618,6 +2683,7 @@ dependencies = [ name = "file_finder" version = "0.1.0" dependencies = [ + "collections", "ctor", "editor", "env_logger 0.9.3", @@ -2648,6 +2714,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2678,13 +2750,12 @@ checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "pin-project", "spin 0.9.8", ] @@ -2788,7 +2859,6 @@ dependencies = [ "lazy_static", "libc", "log", - "lsp", "parking_lot 0.11.2", "regex", "rope", @@ -2800,7 +2870,7 @@ dependencies = [ "sum_tree", "tempfile", "text", - "time 0.3.27", + "time", "util", ] @@ -2912,13 +2982,13 @@ dependencies = [ [[package]] name = "futures-intrusive" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.11.2", + "parking_lot 0.12.1", ] [[package]] @@ -2938,7 +3008,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "waker-fn", ] @@ -2950,7 +3020,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2979,7 +3049,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "pin-utils", "slab", "tokio-io", @@ -3106,7 +3176,7 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.4", + "aho-corasick", "bstr", "fnv", "log", @@ -3165,14 +3235,14 @@ dependencies = [ "futures 0.3.28", "gpui_macros", "image", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "media", "metal", "num_cpus", "objc", - "ordered-float", + "ordered-float 2.10.0", "parking", "parking_lot 0.11.2", "pathfinder_color", @@ -3194,7 +3264,7 @@ dependencies = [ "sum_tree", "taffy", "thiserror", - "time 0.3.27", + "time", "tiny-skia", "usvg", "util", @@ -3259,14 +3329,14 @@ dependencies = [ "gpui3_macros", "gpui_macros", "image", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "media", "metal", "num_cpus", "objc", - "ordered-float", + "ordered-float 2.10.0", "parking", "parking_lot 0.11.2", "pathfinder_geometry", @@ -3289,7 +3359,7 @@ dependencies = [ "sum_tree", "taffy", "thiserror", - "time 0.3.27", + "time", "tiny-skia", "usvg", "util", @@ -3328,7 +3398,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "fnv", "futures-core", "futures-sink", @@ -3337,7 +3407,7 @@ dependencies = [ "indexmap 1.9.3", "slab", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", "tracing", ] @@ -3380,31 +3450,21 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" -dependencies = [ - "hashbrown 0.11.2", -] - -[[package]] -name = "hashlink" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ "hashbrown 0.14.0", ] [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", - "bytes 1.4.0", + "base64 0.21.4", + "bytes 1.5.0", "headers-core", "http", "httpdate", @@ -3459,9 +3519,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -3518,7 +3578,7 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "fnv", "itoa", ] @@ -3529,9 +3589,9 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "http", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", ] [[package]] @@ -3554,9 +3614,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human_bytes" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e2b089f28ad15597b48d8c0a8fe94eeb1c1cb26ca99b6f66ac9582ae10c5e6" +checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" [[package]] name = "humantime" @@ -3570,7 +3630,7 @@ version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "futures-channel", "futures-core", "futures-util", @@ -3580,7 +3640,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "socket2 0.4.9", "tokio", "tower-service", @@ -3595,7 +3655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tokio", "tokio-io-timeout", ] @@ -3606,7 +3666,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "hyper", "native-tls", "tokio", @@ -3709,6 +3769,17 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "inherent" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "install_cli" version = "0.1.0" @@ -3755,7 +3826,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", "windows-sys", ] @@ -3812,8 +3883,8 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", - "rustix 0.38.8", + "hermit-abi 0.3.3", + "rustix 0.38.14", "windows-sys", ] @@ -3853,6 +3924,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -4107,9 +4187,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libgit2-sys" @@ -4151,9 +4231,9 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libsqlite3-sys" -version = "0.24.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", "pkg-config", @@ -4201,9 +4281,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lipsum" @@ -4224,7 +4304,7 @@ dependencies = [ "async-trait", "block", "byteorder", - "bytes 1.4.0", + "bytes 1.5.0", "cocoa", "collections", "core-foundation", @@ -4372,9 +4452,9 @@ checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" [[package]] name = "matrixmultiply" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" dependencies = [ "autocfg", "rawpointer", @@ -4402,7 +4482,7 @@ dependencies = [ "anyhow", "bindgen 0.65.1", "block", - "bytes 1.4.0", + "bytes 1.5.0", "core-foundation", "foreign-types", "metal", @@ -4411,9 +4491,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memfd" @@ -4648,6 +4728,19 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex 0.4.4", + "num-integer", + "num-traits", + "rawpointer", +] + [[package]] name = "ndk" version = "0.7.0" @@ -4714,14 +4807,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", "libc", - "static_assertions", ] [[package]] @@ -4788,7 +4880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ "num-bigint 0.2.6", - "num-complex", + "num-complex 0.2.4", "num-integer", "num-iter", "num-rational 0.2.4", @@ -4834,6 +4926,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.2.4" @@ -4844,6 +4953,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -4915,7 +5033,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", ] @@ -4942,8 +5060,8 @@ dependencies = [ [[package]] name = "nvim-rs" -version = "0.5.0" -source = "git+https://github.com/KillTheMule/nvim-rs?branch=master#d701c2790dcb2579f8f4d7003ba30e2100a7d25b" +version = "0.6.0-pre" +source = "git+https://github.com/KillTheMule/nvim-rs?branch=master#0d2b1c884f3c39a76b5b7aac0b429f4624843954" dependencies = [ "async-trait", "futures 0.3.28", @@ -4952,7 +5070,7 @@ dependencies = [ "rmp", "rmpv", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", ] [[package]] @@ -4988,9 +5106,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -5032,11 +5150,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.56" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -5053,7 +5171,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -5064,9 +5182,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.91" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", @@ -5084,32 +5202,42 @@ dependencies = [ ] [[package]] -name = "os_str_bytes" -version = "6.5.1" +name = "ordered-float" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" - -[[package]] +checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] name = "ouroboros" -version = "0.15.6" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" dependencies = [ "aliasable", "ouroboros_macro", + "static_assertions", ] [[package]] name = "ouroboros_macro" -version = "0.15.6" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" dependencies = [ - "Inflector", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] @@ -5120,7 +5248,7 @@ dependencies = [ "fuzzy", "gpui", "language", - "ordered-float", + "ordered-float 2.10.0", "picker", "postage", "settings", @@ -5136,15 +5264,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "owning_ref" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "parity-tokio-ipc" version = "0.9.0" @@ -5262,11 +5381,11 @@ dependencies = [ [[package]] name = "pathfinder_simd" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff" +checksum = "0444332826c70dc47be74a7c6a5fc44e23a7905ad6858d4162b658320455ef93" dependencies = [ - "rustc_version 0.3.3", + "rustc_version", ] [[package]] @@ -5296,20 +5415,19 @@ dependencies = [ ] [[package]] -name = "percent-encoding" -version = "2.3.0" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] [[package]] -name = "pest" -version = "2.7.2" +name = "percent-encoding" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" -dependencies = [ - "thiserror", - "ucd-trie", -] +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" @@ -5361,7 +5479,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -5372,9 +5490,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -5382,6 +5500,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -5405,12 +5544,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "indexmap 1.9.3", "line-wrap", "quick-xml", "serde", - "time 0.3.27", + "time", ] [[package]] @@ -5475,7 +5614,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "windows-sys", ] @@ -5520,12 +5659,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -5573,9 +5712,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -5595,7 +5734,7 @@ dependencies = [ name = "project" version = "0.1.0" dependencies = [ - "aho-corasick 0.7.20", + "aho-corasick", "anyhow", "async-trait", "backtrace", @@ -5615,7 +5754,7 @@ dependencies = [ "globset", "gpui", "ignore", - "itertools", + "itertools 0.10.5", "language", "lazy_static", "log", @@ -5684,7 +5823,7 @@ dependencies = [ "gpui", "language", "lsp", - "ordered-float", + "ordered-float 2.10.0", "picker", "postage", "project", @@ -5717,7 +5856,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "prost-derive 0.8.0", ] @@ -5727,7 +5866,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "prost-derive 0.9.0", ] @@ -5737,9 +5876,9 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "heck 0.3.3", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "multimap", @@ -5758,7 +5897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -5771,7 +5910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -5783,7 +5922,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "prost 0.8.0", ] @@ -5793,7 +5932,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "prost 0.9.0", ] @@ -5856,7 +5995,7 @@ dependencies = [ name = "quick_action_bar" version = "0.1.0" dependencies = [ - "ai", + "assistant", "editor", "gpui", "search", @@ -5992,9 +6131,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -6002,14 +6141,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -6037,7 +6174,7 @@ dependencies = [ "fuzzy", "gpui", "language", - "ordered-float", + "ordered-float 2.10.0", "picker", "postage", "settings", @@ -6101,14 +6238,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ - "aho-corasick 1.0.4", + "aho-corasick", "memchr", - "regex-automata 0.3.6", - "regex-syntax 0.7.4", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", ] [[package]] @@ -6122,13 +6259,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ - "aho-corasick 1.0.4", + "aho-corasick", "memchr", - "regex-syntax 0.7.4", + "regex-syntax 0.7.5", ] [[package]] @@ -6139,9 +6276,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "region" @@ -6175,12 +6312,12 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.19" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b9b67e2ca7dd9e9f9285b759de30ff538aab981abaaf7bc9bd90b84a0126c3" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.2", - "bytes 1.4.0", + "base64 0.21.4", + "bytes 1.5.0", "encoding_rs", "futures-core", "futures-util", @@ -6196,7 +6333,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "serde", "serde_json", "serde_urlencoded", @@ -6235,6 +6372,24 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rich_text" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "futures 0.3.28", + "gpui", + "language", + "lazy_static", + "pulldown-cmark", + "smallvec", + "smol", + "sum_tree", + "theme", + "util", +] + [[package]] name = "ring" version = "0.16.20" @@ -6350,7 +6505,7 @@ dependencies = [ "prost 0.8.0", "prost-build", "rand 0.8.5", - "rsa", + "rsa 0.4.0", "serde", "serde_derive", "smol", @@ -6363,14 +6518,14 @@ dependencies = [ [[package]] name = "rsa" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0aeddcca1082112a6eeb43bf25fd7820b066aaf6eaef776e19d0a1febe38fe" +checksum = "68ef841a26fc5d040ced0417c6c6a64ee851f42489df11cdf0218e545b6f8d28" dependencies = [ "byteorder", "digest 0.9.0", "lazy_static", - "num-bigint-dig", + "num-bigint-dig 0.7.1", "num-integer", "num-iter", "num-traits", @@ -6381,18 +6536,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest 0.10.7", + "num-bigint-dig 0.8.4", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rusqlite" -version = "0.27.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.7.0", + "hashlink", "libsqlite3-sys", - "memchr", "smallvec", ] @@ -6416,7 +6592,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.29", + "syn 2.0.37", "walkdir", ] @@ -6439,7 +6615,7 @@ checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ "arrayvec 0.7.4", "borsh", - "bytes 1.4.0", + "bytes 1.5.0", "num-traits", "rand 0.8.5", "rkyv", @@ -6459,22 +6635,13 @@ 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 0.11.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.18", + "semver", ] [[package]] @@ -6500,7 +6667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", - "errno 0.3.2", + "errno 0.3.3", "io-lifetimes 1.0.11", "libc", "linux-raw-sys 0.3.8", @@ -6509,14 +6676,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", - "errno 0.3.2", + "errno 0.3.3", "libc", - "linux-raw-sys 0.4.5", + "linux-raw-sys 0.4.7", "windows-sys", ] @@ -6530,19 +6697,18 @@ dependencies = [ "log", "ring", "sct 0.6.1", - "webpki 0.21.4", + "webpki", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "log", "ring", + "rustls-webpki", "sct 0.7.0", - "webpki 0.22.0", ] [[package]] @@ -6551,7 +6717,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -6626,9 +6802,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.12" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" dependencies = [ "dyn-clone", "schemars_derive", @@ -6638,9 +6814,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" dependencies = [ "proc-macro2", "quote", @@ -6694,28 +6870,42 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sea-bae" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "sea-orm" -version = "0.10.5" -source = "git+https://github.com/zed-industries/sea-orm?rev=18f4c691085712ad014a51792af75a9044bacee6#18f4c691085712ad014a51792af75a9044bacee6" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5b2d70c255bc5cbe1d49f69c3c8eadae0fbbaeb18ee978edbf2f75775cb94d" dependencies = [ "async-stream", "async-trait", + "bigdecimal", "chrono", "futures 0.3.28", - "futures-util", "log", "ouroboros", "rust_decimal", "sea-orm-macros", "sea-query", "sea-query-binder", - "sea-strum", "serde", "serde_json", "sqlx", + "strum", "thiserror", - "time 0.3.27", + "time", "tracing", "url", "uuid 1.4.1", @@ -6723,80 +6913,51 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "0.10.5" -source = "git+https://github.com/zed-industries/sea-orm?rev=18f4c691085712ad014a51792af75a9044bacee6#18f4c691085712ad014a51792af75a9044bacee6" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c8d455fad40194fb9774fdc4810c0f2700ff0dc0e93bd5ce9d641cc3f5dd75" dependencies = [ - "bae", - "heck 0.3.3", + "heck 0.4.1", "proc-macro2", "quote", - "syn 1.0.109", + "sea-bae", + "syn 2.0.37", + "unicode-ident", ] [[package]] name = "sea-query" -version = "0.27.2" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0fc4d8e44e1d51c739a68d336252a18bc59553778075d5e32649be6ec92ed" +checksum = "fb3e6bba153bb198646c8762c48414942a38db27d142e44735a133cabddcc820" dependencies = [ + "bigdecimal", "chrono", + "derivative", + "inherent", + "ordered-float 3.9.1", "rust_decimal", - "sea-query-derive", "serde_json", - "time 0.3.27", + "time", "uuid 1.4.1", ] [[package]] name = "sea-query-binder" -version = "0.2.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2585b89c985cfacfe0ec9fc9e7bb055b776c1a2581c4e3c6185af2b8bf8865" +checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" dependencies = [ + "bigdecimal", "chrono", "rust_decimal", "sea-query", "serde_json", "sqlx", - "time 0.3.27", + "time", "uuid 1.4.1", ] -[[package]] -name = "sea-query-derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cdc022b4f606353fe5dc85b09713a04e433323b70163e81513b141c6ae6eb5" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn 1.0.109", - "thiserror", -] - -[[package]] -name = "sea-strum" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391d06a6007842cfe79ac6f7f53911b76dfd69fc9a6769f1cf6569d12ce20e1b" -dependencies = [ - "sea-strum_macros", -] - -[[package]] -name = "sea-strum_macros" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - [[package]] name = "seahash" version = "4.1.0" @@ -6860,9 +7021,10 @@ dependencies = [ name = "semantic_index" version = "0.1.0" dependencies = [ + "ai", "anyhow", "async-trait", - "bincode", + "client", "collections", "ctor", "editor", @@ -6870,13 +7032,13 @@ dependencies = [ "futures 0.3.28", "globset", "gpui", - "isahc", "language", "lazy_static", "log", - "matrixmultiply", + "ndarray", + "node_runtime", + "ordered-float 2.10.0", "parking_lot 0.11.2", - "parse_duration", "picker", "postage", "pretty_assertions", @@ -6884,6 +7046,7 @@ dependencies = [ "rand 0.8.5", "rpc", "rusqlite", + "rust-embed", "schemars", "serde", "serde_json", @@ -6892,7 +7055,7 @@ dependencies = [ "smol", "tempdir", "theme", - "tiktoken-rs 0.5.1", + "tiktoken-rs 0.5.4", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6906,15 +7069,7 @@ dependencies = [ "unindent", "util", "workspace", -] - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", + "zed", ] [[package]] @@ -6923,15 +7078,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "seq-macro" version = "0.2.2" @@ -6940,22 +7086,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -6980,9 +7126,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "indexmap 2.0.0", "itoa", @@ -6992,9 +7138,9 @@ dependencies = [ [[package]] name = "serde_json_lenient" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29591aaa3a13f5ad0f2dd1a8a21bcddab11eaae7c3522b20ade2e85e9df52206" +checksum = "26386958a1344003f2b2bcff51a23fbe70461a478ef29247c6c6ab2c1656f53e" dependencies = [ "indexmap 2.0.0", "itoa", @@ -7010,7 +7156,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -7101,9 +7247,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -7154,9 +7300,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "signal-hook" @@ -7189,6 +7335,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simdutf8" version = "0.1.4" @@ -7276,9 +7432,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smol" @@ -7327,9 +7483,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", @@ -7350,6 +7506,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "spsc-buffer" version = "0.1.1" @@ -7385,115 +7551,230 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" dependencies = [ - "itertools", + "itertools 0.11.0", "nom", "unicode_categories", ] [[package]] name = "sqlx" -version = "0.6.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" dependencies = [ "sqlx-core", "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] name = "sqlx-core" -version = "0.6.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ - "ahash 0.7.6", + "ahash 0.8.3", "atoi", - "base64 0.13.1", - "bitflags 1.3.2", + "bigdecimal", "byteorder", - "bytes 1.4.0", + "bytes 1.5.0", "chrono", "crc", "crossbeam-queue", - "dirs 4.0.0", "dotenvy", "either", "event-listener", - "flume", "futures-channel", "futures-core", - "futures-executor", "futures-intrusive", + "futures-io", "futures-util", - "hashlink 0.8.3", + "hashlink", "hex", - "hkdf", - "hmac 0.12.1", - "indexmap 1.9.3", - "itoa", - "libc", - "libsqlite3-sys", + "indexmap 2.0.0", "log", - "md-5", "memchr", - "num-bigint 0.4.4", "once_cell", "paste", "percent-encoding", - "rand 0.8.5", "rust_decimal", - "rustls 0.20.8", + "rustls 0.21.7", "rustls-pemfile", "serde", "serde_json", - "sha1", "sha2 0.10.7", "smallvec", "sqlformat", - "sqlx-rt", - "stringprep", "thiserror", - "time 0.3.27", + "time", + "tokio", "tokio-stream", + "tracing", "url", "uuid 1.4.1", - "webpki-roots 0.22.6", - "whoami", + "webpki-roots 0.24.0", ] [[package]] name = "sqlx-macros" -version = "0.6.3" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" dependencies = [ "dotenvy", "either", "heck 0.4.1", + "hex", "once_cell", "proc-macro2", "quote", + "serde", "serde_json", "sha2 0.10.7", "sqlx-core", - "sqlx-rt", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", "syn 1.0.109", + "tempfile", + "tokio", "url", ] [[package]] -name = "sqlx-rt" -version = "0.6.3" +name = "sqlx-mysql" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ + "atoi", + "base64 0.21.4", + "bigdecimal", + "bitflags 2.4.0", + "byteorder", + "bytes 1.5.0", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa", + "log", + "md-5", + "memchr", "once_cell", - "tokio", - "tokio-rustls", + "percent-encoding", + "rand 0.8.5", + "rsa 0.9.2", + "rust_decimal", + "serde", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid 1.4.1", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +dependencies = [ + "atoi", + "base64 0.21.4", + "bigdecimal", + "bitflags 2.4.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint 0.4.4", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid 1.4.1", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "uuid 1.4.1", ] [[package]] @@ -7513,15 +7794,20 @@ name = "storybook" version = "0.1.0" dependencies = [ "anyhow", - "derive_more", + "chrono", + "clap 4.4.4", + "fs", + "futures 0.3.28", "gpui2", + "itertools 0.11.0", "log", - "refineable", "rust-embed", "serde", "settings", "simplelog", + "strum", "theme", + "ui", "util", ] @@ -7543,10 +7829,11 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ + "finl_unicode", "unicode-bidi", "unicode-normalization", ] @@ -7557,6 +7844,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.37", +] + [[package]] name = "subtle" version = "2.4.1" @@ -7576,15 +7885,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" +checksum = "05d11eec9fbe2bc8bc71e7349f0e7534db9a96d961fb9f302574275b7880ad06" [[package]] name = "sval_buffer" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" +checksum = "6b7451f69a93c5baf2653d5aa8bb4178934337f16c22830a50b06b386f72d761" dependencies = [ "sval", "sval_ref", @@ -7592,18 +7901,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" +checksum = "c34f5a2cc12b4da2adfb59d5eedfd9b174a23cc3fae84cec71dcbcd9302068f5" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" +checksum = "2f578b2301341e246d00b35957f2952c4ec554ad9c7cfaee10bc86bc92896578" dependencies = [ "itoa", "ryu", @@ -7612,9 +7921,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" +checksum = "8346c00f5dc6efe18bea8d13c1f7ca4f112b20803434bf3657ac17c0f74cbc4b" dependencies = [ "itoa", "ryu", @@ -7623,18 +7932,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" +checksum = "6617cc89952f792aebc0f4a1a76bc51e80c70b18c491bd52215c7989c4c3dd06" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" +checksum = "fe3d1e59f023341d9af75d86f3bc148a6704f3f831eef0dd90bbe9cb445fa024" dependencies = [ "serde", "sval", @@ -7681,9 +7990,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -7708,9 +8017,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.27.8" +version = "0.29.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" +checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -7785,7 +8094,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.8", + "rustix 0.38.14", "windows-sys", ] @@ -7808,11 +8117,11 @@ dependencies = [ "dirs 4.0.0", "futures 0.3.28", "gpui", - "itertools", + "itertools 0.10.5", "lazy_static", "libc", "mio-extras", - "ordered-float", + "ordered-float 2.10.0", "procinfo", "rand 0.8.5", "schemars", @@ -7839,12 +8148,12 @@ dependencies = [ "editor", "futures 0.3.28", "gpui", - "itertools", + "itertools 0.10.5", "language", "lazy_static", "libc", "mio-extras", - "ordered-float", + "ordered-float 2.10.0", "procinfo", "project", "rand 0.8.5", @@ -7931,22 +8240,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -7983,7 +8292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52aacc1cff93ba9d5f198c62c49c77fa0355025c729eed3326beaf7f33bc8614" dependencies = [ "anyhow", - "base64 0.21.2", + "base64 0.21.4", "bstr", "fancy-regex", "lazy_static", @@ -7993,12 +8302,12 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf14cb08d8fda6e484c75ec2bfb6bcef48347d47abcd011fa9d56ee995a3da0" +checksum = "f9ae5a3c24361e5f038af22517ba7f8e3af4099e30e78a3d56f86b48238fce9d" dependencies = [ "anyhow", - "base64 0.21.2", + "base64 0.21.4", "bstr", "fancy-regex", "lazy_static", @@ -8008,20 +8317,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi 0.3.9", -] - -[[package]] -name = "time" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ "deranged", "itoa", @@ -8038,9 +8336,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] @@ -8094,14 +8392,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", - "bytes 1.4.0", + "bytes 1.5.0", "libc", "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "windows-sys", ] @@ -8123,7 +8421,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tokio", ] @@ -8135,7 +8433,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -8148,17 +8446,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.8", - "tokio", - "webpki 0.22.0", -] - [[package]] name = "tokio-stream" version = "0.1.14" @@ -8166,7 +8453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tokio", ] @@ -8188,25 +8475,25 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ - "bytes 1.4.0", + "bytes 1.5.0", "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tokio", "tracing", ] @@ -8222,9 +8509,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", @@ -8243,9 +8530,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.0.0", "serde", @@ -8263,7 +8550,7 @@ dependencies = [ "async-stream", "async-trait", "base64 0.13.1", - "bytes 1.4.0", + "bytes 1.5.0", "futures-core", "futures-util", "h2", @@ -8295,11 +8582,11 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", "tower-layer", "tower-service", "tracing", @@ -8312,13 +8599,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags 1.3.2", - "bytes 1.4.0", + "bytes 1.5.0", "futures-core", "futures-util", "http", "http-body", "http-range-header", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tower", "tower-layer", "tower-service", @@ -8344,7 +8631,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.13", "tracing-attributes", "tracing-core", ] @@ -8357,7 +8644,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -8708,7 +8995,7 @@ checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" dependencies = [ "base64 0.13.1", "byteorder", - "bytes 1.4.0", + "bytes 1.5.0", "http", "httparse", "log", @@ -8727,7 +9014,7 @@ checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ "base64 0.13.1", "byteorder", - "bytes 1.4.0", + "bytes 1.5.0", "http", "httparse", "log", @@ -8740,15 +9027,24 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +name = "ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "gpui2", + "rand 0.8.5", + "serde", + "settings", + "smallvec", + "strum", + "theme", +] [[package]] name = "unicase" @@ -8785,9 +9081,9 @@ checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -8818,9 +9114,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode_categories" @@ -8842,9 +9138,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -9011,11 +9307,12 @@ dependencies = [ "async-trait", "collections", "command_palette", + "diagnostics", "editor", "futures 0.3.28", "gpui", "indoc", - "itertools", + "itertools 0.10.5", "language", "language_selector", "log", @@ -9032,6 +9329,7 @@ dependencies = [ "tokio", "util", "workspace", + "zed-actions", ] [[package]] @@ -9064,9 +9362,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -9087,12 +9385,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9162,7 +9454,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -9196,7 +9488,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9209,9 +9501,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-encoder" -version = "0.31.1" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41763f20eafed1399fff1afb466496d3a959f58241436cfdc17e3f5ca954de16" +checksum = "b39de0723a53d3c8f54bed106cfbc0d06b3e4d945c5c5022115a61e3b29183ae" dependencies = [ "leb128", ] @@ -9433,9 +9725,9 @@ dependencies = [ [[package]] name = "wast" -version = "63.0.0" +version = "65.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2560471f60a48b77fccefaf40796fda61c97ce1e790b59dfcec9dc3995c9f63a" +checksum = "5fd8c1cbadf94a0b0d1071c581d3cfea1b7ed5192c79808dd15406e508dd0afb" dependencies = [ "leb128", "memchr", @@ -9445,11 +9737,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.70" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdc306c2c4c2f2bf2ba69e083731d0d2a77437fc6a350a19db139636e7e416c" +checksum = "3209e35eeaf483714f4c6be93f4a03e69aad5f304e3fa66afa7cb90fe1c8051f" dependencies = [ - "wast 63.0.0", + "wast 65.0.1", ] [[package]] @@ -9472,32 +9764,22 @@ dependencies = [ "untrusted", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki 0.21.4", + "webpki", ] [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" dependencies = [ - "webpki 0.22.0", + "rustls-webpki", ] [[package]] @@ -9532,13 +9814,14 @@ dependencies = [ [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix 0.38.14", ] [[package]] @@ -9546,10 +9829,6 @@ name = "whoami" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -dependencies = [ - "wasm-bindgen", - "web-sys", -] [[package]] name = "wiggle" @@ -9623,9 +9902,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi 0.3.9", ] @@ -9779,9 +10058,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] @@ -9833,7 +10112,7 @@ name = "workspace" version = "0.1.0" dependencies = [ "anyhow", - "async-recursion 1.0.4", + "async-recursion 1.0.5", "bincode", "call", "channel", @@ -9848,7 +10127,7 @@ dependencies = [ "gpui", "indoc", "install_cli", - "itertools", + "itertools 0.10.5", "language", "lazy_static", "log", @@ -9913,7 +10192,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.24", + "clap 4.4.4", "schemars", "serde_json", "theme", @@ -9948,11 +10227,11 @@ dependencies = [ [[package]] name = "zed" -version = "0.104.0" +version = "0.108.0" dependencies = [ "activity_indicator", - "ai", "anyhow", + "assistant", "async-compression", "async-recursion 0.3.2", "async-tar", @@ -10004,7 +10283,6 @@ dependencies = [ "node_runtime", "num_cpus", "outline", - "owning_ref", "parking_lot 0.11.2", "plugin_runtime", "postage", @@ -10016,14 +10294,16 @@ dependencies = [ "recent_projects", "regex", "rpc", - "rsa", + "rsa 0.4.0", "rust-embed", + "schemars", "search", "semantic_index", "serde", "serde_derive", "serde_json", "settings", + "shellexpand", "simplelog", "smallvec", "smol", @@ -10083,9 +10363,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] @@ -10098,7 +10378,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c085a609a84075d8dcb023b877d8f379d20fdbac..6e4cb4f12fffed2856d4c36bfd758f26efac4aad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/activity_indicator", "crates/ai", + "crates/assistant", "crates/audio", "crates/auto_update", "crates/breadcrumbs", @@ -65,6 +66,7 @@ members = [ "crates/sqlez", "crates/sqlez_macros", "crates/feature_flags", + "crates/rich_text", "crates/storybook", "crates/storybook2", "crates/sum_tree", @@ -72,6 +74,7 @@ members = [ "crates/text", "crates/theme", "crates/theme_selector", + "crates/ui", "crates/util", "crates/semantic_index", "crates/vim", @@ -106,12 +109,14 @@ rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } rust-embed = { version = "8.0", features = ["include-exclude"] } +rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } smallvec = { version = "1.6", features = ["union"] } smol = { version = "1.2" } +sysinfo = "0.29.10" tempdir = { version = "0.3.7" } thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } @@ -119,6 +124,8 @@ toml = { version = "0.5" } tree-sitter = "0.20" unindent = { version = "0.1.7" } pretty_assertions = "1.3.0" +git2 = { version = "0.15", default-features = false} +uuid = { version = "1.1.2", features = ["v4"] } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } tree-sitter-c = "0.20.1" diff --git a/Dockerfile b/Dockerfile index 208700f7fb5f25d19dc5e5cfd1477f11219c4391..f3d0b601b9d042df4138e910729cccac0bb0d019 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.72-bullseye as builder +FROM rust:1.73-bullseye as builder WORKDIR app COPY . . diff --git a/Procfile b/Procfile index fcc03f55dc2add371dd02b7b99629eacbce9bddb..2eb7de20fb7e9cae34375dc130c6d27aea01012e 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,4 @@ -web: cd ../zed.dev && PORT=3000 npx vercel dev -collab: cd crates/collab && cargo run serve -livekit: livekit-server --dev \ No newline at end of file +web: cd ../zed.dev && PORT=3000 npm run dev +collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve +livekit: livekit-server --dev +postgrest: postgrest crates/collab/admin_api.conf diff --git a/README.md b/README.md index 961c8f9ff384370cef0c60c4e65cc7f6cf1908e0..b3d4987526a46be3304ca649d90166566c03029e 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,13 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea sudo xcodebuild -license ``` -* Install rustup (rust, cargo, etc.) - ``` - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` - -* Install homebrew and node +* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.) ``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - brew install node + brew install node rustup-init + rustup-init # follow the installation steps ``` - + * Install postgres and configure the database ``` brew install postgresql@15 @@ -31,15 +27,16 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres psql -U postgres -c "CREATE DATABASE zed" ``` - -* Install the `LiveKit` server and the `foreman` process supervisor: + +* Install the `LiveKit` server, the `PostgREST` API server, and the `foreman` process supervisor: ``` brew install livekit + brew install postgrest brew install foreman ``` -* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies: +* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: ``` cd .. diff --git a/assets/icons/Icons/exit.svg b/assets/icons/Icons/exit.svg deleted file mode 100644 index 6d768492482d6c62e1ec10b5f10054796c89cbb7..0000000000000000000000000000000000000000 --- a/assets/icons/Icons/exit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_down_12.svg b/assets/icons/arrow_down_12.svg deleted file mode 100644 index dfad5d4876fcd53732c57170e70e70b618a5405b..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_down_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_down_16.svg b/assets/icons/arrow_down_16.svg deleted file mode 100644 index ec757a8ab40bf3f0f3a9a2234b2f41f6e2b8ac4f..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_down_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_down_8.svg b/assets/icons/arrow_down_8.svg deleted file mode 100644 index f70f3920a308fefd33fabf506315c74160e153cc..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_down_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_left_12.svg b/assets/icons/arrow_left_12.svg deleted file mode 100644 index aaccf25eaf1ce2a777b3d86b58ddadafacabbbf2..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_left_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_left_16.svg b/assets/icons/arrow_left_16.svg deleted file mode 100644 index 317c31e9f0bd7e58158caf6a85dc41330d70ed12..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_left_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_left_8.svg b/assets/icons/arrow_left_8.svg deleted file mode 100644 index e2071d55eb2f1dc2dffc60008f2de3bb788382dd..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_left_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_right_12.svg b/assets/icons/arrow_right_12.svg deleted file mode 100644 index c5f70a4958cae634b22a19cb2a67a597ba6102eb..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_right_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_right_16.svg b/assets/icons/arrow_right_16.svg deleted file mode 100644 index b41e8fc810b7d927e3b298e3321028206253e887..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_right_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_right_8.svg b/assets/icons/arrow_right_8.svg deleted file mode 100644 index fb3f836ef0934452ae624a5df7b012d8f4a95713..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_right_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_up_12.svg b/assets/icons/arrow_up_12.svg deleted file mode 100644 index c9f35d868b46b1e187a6ee7ce83ad96b40b68309..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_up_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_up_16.svg b/assets/icons/arrow_up_16.svg deleted file mode 100644 index 0d8add4ed7c96ed30aae8d39eaf2e66e9a03019d..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_up_8.svg b/assets/icons/arrow_up_8.svg deleted file mode 100644 index 0a1e2c44bf7011f7b6269986f02a23acfe662884..0000000000000000000000000000000000000000 --- a/assets/icons/arrow_up_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_up_right_8.svg b/assets/icons/arrow_up_right.svg similarity index 100% rename from assets/icons/arrow_up_right_8.svg rename to assets/icons/arrow_up_right.svg diff --git a/assets/icons/assist_15.svg b/assets/icons/assist_15.svg deleted file mode 100644 index 3baf8df3e936347415749cf0667c04e32391f828..0000000000000000000000000000000000000000 --- a/assets/icons/assist_15.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/backspace _12.svg b/assets/icons/backspace _12.svg deleted file mode 100644 index 68bad3da268a98b3d1a44f52dd9687ea6865ef2b..0000000000000000000000000000000000000000 --- a/assets/icons/backspace _12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/backspace _16.svg b/assets/icons/backspace _16.svg deleted file mode 100644 index 965470690e2db31d1dd6b4fdd10185d7825b2594..0000000000000000000000000000000000000000 --- a/assets/icons/backspace _16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/backspace _8.svg b/assets/icons/backspace _8.svg deleted file mode 100644 index 60972007b6c4c0a40ddc449d4c8f6a439a22e9e1..0000000000000000000000000000000000000000 --- a/assets/icons/backspace _8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_8.svg b/assets/icons/bolt.svg similarity index 100% rename from assets/icons/bolt_8.svg rename to assets/icons/bolt.svg diff --git a/assets/icons/bolt_12.svg b/assets/icons/bolt_12.svg deleted file mode 100644 index 0125c733e2cb455137657f5cc49f80226b5c7f14..0000000000000000000000000000000000000000 --- a/assets/icons/bolt_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_16.svg b/assets/icons/bolt_16.svg deleted file mode 100644 index aca476ef508173e60f84da60f1ba299f2bdb7009..0000000000000000000000000000000000000000 --- a/assets/icons/bolt_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_slash_12.svg b/assets/icons/bolt_slash_12.svg deleted file mode 100644 index 80d99be6169e3a6c0f8d9616d50d2b8eac449f44..0000000000000000000000000000000000000000 --- a/assets/icons/bolt_slash_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/bolt_slash_16.svg b/assets/icons/bolt_slash_16.svg deleted file mode 100644 index 9520a626c18bf5ee3a72e1c52ecc049d481912a9..0000000000000000000000000000000000000000 --- a/assets/icons/bolt_slash_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_slash_8.svg b/assets/icons/bolt_slash_8.svg deleted file mode 100644 index 3781a91299f67c9d5380936293352469de2cc3e7..0000000000000000000000000000000000000000 --- a/assets/icons/bolt_slash_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/radix/caret-down.svg b/assets/icons/caret_down.svg similarity index 100% rename from assets/icons/radix/caret-down.svg rename to assets/icons/caret_down.svg diff --git a/assets/icons/caret_down_12.svg b/assets/icons/caret_down_12.svg deleted file mode 100644 index 6208814bc2b6290e804ebc43c9f22e09a412dacb..0000000000000000000000000000000000000000 --- a/assets/icons/caret_down_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_down_16.svg b/assets/icons/caret_down_16.svg deleted file mode 100644 index cba930287e17907c3bfef2f3aa43e62218dc323f..0000000000000000000000000000000000000000 --- a/assets/icons/caret_down_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_down_8.svg b/assets/icons/caret_down_8.svg deleted file mode 100644 index 932376d6f8aebeee6fa1c75f4796b8c625220819..0000000000000000000000000000000000000000 --- a/assets/icons/caret_down_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/caret_left_12.svg b/assets/icons/caret_left_12.svg deleted file mode 100644 index 6b6c32513e67aad9092fe96211f65a4b227fe7b9..0000000000000000000000000000000000000000 --- a/assets/icons/caret_left_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_left_16.svg b/assets/icons/caret_left_16.svg deleted file mode 100644 index 5ffd176c590a87910615bc0fe4b3dcf9aef72587..0000000000000000000000000000000000000000 --- a/assets/icons/caret_left_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_left_8.svg b/assets/icons/caret_left_8.svg deleted file mode 100644 index 1b04877a31dbb839d119c31c64b2e25631b3a233..0000000000000000000000000000000000000000 --- a/assets/icons/caret_left_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/caret_right_12.svg b/assets/icons/caret_right_12.svg deleted file mode 100644 index 6670b80cf8fb178245aebfda8773f80a8461120a..0000000000000000000000000000000000000000 --- a/assets/icons/caret_right_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_right_16.svg b/assets/icons/caret_right_16.svg deleted file mode 100644 index da239b95d7a93497c4068b82b991afaa040d3f71..0000000000000000000000000000000000000000 --- a/assets/icons/caret_right_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_right_8.svg b/assets/icons/caret_right_8.svg deleted file mode 100644 index d1350ee809847b44327e43f2253c5a0e402aae34..0000000000000000000000000000000000000000 --- a/assets/icons/caret_right_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/radix/caret-up.svg b/assets/icons/caret_up.svg similarity index 100% rename from assets/icons/radix/caret-up.svg rename to assets/icons/caret_up.svg diff --git a/assets/icons/caret_up_12.svg b/assets/icons/caret_up_12.svg deleted file mode 100644 index 9fe93c47ae42113e87f464b5e658b3c50481e6b5..0000000000000000000000000000000000000000 --- a/assets/icons/caret_up_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_up_16.svg b/assets/icons/caret_up_16.svg deleted file mode 100644 index 10f45523a447b2eafaca2e06f0c23dc01720ca7f..0000000000000000000000000000000000000000 --- a/assets/icons/caret_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_up_8.svg b/assets/icons/caret_up_8.svg deleted file mode 100644 index bf79244d7d315dc6f9d8f3e49cb6df52d75fed16..0000000000000000000000000000000000000000 --- a/assets/icons/caret_up_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/case_insensitive_12.svg b/assets/icons/case_insensitive.svg similarity index 100% rename from assets/icons/case_insensitive_12.svg rename to assets/icons/case_insensitive.svg diff --git a/assets/icons/channel_hash.svg b/assets/icons/channel_hash.svg deleted file mode 100644 index edd04626782e52bc2f3c1a73a08f2de166828c33..0000000000000000000000000000000000000000 --- a/assets/icons/channel_hash.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/check_12.svg b/assets/icons/check_12.svg deleted file mode 100644 index 3e15dd7d1fd4504f4e87e3c8f14881c3ea4c6c72..0000000000000000000000000000000000000000 --- a/assets/icons/check_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/check_16.svg b/assets/icons/check_16.svg deleted file mode 100644 index 7e959b59242742de30144d1eb4859b7fdfc5b43b..0000000000000000000000000000000000000000 --- a/assets/icons/check_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/check_8.svg b/assets/icons/check_8.svg deleted file mode 100644 index 268f8bb498fb623b6554dc3db1d6a4aa89343f26..0000000000000000000000000000000000000000 --- a/assets/icons/check_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_down_12.svg b/assets/icons/chevron_down_12.svg deleted file mode 100644 index 7bba37857a7d71860610158662e9846f61a714c9..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_down_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_down_16.svg b/assets/icons/chevron_down_16.svg deleted file mode 100644 index cc7228cdc9104bc4b7466f6a1127c720a4183874..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_down_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_down_8.svg b/assets/icons/chevron_down_8.svg deleted file mode 100644 index fe60b4968aab80de06acc2882aac6cbb34a64e86..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_down_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_left_12.svg b/assets/icons/chevron_left_12.svg deleted file mode 100644 index a230007c7b13fa489fb3529862805c3f9ab8bce6..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_left_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_left_16.svg b/assets/icons/chevron_left_16.svg deleted file mode 100644 index 2cd1bbd4d246af12e8076406c6697bd06dee5d5d..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_left_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_left_8.svg b/assets/icons/chevron_left_8.svg deleted file mode 100644 index 88ca274f5186d113f50ae8c14d4397c779d22446..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_left_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_right_12.svg b/assets/icons/chevron_right_12.svg deleted file mode 100644 index b463182705918f4ec8380b6ae0abc021ad297052..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_right_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_right_16.svg b/assets/icons/chevron_right_16.svg deleted file mode 100644 index 270a33db70b2e2e412ef1351d16e2964f164e512..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_right_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_right_8.svg b/assets/icons/chevron_right_8.svg deleted file mode 100644 index 7349274681fc89d09715b98a86770284598932aa..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_right_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up_12.svg b/assets/icons/chevron_up_12.svg deleted file mode 100644 index c6bbee4ff7058a11bad86563974b82ff4562124b..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_up_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up_16.svg b/assets/icons/chevron_up_16.svg deleted file mode 100644 index ba2d4e6668a6fff17272468e648b55f9f6518242..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up_8.svg b/assets/icons/chevron_up_8.svg deleted file mode 100644 index 41525aa3eaccf1606203ce5a95949a5e2eb8db04..0000000000000000000000000000000000000000 --- a/assets/icons/chevron_up_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_check_16.svg b/assets/icons/circle_check.svg similarity index 100% rename from assets/icons/circle_check_16.svg rename to assets/icons/circle_check.svg diff --git a/assets/icons/circle_check_12.svg b/assets/icons/circle_check_12.svg deleted file mode 100644 index cb28c8a0515b04a3663ce57d8e1c233a4bdec84f..0000000000000000000000000000000000000000 --- a/assets/icons/circle_check_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_check_8.svg b/assets/icons/circle_check_8.svg deleted file mode 100644 index c4150f058c79006e66da38651505cdf1f7028fac..0000000000000000000000000000000000000000 --- a/assets/icons/circle_check_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_info_12.svg b/assets/icons/circle_info_12.svg deleted file mode 100644 index 26a569737d6d3b1fa1f04efe6b86bdb7c6bccdc0..0000000000000000000000000000000000000000 --- a/assets/icons/circle_info_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_info_16.svg b/assets/icons/circle_info_16.svg deleted file mode 100644 index 48bd4f79a8ff8cfa085717a38f60832b0eb19492..0000000000000000000000000000000000000000 --- a/assets/icons/circle_info_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_info_8.svg b/assets/icons/circle_info_8.svg deleted file mode 100644 index 49bb03224d9fe9d39b5f233a28f047c1d4a95077..0000000000000000000000000000000000000000 --- a/assets/icons/circle_info_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_up_12.svg b/assets/icons/circle_up_12.svg deleted file mode 100644 index 4236037fbddabce3d1a6e706e9bc7606186f5e65..0000000000000000000000000000000000000000 --- a/assets/icons/circle_up_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_up_16.svg b/assets/icons/circle_up_16.svg deleted file mode 100644 index 4eb3886fe43538f8dc3a86981868dae4d20b6537..0000000000000000000000000000000000000000 --- a/assets/icons/circle_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_up_8.svg b/assets/icons/circle_up_8.svg deleted file mode 100644 index e08e0ad492adc074eac4628c41e5766d000b573b..0000000000000000000000000000000000000000 --- a/assets/icons/circle_up_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_x_mark_12.svg b/assets/icons/circle_x_mark_12.svg deleted file mode 100644 index 5f11a71ece40644a02d43594c660b65bb7bf23b1..0000000000000000000000000000000000000000 --- a/assets/icons/circle_x_mark_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_x_mark_16.svg b/assets/icons/circle_x_mark_16.svg deleted file mode 100644 index db3f401615b56efc9cd503d80fca923dea731d08..0000000000000000000000000000000000000000 --- a/assets/icons/circle_x_mark_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_x_mark_8.svg b/assets/icons/circle_x_mark_8.svg deleted file mode 100644 index a0acfc3899f6df9e6cf2c87d2085489acee084ec..0000000000000000000000000000000000000000 --- a/assets/icons/circle_x_mark_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/cloud_12.svg b/assets/icons/cloud_12.svg deleted file mode 100644 index 2ed58f49661307f7a0ff1e7032ce1331534d97ea..0000000000000000000000000000000000000000 --- a/assets/icons/cloud_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/cloud_8.svg b/assets/icons/cloud_8.svg deleted file mode 100644 index 0e0337e7abf074895ce59b1c50b8a6d8fed10afa..0000000000000000000000000000000000000000 --- a/assets/icons/cloud_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/cloud_slash_8.svg b/assets/icons/cloud_slash_8.svg deleted file mode 100644 index 785ded06833553d1f23eda7adeaf9e17fdcfd0a8..0000000000000000000000000000000000000000 --- a/assets/icons/cloud_slash_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/copilot_16.svg b/assets/icons/copilot_16.svg deleted file mode 100644 index e14b61ce8bc73cc09242256706283e7e2831f8fb..0000000000000000000000000000000000000000 --- a/assets/icons/copilot_16.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/copilot_disabled_16.svg b/assets/icons/copilot_disabled.svg similarity index 100% rename from assets/icons/copilot_disabled_16.svg rename to assets/icons/copilot_disabled.svg diff --git a/assets/icons/copilot_error_16.svg b/assets/icons/copilot_error.svg similarity index 100% rename from assets/icons/copilot_error_16.svg rename to assets/icons/copilot_error.svg diff --git a/assets/icons/copilot_init_16.svg b/assets/icons/copilot_init.svg similarity index 100% rename from assets/icons/copilot_init_16.svg rename to assets/icons/copilot_init.svg diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg deleted file mode 100644 index 4aa44979c39de058a96548d66a73fe6b437f22eb..0000000000000000000000000000000000000000 --- a/assets/icons/copy.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/delete_12.svg b/assets/icons/delete_12.svg deleted file mode 100644 index 68bad3da268a98b3d1a44f52dd9687ea6865ef2b..0000000000000000000000000000000000000000 --- a/assets/icons/delete_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/delete_16.svg b/assets/icons/delete_16.svg deleted file mode 100644 index 965470690e2db31d1dd6b4fdd10185d7825b2594..0000000000000000000000000000000000000000 --- a/assets/icons/delete_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/delete_8.svg b/assets/icons/delete_8.svg deleted file mode 100644 index 60972007b6c4c0a40ddc449d4c8f6a439a22e9e1..0000000000000000000000000000000000000000 --- a/assets/icons/delete_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/desktop.svg b/assets/icons/desktop.svg similarity index 100% rename from assets/icons/radix/desktop.svg rename to assets/icons/desktop.svg diff --git a/assets/icons/disable_screen_sharing_12.svg b/assets/icons/disable_screen_sharing_12.svg deleted file mode 100644 index c2a4edd45b26b530c16b8c68e612e620e493ac4f..0000000000000000000000000000000000000000 --- a/assets/icons/disable_screen_sharing_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/cloud_slash_12.svg b/assets/icons/disconnected.svg similarity index 100% rename from assets/icons/cloud_slash_12.svg rename to assets/icons/disconnected.svg diff --git a/assets/icons/dock_bottom_12.svg b/assets/icons/dock_bottom_12.svg deleted file mode 100644 index a8099443be6032e40df758b9b5adff118c575970..0000000000000000000000000000000000000000 --- a/assets/icons/dock_bottom_12.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_bottom_8.svg b/assets/icons/dock_bottom_8.svg deleted file mode 100644 index 005ac423ad51b31b145b0728ed66aa2c6cdb1dfb..0000000000000000000000000000000000000000 --- a/assets/icons/dock_bottom_8.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_modal_12.svg b/assets/icons/dock_modal_12.svg deleted file mode 100644 index 792baee49c33de758bd15216ba33ed06a909f457..0000000000000000000000000000000000000000 --- a/assets/icons/dock_modal_12.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_modal_8.svg b/assets/icons/dock_modal_8.svg deleted file mode 100644 index c6f403900439ae5349d826bc71d212da5d05f45b..0000000000000000000000000000000000000000 --- a/assets/icons/dock_modal_8.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_right_12.svg b/assets/icons/dock_right_12.svg deleted file mode 100644 index 84cc1a0c2b09878a071d2d9e1f31875fe36d64bb..0000000000000000000000000000000000000000 --- a/assets/icons/dock_right_12.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_right_8.svg b/assets/icons/dock_right_8.svg deleted file mode 100644 index 842f41bc8c911cf1198e79a395da1b4bd3695269..0000000000000000000000000000000000000000 --- a/assets/icons/dock_right_8.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/download_12.svg b/assets/icons/download.svg similarity index 100% rename from assets/icons/download_12.svg rename to assets/icons/download.svg diff --git a/assets/icons/download_8.svg b/assets/icons/download_8.svg deleted file mode 100644 index fb8b021d6b79289ba1ffa4f70eef41f6ebef8e8d..0000000000000000000000000000000000000000 --- a/assets/icons/download_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/ellipsis_14.svg b/assets/icons/ellipsis_14.svg deleted file mode 100644 index 5d45af2b6f249f103ae2f1f3e8df48905f2fd832..0000000000000000000000000000000000000000 --- a/assets/icons/ellipsis_14.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/enable_screen_sharing_12.svg b/assets/icons/enable_screen_sharing_12.svg deleted file mode 100644 index 6ae37649d29997107b3ddd42350b6333556a95cf..0000000000000000000000000000000000000000 --- a/assets/icons/enable_screen_sharing_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg index 7e45535773e4e6f871fd80af25452afb5021fdd4..2cc6ce120dc9af17a642ac3bf2f2451209cb5e5e 100644 --- a/assets/icons/exit.svg +++ b/assets/icons/exit.svg @@ -1,4 +1,8 @@ - - - + + diff --git a/assets/icons/link_out_12.svg b/assets/icons/external_link.svg similarity index 100% rename from assets/icons/link_out_12.svg rename to assets/icons/external_link.svg diff --git a/assets/icons/feedback_16.svg b/assets/icons/feedback_16.svg deleted file mode 100644 index b85a40b353051b348d70ebbb1bf842764a8bc2e5..0000000000000000000000000000000000000000 --- a/assets/icons/feedback_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/file.svg b/assets/icons/file.svg similarity index 100% rename from assets/icons/radix/file.svg rename to assets/icons/file.svg diff --git a/assets/icons/file_12.svg b/assets/icons/file_12.svg deleted file mode 100644 index 191e3d7faeb2a6affd334d5cd9eb069ea882d6e5..0000000000000000000000000000000000000000 --- a/assets/icons/file_12.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_16.svg b/assets/icons/file_16.svg deleted file mode 100644 index 79fd1f81cb00fc27ea09c2e98625a5ca0e78833f..0000000000000000000000000000000000000000 --- a/assets/icons/file_16.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_8.svg b/assets/icons/file_8.svg deleted file mode 100644 index 2e636bd3b3a2a2c0011cbc199f0fa95901156cf0..0000000000000000000000000000000000000000 --- a/assets/icons/file_8.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/filter_12.svg b/assets/icons/filter_12.svg deleted file mode 100644 index 9c1ad5ba5cc0ee58244dc9dba5b4a4b2ff347fe1..0000000000000000000000000000000000000000 --- a/assets/icons/filter_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/filter_14.svg b/assets/icons/filter_14.svg deleted file mode 100644 index 379be15b51c491e5a2fabb5028a1efc14713628f..0000000000000000000000000000000000000000 --- a/assets/icons/filter_14.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/folder_tree_12.svg b/assets/icons/folder_tree_12.svg deleted file mode 100644 index 580514f74d227fda1b094b72a8d7ba1c9fa002cd..0000000000000000000000000000000000000000 --- a/assets/icons/folder_tree_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/folder_tree_16.svg b/assets/icons/folder_tree_16.svg deleted file mode 100644 index a264a3257306e656b373dad7acab1412ac023c2f..0000000000000000000000000000000000000000 --- a/assets/icons/folder_tree_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/folder_tree_8.svg b/assets/icons/folder_tree_8.svg deleted file mode 100644 index 07ac18e19f2180910427eb9444e1537b381596eb..0000000000000000000000000000000000000000 --- a/assets/icons/folder_tree_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/git_diff_12.svg b/assets/icons/git_diff_12.svg deleted file mode 100644 index 0a3bb473c2972d9a852edd61e52cfd0f2ac1d62c..0000000000000000000000000000000000000000 --- a/assets/icons/git_diff_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/git_diff_8.svg b/assets/icons/git_diff_8.svg deleted file mode 100644 index 64290de860d043b8b84066cfe92c9e499f667bfe..0000000000000000000000000000000000000000 --- a/assets/icons/git_diff_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/github-copilot-dummy.svg b/assets/icons/github-copilot-dummy.svg deleted file mode 100644 index 4a7ded397623c25fa0c5dda08d639230cd1327b6..0000000000000000000000000000000000000000 --- a/assets/icons/github-copilot-dummy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/html.svg b/assets/icons/html.svg deleted file mode 100644 index 1e676fe313401fc137813827df03cc2c60851df0..0000000000000000000000000000000000000000 --- a/assets/icons/html.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/kebab.svg b/assets/icons/kebab.svg deleted file mode 100644 index 1858c655202cf6940c90278b43241bb1cabc32ac..0000000000000000000000000000000000000000 --- a/assets/icons/kebab.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/leave_12.svg b/assets/icons/leave_12.svg deleted file mode 100644 index 84491384b8cc7f80d4a727e75c142ee509b451ac..0000000000000000000000000000000000000000 --- a/assets/icons/leave_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg deleted file mode 100644 index 652f45a7e843795c288fdaaf4951d40943e3805d..0000000000000000000000000000000000000000 --- a/assets/icons/lock.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/lock_8.svg b/assets/icons/lock_8.svg deleted file mode 100644 index 8df83dc0b5e330447dbc86c3fd27285228857f35..0000000000000000000000000000000000000000 --- a/assets/icons/lock_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/magic-wand.svg b/assets/icons/magic-wand.svg similarity index 100% rename from assets/icons/radix/magic-wand.svg rename to assets/icons/magic-wand.svg diff --git a/assets/icons/magnifying_glass_12.svg b/assets/icons/magnifying_glass_12.svg deleted file mode 100644 index b9ac5d35b22a47c5f19f55cbc3654fbff6e37e56..0000000000000000000000000000000000000000 --- a/assets/icons/magnifying_glass_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/magnifying_glass_16.svg b/assets/icons/magnifying_glass_16.svg deleted file mode 100644 index f35343e8d303c69eb022e04a02ac2cbdb3b8f432..0000000000000000000000000000000000000000 --- a/assets/icons/magnifying_glass_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/magnifying_glass_8.svg b/assets/icons/magnifying_glass_8.svg deleted file mode 100644 index d0deb1cdba75dcc26beb8fed25532a90141172f4..0000000000000000000000000000000000000000 --- a/assets/icons/magnifying_glass_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/match_case.svg b/assets/icons/match_case.svg deleted file mode 100644 index 82f4529c1b054d4218812f7b8a2094f54e9a1ae3..0000000000000000000000000000000000000000 --- a/assets/icons/match_case.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/match_word.svg b/assets/icons/match_word.svg deleted file mode 100644 index 69ba8eb9e6bc52e49e4ace4b1526881222672d6c..0000000000000000000000000000000000000000 --- a/assets/icons/match_word.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index 4dc7755714990ddc5d4b06ffc992859954342c93..f37f6a2087f968728170539b379206cca7551b0e 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/maximize_8.svg b/assets/icons/maximize_8.svg deleted file mode 100644 index 76d29a9d221a68545fdb95bcaa80adbb3e237994..0000000000000000000000000000000000000000 --- a/assets/icons/maximize_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/hamburger_15.svg b/assets/icons/menu.svg similarity index 100% rename from assets/icons/hamburger_15.svg rename to assets/icons/menu.svg diff --git a/assets/icons/radix/mic-mute.svg b/assets/icons/mic-mute.svg similarity index 100% rename from assets/icons/radix/mic-mute.svg rename to assets/icons/mic-mute.svg diff --git a/assets/icons/radix/mic.svg b/assets/icons/mic.svg similarity index 100% rename from assets/icons/radix/mic.svg rename to assets/icons/mic.svg diff --git a/assets/icons/microphone.svg b/assets/icons/microphone.svg deleted file mode 100644 index 8974fd939d233b839d03e94e301abb2a955c665a..0000000000000000000000000000000000000000 --- a/assets/icons/microphone.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index d8941ee1f0ed6a566cf0d07a1b89cefd49d3ee19..ec78f152e13eda0c887a18b99b585d0c65acc8a8 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/minimize_8.svg b/assets/icons/minimize_8.svg deleted file mode 100644 index b511cbd3550d14854e3b84d6dddfde6fa8d8acf7..0000000000000000000000000000000000000000 --- a/assets/icons/minimize_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg index a54dd0ad66226f3c485c33c221f823da87727789..57ce90219bc6f72d92e55011f6dcb9f20ba320eb 100644 --- a/assets/icons/plus.svg +++ b/assets/icons/plus.svg @@ -1,3 +1,8 @@ - - + + diff --git a/assets/icons/plus_12.svg b/assets/icons/plus_12.svg deleted file mode 100644 index f1770fa248c32ff0cb10d1e2e935c7a6e1eee129..0000000000000000000000000000000000000000 --- a/assets/icons/plus_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/plus_16.svg b/assets/icons/plus_16.svg deleted file mode 100644 index c595cf597a70e811b122e34a05dbf453c9eacefa..0000000000000000000000000000000000000000 --- a/assets/icons/plus_16.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/plus_8.svg b/assets/icons/plus_8.svg deleted file mode 100644 index 72efa1574eeaf2489cb210483c6c1386afc4f067..0000000000000000000000000000000000000000 --- a/assets/icons/plus_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/radix/quote.svg b/assets/icons/quote.svg similarity index 100% rename from assets/icons/radix/quote.svg rename to assets/icons/quote.svg diff --git a/assets/icons/quote_15.svg b/assets/icons/quote_15.svg deleted file mode 100644 index be5eabd9b019902a44c03ac5545441702b6d7925..0000000000000000000000000000000000000000 --- a/assets/icons/quote_15.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/radix/accessibility.svg b/assets/icons/radix/accessibility.svg deleted file mode 100644 index 32d78f2d8da1c317727810706a892a63f588463e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/accessibility.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/activity-log.svg b/assets/icons/radix/activity-log.svg deleted file mode 100644 index 8feab7d44942915ef6d49602e272b03125ee8ea4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/activity-log.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-baseline.svg b/assets/icons/radix/align-baseline.svg deleted file mode 100644 index 07213dc1ae61fbf49d3f72b107082b07892fa0c1..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-baseline.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-bottom.svg b/assets/icons/radix/align-bottom.svg deleted file mode 100644 index 7d11c0cd5a6e11be048bcfc04c782fcd3e61f2ee..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-bottom.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-center-horizontally.svg b/assets/icons/radix/align-center-horizontally.svg deleted file mode 100644 index 69509a7d097821d2c0169ae468efc8d74a7e90c9..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-center-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-center-vertically.svg b/assets/icons/radix/align-center-vertically.svg deleted file mode 100644 index 4f1b50cc4366775a792bef2b4475ec864856a3a7..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-center-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-center.svg b/assets/icons/radix/align-center.svg deleted file mode 100644 index caaec36477fbbf2bcfef558aa682092d0bbd9a01..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-center.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-end.svg b/assets/icons/radix/align-end.svg deleted file mode 100644 index 18f1b6491233806086baf55ab67c5d7f4e10ff54..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-end.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-horizontal-centers.svg b/assets/icons/radix/align-horizontal-centers.svg deleted file mode 100644 index 2d1d64ea4b82ef5e0d933b9bf0ec439c9998dd98..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-horizontal-centers.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-left.svg b/assets/icons/radix/align-left.svg deleted file mode 100644 index 0d5dba095c7d0756d489d415276064a91d4fd3ce..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-right.svg b/assets/icons/radix/align-right.svg deleted file mode 100644 index 1b6b3f0ffa9c649b005739baafa9d973013af076..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-start.svg b/assets/icons/radix/align-start.svg deleted file mode 100644 index ada50e1079e481cde5f0f9ee5884a7030ebb0bc6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-start.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-stretch.svg b/assets/icons/radix/align-stretch.svg deleted file mode 100644 index 3cb28605cbf1b1a8470fabd1257370d74b3e5682..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-stretch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-top.svg b/assets/icons/radix/align-top.svg deleted file mode 100644 index 23db80f4dd0ebb04ee703fe74d4b535abbd01da1..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-top.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-vertical-centers.svg b/assets/icons/radix/align-vertical-centers.svg deleted file mode 100644 index 07eaee7bf7d9274c402bb3f4bfaa0dea486eb09b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/align-vertical-centers.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/all-sides.svg b/assets/icons/radix/all-sides.svg deleted file mode 100644 index 8ace7df03f4d17ba1e8f858b94d418eb63618ea6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/all-sides.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/angle.svg b/assets/icons/radix/angle.svg deleted file mode 100644 index a0d93f3460ca940a1bf5e7ad94c46f56d40ccc7b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/angle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/archive.svg b/assets/icons/radix/archive.svg deleted file mode 100644 index 74063f1d1e2346c09ee2a6a5297c30ef7e0c74ad..0000000000000000000000000000000000000000 --- a/assets/icons/radix/archive.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-bottom-left.svg b/assets/icons/radix/arrow-bottom-left.svg deleted file mode 100644 index 7a4511aa2d69b39c305cd80c291c868007cba491..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-bottom-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-bottom-right.svg b/assets/icons/radix/arrow-bottom-right.svg deleted file mode 100644 index 2ba9fef1019774f1e5094f5654d89df848cdbb5b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-bottom-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-down.svg b/assets/icons/radix/arrow-down.svg deleted file mode 100644 index 5dc21a66890fb27f537b4400e96d48b7f7ce84a6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-left.svg b/assets/icons/radix/arrow-left.svg deleted file mode 100644 index 3a64c8394f0825b3708634c2d003a648877c35cd..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-right.svg b/assets/icons/radix/arrow-right.svg deleted file mode 100644 index e3d30988d5e7b4547393281c7bdad60c3006f4f3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-top-left.svg b/assets/icons/radix/arrow-top-left.svg deleted file mode 100644 index 69fef41dee621d3f8cf681e630c0ce623d65124d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-top-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-top-right.svg b/assets/icons/radix/arrow-top-right.svg deleted file mode 100644 index c1016376e3232ead02dde954379ce74b7bfb68f7..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-top-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-up.svg b/assets/icons/radix/arrow-up.svg deleted file mode 100644 index ba426119e901d0a1132d0e47b34c0beebaec22ce..0000000000000000000000000000000000000000 --- a/assets/icons/radix/arrow-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/aspect-ratio.svg b/assets/icons/radix/aspect-ratio.svg deleted file mode 100644 index 0851f2e1e9f46d52cd2974b77a65e3a8b95b339e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/aspect-ratio.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/avatar.svg b/assets/icons/radix/avatar.svg deleted file mode 100644 index cb229c77fe827f64054b6bfa05f2ad2aaf17c2d3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/avatar.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/backpack.svg b/assets/icons/radix/backpack.svg deleted file mode 100644 index a5c9cedbd32dd589c825852f447e8c6125c2a8fb..0000000000000000000000000000000000000000 --- a/assets/icons/radix/backpack.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/badge.svg b/assets/icons/radix/badge.svg deleted file mode 100644 index aa764d4726f449c163b00e1bd993d12c5aa95c24..0000000000000000000000000000000000000000 --- a/assets/icons/radix/badge.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bar-chart.svg b/assets/icons/radix/bar-chart.svg deleted file mode 100644 index f8054781d9ec2ee79f0652ae20753e3e80752bff..0000000000000000000000000000000000000000 --- a/assets/icons/radix/bar-chart.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bell.svg b/assets/icons/radix/bell.svg deleted file mode 100644 index ea1c6dd42e8821b632f6de97d143a7b9f4b97fd2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/bell.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/blending-mode.svg b/assets/icons/radix/blending-mode.svg deleted file mode 100644 index bd58cf4ee38ee66e9860df11a9f4150899a9c8a8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/blending-mode.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bookmark-filled.svg b/assets/icons/radix/bookmark-filled.svg deleted file mode 100644 index 5b725cd88dbf9337d52095a7567a2bc12e15439a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/bookmark-filled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bookmark.svg b/assets/icons/radix/bookmark.svg deleted file mode 100644 index 90c4d827f13cd47a83a030c833a02e15492dc084..0000000000000000000000000000000000000000 --- a/assets/icons/radix/bookmark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-all.svg b/assets/icons/radix/border-all.svg deleted file mode 100644 index 3bfde7d59baa675eeae72eac6f7245eadbe10821..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-all.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/radix/border-bottom.svg b/assets/icons/radix/border-bottom.svg deleted file mode 100644 index f2d3c3d554e09837c464ff425c3af74413db4eb6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-bottom.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-dashed.svg b/assets/icons/radix/border-dashed.svg deleted file mode 100644 index 85fdcdfe5d7f3905f2056912a5bc56d229ca5ee0..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-dashed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-dotted.svg b/assets/icons/radix/border-dotted.svg deleted file mode 100644 index 5eb514ed2a60093e0c4eb904b4cc5c6d18b9a62f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-dotted.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-left.svg b/assets/icons/radix/border-left.svg deleted file mode 100644 index 5deb197da51a7db874b57e1a473d4287b2a3cd49..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-left.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-none.svg b/assets/icons/radix/border-none.svg deleted file mode 100644 index 1ad3f59d7c9b93101657ad1523a2939d02f504d8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-none.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-right.svg b/assets/icons/radix/border-right.svg deleted file mode 100644 index c939095ad78a75eeb5f8b2e31f57e56b201b8a4c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-right.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-solid.svg b/assets/icons/radix/border-solid.svg deleted file mode 100644 index 5c0d26a0583140b8ba0b47e937bc0dedc81e4fb5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-solid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-split.svg b/assets/icons/radix/border-split.svg deleted file mode 100644 index 7fdf6cc34e73e6543fa34e9b52e22382130d6f1a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-split.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-style.svg b/assets/icons/radix/border-style.svg deleted file mode 100644 index f729cb993babfa12140deabc9451eceee6b7885a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-style.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-top.svg b/assets/icons/radix/border-top.svg deleted file mode 100644 index bde739d75539be17496a8ce65b875b4f4b943940..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-top.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-width.svg b/assets/icons/radix/border-width.svg deleted file mode 100644 index 37c270756ec4ec5a8a42b81b64bfbbe8e24f892a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/border-width.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/box-model.svg b/assets/icons/radix/box-model.svg deleted file mode 100644 index 45d1a7ce415aa508a8a8f8d39f8032a22c2b4e5a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/box-model.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/box.svg b/assets/icons/radix/box.svg deleted file mode 100644 index 6e035c21ed8fd3ad1eca7297921da359262e8445..0000000000000000000000000000000000000000 --- a/assets/icons/radix/box.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/button.svg b/assets/icons/radix/button.svg deleted file mode 100644 index 31622bcf159a83dbf7dbc7960da3c490711a14ff..0000000000000000000000000000000000000000 --- a/assets/icons/radix/button.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/calendar.svg b/assets/icons/radix/calendar.svg deleted file mode 100644 index 2adbe0bc2868392e36079a5860ddf706543b210e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/calendar.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/camera.svg b/assets/icons/radix/camera.svg deleted file mode 100644 index d7cccf74c2e416dd8abcd45be121f73eccea3c12..0000000000000000000000000000000000000000 --- a/assets/icons/radix/camera.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/card-stack-minus.svg b/assets/icons/radix/card-stack-minus.svg deleted file mode 100644 index 04d8e51178a0a8ea38a5354aa421e20bd4091298..0000000000000000000000000000000000000000 --- a/assets/icons/radix/card-stack-minus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/card-stack-plus.svg b/assets/icons/radix/card-stack-plus.svg deleted file mode 100644 index a184f4bc1aff9b3b212fc3cce7265cf58bba3948..0000000000000000000000000000000000000000 --- a/assets/icons/radix/card-stack-plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/card-stack.svg b/assets/icons/radix/card-stack.svg deleted file mode 100644 index defea0e1654f9267fa91a8b66e2bf1191b95aadd..0000000000000000000000000000000000000000 --- a/assets/icons/radix/card-stack.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/caret-left.svg b/assets/icons/radix/caret-left.svg deleted file mode 100644 index 969bc3b95c2194b922c1858ddf89b5d2461f11d3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/caret-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/caret-right.svg b/assets/icons/radix/caret-right.svg deleted file mode 100644 index 75c55d8676eebdc09961d63b870e12fc0a91c5c5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/caret-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/caret-sort.svg b/assets/icons/radix/caret-sort.svg deleted file mode 100644 index a65e20b660481333e4e27e32203c9a5d12a5f150..0000000000000000000000000000000000000000 --- a/assets/icons/radix/caret-sort.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chat-bubble.svg b/assets/icons/radix/chat-bubble.svg deleted file mode 100644 index 5766f46de868ad91fc0ff057691a7dea474a0dae..0000000000000000000000000000000000000000 --- a/assets/icons/radix/chat-bubble.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/check-circled.svg b/assets/icons/radix/check-circled.svg deleted file mode 100644 index 19ee22eb511b987dd3acfc5c7c833d6561a4662d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/check-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/check.svg b/assets/icons/radix/check.svg deleted file mode 100644 index 476a3baa18e42bb05edfd7ec0c3a2aef155cc003..0000000000000000000000000000000000000000 --- a/assets/icons/radix/check.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/checkbox.svg b/assets/icons/radix/checkbox.svg deleted file mode 100644 index d6bb3c7ef2f0e97b823bffb1d4ea1edd38609da9..0000000000000000000000000000000000000000 --- a/assets/icons/radix/checkbox.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-down.svg b/assets/icons/radix/chevron-down.svg deleted file mode 100644 index 175c1312fd37417cba0bbcd9230b4dffa24821e4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/chevron-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-left.svg b/assets/icons/radix/chevron-left.svg deleted file mode 100644 index d7628202f29edf1642deb44bf93ff540aa728475..0000000000000000000000000000000000000000 --- a/assets/icons/radix/chevron-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-right.svg b/assets/icons/radix/chevron-right.svg deleted file mode 100644 index e3ebd73d9909a53e3fb721f2ea686f1dca0b477b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/chevron-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-up.svg b/assets/icons/radix/chevron-up.svg deleted file mode 100644 index 0e8e796dab46c9de345166aa4dba818305b68857..0000000000000000000000000000000000000000 --- a/assets/icons/radix/chevron-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/circle-backslash.svg b/assets/icons/radix/circle-backslash.svg deleted file mode 100644 index 40c4dd5398b454220d4d22dbbec08bcdb335be71..0000000000000000000000000000000000000000 --- a/assets/icons/radix/circle-backslash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/circle.svg b/assets/icons/radix/circle.svg deleted file mode 100644 index ba4a8f22fe574008e076c7983dfc5f743d03f2df..0000000000000000000000000000000000000000 --- a/assets/icons/radix/circle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/clipboard-copy.svg b/assets/icons/radix/clipboard-copy.svg deleted file mode 100644 index 5293fdc493f5577936977562c9457bbfa809f012..0000000000000000000000000000000000000000 --- a/assets/icons/radix/clipboard-copy.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/clipboard.svg b/assets/icons/radix/clipboard.svg deleted file mode 100644 index e18b32943be09aca0c53294e8e65187564ba1224..0000000000000000000000000000000000000000 --- a/assets/icons/radix/clipboard.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/clock.svg b/assets/icons/radix/clock.svg deleted file mode 100644 index ac3b526fbbda03c5984d7c9dfaf937be520910a2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/clock.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/code.svg b/assets/icons/radix/code.svg deleted file mode 100644 index 70fe381b68c5b95065275b5163af76dabaa5b22e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/code.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/codesandbox-logo.svg b/assets/icons/radix/codesandbox-logo.svg deleted file mode 100644 index 4a3f549c2f6d7271e9a8fb225e18285d90312df8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/codesandbox-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/color-wheel.svg b/assets/icons/radix/color-wheel.svg deleted file mode 100644 index 2153b84428f354843aa7ffd3be174680440be90c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/color-wheel.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/column-spacing.svg b/assets/icons/radix/column-spacing.svg deleted file mode 100644 index aafcf555cb1ca06550c39419d20c257b02ea1934..0000000000000000000000000000000000000000 --- a/assets/icons/radix/column-spacing.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/columns.svg b/assets/icons/radix/columns.svg deleted file mode 100644 index e1607611b1a24957c7983041a540806b4275d289..0000000000000000000000000000000000000000 --- a/assets/icons/radix/columns.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/commit.svg b/assets/icons/radix/commit.svg deleted file mode 100644 index ac128a2b083d6b94f17ee065d88226ff7dc53da3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/commit.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-1.svg b/assets/icons/radix/component-1.svg deleted file mode 100644 index e3e9f38af1fba0b278ed2c48bfc76cb2a6783307..0000000000000000000000000000000000000000 --- a/assets/icons/radix/component-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-2.svg b/assets/icons/radix/component-2.svg deleted file mode 100644 index df2091d1437ba51b4d1d6647dfa4d16ebd7dac53..0000000000000000000000000000000000000000 --- a/assets/icons/radix/component-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-boolean.svg b/assets/icons/radix/component-boolean.svg deleted file mode 100644 index 942e8832eb4e99cd3af0dc61a1bde6ea01574cb8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/component-boolean.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-instance.svg b/assets/icons/radix/component-instance.svg deleted file mode 100644 index 048c40129134426ed628de6d386be9017b484d32..0000000000000000000000000000000000000000 --- a/assets/icons/radix/component-instance.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-none.svg b/assets/icons/radix/component-none.svg deleted file mode 100644 index a622c3ee960ac4b61d03f4d7b755d98576e37b0d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/component-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-placeholder.svg b/assets/icons/radix/component-placeholder.svg deleted file mode 100644 index b8892d5d23632fd251938af55c0ae34a112ba058..0000000000000000000000000000000000000000 --- a/assets/icons/radix/component-placeholder.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/assets/icons/radix/container.svg b/assets/icons/radix/container.svg deleted file mode 100644 index 1c2a4fd0e18cf47ee793eb6196f6b21e99bda6c0..0000000000000000000000000000000000000000 --- a/assets/icons/radix/container.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cookie.svg b/assets/icons/radix/cookie.svg deleted file mode 100644 index 8c165601a2a8af711ce771ea31b829405bccdfba..0000000000000000000000000000000000000000 --- a/assets/icons/radix/cookie.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/copy.svg b/assets/icons/radix/copy.svg deleted file mode 100644 index bf2b504ecfcb378b1a93cf893b4eb070da9471fb..0000000000000000000000000000000000000000 --- a/assets/icons/radix/copy.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-bottom-left.svg b/assets/icons/radix/corner-bottom-left.svg deleted file mode 100644 index 26df9dbad8c28a6bd041e14bde9cb23624cf66ca..0000000000000000000000000000000000000000 --- a/assets/icons/radix/corner-bottom-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-bottom-right.svg b/assets/icons/radix/corner-bottom-right.svg deleted file mode 100644 index 15e395712342d3f4d5625d6159f3c1a5ba78e108..0000000000000000000000000000000000000000 --- a/assets/icons/radix/corner-bottom-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-top-left.svg b/assets/icons/radix/corner-top-left.svg deleted file mode 100644 index 8fc1b84b825e7ed1d63ac0dee1b93c768ae42048..0000000000000000000000000000000000000000 --- a/assets/icons/radix/corner-top-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-top-right.svg b/assets/icons/radix/corner-top-right.svg deleted file mode 100644 index 533ea6c678c2edb2355862ed4ab2712f2b338bab..0000000000000000000000000000000000000000 --- a/assets/icons/radix/corner-top-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corners.svg b/assets/icons/radix/corners.svg deleted file mode 100644 index c41c4e01839621c0f3a3ec8c6a7c02d7345e97b2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/corners.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/countdown-timer.svg b/assets/icons/radix/countdown-timer.svg deleted file mode 100644 index 58494bd416ab93113128a113c3dbaa5b5f268b2a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/countdown-timer.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/counter-clockwise-clock.svg b/assets/icons/radix/counter-clockwise-clock.svg deleted file mode 100644 index 0b3acbcebf2d7d71a23d9b89648df9ac532ae847..0000000000000000000000000000000000000000 --- a/assets/icons/radix/counter-clockwise-clock.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crop.svg b/assets/icons/radix/crop.svg deleted file mode 100644 index 008457fff6861d102469ef46a234080e6fb0c634..0000000000000000000000000000000000000000 --- a/assets/icons/radix/crop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cross-1.svg b/assets/icons/radix/cross-1.svg deleted file mode 100644 index 62135d27edf689ce7a06092a95248ffeb67b8f9e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/cross-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cross-2.svg b/assets/icons/radix/cross-2.svg deleted file mode 100644 index 4c557009286712b14e716f7e69309b0eb197d768..0000000000000000000000000000000000000000 --- a/assets/icons/radix/cross-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cross-circled.svg b/assets/icons/radix/cross-circled.svg deleted file mode 100644 index df3cb896c8f20de3614ce7adfd4a6774bead4ee5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/cross-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crosshair-1.svg b/assets/icons/radix/crosshair-1.svg deleted file mode 100644 index 05b22f8461a6d1a513b74aeb0ea976936e42f253..0000000000000000000000000000000000000000 --- a/assets/icons/radix/crosshair-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crosshair-2.svg b/assets/icons/radix/crosshair-2.svg deleted file mode 100644 index f5ee0a92af713fb3bd8c366f7400194d291ee7b5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/crosshair-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crumpled-paper.svg b/assets/icons/radix/crumpled-paper.svg deleted file mode 100644 index 33e9b65581b6a35b7f8c687f1b9dbab9edbb32cf..0000000000000000000000000000000000000000 --- a/assets/icons/radix/crumpled-paper.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cube.svg b/assets/icons/radix/cube.svg deleted file mode 100644 index b327158be4afc35744fe0c2e84b5f73662a93472..0000000000000000000000000000000000000000 --- a/assets/icons/radix/cube.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cursor-arrow.svg b/assets/icons/radix/cursor-arrow.svg deleted file mode 100644 index b0227e4ded7aef4a78baebcf10a511e0c5659f6c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/cursor-arrow.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cursor-text.svg b/assets/icons/radix/cursor-text.svg deleted file mode 100644 index 05939503b8a5c4caed24fe8ab938fbef8406ffdd..0000000000000000000000000000000000000000 --- a/assets/icons/radix/cursor-text.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dash.svg b/assets/icons/radix/dash.svg deleted file mode 100644 index d70daf7fed6ec8e6346e5800ef89249d7cf62984..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dashboard.svg b/assets/icons/radix/dashboard.svg deleted file mode 100644 index 38008c64e41e2addfea23f4c5f88bc04a2a49e86..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dashboard.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/desktop-mute.svg b/assets/icons/radix/desktop-mute.svg deleted file mode 100644 index 83d249176fbf067a2732fa4379740cfa54bd018a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/desktop-mute.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/radix/dimensions.svg b/assets/icons/radix/dimensions.svg deleted file mode 100644 index 767d1d289641510dca8f75431192786f294be2a1..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dimensions.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/disc.svg b/assets/icons/radix/disc.svg deleted file mode 100644 index 6e19caab3504eef094cd4cffbe43b657dc1913ad..0000000000000000000000000000000000000000 --- a/assets/icons/radix/disc.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/discord-logo.svg b/assets/icons/radix/discord-logo.svg deleted file mode 100644 index 50567c212eda4dca3f87df399dd0e6d0dc076c2b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/discord-logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/assets/icons/radix/divider-horizontal.svg b/assets/icons/radix/divider-horizontal.svg deleted file mode 100644 index 59e43649c93b1767739548a6bc8122886c6061ad..0000000000000000000000000000000000000000 --- a/assets/icons/radix/divider-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/divider-vertical.svg b/assets/icons/radix/divider-vertical.svg deleted file mode 100644 index 95f5cc8f2f45dabe00fd376a8ac2db99155e686f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/divider-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dot-filled.svg b/assets/icons/radix/dot-filled.svg deleted file mode 100644 index 0c1a17b3bd8a904d7274a18b5a4432681fb867ca..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dot-filled.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/dot-solid.svg b/assets/icons/radix/dot-solid.svg deleted file mode 100644 index 0c1a17b3bd8a904d7274a18b5a4432681fb867ca..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dot-solid.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/dot.svg b/assets/icons/radix/dot.svg deleted file mode 100644 index c553a1422dbd52775efacadede6863d2dc0256c9..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dot.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dots-horizontal.svg b/assets/icons/radix/dots-horizontal.svg deleted file mode 100644 index 347d1ae13d84eaef1bf4ab33d65a9dfcf11292d5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dots-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dots-vertical.svg b/assets/icons/radix/dots-vertical.svg deleted file mode 100644 index 5ca1a181e3887e4b5459c899aedb25acf60d4bed..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dots-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-down.svg b/assets/icons/radix/double-arrow-down.svg deleted file mode 100644 index 8b86db2f8a0baa6350a0ad772c083b22fd520be9..0000000000000000000000000000000000000000 --- a/assets/icons/radix/double-arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-left.svg b/assets/icons/radix/double-arrow-left.svg deleted file mode 100644 index 0ef30ff9554c558469c75252ef56a828cad2c777..0000000000000000000000000000000000000000 --- a/assets/icons/radix/double-arrow-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-right.svg b/assets/icons/radix/double-arrow-right.svg deleted file mode 100644 index 9997fdc40398d3cf1c6ce30c78ae4d5b4f319457..0000000000000000000000000000000000000000 --- a/assets/icons/radix/double-arrow-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-up.svg b/assets/icons/radix/double-arrow-up.svg deleted file mode 100644 index 8d571fcd66980e46d4e26eaf96870df6ff469408..0000000000000000000000000000000000000000 --- a/assets/icons/radix/double-arrow-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/download.svg b/assets/icons/radix/download.svg deleted file mode 100644 index 49a05d5f47f7c07faa1403c5320268e6df2581a5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/download.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drag-handle-dots-1.svg b/assets/icons/radix/drag-handle-dots-1.svg deleted file mode 100644 index fc046bb9d9b03b5bdd5ea49dc1bedab8aacab656..0000000000000000000000000000000000000000 --- a/assets/icons/radix/drag-handle-dots-1.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/drag-handle-dots-2.svg b/assets/icons/radix/drag-handle-dots-2.svg deleted file mode 100644 index aed0e702d7635421fc6674e2daafbccb0573314c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/drag-handle-dots-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drag-handle-horizontal.svg b/assets/icons/radix/drag-handle-horizontal.svg deleted file mode 100644 index c1bb138a244147fc61333952ee898979ce67351f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/drag-handle-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drag-handle-vertical.svg b/assets/icons/radix/drag-handle-vertical.svg deleted file mode 100644 index 8d48c7894afcb4949b1784f93c062014dcd207c6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/drag-handle-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drawing-pin-filled.svg b/assets/icons/radix/drawing-pin-filled.svg deleted file mode 100644 index e1894619c34441eb228587b9c50fc6af61193a44..0000000000000000000000000000000000000000 --- a/assets/icons/radix/drawing-pin-filled.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/assets/icons/radix/drawing-pin-solid.svg b/assets/icons/radix/drawing-pin-solid.svg deleted file mode 100644 index e1894619c34441eb228587b9c50fc6af61193a44..0000000000000000000000000000000000000000 --- a/assets/icons/radix/drawing-pin-solid.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/assets/icons/radix/drawing-pin.svg b/assets/icons/radix/drawing-pin.svg deleted file mode 100644 index 5625e7588f1f33f057bf8ad15bc261c45072b1a9..0000000000000000000000000000000000000000 --- a/assets/icons/radix/drawing-pin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dropdown-menu.svg b/assets/icons/radix/dropdown-menu.svg deleted file mode 100644 index c938052be8e21698e89e8a0f57215c71410492c9..0000000000000000000000000000000000000000 --- a/assets/icons/radix/dropdown-menu.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/enter-full-screen.svg b/assets/icons/radix/enter-full-screen.svg deleted file mode 100644 index d368a6d415fc340db7595a06b5686cbb920ad48a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/enter-full-screen.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/enter.svg b/assets/icons/radix/enter.svg deleted file mode 100644 index cc57d74ceae76b56074e8be073916301a280b9a2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/enter.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/envelope-closed.svg b/assets/icons/radix/envelope-closed.svg deleted file mode 100644 index 4b5e0378401cd9f8530355d84da28d7ca507d0a2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/envelope-closed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/envelope-open.svg b/assets/icons/radix/envelope-open.svg deleted file mode 100644 index df1e3fea9515984d0207b80e3ab03b39511d52db..0000000000000000000000000000000000000000 --- a/assets/icons/radix/envelope-open.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eraser.svg b/assets/icons/radix/eraser.svg deleted file mode 100644 index bb448d4d23511c57ab4216dd28af17232949c0b4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/eraser.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/exclamation-triangle.svg b/assets/icons/radix/exclamation-triangle.svg deleted file mode 100644 index 210d4c45c666164985e0f1998201d444c9a5f2a7..0000000000000000000000000000000000000000 --- a/assets/icons/radix/exclamation-triangle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/exit-full-screen.svg b/assets/icons/radix/exit-full-screen.svg deleted file mode 100644 index 9b6439b043b367c5c300949f511ecb9866f2eaca..0000000000000000000000000000000000000000 --- a/assets/icons/radix/exit-full-screen.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/exit.svg b/assets/icons/radix/exit.svg deleted file mode 100644 index 2cc6ce120dc9af17a642ac3bf2f2451209cb5e5e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/exit.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/external-link.svg b/assets/icons/radix/external-link.svg deleted file mode 100644 index 0ee7420162a88fa92afc958ec9a61242a9a8640c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/external-link.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eye-closed.svg b/assets/icons/radix/eye-closed.svg deleted file mode 100644 index f824fe55f9e2f45e7e12b77420eaeb24d6e9c913..0000000000000000000000000000000000000000 --- a/assets/icons/radix/eye-closed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eye-none.svg b/assets/icons/radix/eye-none.svg deleted file mode 100644 index d4beecd33a4a4a305407e1adfa2f4584c4359635..0000000000000000000000000000000000000000 --- a/assets/icons/radix/eye-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eye-open.svg b/assets/icons/radix/eye-open.svg deleted file mode 100644 index d39d26b2c1bbc40af8548cafe219f7cef2373373..0000000000000000000000000000000000000000 --- a/assets/icons/radix/eye-open.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/face.svg b/assets/icons/radix/face.svg deleted file mode 100644 index 81b14dd8d7932f9db417843798c726422890b32e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/face.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/figma-logo.svg b/assets/icons/radix/figma-logo.svg deleted file mode 100644 index 6c19276554908b11c8742deb0ab4e971bf6856a7..0000000000000000000000000000000000000000 --- a/assets/icons/radix/figma-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/file-minus.svg b/assets/icons/radix/file-minus.svg deleted file mode 100644 index bd1a841881c0cfa6a52364dfe57fd55e5a539fa0..0000000000000000000000000000000000000000 --- a/assets/icons/radix/file-minus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/file-plus.svg b/assets/icons/radix/file-plus.svg deleted file mode 100644 index 2396e20015984b69e2c194c2c9e8552b1a2cc3b5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/file-plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/file-text.svg b/assets/icons/radix/file-text.svg deleted file mode 100644 index f341ab8abfdba5a9aaac3a81b709c75def92e46c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/file-text.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-bold.svg b/assets/icons/radix/font-bold.svg deleted file mode 100644 index 7dc6caf3b052c956c9bb9ad4adc9ca245cfcf083..0000000000000000000000000000000000000000 --- a/assets/icons/radix/font-bold.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/font-family.svg b/assets/icons/radix/font-family.svg deleted file mode 100644 index 9134b9086dd5ddb9aa40a01875033392b2f92f89..0000000000000000000000000000000000000000 --- a/assets/icons/radix/font-family.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/font-italic.svg b/assets/icons/radix/font-italic.svg deleted file mode 100644 index 6e6288d6bc3ffae240721c50c1a85c1a80270aa2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/font-italic.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-roman.svg b/assets/icons/radix/font-roman.svg deleted file mode 100644 index c595b790fc5065d5e4b276d4e73be1ccdeba7be2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/font-roman.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-size.svg b/assets/icons/radix/font-size.svg deleted file mode 100644 index e389a58d73bc4997d64b78426be26e964fd5b2b8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/font-size.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-style.svg b/assets/icons/radix/font-style.svg deleted file mode 100644 index 31c3730130fad5367eb87f1b5ce52b243ee4c1f5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/font-style.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/frame.svg b/assets/icons/radix/frame.svg deleted file mode 100644 index ec61a48efabfc82a55a749860976dd694aee7a83..0000000000000000000000000000000000000000 --- a/assets/icons/radix/frame.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/framer-logo.svg b/assets/icons/radix/framer-logo.svg deleted file mode 100644 index 68be3b317b90d2fa990622857645bf21c1768c74..0000000000000000000000000000000000000000 --- a/assets/icons/radix/framer-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/gear.svg b/assets/icons/radix/gear.svg deleted file mode 100644 index 52f9e17312fb364b410edbcb21f3aa4b6f3c133c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/gear.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/github-logo.svg b/assets/icons/radix/github-logo.svg deleted file mode 100644 index e46612cf566f59ffc8d8b8b6f4a8bcecd8779b12..0000000000000000000000000000000000000000 --- a/assets/icons/radix/github-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/globe.svg b/assets/icons/radix/globe.svg deleted file mode 100644 index 4728b827df862d2e4db3363d9d518cebc860986a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/globe.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - diff --git a/assets/icons/radix/grid.svg b/assets/icons/radix/grid.svg deleted file mode 100644 index 5d9af3357295415ea824128b9806d1ca895e8bb6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/grid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/group.svg b/assets/icons/radix/group.svg deleted file mode 100644 index c3c91d211f47df42ad1c89911fc63e60499d3db6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/group.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/half-1.svg b/assets/icons/radix/half-1.svg deleted file mode 100644 index 9890e26bb815242173bf8a60a01194a9130a361f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/half-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/half-2.svg b/assets/icons/radix/half-2.svg deleted file mode 100644 index 4db1d564cba5c32aae6260095811291c0614fdcf..0000000000000000000000000000000000000000 --- a/assets/icons/radix/half-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/hamburger-menu.svg b/assets/icons/radix/hamburger-menu.svg deleted file mode 100644 index 039168055b20d615f19400c4324857d0c038806e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/hamburger-menu.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/hand.svg b/assets/icons/radix/hand.svg deleted file mode 100644 index 12afac8f5f9fdff743a7b628437ebfb4424fba2a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/hand.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/heading.svg b/assets/icons/radix/heading.svg deleted file mode 100644 index 0a5e2caaf1b10b271da7664dc3636528c6c00942..0000000000000000000000000000000000000000 --- a/assets/icons/radix/heading.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/heart-filled.svg b/assets/icons/radix/heart-filled.svg deleted file mode 100644 index 94928accd7e353b655baf5840ca2be8fb4afd49c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/heart-filled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/heart.svg b/assets/icons/radix/heart.svg deleted file mode 100644 index 91cbc450fd0418c590a1519da9834b6cdb72ff5e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/heart.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/height.svg b/assets/icons/radix/height.svg deleted file mode 100644 index 28424f4d51e008fafd30347e06e1deb8b3a6942f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/height.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/hobby-knife.svg b/assets/icons/radix/hobby-knife.svg deleted file mode 100644 index c2ed3fb1ed89ef2b9ba74e1c94ec778af5dbc7cd..0000000000000000000000000000000000000000 --- a/assets/icons/radix/hobby-knife.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/home.svg b/assets/icons/radix/home.svg deleted file mode 100644 index 733bd791138444e03cb01f52b2e7428f93fbbc36..0000000000000000000000000000000000000000 --- a/assets/icons/radix/home.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/iconjar-logo.svg b/assets/icons/radix/iconjar-logo.svg deleted file mode 100644 index c154b4e86413741786fa3d608f6e466e91c01aab..0000000000000000000000000000000000000000 --- a/assets/icons/radix/iconjar-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/id-card.svg b/assets/icons/radix/id-card.svg deleted file mode 100644 index efde9ffa7e612179911c972a3c048fd389fe3276..0000000000000000000000000000000000000000 --- a/assets/icons/radix/id-card.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/image.svg b/assets/icons/radix/image.svg deleted file mode 100644 index 0ff44752528fa0d4b31613a72446ed9164c419cb..0000000000000000000000000000000000000000 --- a/assets/icons/radix/image.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/info-circled.svg b/assets/icons/radix/info-circled.svg deleted file mode 100644 index 4ab1b260e3d35f9a6243e44ebf0f903add40b6b8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/info-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/inner-shadow.svg b/assets/icons/radix/inner-shadow.svg deleted file mode 100644 index 1056a7bffc268fef67c209f4c81f606d40fa66d6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/inner-shadow.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/input.svg b/assets/icons/radix/input.svg deleted file mode 100644 index 4ed4605b2c60da836327a7064469425d5233858d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/input.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/instagram-logo.svg b/assets/icons/radix/instagram-logo.svg deleted file mode 100644 index 5d7893796655c947c0e6bc0dba60c6e82c86bd65..0000000000000000000000000000000000000000 --- a/assets/icons/radix/instagram-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/assets/icons/radix/justify-center.svg b/assets/icons/radix/justify-center.svg deleted file mode 100644 index 7999a4ea468e87d9f0cd793e80c2a43454c4aeac..0000000000000000000000000000000000000000 --- a/assets/icons/radix/justify-center.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/justify-end.svg b/assets/icons/radix/justify-end.svg deleted file mode 100644 index bb52f493d75d79f91e3a6f34e103023e2cc8b87c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/justify-end.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/justify-start.svg b/assets/icons/radix/justify-start.svg deleted file mode 100644 index 648ca0b60324f4b92a617f377d890b8f1e1adf13..0000000000000000000000000000000000000000 --- a/assets/icons/radix/justify-start.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/justify-stretch.svg b/assets/icons/radix/justify-stretch.svg deleted file mode 100644 index 83df0a8959381ef48a3bd97b53f63f8d9a8bba0f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/justify-stretch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/keyboard.svg b/assets/icons/radix/keyboard.svg deleted file mode 100644 index fc6f86bfc2b48bdd4fb7acf8e9e08422fed2e91e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/keyboard.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/assets/icons/radix/lap-timer.svg b/assets/icons/radix/lap-timer.svg deleted file mode 100644 index 1de0b3be6ce99de994a905cfbaf5e342754bb651..0000000000000000000000000000000000000000 --- a/assets/icons/radix/lap-timer.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/laptop.svg b/assets/icons/radix/laptop.svg deleted file mode 100644 index 6aff5d6d446ea46b131bdea1efbd183bc0010381..0000000000000000000000000000000000000000 --- a/assets/icons/radix/laptop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/layers.svg b/assets/icons/radix/layers.svg deleted file mode 100644 index 821993fc70c13ebdb18a997d849db95424399d82..0000000000000000000000000000000000000000 --- a/assets/icons/radix/layers.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/layout.svg b/assets/icons/radix/layout.svg deleted file mode 100644 index 8e4a352f5022fe33402bd5267f32f925958a2a01..0000000000000000000000000000000000000000 --- a/assets/icons/radix/layout.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-capitalize.svg b/assets/icons/radix/letter-case-capitalize.svg deleted file mode 100644 index 16617ecf7e052db05c5bccfe1da0bb378835f686..0000000000000000000000000000000000000000 --- a/assets/icons/radix/letter-case-capitalize.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-lowercase.svg b/assets/icons/radix/letter-case-lowercase.svg deleted file mode 100644 index 61aefb9aadd3c45a338e5c8048749d62c2c1bfe6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/letter-case-lowercase.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-toggle.svg b/assets/icons/radix/letter-case-toggle.svg deleted file mode 100644 index a021a2b9225d8eda5657a713b94f7145757206a3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/letter-case-toggle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-uppercase.svg b/assets/icons/radix/letter-case-uppercase.svg deleted file mode 100644 index ccd2be04e7757db3050e7675f093288b6d9a5748..0000000000000000000000000000000000000000 --- a/assets/icons/radix/letter-case-uppercase.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-spacing.svg b/assets/icons/radix/letter-spacing.svg deleted file mode 100644 index 073023e0f4df60364dede352b60fdc151e6f05d2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/letter-spacing.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lightning-bolt.svg b/assets/icons/radix/lightning-bolt.svg deleted file mode 100644 index 7c35df9cfea2b54cfffa84161902126234ba3234..0000000000000000000000000000000000000000 --- a/assets/icons/radix/lightning-bolt.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/line-height.svg b/assets/icons/radix/line-height.svg deleted file mode 100644 index 1c302d1ffc1f1b7e1abb1f4a7553b69be224aac2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/line-height.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-1.svg b/assets/icons/radix/link-1.svg deleted file mode 100644 index d5682b113ee37a34a42a65897f501af0ee04ffe3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/link-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-2.svg b/assets/icons/radix/link-2.svg deleted file mode 100644 index be8370606e7fe33fd9eda9e440433236cc3f6d68..0000000000000000000000000000000000000000 --- a/assets/icons/radix/link-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-break-1.svg b/assets/icons/radix/link-break-1.svg deleted file mode 100644 index 05ae93e47a4f16ce18cbe2ca3a709b3abc62d15b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/link-break-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-break-2.svg b/assets/icons/radix/link-break-2.svg deleted file mode 100644 index 78f28f98e815d7fdd822d4a8710d686ad314ccdd..0000000000000000000000000000000000000000 --- a/assets/icons/radix/link-break-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-none-1.svg b/assets/icons/radix/link-none-1.svg deleted file mode 100644 index 6ea56a386fa133bf983a3a7f06b70bd12189e05d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/link-none-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-none-2.svg b/assets/icons/radix/link-none-2.svg deleted file mode 100644 index 0b19d940d109bca37ade399a36b8b10c2812faf8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/link-none-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/linkedin-logo.svg b/assets/icons/radix/linkedin-logo.svg deleted file mode 100644 index 0f0138bdf6cade2297362c820831a995f7a4e02f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/linkedin-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/list-bullet.svg b/assets/icons/radix/list-bullet.svg deleted file mode 100644 index 2630b95ef029e231be2a854efa2cf4c50dbeeb95..0000000000000000000000000000000000000000 --- a/assets/icons/radix/list-bullet.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lock-closed.svg b/assets/icons/radix/lock-closed.svg deleted file mode 100644 index 3871b5d5ada8020c7d7f56510158bd89c4ab5ff2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/lock-closed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lock-open-1.svg b/assets/icons/radix/lock-open-1.svg deleted file mode 100644 index 8f6bfd5bbf82007be6d65ada0beb4914b450faf2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/lock-open-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lock-open-2.svg b/assets/icons/radix/lock-open-2.svg deleted file mode 100644 index ce69f67f2920b6890eb4446dd6b260484e68178d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/lock-open-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/loop.svg b/assets/icons/radix/loop.svg deleted file mode 100644 index bfa90ed0841f6ca8d26c1eef72e00d893d5efe0c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/loop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/magnifying-glass.svg b/assets/icons/radix/magnifying-glass.svg deleted file mode 100644 index a3a89bfa5059192bdb481a043cdde6d7e42c2f24..0000000000000000000000000000000000000000 --- a/assets/icons/radix/magnifying-glass.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/margin.svg b/assets/icons/radix/margin.svg deleted file mode 100644 index 1a513b37d6846849b260a409b9993d3c708bfe30..0000000000000000000000000000000000000000 --- a/assets/icons/radix/margin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mask-off.svg b/assets/icons/radix/mask-off.svg deleted file mode 100644 index 5f847668e8986d4ba9be5cba4b6ddab65e61f0d2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/mask-off.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mask-on.svg b/assets/icons/radix/mask-on.svg deleted file mode 100644 index 684c1b934dce4e99b1485593bb8995576eae186b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/mask-on.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/maximize.svg b/assets/icons/radix/maximize.svg deleted file mode 100644 index f37f6a2087f968728170539b379206cca7551b0e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/maximize.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/radix/minimize.svg b/assets/icons/radix/minimize.svg deleted file mode 100644 index ec78f152e13eda0c887a18b99b585d0c65acc8a8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/minimize.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/radix/minus-circled.svg b/assets/icons/radix/minus-circled.svg deleted file mode 100644 index 2c6df4cebf1ea279fdc43598fff062ea5db72cb7..0000000000000000000000000000000000000000 --- a/assets/icons/radix/minus-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/minus.svg b/assets/icons/radix/minus.svg deleted file mode 100644 index 2b396029795aa7b9bcfb2f9dbb703cb491bf88f2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/minus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mix.svg b/assets/icons/radix/mix.svg deleted file mode 100644 index 9412a018438b79130fbba167176860d2cef38106..0000000000000000000000000000000000000000 --- a/assets/icons/radix/mix.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mixer-horizontal.svg b/assets/icons/radix/mixer-horizontal.svg deleted file mode 100644 index f29ba25548a32eae3979249cd915f074444a0f51..0000000000000000000000000000000000000000 --- a/assets/icons/radix/mixer-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mixer-vertical.svg b/assets/icons/radix/mixer-vertical.svg deleted file mode 100644 index dc85d3a9e7a3c3a5ba9d016bb88368b2b35cdcaa..0000000000000000000000000000000000000000 --- a/assets/icons/radix/mixer-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mobile.svg b/assets/icons/radix/mobile.svg deleted file mode 100644 index b62b6506ff4f7838e025ea98f93caac228fdd88e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/mobile.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/modulz-logo.svg b/assets/icons/radix/modulz-logo.svg deleted file mode 100644 index 754b229db6b03264c0258553b18d5eea2473a316..0000000000000000000000000000000000000000 --- a/assets/icons/radix/modulz-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/moon.svg b/assets/icons/radix/moon.svg deleted file mode 100644 index 1dac2ca2120eb3deebf39e9fdf8a353d14e0fb1e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/moon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/move.svg b/assets/icons/radix/move.svg deleted file mode 100644 index 3d0a0e56c9063858f9d71c0cad7c43cdf448c84d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/move.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/notion-logo.svg b/assets/icons/radix/notion-logo.svg deleted file mode 100644 index c2df1526195d99956c0edb1e8c01a5ac641cbaca..0000000000000000000000000000000000000000 --- a/assets/icons/radix/notion-logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/opacity.svg b/assets/icons/radix/opacity.svg deleted file mode 100644 index a2d01bff82923948a67ac243df677fc3d7331706..0000000000000000000000000000000000000000 --- a/assets/icons/radix/opacity.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/open-in-new-window.svg b/assets/icons/radix/open-in-new-window.svg deleted file mode 100644 index 22baf82cff73662895c6aae20d426319b9ea32a4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/open-in-new-window.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/assets/icons/radix/outer-shadow.svg b/assets/icons/radix/outer-shadow.svg deleted file mode 100644 index b44e3d553c040d855204ac3d543cfa6539db7612..0000000000000000000000000000000000000000 --- a/assets/icons/radix/outer-shadow.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - diff --git a/assets/icons/radix/overline.svg b/assets/icons/radix/overline.svg deleted file mode 100644 index 57262c76e6df8a60a11aa2dddde8a437824ef8e3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/overline.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/padding.svg b/assets/icons/radix/padding.svg deleted file mode 100644 index 483a25a27ea1e7c94b21c91b15d929c5cd95ed81..0000000000000000000000000000000000000000 --- a/assets/icons/radix/padding.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/paper-plane.svg b/assets/icons/radix/paper-plane.svg deleted file mode 100644 index 37ad0703004b817ff2dd52dae5680dabdd5574db..0000000000000000000000000000000000000000 --- a/assets/icons/radix/paper-plane.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pause.svg b/assets/icons/radix/pause.svg deleted file mode 100644 index b399fb2f5a7ba00e088e9fc2ac10042452879e46..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pause.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pencil-1.svg b/assets/icons/radix/pencil-1.svg deleted file mode 100644 index decf0122ef482aab10c213cad07a008e492b2e86..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pencil-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pencil-2.svg b/assets/icons/radix/pencil-2.svg deleted file mode 100644 index 2559a393a9fc2368697619724887a7c7eb8b5a1e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pencil-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/person.svg b/assets/icons/radix/person.svg deleted file mode 100644 index 051abcc7033796d6ad5e65d2d0d5955b6bb51759..0000000000000000000000000000000000000000 --- a/assets/icons/radix/person.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pie-chart.svg b/assets/icons/radix/pie-chart.svg deleted file mode 100644 index bb58e4727465e6c2cebb84e6c7a38b884b9ef13c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pie-chart.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pilcrow.svg b/assets/icons/radix/pilcrow.svg deleted file mode 100644 index 6996765fd60b2e1c09182156b2ba8e19b3cca5f5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pilcrow.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-bottom.svg b/assets/icons/radix/pin-bottom.svg deleted file mode 100644 index ad0842054f082e24c4ab145471c302d00cb9fea6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pin-bottom.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-left.svg b/assets/icons/radix/pin-left.svg deleted file mode 100644 index eb89b2912f0735b57f655fe08a33d6efdb5340de..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pin-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-right.svg b/assets/icons/radix/pin-right.svg deleted file mode 100644 index 89a98bae4ea00e8562392aa2dda764d1d6203f40..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pin-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-top.svg b/assets/icons/radix/pin-top.svg deleted file mode 100644 index edfeb64d5d87b0df6c25509d2077054613c4f543..0000000000000000000000000000000000000000 --- a/assets/icons/radix/pin-top.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/play.svg b/assets/icons/radix/play.svg deleted file mode 100644 index 92af9e1ae7f125fd9f36e1b67f43b9c71aa54296..0000000000000000000000000000000000000000 --- a/assets/icons/radix/play.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/plus-circled.svg b/assets/icons/radix/plus-circled.svg deleted file mode 100644 index 808ddc4c2ce157903747ff88672425d9c39d5f71..0000000000000000000000000000000000000000 --- a/assets/icons/radix/plus-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/plus.svg b/assets/icons/radix/plus.svg deleted file mode 100644 index 57ce90219bc6f72d92e55011f6dcb9f20ba320eb..0000000000000000000000000000000000000000 --- a/assets/icons/radix/plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/question-mark-circled.svg b/assets/icons/radix/question-mark-circled.svg deleted file mode 100644 index be99968787df16246e5fb2bbeee617b27393496f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/question-mark-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/question-mark.svg b/assets/icons/radix/question-mark.svg deleted file mode 100644 index 577aae53496676a657164f0406c50e41566dae3a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/question-mark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/radiobutton.svg b/assets/icons/radix/radiobutton.svg deleted file mode 100644 index f0c3a60aee6f499a3dffd30d5d731612de3d90db..0000000000000000000000000000000000000000 --- a/assets/icons/radix/radiobutton.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/reader.svg b/assets/icons/radix/reader.svg deleted file mode 100644 index e893cfa68510377d91301e796366babcc2cbb7aa..0000000000000000000000000000000000000000 --- a/assets/icons/radix/reader.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/reload.svg b/assets/icons/radix/reload.svg deleted file mode 100644 index cf1dfb7fa20bd8233e8ea75c51061b11f73302f5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/reload.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/reset.svg b/assets/icons/radix/reset.svg deleted file mode 100644 index f21a508514cac8c8da0626237726148ee8833953..0000000000000000000000000000000000000000 --- a/assets/icons/radix/reset.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/resume.svg b/assets/icons/radix/resume.svg deleted file mode 100644 index 79cdec2374c2e06a3f0afced560a27a9042cc63b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/resume.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/rocket.svg b/assets/icons/radix/rocket.svg deleted file mode 100644 index 2226aacb1a7e497f377fbbd607f125782b150f7e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/rocket.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/rotate-counter-clockwise.svg b/assets/icons/radix/rotate-counter-clockwise.svg deleted file mode 100644 index c43c90b90ba001df326c83df80b7d25152782cc3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/rotate-counter-clockwise.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/row-spacing.svg b/assets/icons/radix/row-spacing.svg deleted file mode 100644 index e155bd59479ceaf31dcde7155b0503aa6f305a34..0000000000000000000000000000000000000000 --- a/assets/icons/radix/row-spacing.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/rows.svg b/assets/icons/radix/rows.svg deleted file mode 100644 index fb4ca0f9e3acb960fdeba9d86c973736eda25573..0000000000000000000000000000000000000000 --- a/assets/icons/radix/rows.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/ruler-horizontal.svg b/assets/icons/radix/ruler-horizontal.svg deleted file mode 100644 index db6f1ef488b20f66fe89538461b72ba0b7827b54..0000000000000000000000000000000000000000 --- a/assets/icons/radix/ruler-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/ruler-square.svg b/assets/icons/radix/ruler-square.svg deleted file mode 100644 index 7de70cc5dc1e852283f89a5048cc730e146ddd4a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/ruler-square.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/scissors.svg b/assets/icons/radix/scissors.svg deleted file mode 100644 index 2893b347123f0a29be96b52ee0886ba716f365a0..0000000000000000000000000000000000000000 --- a/assets/icons/radix/scissors.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/section.svg b/assets/icons/radix/section.svg deleted file mode 100644 index 1e939e2b2f31f4eef53496154dc4e7c086b28162..0000000000000000000000000000000000000000 --- a/assets/icons/radix/section.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sewing-pin-filled.svg b/assets/icons/radix/sewing-pin-filled.svg deleted file mode 100644 index 97f6f1120d988746a9ad95d33e8d24b237bec58b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/sewing-pin-filled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sewing-pin-solid.svg b/assets/icons/radix/sewing-pin-solid.svg deleted file mode 100644 index 97f6f1120d988746a9ad95d33e8d24b237bec58b..0000000000000000000000000000000000000000 --- a/assets/icons/radix/sewing-pin-solid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sewing-pin.svg b/assets/icons/radix/sewing-pin.svg deleted file mode 100644 index 068dfd7bdfca25e8ac4834f7011e96b377a3ca49..0000000000000000000000000000000000000000 --- a/assets/icons/radix/sewing-pin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/shadow-inner.svg b/assets/icons/radix/shadow-inner.svg deleted file mode 100644 index 4d073bf35f87e99198fc44258c8af746ff95e0b6..0000000000000000000000000000000000000000 --- a/assets/icons/radix/shadow-inner.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/shadow-none.svg b/assets/icons/radix/shadow-none.svg deleted file mode 100644 index b02d3466adeb08e3ddbf4ecc3b6c554f1dd5872d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/shadow-none.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/shadow-outer.svg b/assets/icons/radix/shadow-outer.svg deleted file mode 100644 index dc7ea840878699d22280f6edf481b7c8ea51fa64..0000000000000000000000000000000000000000 --- a/assets/icons/radix/shadow-outer.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - diff --git a/assets/icons/radix/shadow.svg b/assets/icons/radix/shadow.svg deleted file mode 100644 index c991af6156cb38d143c574bcfb925364768c4f3f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/shadow.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/share-1.svg b/assets/icons/radix/share-1.svg deleted file mode 100644 index 58328e4d1ee1091b8f909ecdfb22b836cb167a93..0000000000000000000000000000000000000000 --- a/assets/icons/radix/share-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/share-2.svg b/assets/icons/radix/share-2.svg deleted file mode 100644 index 1302ea5fbe198800c08b2abc0cb79a2f4136d3b0..0000000000000000000000000000000000000000 --- a/assets/icons/radix/share-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/shuffle.svg b/assets/icons/radix/shuffle.svg deleted file mode 100644 index 8670e1a04898e130c357c933f7edac966e2cfac9..0000000000000000000000000000000000000000 --- a/assets/icons/radix/shuffle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/size.svg b/assets/icons/radix/size.svg deleted file mode 100644 index dece8c51820fb451e57bf6efd313a00ce6050e22..0000000000000000000000000000000000000000 --- a/assets/icons/radix/size.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sketch-logo.svg b/assets/icons/radix/sketch-logo.svg deleted file mode 100644 index 6c54c4c8252e96ec9d762ffbbab596a72c163303..0000000000000000000000000000000000000000 --- a/assets/icons/radix/sketch-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/slash.svg b/assets/icons/radix/slash.svg deleted file mode 100644 index aa7dac30c1af6717056c15f4abafe2b3a1bb09ef..0000000000000000000000000000000000000000 --- a/assets/icons/radix/slash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/slider.svg b/assets/icons/radix/slider.svg deleted file mode 100644 index 66e0452bc0a0469ff6f7ff789f2db55a4fca4e17..0000000000000000000000000000000000000000 --- a/assets/icons/radix/slider.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-between-horizontally.svg b/assets/icons/radix/space-between-horizontally.svg deleted file mode 100644 index a71638d52b0c90597a696e4671ce17f1c342681f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/space-between-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-between-vertically.svg b/assets/icons/radix/space-between-vertically.svg deleted file mode 100644 index bae247222fac0ed744593dcc97befe6051483101..0000000000000000000000000000000000000000 --- a/assets/icons/radix/space-between-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-evenly-horizontally.svg b/assets/icons/radix/space-evenly-horizontally.svg deleted file mode 100644 index 70169492e4072dc561370d6185db255a229dd8e2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/space-evenly-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-evenly-vertically.svg b/assets/icons/radix/space-evenly-vertically.svg deleted file mode 100644 index 469b4c05d4eda8045d2534b0a5e8847d0b423851..0000000000000000000000000000000000000000 --- a/assets/icons/radix/space-evenly-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/speaker-moderate.svg b/assets/icons/radix/speaker-moderate.svg deleted file mode 100644 index 0f1d1b4210991ec8d8718bef86c9959bec264c58..0000000000000000000000000000000000000000 --- a/assets/icons/radix/speaker-moderate.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/speaker-quiet.svg b/assets/icons/radix/speaker-quiet.svg deleted file mode 100644 index eb68cefcee916e168d25a58be9c4015fe131ecf4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/speaker-quiet.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/square.svg b/assets/icons/radix/square.svg deleted file mode 100644 index 82843f51c3b7c98cade0ed914ca18095e3d385fe..0000000000000000000000000000000000000000 --- a/assets/icons/radix/square.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stack.svg b/assets/icons/radix/stack.svg deleted file mode 100644 index 92426ffb0d3aac123f647a9c3bcf07932de91407..0000000000000000000000000000000000000000 --- a/assets/icons/radix/stack.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/star-filled.svg b/assets/icons/radix/star-filled.svg deleted file mode 100644 index 2b17b7f5792c663e533d3fbe8def8ed44f12b7ff..0000000000000000000000000000000000000000 --- a/assets/icons/radix/star-filled.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/star.svg b/assets/icons/radix/star.svg deleted file mode 100644 index 23f09ad7b271cb11e9660901a5d9d819a40ec9a5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/star.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stitches-logo.svg b/assets/icons/radix/stitches-logo.svg deleted file mode 100644 index 319a1481f3e89c5c24535ecc03fffa89c83de737..0000000000000000000000000000000000000000 --- a/assets/icons/radix/stitches-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stop.svg b/assets/icons/radix/stop.svg deleted file mode 100644 index 57aac59cab28050f94d5cb93877e8d967f4661c5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/stop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stopwatch.svg b/assets/icons/radix/stopwatch.svg deleted file mode 100644 index ce5661e5cc9b983676fc97ae0d9c08e78878ee74..0000000000000000000000000000000000000000 --- a/assets/icons/radix/stopwatch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stretch-horizontally.svg b/assets/icons/radix/stretch-horizontally.svg deleted file mode 100644 index 37977363b3046bc59bfd6eb74673a5a49d43d2f8..0000000000000000000000000000000000000000 --- a/assets/icons/radix/stretch-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stretch-vertically.svg b/assets/icons/radix/stretch-vertically.svg deleted file mode 100644 index c4b1fe79ce21f963ad70a17278be8bef7804e43c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/stretch-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/strikethrough.svg b/assets/icons/radix/strikethrough.svg deleted file mode 100644 index b814ef420acc8a4a385eaf29d52a5a167171860f..0000000000000000000000000000000000000000 --- a/assets/icons/radix/strikethrough.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sun.svg b/assets/icons/radix/sun.svg deleted file mode 100644 index 1807a51b4c60c764a6af190dbd957b6c2ebd0d91..0000000000000000000000000000000000000000 --- a/assets/icons/radix/sun.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/switch.svg b/assets/icons/radix/switch.svg deleted file mode 100644 index 6dea528ce9bd25a06962d5ecc64f1ca4b1c9d754..0000000000000000000000000000000000000000 --- a/assets/icons/radix/switch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/symbol.svg b/assets/icons/radix/symbol.svg deleted file mode 100644 index b529b2b08b42a17027566a47d20f8ae93d61ae35..0000000000000000000000000000000000000000 --- a/assets/icons/radix/symbol.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/table.svg b/assets/icons/radix/table.svg deleted file mode 100644 index 8ff059b847b30b73fc31577d88a9a5bc639e6371..0000000000000000000000000000000000000000 --- a/assets/icons/radix/table.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/target.svg b/assets/icons/radix/target.svg deleted file mode 100644 index d67989e01fb7b70c728fdcf85360ac41ac8f2ff5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/target.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-bottom.svg b/assets/icons/radix/text-align-bottom.svg deleted file mode 100644 index 862a5aeb883e236e076caee3bec650d79b9b2cd4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-align-bottom.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-center.svg b/assets/icons/radix/text-align-center.svg deleted file mode 100644 index 673cf8cd0aa97a1ffd39409152efd6fe5cc1ef12..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-align-center.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-justify.svg b/assets/icons/radix/text-align-justify.svg deleted file mode 100644 index df877f95134803f7d07627ec1b22e6d076c6b595..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-align-justify.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-left.svg b/assets/icons/radix/text-align-left.svg deleted file mode 100644 index b7a64fbd439720429ebe73c82340619e3d950391..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-align-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-middle.svg b/assets/icons/radix/text-align-middle.svg deleted file mode 100644 index e739d04efabdf1edada6c848c14c0e3ad3f62832..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-align-middle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-right.svg b/assets/icons/radix/text-align-right.svg deleted file mode 100644 index e7609908ff9436a9e9c4b366ad54b891c9868b64..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-align-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-top.svg b/assets/icons/radix/text-align-top.svg deleted file mode 100644 index 21660fe7d307f5e78cf997778d0bc68f9a83f705..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-align-top.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-none.svg b/assets/icons/radix/text-none.svg deleted file mode 100644 index 2a87f9372a66fd9e3b56807d0adde8fbb29a568c..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text.svg b/assets/icons/radix/text.svg deleted file mode 100644 index bd41d8ac191905eb40201c7779c247d86783bf67..0000000000000000000000000000000000000000 --- a/assets/icons/radix/text.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-down.svg b/assets/icons/radix/thick-arrow-down.svg deleted file mode 100644 index 32923bec58192f66bcce7f067208103d768f5a74..0000000000000000000000000000000000000000 --- a/assets/icons/radix/thick-arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-left.svg b/assets/icons/radix/thick-arrow-left.svg deleted file mode 100644 index 0cfd863903b3ae25d89ca93561d81ec245686913..0000000000000000000000000000000000000000 --- a/assets/icons/radix/thick-arrow-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-right.svg b/assets/icons/radix/thick-arrow-right.svg deleted file mode 100644 index a0cb605693638380d37ad3b6ff09c07d5b7cf3c4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/thick-arrow-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-up.svg b/assets/icons/radix/thick-arrow-up.svg deleted file mode 100644 index 68687be28da3d3500c2ca98113578f65b9465b44..0000000000000000000000000000000000000000 --- a/assets/icons/radix/thick-arrow-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/timer.svg b/assets/icons/radix/timer.svg deleted file mode 100644 index 20c52dff95ae423ef3decf9f88b6e13d7c42cbcc..0000000000000000000000000000000000000000 --- a/assets/icons/radix/timer.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/tokens.svg b/assets/icons/radix/tokens.svg deleted file mode 100644 index 2bbbc82030a9ebe9b9871ec1cd18a572e688ef25..0000000000000000000000000000000000000000 --- a/assets/icons/radix/tokens.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/track-next.svg b/assets/icons/radix/track-next.svg deleted file mode 100644 index 24fd40e36f3d1110f34a4ffb2cc5397f9aa6766a..0000000000000000000000000000000000000000 --- a/assets/icons/radix/track-next.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/track-previous.svg b/assets/icons/radix/track-previous.svg deleted file mode 100644 index d99e7ab53f45d3e749b7d37d76829d8c083979cc..0000000000000000000000000000000000000000 --- a/assets/icons/radix/track-previous.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/transform.svg b/assets/icons/radix/transform.svg deleted file mode 100644 index e913ccc9a7a4297c47e82f978e5a4bda03d1f319..0000000000000000000000000000000000000000 --- a/assets/icons/radix/transform.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/transparency-grid.svg b/assets/icons/radix/transparency-grid.svg deleted file mode 100644 index 6559ef8c2b9e5ba003c6e3712f502a22416d6f04..0000000000000000000000000000000000000000 --- a/assets/icons/radix/transparency-grid.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/assets/icons/radix/trash.svg b/assets/icons/radix/trash.svg deleted file mode 100644 index 18780e492c9a91b117148e72fd4fc0739f671d1e..0000000000000000000000000000000000000000 --- a/assets/icons/radix/trash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-down.svg b/assets/icons/radix/triangle-down.svg deleted file mode 100644 index ebfd8f2a1236e39910eafb25a13e6466caa016db..0000000000000000000000000000000000000000 --- a/assets/icons/radix/triangle-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-left.svg b/assets/icons/radix/triangle-left.svg deleted file mode 100644 index 0014139716308461f550febfc71a83ec3f6506b3..0000000000000000000000000000000000000000 --- a/assets/icons/radix/triangle-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-right.svg b/assets/icons/radix/triangle-right.svg deleted file mode 100644 index aed1393b9c99cf654f3744bc92853c7b222725d4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/triangle-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-up.svg b/assets/icons/radix/triangle-up.svg deleted file mode 100644 index 5eb1b416d389bfcc405056f1e5da510cbe4aa272..0000000000000000000000000000000000000000 --- a/assets/icons/radix/triangle-up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/twitter-logo.svg b/assets/icons/radix/twitter-logo.svg deleted file mode 100644 index 7dcf2f58eb1d15dbe19a53626496a1ef7d87f975..0000000000000000000000000000000000000000 --- a/assets/icons/radix/twitter-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/underline.svg b/assets/icons/radix/underline.svg deleted file mode 100644 index 334468509777c7ab550ea690cdc76f8627478e74..0000000000000000000000000000000000000000 --- a/assets/icons/radix/underline.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/update.svg b/assets/icons/radix/update.svg deleted file mode 100644 index b529b2b08b42a17027566a47d20f8ae93d61ae35..0000000000000000000000000000000000000000 --- a/assets/icons/radix/update.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/upload.svg b/assets/icons/radix/upload.svg deleted file mode 100644 index a7f6bddb2e818210222895de24e072736eef14a2..0000000000000000000000000000000000000000 --- a/assets/icons/radix/upload.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/value-none.svg b/assets/icons/radix/value-none.svg deleted file mode 100644 index a86c08be1a10c961aeb5a61412b891ad3bc9929d..0000000000000000000000000000000000000000 --- a/assets/icons/radix/value-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/value.svg b/assets/icons/radix/value.svg deleted file mode 100644 index 59dd7d9373ccdd355d3c6dc581bdfb18e6624072..0000000000000000000000000000000000000000 --- a/assets/icons/radix/value.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/vercel-logo.svg b/assets/icons/radix/vercel-logo.svg deleted file mode 100644 index 5466fd9f0ebd8ffa94382d899bb250d2cb405872..0000000000000000000000000000000000000000 --- a/assets/icons/radix/vercel-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/video.svg b/assets/icons/radix/video.svg deleted file mode 100644 index e405396bef1c9898d024df78304034d0ad7d8212..0000000000000000000000000000000000000000 --- a/assets/icons/radix/video.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-grid.svg b/assets/icons/radix/view-grid.svg deleted file mode 100644 index 04825a870bb77b3179e51e2b7fedd7a7197ba9e5..0000000000000000000000000000000000000000 --- a/assets/icons/radix/view-grid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-horizontal.svg b/assets/icons/radix/view-horizontal.svg deleted file mode 100644 index 2ca7336b99efb11f67addcc31aca81f43f7078ae..0000000000000000000000000000000000000000 --- a/assets/icons/radix/view-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-none.svg b/assets/icons/radix/view-none.svg deleted file mode 100644 index 71b08a46d2917d9057d7331131ef6849f9335867..0000000000000000000000000000000000000000 --- a/assets/icons/radix/view-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-vertical.svg b/assets/icons/radix/view-vertical.svg deleted file mode 100644 index 0c8f8164b4016a6724945cff0fb76700c2bea724..0000000000000000000000000000000000000000 --- a/assets/icons/radix/view-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/width.svg b/assets/icons/radix/width.svg deleted file mode 100644 index 3ae2b56e3dbd78152ed91966b6b3a2474fc7c1e4..0000000000000000000000000000000000000000 --- a/assets/icons/radix/width.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/zoom-in.svg b/assets/icons/radix/zoom-in.svg deleted file mode 100644 index caac722ad07771ec72005752a124f1b86f080a70..0000000000000000000000000000000000000000 --- a/assets/icons/radix/zoom-in.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/zoom-out.svg b/assets/icons/radix/zoom-out.svg deleted file mode 100644 index 62046a9e0f1f51239c1587aef16317d325ebef07..0000000000000000000000000000000000000000 --- a/assets/icons/radix/zoom-out.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/robot_14.svg b/assets/icons/robot_14.svg deleted file mode 100644 index 7b6dc3f752a23d6a9ff5804cac8ec7d938218663..0000000000000000000000000000000000000000 --- a/assets/icons/robot_14.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg deleted file mode 100644 index 49e097b02325ce3644be662896cd7a3a666b6f8f..0000000000000000000000000000000000000000 --- a/assets/icons/screen.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/select-all.svg b/assets/icons/select-all.svg new file mode 100644 index 0000000000000000000000000000000000000000..45a10bba42648ee0f6f9011f4386630609515e0c --- /dev/null +++ b/assets/icons/select-all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/radix/speaker-loud.svg b/assets/icons/speaker-loud.svg similarity index 100% rename from assets/icons/radix/speaker-loud.svg rename to assets/icons/speaker-loud.svg diff --git a/assets/icons/radix/speaker-off.svg b/assets/icons/speaker-off.svg similarity index 100% rename from assets/icons/radix/speaker-off.svg rename to assets/icons/speaker-off.svg diff --git a/assets/icons/speech_bubble_12.svg b/assets/icons/speech_bubble_12.svg deleted file mode 100644 index 736f39a9840022eb882f8473710e73e8228e50ea..0000000000000000000000000000000000000000 --- a/assets/icons/speech_bubble_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/split_12.svg b/assets/icons/split_12.svg deleted file mode 100644 index e4cf1921fa4219195fb10957c58adc9b69c925a4..0000000000000000000000000000000000000000 --- a/assets/icons/split_12.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/split_message_15.svg b/assets/icons/split_message.svg similarity index 100% rename from assets/icons/split_message_15.svg rename to assets/icons/split_message.svg diff --git a/assets/icons/stop_sharing.svg b/assets/icons/stop_sharing.svg index e9aa7eac5a481ed3f6ae9253fc8c9c8c3a0785e6..b0f06f68eb71c8dacd48b806b0e7fb72243ddabf 100644 --- a/assets/icons/stop_sharing.svg +++ b/assets/icons/stop_sharing.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/success.svg b/assets/icons/success.svg deleted file mode 100644 index 85450cdc433b80f157be94beae5f60c184906f0f..0000000000000000000000000000000000000000 --- a/assets/icons/success.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/terminal_12.svg b/assets/icons/terminal_12.svg deleted file mode 100644 index 9d5a9447b503de8ca358e4230386f97659b15533..0000000000000000000000000000000000000000 --- a/assets/icons/terminal_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/terminal_16.svg b/assets/icons/terminal_16.svg deleted file mode 100644 index 95da7ff4e1e433625938b152417ee0ddc550f330..0000000000000000000000000000000000000000 --- a/assets/icons/terminal_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/terminal_8.svg b/assets/icons/terminal_8.svg deleted file mode 100644 index b09495dcf92440f44fbe3ec1ae267a4221d4cabd..0000000000000000000000000000000000000000 --- a/assets/icons/terminal_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/triangle_exclamation_12.svg b/assets/icons/triangle_exclamation_12.svg deleted file mode 100644 index f87d365bdf6f2693db995d0ae98a07400576f6a0..0000000000000000000000000000000000000000 --- a/assets/icons/triangle_exclamation_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/triangle_exclamation_16.svg b/assets/icons/triangle_exclamation_16.svg deleted file mode 100644 index 2df386203af136590d5e638ddbd11931ac9148e5..0000000000000000000000000000000000000000 --- a/assets/icons/triangle_exclamation_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/triangle_exclamation_8.svg b/assets/icons/triangle_exclamation_8.svg deleted file mode 100644 index 96f11015b1bc280e8df16bfddaed33ae210af495..0000000000000000000000000000000000000000 --- a/assets/icons/triangle_exclamation_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/unlock_8.svg b/assets/icons/unlock_8.svg deleted file mode 100644 index 7a40f94345c4c9f0736e3adee139096df49ed1be..0000000000000000000000000000000000000000 --- a/assets/icons/unlock_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_circle_12.svg b/assets/icons/user_circle_12.svg deleted file mode 100644 index 8631c36fd60114087d6decf464b88042ed433125..0000000000000000000000000000000000000000 --- a/assets/icons/user_circle_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_circle_8.svg b/assets/icons/user_circle_8.svg deleted file mode 100644 index 304001d546c84669f28b82b4bfc7d665f2287301..0000000000000000000000000000000000000000 --- a/assets/icons/user_circle_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_group_12.svg b/assets/icons/user_group_12.svg deleted file mode 100644 index 5eae1d55b7e1406d0956c67cf6b9dba9949faefc..0000000000000000000000000000000000000000 --- a/assets/icons/user_group_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/user_group_8.svg b/assets/icons/user_group_8.svg deleted file mode 100644 index 69d08b0d3b8f0b5298057fb6a06ff2b41afff690..0000000000000000000000000000000000000000 --- a/assets/icons/user_group_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_plus_12.svg b/assets/icons/user_plus_12.svg deleted file mode 100644 index 535d04af45f186a25dbbb76d8a5605e81d111390..0000000000000000000000000000000000000000 --- a/assets/icons/user_plus_12.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/user_plus_16.svg b/assets/icons/user_plus_16.svg deleted file mode 100644 index 150392f6e066d89355e55c4bcc5d408cd5b1f970..0000000000000000000000000000000000000000 --- a/assets/icons/user_plus_16.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/user_plus_8.svg b/assets/icons/user_plus_8.svg deleted file mode 100644 index 100b43af86ab36831cbb02e784cd5d8b8bb0db18..0000000000000000000000000000000000000000 --- a/assets/icons/user_plus_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/version_control_branch_12.svg b/assets/icons/version_control_branch_12.svg deleted file mode 100644 index 3571874a898e6f1bc9dbfb162c81f8708610d5d9..0000000000000000000000000000000000000000 --- a/assets/icons/version_control_branch_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/word_search_14.svg b/assets/icons/word_search.svg similarity index 100% rename from assets/icons/word_search_14.svg rename to assets/icons/word_search.svg diff --git a/assets/icons/word_search_12.svg b/assets/icons/word_search_12.svg deleted file mode 100644 index 4cf6401fd2fc5cd9592ef6a380dbc8c3e43859aa..0000000000000000000000000000000000000000 --- a/assets/icons/word_search_12.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/x_mark_12.svg b/assets/icons/x_mark_12.svg deleted file mode 100644 index 1c95f979d09ac1d52baef11c58baf87f05ecd4aa..0000000000000000000000000000000000000000 --- a/assets/icons/x_mark_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/x_mark_16.svg b/assets/icons/x_mark_16.svg deleted file mode 100644 index 21a7f1c2107750a10302f9247e487d584521bd28..0000000000000000000000000000000000000000 --- a/assets/icons/x_mark_16.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/x_mark_8.svg b/assets/icons/x_mark_8.svg deleted file mode 100644 index f724b1515e8f269b28e8f2d4aa9970753d65601e..0000000000000000000000000000000000000000 --- a/assets/icons/x_mark_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/zed_plus_copilot_32.svg b/assets/icons/zed_x_copilot.svg similarity index 100% rename from assets/icons/zed_plus_copilot_32.svg rename to assets/icons/zed_x_copilot.svg diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 2fb1c6f5fcaa8baf4c3a128644b372408260c501..8422d53abc9143fb88ee92d2ad87ec944126e4cd 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -30,6 +30,7 @@ "cmd-s": "workspace::Save", "cmd-shift-s": "workspace::SaveAs", "cmd-=": "zed::IncreaseBufferFontSize", + "cmd-+": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", "cmd-0": "zed::ResetBufferFontSize", "cmd-,": "zed::OpenSettings", @@ -231,7 +232,14 @@ } }, { - "context": "BufferSearchBar > Editor", + "context": "BufferSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "cmd-enter": "search::ReplaceAll" + } + }, + { + "context": "BufferSearchBar && !in_replace > Editor", "bindings": { "up": "search::PreviousHistoryQuery", "down": "search::NextHistoryQuery" @@ -241,7 +249,11 @@ "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "search::CycleMode" + "alt-tab": "search::CycleMode", + "cmd-shift-h": "search::ToggleReplace", + "alt-cmd-g": "search::ActivateRegexMode", + "alt-cmd-s": "search::ActivateSemanticMode", + "alt-cmd-x": "search::ActivateTextMode" } }, { @@ -251,11 +263,22 @@ "down": "search::NextHistoryQuery" } }, + { + "context": "ProjectSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "cmd-enter": "search::ReplaceAll" + } + }, { "context": "ProjectSearchView", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "search::CycleMode" + "alt-tab": "search::CycleMode", + "cmd-shift-h": "search::ToggleReplace", + "alt-cmd-g": "search::ActivateRegexMode", + "alt-cmd-s": "search::ActivateSemanticMode", + "alt-cmd-x": "search::ActivateTextMode" } }, { @@ -264,11 +287,15 @@ "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", + "cmd-shift-h": "search::ToggleReplace", "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", "alt-tab": "search::CycleMode", - "alt-cmd-f": "project_search::ToggleFilters" + "alt-cmd-f": "project_search::ToggleFilters", + "alt-cmd-g": "search::ActivateRegexMode", + "alt-cmd-s": "search::ActivateSemanticMode", + "alt-cmd-x": "search::ActivateTextMode" } }, // Bindings from VS Code @@ -287,6 +314,7 @@ "replace_newest": false } ], + "cmd-shift-l": "editor::SelectAllMatches", "ctrl-cmd-d": [ "editor::SelectPrevious", { @@ -447,7 +475,6 @@ "bindings": { "ctrl-shift-k": "editor::DeleteLine", "cmd-shift-d": "editor::DuplicateLine", - "cmd-shift-l": "editor::SplitSelectionIntoLines", "ctrl-j": "editor::JoinLines", "ctrl-cmd-up": "editor::MoveLineUp", "ctrl-cmd-down": "editor::MoveLineDown", @@ -482,6 +509,22 @@ "cmd-k cmd-down": [ "workspace::ActivatePaneInDirection", "Down" + ], + "cmd-k shift-left": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "cmd-k shift-right": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "cmd-k shift-up": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "cmd-k shift-down": [ + "workspace::SwapPaneInDirection", + "Down" ] } }, @@ -546,7 +589,7 @@ } }, { - "context": "ProjectSearchBar", + "context": "ProjectSearchBar && !in_replace", "bindings": { "cmd-enter": "project_search::SearchInNew" } @@ -572,12 +615,26 @@ } }, { - "context": "CollabPanel", + "context": "CollabPanel && not_editing", "bindings": { "ctrl-backspace": "collab_panel::Remove", "space": "menu::Confirm" } }, + { + "context": "(CollabPanel && editing) > Editor", + "bindings": { + "space": "collab_panel::InsertSpace" + } + }, + { + "context": "(CollabPanel && not_editing) > Editor", + "bindings": { + "cmd-c": "collab_panel::StartLinkChannel", + "cmd-x": "collab_panel::StartMoveChannel", + "cmd-v": "collab_panel::MoveOrLinkToSelected" + } + }, { "context": "ChannelModal", "bindings": { diff --git a/assets/keymaps/sublime_text.json b/assets/keymaps/sublime_text.json index a70a61af5519f5ecb9ea161d650ccb96f4b99909..dc1fc1c3ef0286b42b67937ecade7905ef233270 100644 --- a/assets/keymaps/sublime_text.json +++ b/assets/keymaps/sublime_text.json @@ -17,6 +17,7 @@ "ctrl-shift-down": "editor::AddSelectionBelow", "cmd-shift-space": "editor::SelectAll", "ctrl-shift-m": "editor::SelectLargerSyntaxNode", + "cmd-shift-l": "editor::SplitSelectionIntoLines", "cmd-shift-a": "editor::SelectLargerSyntaxNode", "shift-f12": "editor::FindAllReferences", "alt-cmd-down": "editor::GoToDefinition", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index b47907783ea1d8397b84a5bbc47b8924a19c285f..07ba8a121f3ee45132c4354c7a072338273b2692 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -18,6 +18,7 @@ } } ], + ":": "command_palette::Toggle", "h": "vim::Left", "left": "vim::Left", "backspace": "vim::Backspace", @@ -32,6 +33,8 @@ "right": "vim::Right", "$": "vim::EndOfLine", "^": "vim::FirstNonWhitespace", + "_": "vim::StartOfLineDownward", + "g _": "vim::EndOfLineDownward", "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", "{": "vim::StartOfParagraph", @@ -92,6 +95,7 @@ } ], "ctrl-o": "pane::GoBack", + "ctrl-i": "pane::GoForward", "ctrl-]": "editor::GoToDefinition", "escape": [ "vim::SwitchMode", @@ -123,8 +127,26 @@ "g shift-t": "pane::ActivatePrevItem", "g d": "editor::GoToDefinition", "g shift-d": "editor::GoToTypeDefinition", + "g n": "vim::SelectNext", + "g shift-n": "vim::SelectPrevious", + "g >": [ + "editor::SelectNext", + { + "replace_newest": true + } + ], + "g <": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], + "g a": "editor::SelectAllMatches", + "g s": "outline::Toggle", + "g shift-s": "project_symbols::Toggle", "g .": "editor::ToggleCodeActions", // zed specific "g shift-a": "editor::FindAllReferences", // zed specific + "g space": "editor::OpenExcerpts", // zed specific "g *": [ "vim::MoveToNext", { @@ -201,13 +223,13 @@ "shift-z shift-q": [ "pane::CloseActiveItem", { - "saveBehavior": "dontSave" + "saveIntent": "skip" } ], "shift-z shift-z": [ "pane::CloseActiveItem", { - "saveBehavior": "promptOnConflict" + "saveIntent": "saveAll" } ], // Count support @@ -296,6 +318,38 @@ "workspace::ActivatePaneInDirection", "Down" ], + "ctrl-w shift-left": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "ctrl-w shift-right": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "ctrl-w shift-up": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "ctrl-w shift-down": [ + "workspace::SwapPaneInDirection", + "Down" + ], + "ctrl-w shift-h": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "ctrl-w shift-l": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "ctrl-w shift-k": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "ctrl-w shift-j": [ + "workspace::SwapPaneInDirection", + "Down" + ], "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", @@ -314,7 +368,17 @@ "ctrl-w c": "pane::CloseAllItems", "ctrl-w ctrl-c": "pane::CloseAllItems", "ctrl-w q": "pane::CloseAllItems", - "ctrl-w ctrl-q": "pane::CloseAllItems" + "ctrl-w ctrl-q": "pane::CloseAllItems", + "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w n": [ + "workspace::NewFileInDirection", + "Up" + ], + "ctrl-w ctrl-n": [ + "workspace::NewFileInDirection", + "Up" + ] } }, { @@ -326,7 +390,7 @@ } }, { - "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting", + "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { ".": "vim::Repeat", "c": [ @@ -353,6 +417,8 @@ "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", "~": "vim::ChangeCase", + "ctrl-a": "vim::Increment", + "ctrl-x": "vim::Decrement", "p": "vim::Paste", "shift-p": [ "vim::Paste", @@ -389,7 +455,7 @@ } }, { - "context": "Editor && vim_operator == n", + "context": "Editor && VimCount", "bindings": { "0": [ "vim::Number", @@ -448,7 +514,10 @@ "shift-o": "vim::OtherEnd", "d": "vim::VisualDelete", "x": "vim::VisualDelete", + "shift-d": "vim::VisualDelete", + "shift-x": "vim::VisualDelete", "y": "vim::VisualYank", + "shift-y": "vim::VisualYank", "p": "vim::Paste", "shift-p": [ "vim::Paste", @@ -461,6 +530,20 @@ "shift-r": "vim::SubstituteLine", "c": "vim::Substitute", "~": "vim::ChangeCase", + "ctrl-a": "vim::Increment", + "ctrl-x": "vim::Decrement", + "g ctrl-a": [ + "vim::Increment", + { + "step": true + } + ], + "g ctrl-x": [ + "vim::Decrement", + { + "step": true + } + ], "shift-i": "vim::InsertBefore", "shift-a": "vim::InsertAfter", "shift-j": "vim::JoinLines", @@ -497,15 +580,20 @@ "around": true } } - ], + ] } }, { - "context": "Editor && vim_mode == insert && !menu", + "context": "Editor && vim_mode == insert", "bindings": { "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", - "ctrl-[": "vim::NormalBefore" + "ctrl-[": "vim::NormalBefore", + "ctrl-x ctrl-o": "editor::ShowCompletions", + "ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific + "ctrl-x ctrl-c": "copilot::Suggest", // zed specific + "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific + "ctrl-x ctrl-z": "editor::Cancel" } }, { @@ -524,7 +612,7 @@ } }, { - "context": "BufferSearchBar > VimEnabled", + "context": "BufferSearchBar && !in_replace > VimEnabled", "bindings": { "enter": "vim::SearchSubmit", "escape": "buffer_search::Dismiss" diff --git a/assets/settings/default.json b/assets/settings/default.json index 6739819e713f38f9d0628eaf061bdd2ff509da69..8fb73a2ecb0b8143f7e42981a71966327edd0f54 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -131,6 +131,14 @@ // Default width of the channels panel. "default_width": 240 }, + "chat_panel": { + // 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", + // Default width of the channels panel. + "default_width": 240 + }, "assistant": { // Whether to show the assistant panel button in the status bar. "button": true, @@ -219,6 +227,11 @@ }, // Automatically update Zed "auto_update": true, + // Diagnostics configuration. + "diagnostics": { + // Whether to show warnings or not by default. + "include_warnings": true + }, // Git gutter behavior configuration. "git": { // Control whether the git gutter is shown. May take 2 values: @@ -348,7 +361,7 @@ ".venv", "venv" ], - // Can also be 'csh' and 'fish' + // Can also be 'csh', 'fish', and `nushell` "activate_script": "default" } } @@ -362,8 +375,28 @@ }, // Difference settings for semantic_index "semantic_index": { - "enabled": false, - "reindexing_delay_seconds": 600 + "enabled": true + }, + // Settings specific to our elixir integration + "elixir": { + // Change the LSP zed uses for elixir. + // Note that changing this setting requires a restart of Zed + // to take effect. + // + // May take 3 values: + // 1. Use the standard ElixirLS, this is the default + // "lsp": "elixir_ls" + // 2. Use the experimental NextLs + // "lsp": "next_ls", + // 3. Use a language server installed locally on your machine: + // "lsp": { + // "local": { + // "path": "~/next-ls/bin/start", + // "arguments": ["--stdio"] + // } + // }, + // + "lsp": "elixir_ls" }, // Different settings for specific languages. "languages": { diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 6d1db5ada561a69d8fbd1dd8e74edc6d6f607930..f9b34add9a7876431d8a71233b9a76b0deb200ba 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -16,8 +16,8 @@ use workspace::{item::ItemHandle, StatusItemView, Workspace}; actions!(lsp_status, [ShowErrorMessage]); -const DOWNLOAD_ICON: &str = "icons/download_12.svg"; -const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg"; +const DOWNLOAD_ICON: &str = "icons/download.svg"; +const WARNING_ICON: &str = "icons/warning.svg"; pub enum Event { ShowError { lsp_name: Arc, error: String }, diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 4438f88108988e715d476a65fc2566d8a5f8e090..542d7f422fe8c1eaec7d10bf59cb5ccaa2d65ca3 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -9,36 +9,26 @@ path = "src/ai.rs" doctest = false [dependencies] -collections = { path = "../collections"} -editor = { path = "../editor" } -fs = { path = "../fs" } gpui = { path = "../gpui" } -language = { path = "../language" } -menu = { path = "../menu" } -search = { path = "../search" } -settings = { path = "../settings" } -theme = { path = "../theme" } util = { path = "../util" } -workspace = { path = "../workspace" } - +async-trait.workspace = true anyhow.workspace = true -chrono = { version = "0.4", features = ["serde"] } futures.workspace = true -indoc.workspace = true -isahc.workspace = true +lazy_static.workspace = true ordered-float.workspace = true +parking_lot.workspace = true +isahc.workspace = true regex.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true -smol.workspace = true -tiktoken-rs = "0.4" +postage.workspace = true +rand.workspace = true +log.workspace = true +parse_duration = "2.1.1" +tiktoken-rs = "0.5.0" +matrixmultiply = "0.3.7" +rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } +bincode = "1.3.3" [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } - -ctor.workspace = true -env_logger.workspace = true -log.workspace = true -rand.workspace = true +gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 2c2d7e774e120980d212cdcbd887289ebee0e768..5256a6a6432907dd22c30d6a03e492a46fef77df 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,292 +1,2 @@ -pub mod assistant; -mod assistant_settings; -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; -use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; -use gpui::{executor::Background, AppContext}; -use isahc::{http::StatusCode, Request, RequestExt}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::{ - cmp::Reverse, - ffi::OsStr, - fmt::{self, Display}, - io, - path::PathBuf, - sync::Arc, -}; -use util::paths::CONVERSATIONS_DIR; - -const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; - -// Data types for chat completion requests -#[derive(Debug, Serialize)] -pub struct OpenAIRequest { - model: String, - messages: Vec, - stream: bool, -} - -#[derive( - Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] -struct MessageId(usize); - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct MessageMetadata { - role: Role, - sent_at: DateTime, - status: MessageStatus, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum MessageStatus { - Pending, - Done, - Error(Arc), -} - -#[derive(Serialize, Deserialize)] -struct SavedMessage { - id: MessageId, - start: usize, -} - -#[derive(Serialize, Deserialize)] -struct SavedConversation { - zed: String, - version: String, - text: String, - messages: Vec, - message_metadata: HashMap, - summary: String, - model: OpenAIModel, -} - -impl SavedConversation { - const VERSION: &'static str = "0.1.0"; -} - -struct SavedConversationMetadata { - title: String, - path: PathBuf, - mtime: chrono::DateTime, -} - -impl SavedConversationMetadata { - pub async fn list(fs: Arc) -> Result> { - fs.create_dir(&CONVERSATIONS_DIR).await?; - - let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; - let mut conversations = Vec::::new(); - while let Some(path) = paths.next().await { - let path = path?; - if path.extension() != Some(OsStr::new("json")) { - continue; - } - - let pattern = r" - \d+.zed.json$"; - let re = Regex::new(pattern).unwrap(); - - let metadata = fs.metadata(&path).await?; - if let Some((file_name, metadata)) = path - .file_name() - .and_then(|name| name.to_str()) - .zip(metadata) - { - let title = re.replace(file_name, ""); - conversations.push(Self { - title: title.into_owned(), - path, - mtime: metadata.mtime.into(), - }); - } - } - conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); - - Ok(conversations) - } -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -struct RequestMessage { - role: Role, - content: String, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessage { - role: Option, - content: Option, -} - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -enum Role { - User, - Assistant, - System, -} - -impl Role { - pub fn cycle(&mut self) { - *self = match self { - Role::User => Role::Assistant, - Role::Assistant => Role::System, - Role::System => Role::User, - } - } -} - -impl Display for Role { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Role::User => write!(f, "User"), - Role::Assistant => write!(f, "Assistant"), - Role::System => write!(f, "System"), - } - } -} - -#[derive(Deserialize, Debug)] -pub struct OpenAIResponseStreamEvent { - pub id: Option, - pub object: String, - pub created: u32, - pub model: String, - pub choices: Vec, - pub usage: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u32, - pub completion_tokens: u32, - pub total_tokens: u32, -} - -#[derive(Deserialize, Debug)] -pub struct ChatChoiceDelta { - pub index: u32, - pub delta: ResponseMessage, - pub finish_reason: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAIUsage { - prompt_tokens: u64, - completion_tokens: u64, - total_tokens: u64, -} - -#[derive(Deserialize, Debug)] -struct OpenAIChoice { - text: String, - index: u32, - logprobs: Option, - finish_reason: Option, -} - -pub fn init(cx: &mut AppContext) { - assistant::init(cx); -} - -pub async fn stream_completion( - api_key: String, - executor: Arc, - mut request: OpenAIRequest, -) -> Result>> { - request.stream = true; - - let (tx, rx) = futures::channel::mpsc::unbounded::>(); - - let json_data = serde_json::to_string(&request)?; - let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .body(json_data)? - .send_async() - .await?; - - let status = response.status(); - if status == StatusCode::OK { - executor - .spawn(async move { - let mut lines = BufReader::new(response.body_mut()).lines(); - - fn parse_line( - line: Result, - ) -> Result> { - if let Some(data) = line?.strip_prefix("data: ") { - let event = serde_json::from_str(&data)?; - Ok(Some(event)) - } else { - Ok(None) - } - } - - while let Some(line) = lines.next().await { - if let Some(event) = parse_line(line).transpose() { - let done = event.as_ref().map_or(false, |event| { - event - .choices - .last() - .map_or(false, |choice| choice.finish_reason.is_some()) - }); - if tx.unbounded_send(event).is_err() { - break; - } - - if done { - break; - } - } - } - - anyhow::Ok(()) - }) - .detach(); - - Ok(rx) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - #[derive(Deserialize)] - struct OpenAIResponse { - error: OpenAIError, - } - - #[derive(Deserialize)] - struct OpenAIError { - message: String, - } - - match serde_json::from_str::(&body) { - Ok(response) if !response.error.message.is_empty() => Err(anyhow!( - "Failed to connect to OpenAI API: {}", - response.error.message, - )), - - _ => Err(anyhow!( - "Failed to connect to OpenAI API: {} {}", - response.status(), - body, - )), - } - } -} - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } -} +pub mod completion; +pub mod embedding; diff --git a/crates/ai/src/completion.rs b/crates/ai/src/completion.rs new file mode 100644 index 0000000000000000000000000000000000000000..170b2268f9ed1132fad1bfe69194d8cc7a2e91bf --- /dev/null +++ b/crates/ai/src/completion.rs @@ -0,0 +1,212 @@ +use anyhow::{anyhow, Result}; +use futures::{ + future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt, + Stream, StreamExt, +}; +use gpui::executor::Background; +use isahc::{http::StatusCode, Request, RequestExt}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + io, + sync::Arc, +}; + +pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; + +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Role { + User, + Assistant, + System, +} + +impl Role { + pub fn cycle(&mut self) { + *self = match self { + Role::User => Role::Assistant, + Role::Assistant => Role::System, + Role::System => Role::User, + } + } +} + +impl Display for Role { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Role::User => write!(f, "User"), + Role::Assistant => write!(f, "Assistant"), + Role::System => write!(f, "System"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct RequestMessage { + pub role: Role, + pub content: String, +} + +#[derive(Debug, Default, Serialize)] +pub struct OpenAIRequest { + pub model: String, + pub messages: Vec, + pub stream: bool, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ResponseMessage { + pub role: Option, + pub content: Option, +} + +#[derive(Deserialize, Debug)] +pub struct OpenAIUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, +} + +#[derive(Deserialize, Debug)] +pub struct ChatChoiceDelta { + pub index: u32, + pub delta: ResponseMessage, + pub finish_reason: Option, +} + +#[derive(Deserialize, Debug)] +pub struct OpenAIResponseStreamEvent { + pub id: Option, + pub object: String, + pub created: u32, + pub model: String, + pub choices: Vec, + pub usage: Option, +} + +pub async fn stream_completion( + api_key: String, + executor: Arc, + mut request: OpenAIRequest, +) -> Result>> { + request.stream = true; + + let (tx, rx) = futures::channel::mpsc::unbounded::>(); + + let json_data = serde_json::to_string(&request)?; + let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .body(json_data)? + .send_async() + .await?; + + let status = response.status(); + if status == StatusCode::OK { + executor + .spawn(async move { + let mut lines = BufReader::new(response.body_mut()).lines(); + + fn parse_line( + line: Result, + ) -> Result> { + if let Some(data) = line?.strip_prefix("data: ") { + let event = serde_json::from_str(&data)?; + Ok(Some(event)) + } else { + Ok(None) + } + } + + while let Some(line) = lines.next().await { + if let Some(event) = parse_line(line).transpose() { + let done = event.as_ref().map_or(false, |event| { + event + .choices + .last() + .map_or(false, |choice| choice.finish_reason.is_some()) + }); + if tx.unbounded_send(event).is_err() { + break; + } + + if done { + break; + } + } + } + + anyhow::Ok(()) + }) + .detach(); + + Ok(rx) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + #[derive(Deserialize)] + struct OpenAIResponse { + error: OpenAIError, + } + + #[derive(Deserialize)] + struct OpenAIError { + message: String, + } + + match serde_json::from_str::(&body) { + Ok(response) if !response.error.message.is_empty() => Err(anyhow!( + "Failed to connect to OpenAI API: {}", + response.error.message, + )), + + _ => Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )), + } + } +} + +pub trait CompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>>; +} + +pub struct OpenAICompletionProvider { + api_key: String, + executor: Arc, +} + +impl OpenAICompletionProvider { + pub fn new(api_key: String, executor: Arc) -> Self { + Self { api_key, executor } + } +} + +impl CompletionProvider for OpenAICompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>> { + let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt); + async move { + let response = request.await?; + let stream = response + .filter_map(|response| async move { + match response { + Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)), + Err(error) => Some(Err(error)), + } + }) + .boxed(); + Ok(stream) + } + .boxed() + } +} diff --git a/crates/semantic_index/src/embedding.rs b/crates/ai/src/embedding.rs similarity index 88% rename from crates/semantic_index/src/embedding.rs rename to crates/ai/src/embedding.rs index 42d90f0fdb23b1838966926d73664275981f1430..332470aa546832fc083ca9064d842dc5b66dafd4 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -7,6 +7,7 @@ use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; use lazy_static::lazy_static; +use ordered_float::OrderedFloat; use parking_lot::Mutex; use parse_duration::parse; use postage::watch; @@ -26,8 +27,30 @@ lazy_static! { } #[derive(Debug, PartialEq, Clone)] -pub struct Embedding(Vec); +pub struct Embedding(pub Vec); +// This is needed for semantic index functionality +// Unfortunately it has to live wherever the "Embedding" struct is created. +// Keeping this in here though, introduces a 'rusqlite' dependency into AI +// which is less than ideal +impl FromSql for Embedding { + fn column_result(value: ValueRef) -> FromSqlResult { + let bytes = value.as_blob()?; + let embedding: Result, Box> = bincode::deserialize(bytes); + if embedding.is_err() { + return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); + } + Ok(Embedding(embedding.unwrap())) + } +} + +impl ToSql for Embedding { + fn to_sql(&self) -> rusqlite::Result { + let bytes = bincode::serialize(&self.0) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; + Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) + } +} impl From> for Embedding { fn from(value: Vec) -> Self { Embedding(value) @@ -35,7 +58,7 @@ impl From> for Embedding { } impl Embedding { - pub fn similarity(&self, other: &Self) -> f32 { + pub fn similarity(&self, other: &Self) -> OrderedFloat { let len = self.0.len(); assert_eq!(len, other.0.len()); @@ -58,28 +81,28 @@ impl Embedding { 1, ); } - result - } -} - -impl FromSql for Embedding { - fn column_result(value: ValueRef) -> FromSqlResult { - let bytes = value.as_blob()?; - let embedding: Result, Box> = bincode::deserialize(bytes); - if embedding.is_err() { - return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); - } - Ok(Embedding(embedding.unwrap())) + OrderedFloat(result) } } -impl ToSql for Embedding { - fn to_sql(&self) -> rusqlite::Result { - let bytes = bincode::serialize(&self.0) - .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; - Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) - } -} +// impl FromSql for Embedding { +// fn column_result(value: ValueRef) -> FromSqlResult { +// let bytes = value.as_blob()?; +// let embedding: Result, Box> = bincode::deserialize(bytes); +// if embedding.is_err() { +// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); +// } +// Ok(Embedding(embedding.unwrap())) +// } +// } + +// impl ToSql for Embedding { +// fn to_sql(&self) -> rusqlite::Result { +// let bytes = bincode::serialize(&self.0) +// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; +// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) +// } +// } #[derive(Clone)] pub struct OpenAIEmbeddings { @@ -116,6 +139,7 @@ struct OpenAIEmbeddingUsage { #[async_trait] pub trait EmbeddingProvider: Sync + Send { + fn is_authenticated(&self) -> bool; async fn embed_batch(&self, spans: Vec) -> Result>; fn max_tokens_per_batch(&self) -> usize; fn truncate(&self, span: &str) -> (String, usize); @@ -126,6 +150,9 @@ pub struct DummyEmbeddings {} #[async_trait] impl EmbeddingProvider for DummyEmbeddings { + fn is_authenticated(&self) -> bool { + true + } fn rate_limit_expiration(&self) -> Option { None } @@ -228,6 +255,9 @@ impl OpenAIEmbeddings { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddings { + fn is_authenticated(&self) -> bool { + OPENAI_API_KEY.as_ref().is_some() + } fn max_tokens_per_batch(&self) -> usize { 50000 } @@ -379,13 +409,13 @@ mod tests { ); } - fn round_to_decimals(n: f32, decimal_places: i32) -> f32 { + fn round_to_decimals(n: OrderedFloat, decimal_places: i32) -> f32 { let factor = (10.0 as f32).powi(decimal_places); (n * factor).round() / factor } - fn reference_dot(a: &[f32], b: &[f32]) -> f32 { - a.iter().zip(b.iter()).map(|(a, b)| a * b).sum() + fn reference_dot(a: &[f32], b: &[f32]) -> OrderedFloat { + OrderedFloat(a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()) } } } diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f1daf47bab05c96acc0876b1aff9a2871d5c416a --- /dev/null +++ b/crates/assistant/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "assistant" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/assistant.rs" +doctest = false + +[dependencies] +ai = { path = "../ai" } +client = { path = "../client" } +collections = { path = "../collections"} +editor = { path = "../editor" } +fs = { path = "../fs" } +gpui = { path = "../gpui" } +language = { path = "../language" } +menu = { path = "../menu" } +search = { path = "../search" } +settings = { path = "../settings" } +theme = { path = "../theme" } +util = { path = "../util" } +workspace = { path = "../workspace" } +uuid.workspace = true + +anyhow.workspace = true +chrono = { version = "0.4", features = ["serde"] } +futures.workspace = true +indoc.workspace = true +isahc.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true +regex.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +smol.workspace = true +tiktoken-rs = "0.4" + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +log.workspace = true +rand.workspace = true diff --git a/crates/ai/README.zmd b/crates/assistant/README.zmd similarity index 100% rename from crates/ai/README.zmd rename to crates/assistant/README.zmd diff --git a/crates/ai/features.zmd b/crates/assistant/features.zmd similarity index 100% rename from crates/ai/features.zmd rename to crates/assistant/features.zmd diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs new file mode 100644 index 0000000000000000000000000000000000000000..6c9b14333e34cbf5fd49d8299ba7bd891b607526 --- /dev/null +++ b/crates/assistant/src/assistant.rs @@ -0,0 +1,113 @@ +pub mod assistant_panel; +mod assistant_settings; +mod codegen; +mod prompts; +mod streaming_diff; + +use ai::completion::Role; +use anyhow::Result; +pub use assistant_panel::AssistantPanel; +use assistant_settings::OpenAIModel; +use chrono::{DateTime, Local}; +use collections::HashMap; +use fs::Fs; +use futures::StreamExt; +use gpui::AppContext; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; +use util::paths::CONVERSATIONS_DIR; + +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +struct MessageId(usize); + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MessageMetadata { + role: Role, + sent_at: DateTime, + status: MessageStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum MessageStatus { + Pending, + Done, + Error(Arc), +} + +#[derive(Serialize, Deserialize)] +struct SavedMessage { + id: MessageId, + start: usize, +} + +#[derive(Serialize, Deserialize)] +struct SavedConversation { + id: Option, + zed: String, + version: String, + text: String, + messages: Vec, + message_metadata: HashMap, + summary: String, + model: OpenAIModel, +} + +impl SavedConversation { + const VERSION: &'static str = "0.1.0"; +} + +struct SavedConversationMetadata { + title: String, + path: PathBuf, + mtime: chrono::DateTime, +} + +impl SavedConversationMetadata { + pub async fn list(fs: Arc) -> Result> { + fs.create_dir(&CONVERSATIONS_DIR).await?; + + let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; + let mut conversations = Vec::::new(); + while let Some(path) = paths.next().await { + let path = path?; + if path.extension() != Some(OsStr::new("json")) { + continue; + } + + let pattern = r" - \d+.zed.json$"; + let re = Regex::new(pattern).unwrap(); + + let metadata = fs.metadata(&path).await?; + if let Some((file_name, metadata)) = path + .file_name() + .and_then(|name| name.to_str()) + .zip(metadata) + { + let title = re.replace(file_name, ""); + conversations.push(Self { + title: title.into_owned(), + path, + mtime: metadata.mtime.into(), + }); + } + } + conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); + + Ok(conversations) + } +} + +pub fn init(cx: &mut AppContext) { + assistant_panel::init(cx); +} + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} diff --git a/crates/ai/src/assistant.rs b/crates/assistant/src/assistant_panel.rs similarity index 82% rename from crates/ai/src/assistant.rs rename to crates/assistant/src/assistant_panel.rs index 9b384252fc0dfea3fe7897c1152fbca18fbcd9e0..b69c12a2a328ed8643315f091be11d764dcdc00d 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,22 +1,26 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - stream_completion, - streaming_diff::{Hunk, StreamingDiff}, - MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role, - SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, + codegen::{self, Codegen, CodegenKind}, + prompts::generate_content_prompt, + MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, +}; +use ai::completion::{ + stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; +use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings}; use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ display_map::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, }; use fs::Fs; -use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use futures::StreamExt; use gpui::{ actions, elements::{ @@ -30,17 +34,14 @@ use gpui::{ ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::{ - language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _, - TransactionId, -}; +use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use search::BufferSearchBar; use settings::SettingsStore; use std::{ cell::{Cell, RefCell}, cmp, env, fmt::Write, - future, iter, + iter, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -52,6 +53,7 @@ use theme::{ AssistantStyle, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; +use uuid::Uuid; use workspace::{ dock::{DockPosition, Panel}, searchable::Direction, @@ -266,23 +268,45 @@ impl AssistantPanel { } fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { + let api_key = if let Some(api_key) = self.api_key.borrow().clone() { + api_key + } else { + return; + }; + + let selection = editor.read(cx).selections.newest_anchor().clone(); + if selection.start.excerpt_id() != selection.end.excerpt_id() { + return; + } + let inline_assist_id = post_inc(&mut self.next_inline_assist_id); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let selection = editor.read(cx).selections.newest_anchor().clone(); - let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); - let assist_kind = if editor.read(cx).selections.newest::(cx).is_empty() { - InlineAssistKind::Generate + let provider = Arc::new(OpenAICompletionProvider::new( + api_key, + cx.background().clone(), + )); + let codegen_kind = if editor.read(cx).selections.newest::(cx).is_empty() { + CodegenKind::Generate { + position: selection.start, + } } else { - InlineAssistKind::Transform + CodegenKind::Transform { + range: selection.start..selection.end, + } }; + let codegen = cx.add_model(|cx| { + Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) + }); + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant::new( inline_assist_id, - assist_kind, measurements.clone(), self.include_conversation_in_next_inline_assist, self.inline_prompt_history.clone(), + codegen.clone(), + self.workspace.clone(), cx, ); cx.focus_self(); @@ -321,45 +345,63 @@ impl AssistantPanel { self.pending_inline_assists.insert( inline_assist_id, PendingInlineAssist { - kind: assist_kind, editor: editor.downgrade(), - range, - highlighted_ranges: Default::default(), inline_assistant: Some((block_id, inline_assistant.clone())), - code_generation: Task::ready(None), - transaction_id: None, + codegen: codegen.clone(), _subscriptions: vec![ cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), cx.subscribe(editor, { let inline_assistant = inline_assistant.downgrade(); - move |this, editor, event, cx| { + move |_, editor, event, cx| { if let Some(inline_assistant) = inline_assistant.upgrade(cx) { - match event { - editor::Event::SelectionsChanged { local } => { - if *local && inline_assistant.read(cx).has_focus { - cx.focus(&editor); - } + if let editor::Event::SelectionsChanged { local } = event { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); } - editor::Event::TransactionUndone { - transaction_id: tx_id, - } => { - if let Some(pending_assist) = - this.pending_inline_assists.get(&inline_assist_id) - { - if pending_assist.transaction_id == Some(*tx_id) { - // Notice we are supplying `undo: false` here. This - // is because there's no need to undo the transaction - // because the user just did so. - this.close_inline_assist( - inline_assist_id, - false, - cx, - ); - } - } + } + } + } + }), + cx.observe(&codegen, { + let editor = editor.downgrade(); + move |this, _, cx| { + if let Some(editor) = editor.upgrade(cx) { + this.update_highlights_for_editor(&editor, cx); + } + } + }), + cx.subscribe(&codegen, move |this, codegen, event, cx| match event { + codegen::Event::Undone => { + this.finish_inline_assist(inline_assist_id, false, cx) + } + codegen::Event::Finished => { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + let error = codegen + .read(cx) + .error() + .map(|error| format!("Inline assistant error: {}", error)); + if let Some(error) = error { + if pending_assist.inline_assistant.is_none() { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new(inline_assist_id, error), + cx, + ); + }) } - _ => {} + + this.finish_inline_assist(inline_assist_id, false, cx); } + } else { + this.finish_inline_assist(inline_assist_id, false, cx); } } }), @@ -388,7 +430,7 @@ impl AssistantPanel { self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx); } InlineAssistantEvent::Canceled => { - self.close_inline_assist(assist_id, true, cx); + self.finish_inline_assist(assist_id, true, cx); } InlineAssistantEvent::Dismissed => { self.hide_inline_assist(assist_id, cx); @@ -417,7 +459,7 @@ impl AssistantPanel { .get(&editor.downgrade()) .and_then(|assist_ids| assist_ids.last().copied()) { - panel.close_inline_assist(assist_id, true, cx); + panel.finish_inline_assist(assist_id, true, cx); true } else { false @@ -432,7 +474,7 @@ impl AssistantPanel { cx.propagate_action(); } - fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { + fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { self.hide_inline_assist(assist_id, cx); if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { @@ -450,13 +492,9 @@ impl AssistantPanel { self.update_highlights_for_editor(&editor, cx); if undo { - if let Some(transaction_id) = pending_assist.transaction_id { - editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - buffer.undo_transaction(transaction_id, cx) - }); - }); - } + pending_assist + .codegen + .update(cx, |codegen, cx| codegen.undo(cx)); } } } @@ -481,12 +519,6 @@ impl AssistantPanel { include_conversation: bool, cx: &mut ViewContext, ) { - let api_key = if let Some(api_key) = self.api_key.borrow().clone() { - api_key - } else { - return; - }; - let conversation = if include_conversation { self.active_editor() .map(|editor| editor.read(cx).conversation.clone()) @@ -514,58 +546,26 @@ impl AssistantPanel { self.inline_prompt_history.pop_front(); } - let range = pending_assist.range.clone(); + let codegen = pending_assist.codegen.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let selected_text = snapshot - .text_for_range(range.start..range.end) - .collect::(); - - let selection_start = range.start.to_point(&snapshot); - let selection_end = range.end.to_point(&snapshot); - - let mut base_indent: Option = None; - let mut start_row = selection_start.row; - if snapshot.is_line_blank(start_row) { - if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { - start_row = prev_non_blank_row; - } - } - for row in start_row..=selection_end.row { - if snapshot.is_line_blank(row) { - continue; - } - - let line_indent = snapshot.indent_size_for_line(row); - if let Some(base_indent) = base_indent.as_mut() { - if line_indent.len < base_indent.len { - *base_indent = line_indent; - } + let range = codegen.read(cx).range(); + let start = snapshot.point_to_buffer_offset(range.start); + let end = snapshot.point_to_buffer_offset(range.end); + let (buffer, range) = if let Some((start, end)) = start.zip(end) { + let (start_buffer, start_buffer_offset) = start; + let (end_buffer, end_buffer_offset) = end; + if start_buffer.remote_id() == end_buffer.remote_id() { + (start_buffer.clone(), start_buffer_offset..end_buffer_offset) } else { - base_indent = Some(line_indent); - } - } - - let mut normalized_selected_text = selected_text.clone(); - if let Some(base_indent) = base_indent { - for row in selection_start.row..=selection_end.row { - let selection_row = row - selection_start.row; - let line_start = - normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); - let indent_len = if row == selection_start.row { - base_indent.len.saturating_sub(selection_start.column) - } else { - let line_len = normalized_selected_text.line_len(selection_row); - cmp::min(line_len, base_indent.len) - }; - let indent_end = cmp::min( - line_start + indent_len as usize, - normalized_selected_text.len(), - ); - normalized_selected_text.replace(line_start..indent_end, ""); + self.finish_inline_assist(inline_assist_id, false, cx); + return; } - } + } else { + self.finish_inline_assist(inline_assist_id, false, cx); + return; + }; - let language = snapshot.language_at(range.start); + let language = buffer.language_at(range.start); let language_name = if let Some(language) = language.as_ref() { if Arc::ptr_eq(language, &language::PLAIN_TEXT) { None @@ -575,96 +575,13 @@ impl AssistantPanel { } else { None }; - let language_name = language_name.as_deref(); - - let mut prompt = String::new(); - if let Some(language_name) = language_name { - writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); - } - match pending_assist.kind { - InlineAssistKind::Transform => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt, "```").unwrap(); - - writeln!( - prompt, - "In particular, the user has selected the following text:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - writeln!(prompt, "{normalized_selected_text}").unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!(prompt).unwrap(); - writeln!( - prompt, - "Modify the selected text given the user prompt: {user_prompt}" - ) - .unwrap(); - writeln!( - prompt, - "You MUST reply only with the edited selected text, not the entire file." - ) - .unwrap(); - } - InlineAssistKind::Generate => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..range.start) { - write!(prompt, "{chunk}").unwrap(); - } - write!(prompt, "<|>").unwrap(); - for chunk in snapshot.text_for_range(range.start..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt).unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!( - prompt, - "Assume the cursor is located where the `<|>` marker is." - ) - .unwrap(); - writeln!( - prompt, - "Text can't be replaced, so assume your answer will be inserted at the cursor." - ) - .unwrap(); - writeln!( - prompt, - "Complete the text given the user prompt: {user_prompt}" - ) - .unwrap(); - } - } - if let Some(language_name) = language_name { - writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); - } - writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!(prompt, "Never make remarks about the output.").unwrap(); + let codegen_kind = codegen.read(cx).kind().clone(); + let user_prompt = user_prompt.to_string(); + let prompt = cx.background().spawn(async move { + let language_name = language_name.as_deref(); + generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind) + }); let mut messages = Vec::new(); let mut model = settings::get::(cx) .default_open_ai_model @@ -680,218 +597,21 @@ impl AssistantPanel { model = conversation.model.clone(); } - messages.push(RequestMessage { - role: Role::User, - content: prompt, - }); - let request = OpenAIRequest { - model: model.full_name().into(), - messages, - stream: true, - }; - let response = stream_completion(api_key, cx.background().clone(), request); - let editor = editor.downgrade(); - - pending_assist.code_generation = cx.spawn(|this, mut cx| { - async move { - let mut edit_start = range.start.to_offset(&snapshot); - - let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background().spawn(async move { - let chunks = strip_markdown_codeblock(response.await?.filter_map( - |message| async move { - match message { - Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)), - Err(error) => Some(Err(error)), - } - }, - )); - futures::pin_mut!(chunks); - let mut diff = StreamingDiff::new(selected_text.to_string()); - - let mut indent_len; - let indent_text; - if let Some(base_indent) = base_indent { - indent_len = base_indent.len; - indent_text = match base_indent.kind { - language::IndentKind::Space => " ", - language::IndentKind::Tab => "\t", - }; - } else { - indent_len = 0; - indent_text = ""; - }; - - let mut first_line_len = 0; - let mut first_line_non_whitespace_char_ix = None; - let mut first_line = true; - let mut new_text = String::new(); - - while let Some(chunk) = chunks.next().await { - let chunk = chunk?; - - let mut lines = chunk.split('\n'); - if let Some(mut line) = lines.next() { - if first_line { - if first_line_non_whitespace_char_ix.is_none() { - if let Some(mut char_ix) = - line.find(|ch: char| !ch.is_whitespace()) - { - line = &line[char_ix..]; - char_ix += first_line_len; - first_line_non_whitespace_char_ix = Some(char_ix); - let first_line_indent = char_ix - .saturating_sub(selection_start.column as usize) - as usize; - new_text.push_str(&indent_text.repeat(first_line_indent)); - indent_len = indent_len.saturating_sub(char_ix as u32); - } - } - first_line_len += line.len(); - } - - if first_line_non_whitespace_char_ix.is_some() { - new_text.push_str(line); - } - } - - for line in lines { - first_line = false; - new_text.push('\n'); - if !line.is_empty() { - new_text.push_str(&indent_text.repeat(indent_len as usize)); - } - new_text.push_str(line); - } - - let hunks = diff.push_new(&new_text); - hunks_tx.send(hunks).await?; - new_text.clear(); - } - hunks_tx.send(diff.finish()).await?; - - anyhow::Ok(()) - }); - - while let Some(hunks) = hunks_rx.next().await { - let editor = if let Some(editor) = editor.upgrade(&cx) { - editor - } else { - break; - }; - - let this = if let Some(this) = this.upgrade(&cx) { - this - } else { - break; - }; - - this.update(&mut cx, |this, cx| { - let pending_assist = if let Some(pending_assist) = - this.pending_inline_assists.get_mut(&inline_assist_id) - { - pending_assist - } else { - return; - }; - - pending_assist.highlighted_ranges.clear(); - editor.update(cx, |editor, cx| { - let transaction = editor.buffer().update(cx, |buffer, cx| { - // Avoid grouping assistant edits with user edits. - buffer.finalize_last_transaction(cx); - - buffer.start_transaction(cx); - buffer.edit( - hunks.into_iter().filter_map(|hunk| match hunk { - Hunk::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - Some((edit_start..edit_start, text)) - } - Hunk::Remove { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start = edit_end; - Some((edit_range, String::new())) - } - Hunk::Keep { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start += len; - pending_assist.highlighted_ranges.push(edit_range); - None - } - }), - None, - cx, - ); - - buffer.end_transaction(cx) - }); - - if let Some(transaction) = transaction { - if let Some(first_transaction) = pending_assist.transaction_id { - // Group all assistant edits into the first transaction. - editor.buffer().update(cx, |buffer, cx| { - buffer.merge_transactions( - transaction, - first_transaction, - cx, - ) - }); - } else { - pending_assist.transaction_id = Some(transaction); - editor.buffer().update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx) - }); - } - } - }); - - this.update_highlights_for_editor(&editor, cx); - }); - } - - if let Err(error) = diff.await { - this.update(&mut cx, |this, cx| { - let pending_assist = if let Some(pending_assist) = - this.pending_inline_assists.get_mut(&inline_assist_id) - { - pending_assist - } else { - return; - }; - - if let Some((_, inline_assistant)) = - pending_assist.inline_assistant.as_ref() - { - inline_assistant.update(cx, |inline_assistant, cx| { - inline_assistant.set_error(error, cx); - }); - } else if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - inline_assist_id, - format!("Inline assistant error: {}", error), - ), - cx, - ); - }) - } - })?; - } else { - let _ = this.update(&mut cx, |this, cx| { - this.close_inline_assist(inline_assist_id, false, cx) - }); - } + cx.spawn(|_, mut cx| async move { + let prompt = prompt.await; - anyhow::Ok(()) - } - .log_err() - }); + messages.push(RequestMessage { + role: Role::User, + content: prompt, + }); + let request = OpenAIRequest { + model: model.full_name().into(), + messages, + stream: true, + }; + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + }) + .detach(); } fn update_highlights_for_editor( @@ -909,8 +629,9 @@ impl AssistantPanel { for inline_assist_id in inline_assist_ids { if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { - background_ranges.push(pending_assist.range.clone()); - foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned()); + let codegen = pending_assist.codegen.read(cx); + background_ranges.push(codegen.range()); + foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned()); } } @@ -929,7 +650,7 @@ impl AssistantPanel { } if foreground_ranges.is_empty() { - editor.clear_text_highlights::(cx); + editor.clear_highlights::(cx); } else { editor.highlight_text::( foreground_ranges, @@ -949,6 +670,7 @@ impl AssistantPanel { self.api_key.clone(), self.languages.clone(), self.fs.clone(), + self.workspace.clone(), cx, ) }); @@ -1284,6 +1006,7 @@ impl AssistantPanel { } let fs = self.fs.clone(); + let workspace = self.workspace.clone(); let api_key = self.api_key.clone(); let languages = self.languages.clone(); cx.spawn(|this, mut cx| async move { @@ -1298,8 +1021,9 @@ impl AssistantPanel { if let Some(ix) = this.editor_index_for_path(&path, cx) { this.set_active_editor_index(Some(ix), cx); } else { - let editor = cx - .add_view(|cx| ConversationEditor::for_conversation(conversation, fs, cx)); + let editor = cx.add_view(|cx| { + ConversationEditor::for_conversation(conversation, fs, workspace, cx) + }); this.add_conversation(editor, cx); } })?; @@ -1573,6 +1297,7 @@ struct Summary { } struct Conversation { + id: Option, buffer: ModelHandle, message_anchors: Vec, messages_metadata: HashMap, @@ -1623,6 +1348,7 @@ impl Conversation { let model = settings.default_open_ai_model.clone(); let mut this = Self { + id: Some(Uuid::new_v4().to_string()), message_anchors: Default::default(), messages_metadata: Default::default(), next_message_id: Default::default(), @@ -1660,6 +1386,7 @@ impl Conversation { fn serialize(&self, cx: &AppContext) -> SavedConversation { SavedConversation { + id: self.id.clone(), zed: "conversation".into(), version: SavedConversation::VERSION.into(), text: self.buffer.read(cx).text(), @@ -1687,6 +1414,10 @@ impl Conversation { language_registry: Arc, cx: &mut ModelContext, ) -> Self { + let id = match saved_conversation.id { + Some(id) => Some(id), + None => Some(Uuid::new_v4().to_string()), + }; let model = saved_conversation.model; let markdown = language_registry.language_for_name("Markdown"); let mut message_anchors = Vec::new(); @@ -1716,6 +1447,7 @@ impl Conversation { }); let mut this = Self { + id, message_anchors, messages_metadata: saved_conversation.message_metadata, next_message_id, @@ -2333,6 +2065,7 @@ struct ScrollPosition { struct ConversationEditor { conversation: ModelHandle, fs: Arc, + workspace: WeakViewHandle, editor: ViewHandle, blocks: HashSet, scroll_position: Option, @@ -2344,15 +2077,17 @@ impl ConversationEditor { api_key: Rc>>, language_registry: Arc, fs: Arc, + workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx)); - Self::for_conversation(conversation, fs, cx) + Self::for_conversation(conversation, fs, workspace, cx) } fn for_conversation( conversation: ModelHandle, fs: Arc, + workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { let editor = cx.add_view(|cx| { @@ -2375,6 +2110,7 @@ impl ConversationEditor { blocks: Default::default(), scroll_position: None, fs, + workspace, _subscriptions, }; this.update_message_headers(cx); @@ -2382,6 +2118,13 @@ impl ConversationEditor { } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { + report_assistant_event( + self.workspace.clone(), + self.conversation.read(cx).id.clone(), + AssistantKind::Panel, + cx, + ); + let cursors = self.cursors(cx); let user_messages = self.conversation.update(cx, |conversation, cx| { @@ -2601,7 +2344,7 @@ impl ConversationEditor { .with_children( if let MessageStatus::Error(error) = &message.status { Some( - Svg::new("icons/circle_x_mark_12.svg") + Svg::new("icons/error.svg") .with_color(style.error_icon.color) .constrained() .with_width(style.error_icon.width) @@ -2887,24 +2630,19 @@ enum InlineAssistantEvent { }, } -#[derive(Copy, Clone)] -enum InlineAssistKind { - Transform, - Generate, -} - struct InlineAssistant { id: usize, prompt_editor: ViewHandle, + workspace: WeakViewHandle, confirmed: bool, has_focus: bool, include_conversation: bool, measurements: Rc>, - error: Option, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, - _subscription: Subscription, + codegen: ModelHandle, + _subscriptions: Vec, } impl Entity for InlineAssistant { @@ -2933,9 +2671,9 @@ impl View for InlineAssistant { .element() .aligned(), ) - .with_children(if let Some(error) = self.error.as_ref() { + .with_children(if let Some(error) = self.codegen.read(cx).error() { Some( - Svg::new("icons/circle_x_mark_12.svg") + Svg::new("icons/error.svg") .with_color(theme.assistant.error_icon.color) .constrained() .with_width(theme.assistant.error_icon.width) @@ -3007,10 +2745,11 @@ impl View for InlineAssistant { impl InlineAssistant { fn new( id: usize, - kind: InlineAssistKind, measurements: Rc>, include_conversation: bool, prompt_history: VecDeque, + codegen: ModelHandle, + workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { let prompt_editor = cx.add_view(|cx| { @@ -3018,26 +2757,30 @@ impl InlineAssistant { Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), cx, ); - let placeholder = match kind { - InlineAssistKind::Transform => "Enter transformation prompt…", - InlineAssistKind::Generate => "Enter generation prompt…", + let placeholder = match codegen.read(cx).kind() { + CodegenKind::Transform { .. } => "Enter transformation prompt…", + CodegenKind::Generate { .. } => "Enter generation prompt…", }; editor.set_placeholder_text(placeholder, cx); editor }); - let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events); + let subscriptions = vec![ + cx.observe(&codegen, Self::handle_codegen_changed), + cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), + ]; Self { id, prompt_editor, + workspace, confirmed: false, has_focus: false, include_conversation, measurements, - error: None, prompt_history, prompt_history_ix: None, pending_prompt: String::new(), - _subscription: subscription, + codegen, + _subscriptions: subscriptions, } } @@ -3053,6 +2796,32 @@ impl InlineAssistant { } } + fn handle_codegen_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + let is_read_only = !self.codegen.read(cx).idle(); + self.prompt_editor.update(cx, |editor, cx| { + let was_read_only = editor.read_only(); + if was_read_only != is_read_only { + if is_read_only { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + } else { + self.confirmed = false; + editor.set_read_only(false); + editor.set_field_editor_style( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + } + } + }); + cx.notify(); + } + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { cx.emit(InlineAssistantEvent::Canceled); } @@ -3061,6 +2830,8 @@ impl InlineAssistant { if self.confirmed { cx.emit(InlineAssistantEvent::Dismissed); } else { + report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx); + let prompt = self.prompt_editor.read(cx).text(cx); self.prompt_editor.update(cx, |editor, cx| { editor.set_read_only(true); @@ -3076,7 +2847,6 @@ impl InlineAssistant { include_conversation: self.include_conversation, }); self.confirmed = true; - self.error = None; cx.notify(); } } @@ -3093,19 +2863,6 @@ impl InlineAssistant { cx.notify(); } - fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext) { - self.error = Some(error); - self.confirmed = false; - self.prompt_editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.set_field_editor_style( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); - }); - cx.notify(); - } - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { if let Some(ix) = self.prompt_history_ix { if ix > 0 { @@ -3152,13 +2909,9 @@ struct BlockMeasurements { } struct PendingInlineAssist { - kind: InlineAssistKind, editor: WeakViewHandle, - range: Range, - highlighted_ranges: Vec>, inline_assistant: Option<(BlockId, ViewHandle)>, - code_generation: Task>, - transaction_id: Option, + codegen: ModelHandle, _subscriptions: Vec, } @@ -3184,65 +2937,10 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } -fn strip_markdown_codeblock( - stream: impl Stream>, -) -> impl Stream> { - let mut first_line = true; - let mut buffer = String::new(); - let mut starts_with_fenced_code_block = false; - stream.filter_map(move |chunk| { - let chunk = match chunk { - Ok(chunk) => chunk, - Err(err) => return future::ready(Some(Err(err))), - }; - buffer.push_str(&chunk); - - if first_line { - if buffer == "" || buffer == "`" || buffer == "``" { - return future::ready(None); - } else if buffer.starts_with("```") { - starts_with_fenced_code_block = true; - if let Some(newline_ix) = buffer.find('\n') { - buffer.replace_range(..newline_ix + 1, ""); - first_line = false; - } else { - return future::ready(None); - } - } - } - - let text = if starts_with_fenced_code_block { - buffer - .strip_suffix("\n```\n") - .or_else(|| buffer.strip_suffix("\n```")) - .or_else(|| buffer.strip_suffix("\n``")) - .or_else(|| buffer.strip_suffix("\n`")) - .or_else(|| buffer.strip_suffix('\n')) - .unwrap_or(&buffer) - } else { - &buffer - }; - - if text.contains('\n') { - first_line = false; - } - - let remainder = buffer.split_off(text.len()); - let result = if buffer.is_empty() { - None - } else { - Some(Ok(buffer.clone())) - }; - buffer = remainder; - future::ready(result) - }) -} - #[cfg(test)] mod tests { use super::*; use crate::MessageId; - use futures::stream; use gpui::AppContext; #[gpui::test] @@ -3611,62 +3309,6 @@ mod tests { ); } - #[gpui::test] - async fn test_strip_markdown_codeblock() { - assert_eq!( - strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "```js\nLorem ipsum dolor\n```" - ); - assert_eq!( - strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "``\nLorem ipsum dolor\n```" - ); - - fn chunks(text: &str, size: usize) -> impl Stream> { - stream::iter( - text.chars() - .collect::>() - .chunks(size) - .map(|chunk| Ok(chunk.iter().collect::())) - .collect::>(), - ) - } - } - fn messages( conversation: &ModelHandle, cx: &AppContext, @@ -3678,3 +3320,30 @@ mod tests { .collect() } } + +fn report_assistant_event( + workspace: WeakViewHandle, + conversation_id: Option, + assistant_kind: AssistantKind, + cx: &AppContext, +) { + let Some(workspace) = workspace.upgrade(cx) else { + return; + }; + + let client = workspace.read(cx).project().read(cx).client(); + let telemetry = client.telemetry(); + + let model = settings::get::(cx) + .default_open_ai_model + .clone(); + + let event = ClickhouseEvent::Assistant { + conversation_id, + kind: assistant_kind, + model: model.full_name(), + }; + let telemetry_settings = *settings::get::(cx); + + telemetry.report_clickhouse_event(event, telemetry_settings) +} diff --git a/crates/ai/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs similarity index 100% rename from crates/ai/src/assistant_settings.rs rename to crates/assistant/src/assistant_settings.rs diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs new file mode 100644 index 0000000000000000000000000000000000000000..e956d722606f6db27c73385d7cf54d58bc82958b --- /dev/null +++ b/crates/assistant/src/codegen.rs @@ -0,0 +1,663 @@ +use crate::streaming_diff::{Hunk, StreamingDiff}; +use ai::completion::{CompletionProvider, OpenAIRequest}; +use anyhow::Result; +use editor::{ + multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, +}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use gpui::{Entity, ModelContext, ModelHandle, Task}; +use language::{Rope, TransactionId}; +use std::{cmp, future, ops::Range, sync::Arc}; + +pub enum Event { + Finished, + Undone, +} + +#[derive(Clone)] +pub enum CodegenKind { + Transform { range: Range }, + Generate { position: Anchor }, +} + +pub struct Codegen { + provider: Arc, + buffer: ModelHandle, + snapshot: MultiBufferSnapshot, + kind: CodegenKind, + last_equal_ranges: Vec>, + transaction_id: Option, + error: Option, + generation: Task<()>, + idle: bool, + _subscription: gpui::Subscription, +} + +impl Entity for Codegen { + type Event = Event; +} + +impl Codegen { + pub fn new( + buffer: ModelHandle, + mut kind: CodegenKind, + provider: Arc, + cx: &mut ModelContext, + ) -> Self { + let snapshot = buffer.read(cx).snapshot(cx); + match &mut kind { + CodegenKind::Transform { range } => { + let mut point_range = range.to_point(&snapshot); + point_range.start.column = 0; + if point_range.end.column > 0 || point_range.start.row == point_range.end.row { + point_range.end.column = snapshot.line_len(point_range.end.row); + } + range.start = snapshot.anchor_before(point_range.start); + range.end = snapshot.anchor_after(point_range.end); + } + CodegenKind::Generate { position } => { + *position = position.bias_right(&snapshot); + } + } + + Self { + provider, + buffer: buffer.clone(), + snapshot, + kind, + last_equal_ranges: Default::default(), + transaction_id: Default::default(), + error: Default::default(), + idle: true, + generation: Task::ready(()), + _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), + } + } + + fn handle_buffer_event( + &mut self, + _buffer: ModelHandle, + event: &multi_buffer::Event, + cx: &mut ModelContext, + ) { + if let multi_buffer::Event::TransactionUndone { transaction_id } = event { + if self.transaction_id == Some(*transaction_id) { + self.transaction_id = None; + self.generation = Task::ready(()); + cx.emit(Event::Undone); + } + } + } + + pub fn range(&self) -> Range { + match &self.kind { + CodegenKind::Transform { range } => range.clone(), + CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position, + } + } + + pub fn kind(&self) -> &CodegenKind { + &self.kind + } + + pub fn last_equal_ranges(&self) -> &[Range] { + &self.last_equal_ranges + } + + pub fn idle(&self) -> bool { + self.idle + } + + pub fn error(&self) -> Option<&anyhow::Error> { + self.error.as_ref() + } + + pub fn start(&mut self, prompt: OpenAIRequest, cx: &mut ModelContext) { + let range = self.range(); + let snapshot = self.snapshot.clone(); + let selected_text = snapshot + .text_for_range(range.start..range.end) + .collect::(); + + let selection_start = range.start.to_point(&snapshot); + let suggested_line_indent = snapshot + .suggested_indents(selection_start.row..selection_start.row + 1, cx) + .into_values() + .next() + .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); + + let response = self.provider.complete(prompt); + self.generation = cx.spawn_weak(|this, mut cx| { + async move { + let generate = async { + let mut edit_start = range.start.to_offset(&snapshot); + + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let chunks = strip_markdown_codeblock(response.await?); + futures::pin_mut!(chunks); + let mut diff = StreamingDiff::new(selected_text.to_string()); + + let mut new_text = String::new(); + let mut base_indent = None; + let mut line_indent = None; + let mut first_line = true; + + while let Some(chunk) = chunks.next().await { + let chunk = chunk?; + + let mut lines = chunk.split('\n').peekable(); + while let Some(line) = lines.next() { + new_text.push_str(line); + if line_indent.is_none() { + if let Some(non_whitespace_ch_ix) = + new_text.find(|ch: char| !ch.is_whitespace()) + { + line_indent = Some(non_whitespace_ch_ix); + base_indent = base_indent.or(line_indent); + + let line_indent = line_indent.unwrap(); + let base_indent = base_indent.unwrap(); + let indent_delta = line_indent as i32 - base_indent as i32; + let mut corrected_indent_len = cmp::max( + 0, + suggested_line_indent.len as i32 + indent_delta, + ) + as usize; + if first_line { + corrected_indent_len = corrected_indent_len + .saturating_sub(selection_start.column as usize); + } + + let indent_char = suggested_line_indent.char(); + let mut indent_buffer = [0; 4]; + let indent_str = + indent_char.encode_utf8(&mut indent_buffer); + new_text.replace_range( + ..line_indent, + &indent_str.repeat(corrected_indent_len), + ); + } + } + + if line_indent.is_some() { + hunks_tx.send(diff.push_new(&new_text)).await?; + new_text.clear(); + } + + if lines.peek().is_some() { + hunks_tx.send(diff.push_new("\n")).await?; + line_indent = None; + first_line = false; + } + } + } + hunks_tx.send(diff.push_new(&new_text)).await?; + hunks_tx.send(diff.finish()).await?; + + anyhow::Ok(()) + }); + + while let Some(hunks) = hunks_rx.next().await { + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; + + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + + let transaction = this.buffer.update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); + + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + this.last_equal_ranges.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + if let Some(first_transaction) = this.transaction_id { + // Group all assistant edits into the first transaction. + this.buffer.update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + this.transaction_id = Some(transaction); + this.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } + + cx.notify(); + }); + } + + diff.await?; + anyhow::Ok(()) + }; + + let result = generate.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + this.idle = true; + if let Err(error) = result { + this.error = Some(error); + } + cx.emit(Event::Finished); + cx.notify(); + }); + } + } + }); + self.error.take(); + self.idle = false; + cx.notify(); + } + + pub fn undo(&mut self, cx: &mut ModelContext) { + if let Some(transaction_id) = self.transaction_id { + self.buffer + .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } + } +} + +fn strip_markdown_codeblock( + stream: impl Stream>, +) -> impl Stream> { + let mut first_line = true; + let mut buffer = String::new(); + let mut starts_with_fenced_code_block = false; + stream.filter_map(move |chunk| { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => return future::ready(Some(Err(err))), + }; + buffer.push_str(&chunk); + + if first_line { + if buffer == "" || buffer == "`" || buffer == "``" { + return future::ready(None); + } else if buffer.starts_with("```") { + starts_with_fenced_code_block = true; + if let Some(newline_ix) = buffer.find('\n') { + buffer.replace_range(..newline_ix + 1, ""); + first_line = false; + } else { + return future::ready(None); + } + } + } + + let text = if starts_with_fenced_code_block { + buffer + .strip_suffix("\n```\n") + .or_else(|| buffer.strip_suffix("\n```")) + .or_else(|| buffer.strip_suffix("\n``")) + .or_else(|| buffer.strip_suffix("\n`")) + .or_else(|| buffer.strip_suffix('\n')) + .unwrap_or(&buffer) + } else { + &buffer + }; + + if text.contains('\n') { + first_line = false; + } + + let remainder = buffer.split_off(text.len()); + let result = if buffer.is_empty() { + None + } else { + Some(Ok(buffer.clone())) + }; + buffer = remainder; + future::ready(result) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::{ + future::BoxFuture, + stream::{self, BoxStream}, + }; + use gpui::{executor::Deterministic, TestAppContext}; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use parking_lot::Mutex; + use rand::prelude::*; + use settings::SettingsStore; + use smol::future::FutureExt; + + #[gpui::test(iterations = 10)] + async fn test_transform_autoindent( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + let x = 0; + for _ in 0..10 { + x += 1; + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let range = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Transform { range }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + " let mut x = 0;\n", + " while x < 10 {\n", + " x += 1;\n", + " }", + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_past_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + le + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 6)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + "t mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_before_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = concat!( + "fn main() {\n", + " \n", + "}\n" // + ); + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 2)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + "let mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test] + async fn test_strip_markdown_codeblock() { + assert_eq!( + strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "```js\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "``\nLorem ipsum dolor\n```" + ); + + fn chunks(text: &str, size: usize) -> impl Stream> { + stream::iter( + text.chars() + .collect::>() + .chunks(size) + .map(|chunk| Ok(chunk.iter().collect::())) + .collect::>(), + ) + } + } + + struct TestCompletionProvider { + last_completion_tx: Mutex>>, + } + + impl TestCompletionProvider { + fn new() -> Self { + Self { + last_completion_tx: Mutex::new(None), + } + } + + fn send_completion(&self, completion: impl Into) { + let mut tx = self.last_completion_tx.lock(); + tx.as_mut().unwrap().try_send(completion.into()).unwrap(); + } + + fn finish_completion(&self) { + self.last_completion_tx.lock().take().unwrap(); + } + } + + impl CompletionProvider for TestCompletionProvider { + fn complete( + &self, + _prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>> { + let (tx, rx) = mpsc::channel(1); + *self.last_completion_tx.lock() = Some(tx); + async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed() + } + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + } +} diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf041dff523d57d62cfbc3f312a350ad4766d160 --- /dev/null +++ b/crates/assistant/src/prompts.rs @@ -0,0 +1,404 @@ +use crate::codegen::CodegenKind; +use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; +use std::cmp::{self, Reverse}; +use std::fmt::Write; +use std::ops::Range; + +fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { + #[derive(Debug)] + struct Match { + collapse: Range, + keep: Vec>, + } + + let selected_range = selected_range.to_offset(buffer); + let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| { + Some(&grammar.embedding_config.as_ref()?.query) + }); + let configs = ts_matches + .grammars() + .iter() + .map(|g| g.embedding_config.as_ref().unwrap()) + .collect::>(); + let mut matches = Vec::new(); + while let Some(mat) = ts_matches.peek() { + let config = &configs[mat.grammar_index]; + if let Some(collapse) = mat.captures.iter().find_map(|cap| { + if Some(cap.index) == config.collapse_capture_ix { + Some(cap.node.byte_range()) + } else { + None + } + }) { + let mut keep = Vec::new(); + for capture in mat.captures.iter() { + if Some(capture.index) == config.keep_capture_ix { + keep.push(capture.node.byte_range()); + } else { + continue; + } + } + ts_matches.advance(); + matches.push(Match { collapse, keep }); + } else { + ts_matches.advance(); + } + } + matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end))); + let mut matches = matches.into_iter().peekable(); + + let mut summary = String::new(); + let mut offset = 0; + let mut flushed_selection = false; + while let Some(mat) = matches.next() { + // Keep extending the collapsed range if the next match surrounds + // the current one. + while let Some(next_mat) = matches.peek() { + if mat.collapse.start <= next_mat.collapse.start + && mat.collapse.end >= next_mat.collapse.end + { + matches.next().unwrap(); + } else { + break; + } + } + + if offset > mat.collapse.start { + // Skip collapsed nodes that have already been summarized. + offset = cmp::max(offset, mat.collapse.end); + continue; + } + + if offset <= selected_range.start && selected_range.start <= mat.collapse.end { + if !flushed_selection { + // The collapsed node ends after the selection starts, so we'll flush the selection first. + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|START|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); + } else { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|END|>"); + } + offset = selected_range.end; + flushed_selection = true; + } + + // If the selection intersects the collapsed node, we won't collapse it. + if selected_range.end >= mat.collapse.start { + continue; + } + } + + summary.extend(buffer.text_for_range(offset..mat.collapse.start)); + for keep in mat.keep { + summary.extend(buffer.text_for_range(keep)); + } + offset = mat.collapse.end; + } + + // Flush selection if we haven't already done so. + if !flushed_selection && offset <= selected_range.start { + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|START|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); + } else { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|END|>"); + } + offset = selected_range.end; + } + + summary.extend(buffer.text_for_range(offset..buffer.len())); + summary +} + +pub fn generate_content_prompt( + user_prompt: String, + language_name: Option<&str>, + buffer: &BufferSnapshot, + range: Range, + kind: CodegenKind, +) -> String { + let mut prompt = String::new(); + + // General Preamble + if let Some(language_name) = language_name { + writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap(); + } else { + writeln!(prompt, "You're an expert engineer.\n").unwrap(); + } + + let outline = summarize(buffer, range); + writeln!( + prompt, + "The file you are currently working on has the following outline:" + ) + .unwrap(); + if let Some(language_name) = language_name { + let language_name = language_name.to_lowercase(); + writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + } else { + writeln!(prompt, "```\n{outline}\n```").unwrap(); + } + + match kind { + CodegenKind::Generate { position: _ } => { + writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap(); + writeln!( + prompt, + "Assume the cursor is located where the `<|START|` marker is." + ) + .unwrap(); + writeln!( + prompt, + "Text can't be replaced, so assume your answer will be inserted at the cursor." + ) + .unwrap(); + writeln!( + prompt, + "Generate text based on the users prompt: {user_prompt}" + ) + .unwrap(); + } + CodegenKind::Transform { range: _ } => { + writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); + writeln!( + prompt, + "Modify the users code selected text based upon the users prompt: {user_prompt}" + ) + .unwrap(); + writeln!( + prompt, + "You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file." + ) + .unwrap(); + } + } + + if let Some(language_name) = language_name { + writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap(); + } + writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap(); + writeln!(prompt, "Never make remarks about the output.").unwrap(); + + prompt +} + +#[cfg(test)] +pub(crate) mod tests { + + use super::*; + use std::sync::Arc; + + use gpui::AppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use settings::SettingsStore; + + pub(crate) fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_embedding_query( + r#" + ( + [(line_comment) (attribute_item)]* @context + . + [ + (struct_item + name: (_) @name) + + (enum_item + name: (_) @name) + + (impl_item + trait: (_)? @name + "for"? @name + type: (_) @name) + + (trait_item + name: (_) @name) + + (function_item + name: (_) @name + body: (block + "{" @keep + "}" @keep) @collapse) + + (macro_definition + name: (_) @name) + ] @item + ) + "#, + ) + .unwrap() + } + + #[gpui::test] + fn test_outline_for_prompt(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + language_settings::init(cx); + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + self.a + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.read(cx).snapshot(); + + assert_eq!( + summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)), + indoc! {" + struct X { + <|START|>a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let <|START|a |END|>= 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + <|START|> + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + <|START|>"} + ); + + // Ensure nested functions get collapsed properly. + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + let a = 30; + fn nested() -> usize { + 3 + } + self.a + nested() + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let snapshot = buffer.read(cx).snapshot(); + assert_eq!( + summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)), + indoc! {" + <|START|>struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + } +} diff --git a/crates/ai/src/streaming_diff.rs b/crates/assistant/src/streaming_diff.rs similarity index 100% rename from crates/ai/src/streaming_diff.rs rename to crates/assistant/src/streaming_diff.rs diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 822886b58018e1cb8cef694a2cd2b8274a20c949..0d537b882a85fe5e7ce54f1270c8d7b28de1f9c4 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) { fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { - let server_url = &auto_updater.read(cx).server_url; + let auto_updater = auto_updater.read(cx); + let server_url = &auto_updater.server_url; + let current_version = auto_updater.current_version; let latest_release_url = if cx.has_global::() && *cx.global::() == ReleaseChannel::Preview { - format!("{server_url}/releases/preview/latest") + format!("{server_url}/releases/preview/{current_version}") } else { - format!("{server_url}/releases/stable/latest") + format!("{server_url}/releases/stable/{current_version}") }; cx.platform().open_url(&latest_release_url); } diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 8397fa0745f3aeb7a29659ce08190d374dc49829..e4a5c235346acd71cf7bf72ba46be94921097c04 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -50,7 +50,7 @@ impl View for UpdateNotification { .with_child( MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") + Svg::new("icons/x.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4db298fe98eb5718f1d641dd6539d2005f90ff4b..d86ed1be37e38da86bb9715e187528447e5b1abc 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -2,22 +2,23 @@ pub mod call_settings; pub mod participant; pub mod room; -use std::sync::Arc; - use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; use channel::ChannelId; -use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; +use client::{ + proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, + ZED_ALWAYS_ACTIVE, +}; use collections::HashSet; use futures::{future::Shared, FutureExt}; -use postage::watch; - use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, WeakModelHandle, }; +use postage::watch; use project::Project; +use std::sync::Arc; pub use participant::ParticipantLocation; pub use room::Room; @@ -68,6 +69,7 @@ impl ActiveCall { location: None, pending_invites: Default::default(), incoming_call: watch::channel(), + _subscriptions: vec![ client.add_request_handler(cx.handle(), Self::handle_incoming_call), client.add_message_handler(cx.handle(), Self::handle_call_canceled), @@ -206,9 +208,14 @@ impl ActiveCall { cx.spawn(|this, mut cx| async move { let result = invite.await; + if result.is_ok() { + this.update(&mut cx, |this, cx| this.report_call_event("invite", cx)); + } else { + // TODO: Resport collaboration error + } + this.update(&mut cx, |this, cx| { this.pending_invites.remove(&called_user_id); - this.report_call_event("invite", cx); cx.notify(); }); result @@ -273,13 +280,7 @@ impl ActiveCall { .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - Self::report_call_event_for_room( - "decline incoming", - Some(call.room_id), - None, - &self.client, - cx, - ); + report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -290,10 +291,10 @@ impl ActiveCall { &mut self, channel_id: u64, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { - return Task::ready(Ok(())); + return Task::ready(Ok(room)); } else { room.update(cx, |room, cx| room.clear_state(cx)); } @@ -308,7 +309,7 @@ impl ActiveCall { this.update(&mut cx, |this, cx| { this.report_call_event("join channel", cx) }); - Ok(()) + Ok(room) }) } @@ -349,17 +350,22 @@ impl ActiveCall { } } + pub fn location(&self) -> Option<&WeakModelHandle> { + self.location.as_ref() + } + pub fn set_location( &mut self, project: Option<&ModelHandle>, cx: &mut ModelContext, ) -> Task> { - self.location = project.map(|project| project.downgrade()); - if let Some((room, _)) = self.room.as_ref() { - room.update(cx, |room, cx| room.set_location(project, cx)) - } else { - Task::ready(Ok(())) + if project.is_some() || !*ZED_ALWAYS_ACTIVE { + self.location = project.map(|project| project.downgrade()); + if let Some((room, _)) = self.room.as_ref() { + return room.update(cx, |room, cx| room.set_location(project, cx)); + } } + Task::ready(Ok(())) } fn set_room( @@ -409,31 +415,46 @@ impl ActiveCall { &self.pending_invites } - fn report_call_event(&self, operation: &'static str, cx: &AppContext) { - let (room_id, channel_id) = match self.room() { - Some(room) => { - let room = room.read(cx); - (Some(room.id()), room.channel_id()) - } - None => (None, None), - }; - Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx) + pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + if let Some(room) = self.room() { + let room = room.read(cx); + 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: Option, - channel_id: Option, - client: &Arc, - cx: &AppContext, - ) { - let telemetry = client.telemetry(); - let telemetry_settings = *settings::get::(cx); - let event = ClickhouseEvent::Call { - operation, - room_id, - channel_id, - }; - telemetry.report_clickhouse_event(event, telemetry_settings); - } +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: Some(room_id), + channel_id, + }; + telemetry.report_clickhouse_event(event, telemetry_settings); +} + +pub fn report_call_event_for_channel( + operation: &'static str, + channel_id: u64, + client: &Arc, + cx: &AppContext, +) { + let room = ActiveCall::global(cx).read(cx).room(); + + let telemetry = client.telemetry(); + let telemetry_settings = *settings::get::(cx); + + let event = ClickhouseEvent::Call { + operation, + room_id: room.map(|r| r.read(cx).id()), + channel_id: Some(channel_id), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); } diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index e7858869ce63906b75f9cd0cb117cf7b54283efd..ab796e56b08d3a8b1a00b541135f90edada3a645 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use client::ParticipantIndex; use client::{proto, User}; use collections::HashMap; use gpui::WeakModelHandle; @@ -43,6 +44,7 @@ pub struct RemoteParticipant { pub peer_id: proto::PeerId, pub projects: Vec, pub location: ParticipantLocation, + pub participant_index: ParticipantIndex, pub muted: bool, pub speaking: bool, pub video_tracks: HashMap>, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index cc7445dbcc74ff620968c9ff5a2a99686bd800d9..72db174d7256b0a5686bee0210bb5c37464bc97d 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, User, UserStore, + Client, ParticipantIndex, TypedEnvelope, User, UserStore, }; use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; @@ -44,6 +44,12 @@ pub enum Event { RemoteProjectUnshared { project_id: u64, }, + RemoteProjectJoined { + project_id: u64, + }, + RemoteProjectInvitationDiscarded { + project_id: u64, + }, Left, } @@ -98,6 +104,10 @@ impl Room { self.channel_id } + pub fn is_sharing_project(&self) -> bool { + !self.shared_projects.is_empty() + } + #[cfg(any(test, feature = "test-support"))] pub fn is_connected(&self) -> bool { if let Some(live_kit) = self.live_kit.as_ref() { @@ -172,7 +182,7 @@ impl Room { cx.spawn(|this, mut cx| async move { connect.await?; - if !cx.read(|cx| settings::get::(cx).mute_on_join) { + if !cx.read(Self::mute_on_join) { this.update(&mut cx, |this, cx| this.share_microphone(cx)) .await?; } @@ -301,6 +311,10 @@ impl Room { }) } + pub fn mute_on_join(cx: &AppContext) -> bool { + settings::get::(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + } + fn from_join_response( response: proto::JoinRoomResponse, client: Arc, @@ -584,6 +598,31 @@ impl Room { .map_or(&[], |v| v.as_slice()) } + /// Returns the most 'active' projects, defined as most people in the project + pub fn most_active_project(&self) -> Option<(u64, u64)> { + let mut projects = HashMap::default(); + let mut hosts = HashMap::default(); + for participant in self.remote_participants.values() { + match participant.location { + ParticipantLocation::SharedProject { project_id } => { + *projects.entry(project_id).or_insert(0) += 1; + } + ParticipantLocation::External | ParticipantLocation::UnsharedProject => {} + } + for project in &participant.projects { + *projects.entry(project.id).or_insert(0) += 1; + hosts.insert(project.id, participant.user.id); + } + } + + let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect(); + pairs.sort_by_key(|(_, count)| *count as i32); + + pairs + .first() + .map(|(project_id, _)| (*project_id, hosts[&project_id])) + } + async fn handle_room_updated( this: ModelHandle, envelope: TypedEnvelope, @@ -710,6 +749,9 @@ impl Room { participant.user_id, RemoteParticipant { user: user.clone(), + participant_index: ParticipantIndex( + participant.participant_index, + ), peer_id, projects: participant.projects, location, @@ -803,6 +845,15 @@ impl Room { let _ = this.leave(cx); } + this.user_store.update(cx, |user_store, cx| { + let participant_indices_by_user_id = this + .remote_participants + .iter() + .map(|(user_id, participant)| (*user_id, participant.participant_index)) + .collect(); + user_store.set_participant_indices(participant_indices_by_user_id, cx); + }); + this.check_invariants(); cx.notify(); }); @@ -999,6 +1050,7 @@ impl Room { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); + cx.emit(Event::RemoteProjectJoined { project_id: id }); cx.spawn(|this, mut cx| async move { let project = Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; @@ -1124,7 +1176,7 @@ impl Room { self.live_kit .as_ref() .and_then(|live_kit| match &live_kit.microphone_track { - LocalTrack::None => Some(settings::get::(cx).mute_on_join), + LocalTrack::None => Some(Self::mute_on_join(cx)), LocalTrack::Pending { muted, .. } => Some(*muted), LocalTrack::Published { muted, .. } => Some(*muted), }) diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index c2191fdfa3edaaf0824e5e59ed974a7c53030ccd..6bd177bed573bc8a034dbb50588c77b111422242 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -23,11 +23,13 @@ language = { path = "../language" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +clock = { path = "../clock" } anyhow.workspace = true futures.workspace = true image = "0.23" lazy_static.workspace = true +smallvec.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true @@ -37,7 +39,7 @@ smol.workspace = true thiserror.workspace = true time.workspace = true tiny_http = "0.8" -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true url = "2.2" serde.workspace = true serde_derive.workspace = true @@ -47,5 +49,6 @@ tempfile = "3" collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } +client = { path = "../client", 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 index 15631b7dd312f36126ec1e13b2413fc01e5ca8af..160b8441ffd74f1ca835c70234fcbb166c7fa477 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -1,14 +1,20 @@ +mod channel_buffer; +mod channel_chat; mod channel_store; -pub mod channel_buffer; -use std::sync::Arc; +pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; +pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId}; +pub use channel_store::{ + Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, +}; -pub use channel_store::*; use client::Client; +use std::sync::Arc; #[cfg(test)] mod channel_store_tests; pub fn init(client: &Arc) { channel_buffer::init(client); + channel_chat::init(client); } diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index e11282cf7963a9ba4f34c7530a6e8267fbe35274..7de8b956f1e897b4293274441689570f63780dcb 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,38 +1,49 @@ 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 client::{Client, Collaborator, UserStore}; +use collections::HashMap; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use language::proto::serialize_version; +use rpc::{ + proto::{self, PeerId}, + TypedEnvelope, +}; +use std::{sync::Arc, time::Duration}; use util::ResultExt; +pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250); + 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); - client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators); } pub struct ChannelBuffer { pub(crate) channel: Arc, connected: bool, - collaborators: Vec, + collaborators: HashMap, + user_store: ModelHandle, buffer: ModelHandle, buffer_epoch: u64, client: Arc, subscription: Option, + acknowledge_task: Option>>, } -pub enum Event { +pub enum ChannelBufferEvent { CollaboratorsChanged, Disconnected, + BufferEdited, } impl Entity for ChannelBuffer { - type Event = Event; + type Event = ChannelBufferEvent; fn release(&mut self, _: &mut AppContext) { if self.connected { + if let Some(task) = self.acknowledge_task.take() { + task.detach(); + } self.client .send(proto::LeaveChannelBuffer { channel_id: self.channel.id, @@ -46,6 +57,7 @@ impl ChannelBuffer { pub(crate) async fn new( channel: Arc, client: Arc, + user_store: ModelHandle, mut cx: AsyncAppContext, ) -> Result> { let response = client @@ -61,8 +73,6 @@ impl ChannelBuffer { .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) }); @@ -73,35 +83,47 @@ impl ChannelBuffer { anyhow::Ok(cx.add_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); - Self { + let mut this = Self { buffer, buffer_epoch: response.epoch, client, connected: true, - collaborators, + collaborators: Default::default(), + acknowledge_task: None, channel, subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), - } + user_store, + }; + this.replace_collaborators(response.collaborators, cx); + this })) } + pub fn user_store(&self) -> &ModelHandle { + &self.user_store + } + pub(crate) fn replace_collaborators( &mut self, collaborators: Vec, cx: &mut ModelContext, ) { - for old_collaborator in &self.collaborators { - if collaborators - .iter() - .any(|c| c.replica_id == old_collaborator.replica_id) - { + let mut new_collaborators = HashMap::default(); + for collaborator in collaborators { + if let Ok(collaborator) = Collaborator::from_proto(collaborator) { + new_collaborators.insert(collaborator.peer_id, collaborator); + } + } + + for (_, old_collaborator) in &self.collaborators { + if !new_collaborators.contains_key(&old_collaborator.peer_id) { self.buffer.update(cx, |buffer, cx| { buffer.remove_peer(old_collaborator.replica_id as u16, cx) }); } } - self.collaborators = collaborators; - cx.emit(Event::CollaboratorsChanged); + self.collaborators = new_collaborators; + cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); } @@ -127,65 +149,15 @@ impl ChannelBuffer { 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( + async fn handle_update_channel_buffer_collaborators( this: ModelHandle, - message: TypedEnvelope, + 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(()) - } - - async fn handle_update_channel_buffer_collaborator( - this: ModelHandle, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - for collaborator in &mut this.collaborators { - if collaborator.peer_id == message.payload.old_peer_id { - collaborator.peer_id = message.payload.new_peer_id; - break; - } - } - cx.emit(Event::CollaboratorsChanged); + this.replace_collaborators(message.payload.collaborators, cx); + cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); }); @@ -196,19 +168,45 @@ impl ChannelBuffer { &mut self, _: ModelHandle, event: &language::Event, - _: &mut ModelContext, + cx: &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(); + match event { + language::Event::Operation(operation) => { + let operation = language::proto::serialize_operation(operation); + self.client + .send(proto::UpdateChannelBuffer { + channel_id: self.channel.id, + operations: vec![operation], + }) + .log_err(); + } + language::Event::Edited => { + cx.emit(ChannelBufferEvent::BufferEdited); + } + _ => {} } } + pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) { + let buffer = self.buffer.read(cx); + let version = buffer.version(); + let buffer_id = buffer.remote_id(); + let client = self.client.clone(); + let epoch = self.epoch(); + + self.acknowledge_task = Some(cx.spawn_weak(|_, cx| async move { + cx.background().timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL).await; + client + .send(proto::AckBufferOperation { + buffer_id, + epoch, + version: serialize_version(&version), + }) + .ok(); + Ok(()) + })); + } + pub fn epoch(&self) -> u64 { self.buffer_epoch } @@ -217,7 +215,7 @@ impl ChannelBuffer { self.buffer.clone() } - pub fn collaborators(&self) -> &[proto::Collaborator] { + pub fn collaborators(&self) -> &HashMap { &self.collaborators } @@ -230,7 +228,7 @@ impl ChannelBuffer { if self.connected { self.connected = false; self.subscription.take(); - cx.emit(Event::Disconnected); + cx.emit(ChannelBufferEvent::Disconnected); cx.notify() } } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs new file mode 100644 index 0000000000000000000000000000000000000000..734182886b3bebeacd03dbc177bf8ffcb8ab64e2 --- /dev/null +++ b/crates/channel/src/channel_chat.rs @@ -0,0 +1,540 @@ +use crate::{Channel, ChannelId, ChannelStore}; +use anyhow::{anyhow, Result}; +use client::{ + proto, + user::{User, UserStore}, + Client, Subscription, TypedEnvelope, +}; +use futures::lock::Mutex; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use rand::prelude::*; +use std::{collections::HashSet, mem, ops::Range, sync::Arc}; +use sum_tree::{Bias, SumTree}; +use time::OffsetDateTime; +use util::{post_inc, ResultExt as _, TryFutureExt}; + +pub struct ChannelChat { + channel: Arc, + messages: SumTree, + channel_store: ModelHandle, + loaded_all_messages: bool, + last_acknowledged_id: Option, + next_pending_message_id: usize, + user_store: ModelHandle, + rpc: Arc, + outgoing_messages_lock: Arc>, + rng: StdRng, + _subscription: Subscription, +} + +#[derive(Clone, Debug)] +pub struct ChannelMessage { + pub id: ChannelMessageId, + pub body: String, + pub timestamp: OffsetDateTime, + pub sender: Arc, + pub nonce: u128, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ChannelMessageId { + Saved(u64), + Pending(usize), +} + +#[derive(Clone, Debug, Default)] +pub struct ChannelMessageSummary { + max_id: ChannelMessageId, + count: usize, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct Count(usize); + +#[derive(Clone, Debug, PartialEq)] +pub enum ChannelChatEvent { + MessagesUpdated { + old_range: Range, + new_count: usize, + }, + NewMessage { + channel_id: ChannelId, + message_id: u64, + }, +} + +pub fn init(client: &Arc) { + client.add_model_message_handler(ChannelChat::handle_message_sent); + client.add_model_message_handler(ChannelChat::handle_message_removed); +} + +impl Entity for ChannelChat { + type Event = ChannelChatEvent; + + fn release(&mut self, _: &mut AppContext) { + self.rpc + .send(proto::LeaveChannelChat { + channel_id: self.channel.id, + }) + .log_err(); + } +} + +impl ChannelChat { + pub async fn new( + channel: Arc, + channel_store: ModelHandle, + user_store: ModelHandle, + client: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let channel_id = channel.id; + let subscription = client.subscribe_to_entity(channel_id).unwrap(); + + let response = client + .request(proto::JoinChannelChat { channel_id }) + .await?; + let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; + let loaded_all_messages = response.done; + + Ok(cx.add_model(|cx| { + let mut this = Self { + channel, + user_store, + channel_store, + rpc: client, + outgoing_messages_lock: Default::default(), + messages: Default::default(), + loaded_all_messages, + next_pending_message_id: 0, + last_acknowledged_id: None, + rng: StdRng::from_entropy(), + _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + }; + this.insert_messages(messages, cx); + this + })) + } + + pub fn channel(&self) -> &Arc { + &self.channel + } + + pub fn send_message( + &mut self, + body: String, + cx: &mut ModelContext, + ) -> Result>> { + if body.is_empty() { + Err(anyhow!("message body can't be empty"))?; + } + + let current_user = self + .user_store + .read(cx) + .current_user() + .ok_or_else(|| anyhow!("current_user is not present"))?; + + let channel_id = self.channel.id; + let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id)); + let nonce = self.rng.gen(); + self.insert_messages( + SumTree::from_item( + ChannelMessage { + id: pending_id, + body: body.clone(), + sender: current_user, + timestamp: OffsetDateTime::now_utc(), + nonce, + }, + &(), + ), + cx, + ); + let user_store = self.user_store.clone(); + let rpc = self.rpc.clone(); + let outgoing_messages_lock = self.outgoing_messages_lock.clone(); + Ok(cx.spawn(|this, mut cx| async move { + let outgoing_message_guard = outgoing_messages_lock.lock().await; + let request = rpc.request(proto::SendChannelMessage { + channel_id, + body, + nonce: Some(nonce.into()), + }); + let response = request.await?; + drop(outgoing_message_guard); + let message = ChannelMessage::from_proto( + response.message.ok_or_else(|| anyhow!("invalid message"))?, + &user_store, + &mut cx, + ) + .await?; + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx); + Ok(()) + }) + })) + } + + pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext) -> Task> { + let response = self.rpc.request(proto::RemoveChannelMessage { + channel_id: self.channel.id, + message_id: id, + }); + cx.spawn(|this, mut cx| async move { + response.await?; + + this.update(&mut cx, |this, cx| { + this.message_removed(id, cx); + Ok(()) + }) + }) + } + + pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> bool { + if !self.loaded_all_messages { + let rpc = self.rpc.clone(); + let user_store = self.user_store.clone(); + let channel_id = self.channel.id; + if let Some(before_message_id) = + self.messages.first().and_then(|message| match message.id { + ChannelMessageId::Saved(id) => Some(id), + ChannelMessageId::Pending(_) => None, + }) + { + cx.spawn(|this, mut cx| { + async move { + let response = rpc + .request(proto::GetChannelMessages { + channel_id, + before_message_id, + }) + .await?; + let loaded_all_messages = response.done; + let messages = + messages_from_proto(response.messages, &user_store, &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.loaded_all_messages = loaded_all_messages; + this.insert_messages(messages, cx); + }); + anyhow::Ok(()) + } + .log_err() + }) + .detach(); + return true; + } + } + false + } + + pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext) { + if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id { + if self + .last_acknowledged_id + .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id) + { + self.rpc + .send(proto::AckChannelMessage { + channel_id: self.channel.id, + message_id: latest_message_id, + }) + .ok(); + self.last_acknowledged_id = Some(latest_message_id); + self.channel_store.update(cx, |store, cx| { + store.acknowledge_message_id(self.channel.id, latest_message_id, cx); + }); + } + } + } + + pub fn rejoin(&mut self, cx: &mut ModelContext) { + let user_store = self.user_store.clone(); + let rpc = self.rpc.clone(); + let channel_id = self.channel.id; + cx.spawn(|this, mut cx| { + async move { + let response = rpc.request(proto::JoinChannelChat { channel_id }).await?; + let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; + let loaded_all_messages = response.done; + + let pending_messages = this.update(&mut cx, |this, cx| { + if let Some((first_new_message, last_old_message)) = + messages.first().zip(this.messages.last()) + { + if first_new_message.id > last_old_message.id { + let old_messages = mem::take(&mut this.messages); + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: 0..old_messages.summary().count, + new_count: 0, + }); + this.loaded_all_messages = loaded_all_messages; + } + } + + this.insert_messages(messages, cx); + if loaded_all_messages { + this.loaded_all_messages = loaded_all_messages; + } + + this.pending_messages().cloned().collect::>() + }); + + for pending_message in pending_messages { + let request = rpc.request(proto::SendChannelMessage { + channel_id, + body: pending_message.body, + nonce: Some(pending_message.nonce.into()), + }); + let response = request.await?; + let message = ChannelMessage::from_proto( + response.message.ok_or_else(|| anyhow!("invalid message"))?, + &user_store, + &mut cx, + ) + .await?; + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx); + }); + } + + anyhow::Ok(()) + } + .log_err() + }) + .detach(); + } + + pub fn message_count(&self) -> usize { + self.messages.summary().count + } + + pub fn messages(&self) -> &SumTree { + &self.messages + } + + pub fn message(&self, ix: usize) -> &ChannelMessage { + let mut cursor = self.messages.cursor::(); + cursor.seek(&Count(ix), Bias::Right, &()); + cursor.item().unwrap() + } + + pub fn messages_in_range(&self, range: Range) -> impl Iterator { + let mut cursor = self.messages.cursor::(); + cursor.seek(&Count(range.start), Bias::Right, &()); + cursor.take(range.len()) + } + + pub fn pending_messages(&self) -> impl Iterator { + let mut cursor = self.messages.cursor::(); + cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &()); + cursor + } + + async fn handle_message_sent( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let message = message + .payload + .message + .ok_or_else(|| anyhow!("empty message"))?; + let message_id = message.id; + + let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx); + cx.emit(ChannelChatEvent::NewMessage { + channel_id: this.channel.id, + message_id, + }) + }); + + Ok(()) + } + + async fn handle_message_removed( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.message_removed(message.payload.message_id, cx) + }); + Ok(()) + } + + fn insert_messages(&mut self, messages: SumTree, cx: &mut ModelContext) { + if let Some((first_message, last_message)) = messages.first().zip(messages.last()) { + let nonces = messages + .cursor::<()>() + .map(|m| m.nonce) + .collect::>(); + + let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>(); + let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &()); + let start_ix = old_cursor.start().1 .0; + let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &()); + let removed_count = removed_messages.summary().count; + let new_count = messages.summary().count; + let end_ix = start_ix + removed_count; + + new_messages.append(messages, &()); + + let mut ranges = Vec::>::new(); + if new_messages.last().unwrap().is_pending() { + new_messages.append(old_cursor.suffix(&()), &()); + } else { + new_messages.append( + old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()), + &(), + ); + + while let Some(message) = old_cursor.item() { + let message_ix = old_cursor.start().1 .0; + if nonces.contains(&message.nonce) { + if ranges.last().map_or(false, |r| r.end == message_ix) { + ranges.last_mut().unwrap().end += 1; + } else { + ranges.push(message_ix..message_ix + 1); + } + } else { + new_messages.push(message.clone(), &()); + } + old_cursor.next(&()); + } + } + + drop(old_cursor); + self.messages = new_messages; + + for range in ranges.into_iter().rev() { + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: range, + new_count: 0, + }); + } + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: start_ix..end_ix, + new_count, + }); + + cx.notify(); + } + } + + fn message_removed(&mut self, id: u64, cx: &mut ModelContext) { + let mut cursor = self.messages.cursor::(); + let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &()); + if let Some(item) = cursor.item() { + if item.id == ChannelMessageId::Saved(id) { + let ix = messages.summary().count; + cursor.next(&()); + messages.append(cursor.suffix(&()), &()); + drop(cursor); + self.messages = messages; + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: ix..ix + 1, + new_count: 0, + }); + } + } + } +} + +async fn messages_from_proto( + proto_messages: Vec, + user_store: &ModelHandle, + cx: &mut AsyncAppContext, +) -> Result> { + let unique_user_ids = proto_messages + .iter() + .map(|m| m.sender_id) + .collect::>() + .into_iter() + .collect(); + user_store + .update(cx, |user_store, cx| { + user_store.get_users(unique_user_ids, cx) + }) + .await?; + + let mut messages = Vec::with_capacity(proto_messages.len()); + for message in proto_messages { + messages.push(ChannelMessage::from_proto(message, user_store, cx).await?); + } + let mut result = SumTree::new(); + result.extend(messages, &()); + Ok(result) +} + +impl ChannelMessage { + pub async fn from_proto( + message: proto::ChannelMessage, + user_store: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> Result { + let sender = user_store + .update(cx, |user_store, cx| { + user_store.get_user(message.sender_id, cx) + }) + .await?; + Ok(ChannelMessage { + id: ChannelMessageId::Saved(message.id), + body: message.body, + timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?, + sender, + nonce: message + .nonce + .ok_or_else(|| anyhow!("nonce is required"))? + .into(), + }) + } + + pub fn is_pending(&self) -> bool { + matches!(self.id, ChannelMessageId::Pending(_)) + } +} + +impl sum_tree::Item for ChannelMessage { + type Summary = ChannelMessageSummary; + + fn summary(&self) -> Self::Summary { + ChannelMessageSummary { + max_id: self.id, + count: 1, + } + } +} + +impl Default for ChannelMessageId { + fn default() -> Self { + Self::Saved(0) + } +} + +impl sum_tree::Summary for ChannelMessageSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.max_id = summary.max_id; + self.count += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + debug_assert!(summary.max_id > *self); + *self = summary.max_id; + } +} + +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + self.0 += summary.count; + } +} diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a4c8da6df4f594553ac3629a4d4fc4a1176d89a3..bd72c92c7db768c558f7bc1b39a371f01f5dfd6c 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,26 +1,34 @@ -use crate::channel_buffer::ChannelBuffer; +mod channel_index; + +use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; -use rpc::{proto, TypedEnvelope}; -use std::{mem, sync::Arc, time::Duration}; +use rpc::{ + proto::{self, ChannelEdge, ChannelPermission}, + TypedEnvelope, +}; +use serde_derive::{Deserialize, Serialize}; +use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration}; use util::ResultExt; +use self::channel_index::ChannelIndex; + pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub type ChannelId = u64; pub struct ChannelStore { - channels_by_id: HashMap>, - channel_paths: Vec>, + channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, - opened_buffers: HashMap, + opened_buffers: HashMap>, + opened_chats: HashMap>, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -29,12 +37,19 @@ pub struct ChannelStore { _update_channels: Task<()>, } +pub type ChannelData = (Channel, ChannelPath); + #[derive(Clone, Debug, PartialEq)] pub struct Channel { pub id: ChannelId, pub name: String, + pub unseen_note_version: Option<(u64, clock::Global)>, + pub unseen_message_id: Option, } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] +pub struct ChannelPath(Arc<[ChannelId]>); + pub struct ChannelMembership { pub user: Arc, pub kind: proto::channel_member::Kind, @@ -50,15 +65,9 @@ impl Entity for ChannelStore { type Event = ChannelEvent; } -pub enum ChannelMemberStatus { - Invited, - Member, - NotMember, -} - -enum OpenedChannelBuffer { - Open(WeakModelHandle), - Loading(Shared, Arc>>>), +enum OpenedModelHandle { + Open(WeakModelHandle), + Loading(Shared, Arc>>>), } impl ChannelStore { @@ -87,13 +96,13 @@ impl ChannelStore { }); Self { - channels_by_id: HashMap::default(), channel_invitations: Vec::default(), - channel_paths: Vec::default(), + channel_index: ChannelIndex::default(), channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), + opened_chats: Default::default(), update_channels_tx, client, user_store, @@ -115,8 +124,12 @@ impl ChannelStore { } } + pub fn client(&self) -> Arc { + self.client.clone() + } + pub fn has_children(&self, channel_id: ChannelId) -> bool { - self.channel_paths.iter().any(|path| { + self.channel_index.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { path.len() > ix + 1 } else { @@ -125,23 +138,43 @@ impl ChannelStore { }) } + /// Returns the number of unique channels in the store pub fn channel_count(&self) -> usize { - self.channel_paths.len() + self.channel_index.by_id().len() + } + + /// Returns the index of a channel ID in the list of unique channels + pub fn index_of_channel(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .keys() + .position(|id| *id == channel_id) } - pub fn channels(&self) -> impl '_ + Iterator)> { - self.channel_paths.iter().map(move |path| { + /// Returns an iterator over all unique channels + pub fn channels(&self) -> impl '_ + Iterator> { + self.channel_index.by_id().values() + } + + /// Iterate over all entries in the channel DAG + pub fn channel_dag_entries(&self) -> impl '_ + Iterator)> { + self.channel_index.iter().map(move |path| { let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); (path.len() - 1, channel) }) } - pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc)> { - let path = self.channel_paths.get(ix)?; + pub fn channel_dag_entry_at(&self, ix: usize) -> Option<(&Arc, &ChannelPath)> { + let path = self.channel_index.get(ix)?; let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); - Some((path.len() - 1, channel)) + + Some((channel, path)) + } + + pub fn channel_at(&self, ix: usize) -> Option<&Arc> { + self.channel_index.by_id().values().nth(ix) } pub fn channel_invitations(&self) -> &[Arc] { @@ -149,12 +182,12 @@ impl ChannelStore { } pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc> { - self.channels_by_id.get(&channel_id) + self.channel_index.by_id().get(&channel_id) } pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool { if let Some(buffer) = self.opened_buffers.get(&channel_id) { - if let OpenedChannelBuffer::Open(buffer) = buffer { + if let OpenedModelHandle::Open(buffer) = buffer { return buffer.upgrade(cx).is_some(); } } @@ -166,24 +199,122 @@ impl ChannelStore { 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 client = self.client.clone(); + let user_store = self.user_store.clone(); + self.open_channel_resource( + channel_id, + |this| &mut this.opened_buffers, + |channel, cx| ChannelBuffer::new(channel, client, user_store, cx), + cx, + ) + } + + pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .get(&channel_id) + .map(|channel| channel.unseen_note_version.is_some()) + } + + pub fn has_new_messages(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .get(&channel_id) + .map(|channel| channel.unseen_message_id.is_some()) + } + + pub fn notes_changed( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + cx: &mut ModelContext, + ) { + self.channel_index.note_changed(channel_id, epoch, version); + cx.notify(); + } + + pub fn new_message( + &mut self, + channel_id: ChannelId, + message_id: u64, + cx: &mut ModelContext, + ) { + self.channel_index.new_message(channel_id, message_id); + cx.notify(); + } + + pub fn acknowledge_message_id( + &mut self, + channel_id: ChannelId, + message_id: u64, + cx: &mut ModelContext, + ) { + self.channel_index + .acknowledge_message_id(channel_id, message_id); + cx.notify(); + } + + pub fn acknowledge_notes_version( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + cx: &mut ModelContext, + ) { + self.channel_index + .acknowledge_note_version(channel_id, epoch, version); + cx.notify(); + } + + pub fn open_channel_chat( + &mut self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let this = cx.handle(); + self.open_channel_resource( + channel_id, + |this| &mut this.opened_chats, + |channel, cx| ChannelChat::new(channel, this, user_store, client, cx), + cx, + ) + } + + /// Asynchronously open a given resource associated with a channel. + /// + /// Make sure that the resource is only opened once, even if this method + /// is called multiple times with the same channel id while the first task + /// is still running. + fn open_channel_resource( + &mut self, + channel_id: ChannelId, + get_map: fn(&mut Self) -> &mut HashMap>, + load: F, + cx: &mut ModelContext, + ) -> Task>> + where + F: 'static + FnOnce(Arc, AsyncAppContext) -> Fut, + Fut: Future>>, + { let task = loop { - match self.opened_buffers.entry(channel_id) { + match get_map(self).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(); + OpenedModelHandle::Open(model) => { + if let Some(model) = model.upgrade(cx) { + break Task::ready(Ok(model)).shared(); } else { - self.opened_buffers.remove(&channel_id); + get_map(self).remove(&channel_id); continue; } } - OpenedChannelBuffer::Loading(task) => break task.clone(), + OpenedModelHandle::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, _| { @@ -192,30 +323,24 @@ impl ChannelStore { }) })?; - ChannelBuffer::new(channel, client, cx) - .await - .map_err(Arc::new) + load(channel, cx).await.map_err(Arc::new) }) .shared(); - e.insert(OpenedChannelBuffer::Loading(task.clone())); + + e.insert(OpenedModelHandle::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( + this.update(&mut cx, |this, _| match result { + Ok(model) => { + get_map(this).insert( channel_id, - OpenedChannelBuffer::Open(buffer.downgrade()), + OpenedModelHandle::Open(model.downgrade()), ); } - Err(error) => { - log::error!("failed to open channel buffer {error:?}"); - this.opened_buffers.remove(&channel_id); + Err(_) => { + get_map(this).remove(&channel_id); } }); } @@ -230,7 +355,7 @@ impl ChannelStore { } pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { - self.channel_paths.iter().any(|path| { + self.channel_index.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { path[..=ix] .iter() @@ -256,18 +381,33 @@ impl ChannelStore { let client = self.client.clone(); let name = name.trim_start_matches("#").to_owned(); cx.spawn(|this, mut cx| async move { - let channel = client + let response = client .request(proto::CreateChannel { name, parent_id }) - .await? + .await?; + + let channel = response .channel .ok_or_else(|| anyhow!("missing channel in response"))?; - let channel_id = channel.id; + let parent_edge = if let Some(parent_id) = parent_id { + vec![ChannelEdge { + channel_id: channel.id, + parent_id, + }] + } else { + vec![] + }; + this.update(&mut cx, |this, cx| { let task = this.update_channels( proto::UpdateChannels { channels: vec![channel], + insert_edge: parent_edge, + channel_permissions: vec![ChannelPermission { + channel_id, + is_admin: true, + }], ..Default::default() }, cx, @@ -285,6 +425,59 @@ impl ChannelStore { }) } + pub fn link_channel( + &mut self, + channel_id: ChannelId, + to: ChannelId, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(|_, _| async move { + let _ = client + .request(proto::LinkChannel { channel_id, to }) + .await?; + + Ok(()) + }) + } + + pub fn unlink_channel( + &mut self, + channel_id: ChannelId, + from: ChannelId, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(|_, _| async move { + let _ = client + .request(proto::UnlinkChannel { channel_id, from }) + .await?; + + Ok(()) + }) + } + + pub fn move_channel( + &mut self, + channel_id: ChannelId, + from: ChannelId, + to: ChannelId, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(|_, _| async move { + let _ = client + .request(proto::MoveChannel { + channel_id, + from, + to, + }) + .await?; + + Ok(()) + }) + } + pub fn invite_member( &mut self, channel_id: ChannelId, @@ -464,7 +657,7 @@ impl ChannelStore { pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { let client = self.client.clone(); async move { - client.request(proto::RemoveChannel { channel_id }).await?; + client.request(proto::DeleteChannel { channel_id }).await?; Ok(()) } } @@ -494,9 +687,19 @@ impl ChannelStore { fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { self.disconnect_channel_buffers_task.take(); + for chat in self.opened_chats.values() { + if let OpenedModelHandle::Open(chat) = chat { + if let Some(chat) = chat.upgrade(cx) { + chat.update(cx, |chat, cx| { + chat.rejoin(cx); + }); + } + } + } + let mut buffer_versions = Vec::new(); for buffer in self.opened_buffers.values() { - if let OpenedChannelBuffer::Open(buffer) = buffer { + if let OpenedModelHandle::Open(buffer) = buffer { if let Some(buffer) = buffer.upgrade(cx) { let channel_buffer = buffer.read(cx); let buffer = channel_buffer.buffer().read(cx); @@ -522,7 +725,7 @@ impl ChannelStore { this.update(&mut cx, |this, cx| { this.opened_buffers.retain(|_, buffer| match buffer { - OpenedChannelBuffer::Open(channel_buffer) => { + OpenedModelHandle::Open(channel_buffer) => { let Some(channel_buffer) = channel_buffer.upgrade(cx) else { return false; }; @@ -583,7 +786,7 @@ impl ChannelStore { false }) } - OpenedChannelBuffer::Loading(_) => true, + OpenedModelHandle::Loading(_) => true, }); }); anyhow::Ok(()) @@ -591,11 +794,11 @@ impl ChannelStore { } fn handle_disconnect(&mut self, cx: &mut ModelContext) { - self.channels_by_id.clear(); + self.channel_index.clear(); self.channel_invitations.clear(); self.channel_participants.clear(); self.channels_with_admin_privileges.clear(); - self.channel_paths.clear(); + self.channel_index.clear(); self.outgoing_invites.clear(); cx.notify(); @@ -605,7 +808,7 @@ impl ChannelStore { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { for (_, buffer) in this.opened_buffers.drain() { - if let OpenedChannelBuffer::Open(buffer) = buffer { + if let OpenedModelHandle::Open(buffer) = buffer { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); } @@ -637,24 +840,31 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, + unseen_note_version: None, + unseen_message_id: None, }), ), } } - let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty(); + let channels_changed = !payload.channels.is_empty() + || !payload.delete_channels.is_empty() + || !payload.insert_edge.is_empty() + || !payload.delete_edge.is_empty() + || !payload.unseen_channel_messages.is_empty() + || !payload.unseen_channel_buffer_changes.is_empty(); + if channels_changed { - if !payload.remove_channels.is_empty() { - self.channels_by_id - .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); + if !payload.delete_channels.is_empty() { + self.channel_index.delete_channels(&payload.delete_channels); self.channel_participants - .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); + .retain(|channel_id, _| !payload.delete_channels.contains(channel_id)); self.channels_with_admin_privileges - .retain(|channel_id| !payload.remove_channels.contains(channel_id)); + .retain(|channel_id| !payload.delete_channels.contains(channel_id)); - for channel_id in &payload.remove_channels { + for channel_id in &payload.delete_channels { let channel_id = *channel_id; - if let Some(OpenedChannelBuffer::Open(buffer)) = + if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.remove(&channel_id) { if let Some(buffer) = buffer.upgrade(cx) { @@ -664,44 +874,34 @@ impl ChannelStore { } } - 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; - } - } else { - self.channel_paths.push(vec![channel.id]); - } - } + let mut index = self.channel_index.bulk_insert(); + for channel in payload.channels { + index.insert(channel) } - self.channel_paths.sort_by(|a, b| { - let a = Self::channel_path_sorting_key(a, &self.channels_by_id); - let b = Self::channel_path_sorting_key(b, &self.channels_by_id); - a.cmp(b) - }); - self.channel_paths.dedup(); - self.channel_paths.retain(|path| { - path.iter() - .all(|channel_id| self.channels_by_id.contains_key(channel_id)) - }); + for unseen_buffer_change in payload.unseen_channel_buffer_changes { + let version = language::proto::deserialize_version(&unseen_buffer_change.version); + index.note_changed( + unseen_buffer_change.channel_id, + unseen_buffer_change.epoch, + &version, + ); + } + + for unseen_channel_message in payload.unseen_channel_messages { + index.new_messages( + unseen_channel_message.channel_id, + unseen_channel_message.message_id, + ); + } + + for edge in payload.insert_edge { + index.insert_edge(edge.channel_id, edge.parent_id); + } + + for edge in payload.delete_edge { + index.delete_edge(edge.parent_id, edge.channel_id); + } } for permission in payload.channel_permissions { @@ -759,12 +959,45 @@ impl ChannelStore { anyhow::Ok(()) })) } +} + +impl Deref for ChannelPath { + type Target = [ChannelId]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ChannelPath { + pub fn new(path: Arc<[ChannelId]>) -> Self { + debug_assert!(path.len() >= 1); + Self(path) + } + + pub fn parent_id(&self) -> Option { + self.0.len().checked_sub(2).map(|i| self.0[i]) + } + + pub fn channel_id(&self) -> ChannelId { + self.0[self.0.len() - 1] + } +} + +impl From for Cow<'static, ChannelPath> { + fn from(value: ChannelPath) -> Self { + Cow::Owned(value) + } +} + +impl<'a> From<&'a ChannelPath> for Cow<'a, ChannelPath> { + fn from(value: &'a ChannelPath) -> Self { + Cow::Borrowed(value) + } +} - fn channel_path_sorting_key<'a>( - path: &'a [ChannelId], - channels_by_id: &'a HashMap>, - ) -> impl 'a + Iterator> { - path.iter() - .map(|id| Some(channels_by_id.get(id)?.name.as_str())) +impl Default for ChannelPath { + fn default() -> Self { + ChannelPath(Arc::from([])) } } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf0de1b644cee0df8df3948e504e406a8f9cd3e7 --- /dev/null +++ b/crates/channel/src/channel_store/channel_index.rs @@ -0,0 +1,238 @@ +use std::{ops::Deref, sync::Arc}; + +use crate::{Channel, ChannelId}; +use collections::BTreeMap; +use rpc::proto; + +use super::ChannelPath; + +#[derive(Default, Debug)] +pub struct ChannelIndex { + paths: Vec, + channels_by_id: BTreeMap>, +} + +impl ChannelIndex { + pub fn by_id(&self) -> &BTreeMap> { + &self.channels_by_id + } + + pub fn clear(&mut self) { + self.paths.clear(); + self.channels_by_id.clear(); + } + + /// Delete the given channels from this index. + pub fn delete_channels(&mut self, channels: &[ChannelId]) { + self.channels_by_id + .retain(|channel_id, _| !channels.contains(channel_id)); + self.paths.retain(|path| { + path.iter() + .all(|channel_id| self.channels_by_id.contains_key(channel_id)) + }); + } + + pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { + ChannelPathsInsertGuard { + paths: &mut self.paths, + channels_by_id: &mut self.channels_by_id, + } + } + + pub fn acknowledge_note_version( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + ) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + let channel = Arc::make_mut(channel); + if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version { + if epoch > *unseen_epoch + || epoch == *unseen_epoch && version.observed_all(unseen_version) + { + channel.unseen_note_version = None; + } + } + } + } + + pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + let channel = Arc::make_mut(channel); + if let Some(unseen_message_id) = channel.unseen_message_id { + if message_id >= unseen_message_id { + channel.unseen_message_id = None; + } + } + } + } + + pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { + insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version); + } + + pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) { + insert_new_message(&mut self.channels_by_id, channel_id, message_id) + } +} + +impl Deref for ChannelIndex { + type Target = [ChannelPath]; + + fn deref(&self) -> &Self::Target { + &self.paths + } +} + +/// A guard for ensuring that the paths index maintains its sort and uniqueness +/// invariants after a series of insertions +#[derive(Debug)] +pub struct ChannelPathsInsertGuard<'a> { + paths: &'a mut Vec, + channels_by_id: &'a mut BTreeMap>, +} + +impl<'a> ChannelPathsInsertGuard<'a> { + /// Remove the given edge from this index. This will not remove the channel. + /// If this operation would result in a dangling edge, re-insert it. + pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + self.paths.retain(|path| { + !path + .windows(2) + .any(|window| window == [parent_id, channel_id]) + }); + + // Ensure that there is at least one channel path in the index + if !self + .paths + .iter() + .any(|path| path.iter().any(|id| id == &channel_id)) + { + self.insert_root(channel_id); + } + } + + pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { + insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version); + } + + pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) { + insert_new_message(&mut self.channels_by_id, channel_id, message_id) + } + + pub fn insert(&mut self, channel_proto: proto::Channel) { + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + Arc::make_mut(existing_channel).name = channel_proto.name; + } else { + self.channels_by_id.insert( + channel_proto.id, + Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + unseen_note_version: None, + unseen_message_id: None, + }), + ); + self.insert_root(channel_proto.id); + } + } + + pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) { + let mut parents = Vec::new(); + let mut descendants = Vec::new(); + let mut ixs_to_remove = Vec::new(); + + for (ix, path) in self.paths.iter().enumerate() { + if path + .windows(2) + .any(|window| window[0] == parent_id && window[1] == channel_id) + { + // We already have this edge in the index + return; + } + if path.ends_with(&[parent_id]) { + parents.push(path); + } else if let Some(position) = path.iter().position(|id| id == &channel_id) { + if position == 0 { + ixs_to_remove.push(ix); + } + descendants.push(path.split_at(position).1); + } + } + + let mut new_paths = Vec::new(); + for parent in parents.iter() { + if descendants.is_empty() { + let mut new_path = Vec::with_capacity(parent.len() + 1); + new_path.extend_from_slice(parent); + new_path.push(channel_id); + new_paths.push(ChannelPath::new(new_path.into())); + } else { + for descendant in descendants.iter() { + let mut new_path = Vec::with_capacity(parent.len() + descendant.len()); + new_path.extend_from_slice(parent); + new_path.extend_from_slice(descendant); + new_paths.push(ChannelPath::new(new_path.into())); + } + } + } + + for ix in ixs_to_remove.into_iter().rev() { + self.paths.swap_remove(ix); + } + self.paths.extend(new_paths) + } + + fn insert_root(&mut self, channel_id: ChannelId) { + self.paths.push(ChannelPath::new(Arc::from([channel_id]))); + } +} + +impl<'a> Drop for ChannelPathsInsertGuard<'a> { + fn drop(&mut self) { + self.paths.sort_by(|a, b| { + let a = channel_path_sorting_key(a, &self.channels_by_id); + let b = channel_path_sorting_key(b, &self.channels_by_id); + a.cmp(b) + }); + self.paths.dedup(); + } +} + +fn channel_path_sorting_key<'a>( + path: &'a [ChannelId], + channels_by_id: &'a BTreeMap>, +) -> impl 'a + Iterator> { + path.iter() + .map(|id| Some(channels_by_id.get(id)?.name.as_str())) +} + +fn insert_note_changed( + channels_by_id: &mut BTreeMap>, + channel_id: u64, + epoch: u64, + version: &clock::Global, +) { + if let Some(channel) = channels_by_id.get_mut(&channel_id) { + let unseen_version = Arc::make_mut(channel) + .unseen_note_version + .get_or_insert((0, clock::Global::new())); + if epoch > unseen_version.0 { + *unseen_version = (epoch, version.clone()); + } else { + unseen_version.1.join(&version); + } + } +} + +fn insert_new_message( + channels_by_id: &mut BTreeMap>, + channel_id: u64, + message_id: u64, +) { + if let Some(channel) = channels_by_id.get_mut(&channel_id) { + let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0); + *unseen_message_id = message_id.max(*unseen_message_id); + } +} diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 18894b1f472f907d3b54ad35df57d78e5e974565..41acafa3a30525b4c1fd54ecf479a674b2f67df0 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -1,16 +1,15 @@ +use crate::channel_chat::ChannelChatEvent; + use super::*; -use client::{Client, UserStore}; -use gpui::{AppContext, ModelHandle}; +use client::{test::FakeServer, Client, UserStore}; +use gpui::{AppContext, ModelHandle, TestAppContext}; use rpc::proto; +use settings::SettingsStore; use util::http::FakeHttpClient; #[gpui::test] fn test_update_channels(cx: &mut AppContext) { - let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - - let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); + let channel_store = init_test(cx); update_channels( &channel_store, @@ -19,12 +18,10 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 1, name: "b".to_string(), - parent_id: None, }, proto::Channel { id: 2, name: "a".to_string(), - parent_id: None, }, ], channel_permissions: vec![proto::ChannelPermission { @@ -52,12 +49,20 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 3, name: "x".to_string(), - parent_id: Some(1), }, proto::Channel { id: 4, name: "y".to_string(), - parent_id: Some(2), + }, + ], + insert_edge: vec![ + proto::ChannelEdge { + parent_id: 1, + channel_id: 3, + }, + proto::ChannelEdge { + parent_id: 2, + channel_id: 4, }, ], ..Default::default() @@ -78,11 +83,7 @@ fn test_update_channels(cx: &mut AppContext) { #[gpui::test] fn test_dangling_channel_paths(cx: &mut AppContext) { - let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - - let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); + let channel_store = init_test(cx); update_channels( &channel_store, @@ -91,17 +92,24 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { proto::Channel { id: 0, name: "a".to_string(), - parent_id: None, }, proto::Channel { id: 1, name: "b".to_string(), - parent_id: Some(0), }, proto::Channel { id: 2, name: "c".to_string(), - parent_id: Some(1), + }, + ], + insert_edge: vec![ + proto::ChannelEdge { + parent_id: 0, + channel_id: 1, + }, + proto::ChannelEdge { + parent_id: 1, + channel_id: 2, }, ], channel_permissions: vec![proto::ChannelPermission { @@ -127,7 +135,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { update_channels( &channel_store, proto::UpdateChannels { - remove_channels: vec![1, 2], + delete_channels: vec![1, 2], ..Default::default() }, cx, @@ -137,6 +145,207 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx); } +#[gpui::test] +async fn test_channel_messages(cx: &mut TestAppContext) { + let user_id = 5; + let channel_id = 5; + let channel_store = cx.update(init_test); + let client = channel_store.read_with(cx, |s, _| s.client()); + let server = FakeServer::for_client(user_id, &client, cx).await; + + // Get the available channels. + server.send(proto::UpdateChannels { + channels: vec![proto::Channel { + id: channel_id, + name: "the-channel".to_string(), + }], + ..Default::default() + }); + cx.foreground().run_until_parked(); + cx.read(|cx| { + assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx); + }); + + let get_users = server.receive::().await.unwrap(); + assert_eq!(get_users.payload.user_ids, vec![5]); + server.respond( + get_users.receipt(), + proto::UsersResponse { + users: vec![proto::User { + id: 5, + github_login: "nathansobo".into(), + avatar_url: "http://avatar.com/nathansobo".into(), + }], + }, + ); + + // Join a channel and populate its existing messages. + let channel = channel_store.update(cx, |store, cx| { + let channel_id = store.channel_dag_entries().next().unwrap().1.id; + store.open_channel_chat(channel_id, cx) + }); + let join_channel = server.receive::().await.unwrap(); + server.respond( + join_channel.receipt(), + proto::JoinChannelChatResponse { + messages: vec![ + proto::ChannelMessage { + id: 10, + body: "a".into(), + timestamp: 1000, + sender_id: 5, + nonce: Some(1.into()), + }, + proto::ChannelMessage { + id: 11, + body: "b".into(), + timestamp: 1001, + sender_id: 6, + nonce: Some(2.into()), + }, + ], + done: false, + }, + ); + + cx.foreground().start_waiting(); + + // Client requests all users for the received messages + let mut get_users = server.receive::().await.unwrap(); + get_users.payload.user_ids.sort(); + assert_eq!(get_users.payload.user_ids, vec![6]); + server.respond( + get_users.receipt(), + proto::UsersResponse { + users: vec![proto::User { + id: 6, + github_login: "maxbrunsfeld".into(), + avatar_url: "http://avatar.com/maxbrunsfeld".into(), + }], + }, + ); + + let channel = channel.await.unwrap(); + channel.read_with(cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[ + ("nathansobo".into(), "a".into()), + ("maxbrunsfeld".into(), "b".into()) + ] + ); + }); + + // Receive a new message. + server.send(proto::ChannelMessageSent { + channel_id, + message: Some(proto::ChannelMessage { + id: 12, + body: "c".into(), + timestamp: 1002, + sender_id: 7, + nonce: Some(3.into()), + }), + }); + + // Client requests user for message since they haven't seen them yet + let get_users = server.receive::().await.unwrap(); + assert_eq!(get_users.payload.user_ids, vec![7]); + server.respond( + get_users.receipt(), + proto::UsersResponse { + users: vec![proto::User { + id: 7, + github_login: "as-cii".into(), + avatar_url: "http://avatar.com/as-cii".into(), + }], + }, + ); + + assert_eq!( + channel.next_event(cx).await, + ChannelChatEvent::MessagesUpdated { + old_range: 2..2, + new_count: 1, + } + ); + channel.read_with(cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(2..3) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[("as-cii".into(), "c".into())] + ) + }); + + // Scroll up to view older messages. + channel.update(cx, |channel, cx| { + assert!(channel.load_more_messages(cx)); + }); + let get_messages = server.receive::().await.unwrap(); + assert_eq!(get_messages.payload.channel_id, 5); + assert_eq!(get_messages.payload.before_message_id, 10); + server.respond( + get_messages.receipt(), + proto::GetChannelMessagesResponse { + done: true, + messages: vec![ + proto::ChannelMessage { + id: 8, + body: "y".into(), + timestamp: 998, + sender_id: 5, + nonce: Some(4.into()), + }, + proto::ChannelMessage { + id: 9, + body: "z".into(), + timestamp: 999, + sender_id: 6, + nonce: Some(5.into()), + }, + ], + }, + ); + + assert_eq!( + channel.next_event(cx).await, + ChannelChatEvent::MessagesUpdated { + old_range: 0..0, + new_count: 2, + } + ); + channel.read_with(cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[ + ("nathansobo".into(), "y".into()), + ("maxbrunsfeld".into(), "z".into()) + ] + ); + }); +} + +fn init_test(cx: &mut AppContext) -> ModelHandle { + let http = FakeHttpClient::with_404_response(); + let client = Client::new(http.clone(), cx); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + + cx.foreground().forbid_parking(); + cx.set_global(SettingsStore::test(cx)); + crate::init(&client); + client::init(&client, cx); + + cx.add_model(|cx| ChannelStore::new(client, user_store, cx)) +} + fn update_channels( channel_store: &ModelHandle, message: proto::UpdateChannels, @@ -154,7 +363,7 @@ fn assert_channels( ) { let actual = channel_store.read_with(cx, |store, _| { store - .channels() + .channel_dag_entries() .map(|(depth, channel)| { ( depth, diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e3038e5bcc49bd41b756062b676e00f4f355867a..c8085f807bd5786daa6e8579bdf263c978dd4e74 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -33,15 +33,16 @@ parking_lot.workspace = true postage.workspace = true rand.workspace = true schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true smol.workspace = true +sysinfo.workspace = true +tempfile = "3" thiserror.workspace = true time.workspace = true tiny_http = "0.8" -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true url = "2.2" -serde.workspace = true -serde_derive.workspace = true -tempfile = "3" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index d28c1ab1a9bc27eac98d1c912e7031b36fd079de..5767ac54b7893f7425dfd56202b7512d17314f0f 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -34,7 +34,7 @@ use std::{ future::Future, marker::PhantomData, path::PathBuf, - sync::{Arc, Weak}, + sync::{atomic::AtomicU64, Arc, Weak}, time::{Duration, Instant}, }; use telemetry::Telemetry; @@ -62,6 +62,8 @@ lazy_static! { .and_then(|v| v.parse().ok()); pub static ref ZED_APP_PATH: Option = std::env::var("ZED_APP_PATH").ok().map(PathBuf::from); + pub static ref ZED_ALWAYS_ACTIVE: bool = + std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0); } pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; @@ -103,7 +105,7 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } pub struct Client { - id: usize, + id: AtomicU64, peer: Arc, http: Arc, telemetry: Arc, @@ -372,7 +374,7 @@ impl settings::Setting for TelemetrySettings { impl Client { pub fn new(http: Arc, cx: &AppContext) -> Arc { Arc::new(Self { - id: 0, + id: AtomicU64::new(0), peer: Peer::new(0), telemetry: Telemetry::new(http.clone(), cx), http, @@ -385,17 +387,16 @@ impl Client { }) } - pub fn id(&self) -> usize { - self.id + pub fn id(&self) -> u64 { + self.id.load(std::sync::atomic::Ordering::SeqCst) } pub fn http_client(&self) -> Arc { self.http.clone() } - #[cfg(any(test, feature = "test-support"))] - pub fn set_id(&mut self, id: usize) -> &Self { - self.id = id; + pub fn set_id(&self, id: u64) -> &Self { + self.id.store(id, std::sync::atomic::Ordering::SeqCst); self } @@ -452,7 +453,7 @@ impl Client { } fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { - log::info!("set status on client {}: {:?}", self.id, status); + log::info!("set status on client {}: {:?}", self.id(), status); let mut state = self.state.write(); *state.status.0.borrow_mut() = status; @@ -803,6 +804,7 @@ impl Client { } } let credentials = credentials.unwrap(); + self.set_id(credentials.user_id); if was_disconnected { self.set_status(Status::Connecting, cx); @@ -1219,7 +1221,7 @@ impl Client { } pub fn send(&self, message: T) -> Result<()> { - log::debug!("rpc send. client_id:{}, name:{}", self.id, T::NAME); + log::debug!("rpc send. client_id:{}, name:{}", self.id(), T::NAME); self.peer.send(self.connection_id()?, message) } @@ -1235,7 +1237,7 @@ impl Client { &self, request: T, ) -> impl Future>> { - let client_id = self.id; + let client_id = self.id(); log::debug!( "rpc request start. client_id:{}. name:{}", client_id, @@ -1256,7 +1258,7 @@ impl Client { } fn respond(&self, receipt: Receipt, response: T::Response) -> Result<()> { - log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME); + log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME); self.peer.respond(receipt, response) } @@ -1265,7 +1267,7 @@ impl Client { receipt: Receipt, error: proto::Error, ) -> Result<()> { - log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME); + log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME); self.peer.respond_with_error(receipt, error) } @@ -1334,7 +1336,7 @@ impl Client { if let Some(handler) = handler { let future = handler(subscriber, message, &self, cx.clone()); - let client_id = self.id; + let client_id = self.id(); log::debug!( "rpc message received. client_id:{}, sender_id:{:?}, type:{}", client_id, diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index f8642dd7fad3c8452b06a2c5120bf60326a72adb..0f753679e1a9d7e45b19ff8c55c3911cf24da6aa 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -4,9 +4,11 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; +use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; +use uuid::Uuid; pub struct Telemetry { http_client: Arc, @@ -17,7 +19,8 @@ pub struct Telemetry { #[derive(Default)] struct TelemetryState { metrics_id: Option>, // Per logged-in user - installation_id: Option>, // Per app installation + installation_id: Option>, // Per app installation (different for dev, preview, and stable) + session_id: String, // Per app launch app_version: Option>, release_channel: Option<&'static str>, os_name: &'static str, @@ -40,6 +43,7 @@ lazy_static! { struct ClickhouseEventRequestBody { token: &'static str, installation_id: Option>, + session_id: String, is_staff: Option, app_version: Option>, os_name: &'static str, @@ -56,6 +60,13 @@ struct ClickhouseEventWrapper { event: ClickhouseEvent, } +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum AssistantKind { + Panel, + Inline, +} + #[derive(Serialize, Debug)] #[serde(tag = "type")] pub enum ClickhouseEvent { @@ -76,6 +87,19 @@ pub enum ClickhouseEvent { room_id: Option, channel_id: Option, }, + Assistant { + conversation_id: Option, + kind: AssistantKind, + model: &'static str, + }, + Cpu { + usage_as_percentage: f32, + core_count: u32, + }, + Memory { + memory_in_bytes: u64, + virtual_memory_in_bytes: u64, + }, } #[cfg(debug_assertions)] @@ -110,6 +134,7 @@ impl Telemetry { release_channel, installation_id: None, metrics_id: None, + session_id: Uuid::new_v4().to_string(), clickhouse_events_queue: Default::default(), flush_clickhouse_events_task: Default::default(), log_file: None, @@ -124,7 +149,7 @@ impl Telemetry { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } - pub fn start(self: &Arc, installation_id: Option) { + pub fn start(self: &Arc, installation_id: Option, cx: &mut AppContext) { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); @@ -133,6 +158,46 @@ impl Telemetry { if has_clickhouse_events { self.flush_clickhouse_events(); } + + let this = self.clone(); + cx.spawn(|mut cx| async move { + let mut system = System::new_all(); + system.refresh_all(); + + loop { + // Waiting some amount of time before the first query is important to get a reasonable value + // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage + const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60); + smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; + + system.refresh_memory(); + system.refresh_processes(); + + let current_process = Pid::from_u32(std::process::id()); + let Some(process) = system.processes().get(¤t_process) else { + let process = current_process; + log::error!("Failed to find own process {process:?} in system process table"); + // TODO: Fire an error telemetry event + return; + }; + + let memory_event = ClickhouseEvent::Memory { + memory_in_bytes: process.memory(), + virtual_memory_in_bytes: process.virtual_memory(), + }; + + let cpu_event = ClickhouseEvent::Cpu { + usage_as_percentage: process.cpu_usage(), + core_count: system.cpus().len() as u32, + }; + + let telemetry_settings = cx.update(|cx| *settings::get::(cx)); + + this.report_clickhouse_event(memory_event, telemetry_settings); + this.report_clickhouse_event(cpu_event, telemetry_settings); + } + }) + .detach(); } pub fn set_authenticated_user_info( @@ -224,6 +289,7 @@ impl Telemetry { &ClickhouseEventRequestBody { token: ZED_SECRET_CLIENT_TOKEN, installation_id: state.installation_id.clone(), + session_id: state.session_id.clone(), is_staff: state.is_staff.clone(), app_version: state.app_version.clone(), os_name: state.os_name, diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 00e7cd1508613c60a05ddbba8cabff86bbaf1d14..38cd12f21cdf74ed895ff9d55ae1995e2efcfafa 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -170,8 +170,7 @@ impl FakeServer { staff: false, flags: Default::default(), }, - ) - .await; + ); continue; } @@ -182,11 +181,7 @@ impl FakeServer { } } - pub async fn respond( - &self, - receipt: Receipt, - response: T::Response, - ) { + pub fn respond(&self, receipt: Receipt, response: T::Response) { self.peer.respond(receipt, response).unwrap() } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 5f13aa40acee9063bfd90c10b43044ff40952db2..6aa41708e3ae3e3c3504ab82791278c8a1837c0a 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; +use text::ReplicaId; use util::http::HttpClient; use util::TryFutureExt as _; pub type UserId = u64; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ParticipantIndex(pub u32); + #[derive(Default, Debug)] pub struct User { pub id: UserId, @@ -19,6 +23,13 @@ pub struct User { pub avatar: Option>, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Collaborator { + pub peer_id: proto::PeerId, + pub replica_id: ReplicaId, + pub user_id: UserId, +} + impl PartialOrd for User { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -56,6 +67,7 @@ pub enum ContactRequestStatus { pub struct UserStore { users: HashMap>, + participant_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, current_user: watch::Receiver>>, contacts: Vec>, @@ -81,6 +93,7 @@ pub enum Event { kind: ContactEventKind, }, ShowContacts, + ParticipantIndicesChanged, } #[derive(Clone, Copy)] @@ -118,6 +131,7 @@ impl UserStore { current_user: current_user_rx, contacts: Default::default(), incoming_contact_requests: Default::default(), + participant_indices: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, client: Arc::downgrade(&client), @@ -581,6 +595,10 @@ impl UserStore { self.load_users(proto::FuzzySearchUsers { query }, cx) } + pub fn get_cached_user(&self, user_id: u64) -> Option> { + self.users.get(&user_id).cloned() + } + pub fn get_user( &mut self, user_id: u64, @@ -641,6 +659,21 @@ impl UserStore { } }) } + + pub fn set_participant_indices( + &mut self, + participant_indices: HashMap, + cx: &mut ModelContext, + ) { + if participant_indices != self.participant_indices { + self.participant_indices = participant_indices; + cx.emit(Event::ParticipantIndicesChanged); + } + } + + pub fn participant_indices(&self) -> &HashMap { + &self.participant_indices + } } impl User { @@ -672,6 +705,16 @@ impl Contact { } } +impl Collaborator { + pub fn from_proto(message: proto::Collaborator) -> Result { + Ok(Self { + peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, + replica_id: message.replica_id as ReplicaId, + user_id: message.user_id as UserId, + }) + } +} + async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { let mut response = http .get(url, Default::default(), true) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 4b29a0801551fae8496eeaa86c55bfb303a8aeb0..0182129299fbcfbcaf9cfb1a1e1990c247618dc8 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.20.0" +version = "0.23.2" publish = false [[bin]] @@ -41,14 +41,13 @@ prost.workspace = true rand.workspace = true reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" -# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released. -sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] } -sea-query = "0.27" +smallvec.workspace = true +sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } serde.workspace = true serde_derive.workspace = true serde_json.workspace = true sha-1 = "0.9" -sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } time.workspace = true tokio = { version = "1", features = ["full"] } tokio-tungstenite = "0.17" @@ -58,6 +57,7 @@ toml.workspace = true tracing = "0.1.34" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } +uuid.workspace = true [dev-dependencies] audio = { path = "../audio" } @@ -72,7 +72,6 @@ fs = { path = "../fs", features = ["test-support"] } git = { path = "../git", features = ["test-support"] } live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } -pretty_assertions.workspace = true project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } @@ -81,14 +80,15 @@ workspace = { path = "../workspace", features = ["test-support"] } collab_ui = { path = "../collab_ui", features = ["test-support"] } async-trait.workspace = true +pretty_assertions.workspace = true ctor.workspace = true env_logger.workspace = true indoc.workspace = true util = { path = "../util" } lazy_static.workspace = true -sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] } +sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] } serde_json.workspace = true -sqlx = { version = "0.6", features = ["sqlite"] } +sqlx = { version = "0.7", features = ["sqlite"] } unindent.workspace = true [features] diff --git a/crates/collab/admin_api.conf b/crates/collab/admin_api.conf new file mode 100644 index 0000000000000000000000000000000000000000..5d3b0e65b738ed2291c782f62d7e45a8b43c9895 --- /dev/null +++ b/crates/collab/admin_api.conf @@ -0,0 +1,4 @@ +db-uri = "postgres://postgres@localhost/zed" +server-port = 8081 +jwt-secret = "the-postgrest-jwt-secret-for-authorization" +log-level = "info" diff --git a/crates/collab/k8s/manifest.template.yml b/crates/collab/k8s/manifest.template.yml index 79dd2b885104bac14afcdfbb3bb40e9d65d40b8c..d4a7a7033e8427ca87d03b4055e86b28b425dbe0 100644 --- a/crates/collab/k8s/manifest.template.yml +++ b/crates/collab/k8s/manifest.template.yml @@ -3,6 +3,7 @@ apiVersion: v1 kind: Namespace metadata: name: ${ZED_KUBE_NAMESPACE} + --- kind: Service apiVersion: v1 @@ -11,7 +12,7 @@ metadata: name: collab annotations: service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" - service.beta.kubernetes.io/do-loadbalancer-certificate-id: "08d9d8ce-761f-4ab3-bc78-4923ab5b0e33" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} spec: type: LoadBalancer selector: @@ -21,6 +22,26 @@ spec: protocol: TCP port: 443 targetPort: 8080 + +--- +kind: Service +apiVersion: v1 +metadata: + namespace: ${ZED_KUBE_NAMESPACE} + name: pgadmin + annotations: + service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} +spec: + type: LoadBalancer + selector: + app: postgrest + ports: + - name: web + protocol: TCP + port: 443 + targetPort: 8080 + --- apiVersion: apps/v1 kind: Deployment @@ -117,3 +138,40 @@ spec: # FIXME - Switch to the more restrictive `PERFMON` capability. # This capability isn't yet available in a stable version of Debian. add: ["SYS_ADMIN"] + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: ${ZED_KUBE_NAMESPACE} + name: postgrest + +spec: + replicas: 1 + selector: + matchLabels: + app: postgrest + template: + metadata: + labels: + app: postgrest + spec: + containers: + - name: postgrest + image: "postgrest/postgrest" + ports: + - containerPort: 8080 + protocol: TCP + env: + - name: PGRST_SERVER_PORT + value: "8080" + - name: PGRST_DB_URI + valueFrom: + secretKeyRef: + name: database + key: url + - name: PGRST_JWT_SECRET + valueFrom: + secretKeyRef: + name: postgrest + key: jwt_secret diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 80477dcb3c3b9f4fc1efd25622243b59901cf4fc..2d963ff15fa4717aa7faee092356f4b06d8a5814 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -158,7 +158,8 @@ CREATE TABLE "room_participants" ( "initial_project_id" INTEGER, "calling_user_id" INTEGER NOT NULL REFERENCES users (id), "calling_connection_id" INTEGER NOT NULL, - "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL + "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL, + "participant_index" INTEGER ); CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id"); CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id"); @@ -192,6 +193,26 @@ CREATE TABLE "channels" ( "created_at" TIMESTAMP NOT NULL DEFAULT now ); +CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "user_id" INTEGER NOT NULL REFERENCES users (id), + "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 +); +CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id"); + +CREATE TABLE IF NOT EXISTS "channel_messages" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "sender_id" INTEGER NOT NULL REFERENCES users (id), + "body" TEXT NOT NULL, + "sent_at" TIMESTAMP, + "nonce" BLOB NOT NULL +); +CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce"); + CREATE TABLE "channel_paths" ( "id_path" TEXT NOT NULL PRIMARY KEY, "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE @@ -268,3 +289,24 @@ CREATE TABLE "user_features" ( CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); + + +CREATE TABLE "observed_buffer_edits" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + PRIMARY KEY (user_id, buffer_id) +); + +CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id"); + +CREATE TABLE IF NOT EXISTS "observed_channel_messages" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "channel_message_id" INTEGER NOT NULL, + PRIMARY KEY (user_id, channel_id) +); + +CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id"); diff --git a/crates/collab/migrations/20230907114200_add_channel_messages.sql b/crates/collab/migrations/20230907114200_add_channel_messages.sql new file mode 100644 index 0000000000000000000000000000000000000000..abe7753ca69fb45a1f0a56b732963d8dc5605e31 --- /dev/null +++ b/crates/collab/migrations/20230907114200_add_channel_messages.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "channel_messages" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "sender_id" INTEGER NOT NULL REFERENCES users (id), + "body" TEXT NOT NULL, + "sent_at" TIMESTAMP, + "nonce" UUID NOT NULL +); +CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce"); + +CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL REFERENCES users (id), + "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 +); +CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id"); diff --git a/crates/collab/migrations/20230925210437_add_channel_changes.sql b/crates/collab/migrations/20230925210437_add_channel_changes.sql new file mode 100644 index 0000000000000000000000000000000000000000..250a9ac731b59489e85cf34a6754307bfac543ee --- /dev/null +++ b/crates/collab/migrations/20230925210437_add_channel_changes.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "observed_buffer_edits" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + PRIMARY KEY (user_id, buffer_id) +); + +CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id"); + +CREATE TABLE IF NOT EXISTS "observed_channel_messages" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "channel_message_id" INTEGER NOT NULL, + PRIMARY KEY (user_id, channel_id) +); + +CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id"); diff --git a/crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql b/crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql new file mode 100644 index 0000000000000000000000000000000000000000..1493119e2a97ac42f5d69ebc82ac3d3d0dc4dd63 --- /dev/null +++ b/crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql @@ -0,0 +1 @@ +ALTER TABLE room_participants ADD COLUMN participant_index INTEGER; diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 7191400f4488dd221e175d8e099292dfb3717bcf..a84fcf328ba4e92214074b55fc0d849e5b69db61 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -1,8 +1,7 @@ use crate::{ auth, - db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary}, - rpc::{self, ResultExt}, - AppState, Error, Result, + db::{User, UserId}, + rpc, AppState, Error, Result, }; use anyhow::anyhow; use axum::{ @@ -11,7 +10,7 @@ use axum::{ http::{self, Request, StatusCode}, middleware::{self, Next}, response::IntoResponse, - routing::{get, post, put}, + routing::{get, post}, Extension, Json, Router, }; use axum_extra::response::ErasedJson; @@ -23,18 +22,9 @@ use tracing::instrument; pub fn routes(rpc_server: Arc, state: Arc) -> Router { Router::new() .route("/user", get(get_authenticated_user)) - .route("/users", get(get_users).post(create_user)) - .route("/users/:id", put(update_user).delete(destroy_user)) .route("/users/:id/access_tokens", post(create_access_token)) - .route("/users_with_no_invites", get(get_users_with_no_invites)) - .route("/invite_codes/:code", get(get_user_for_invite_code)) .route("/panic", post(trace_panic)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) - .route("/signups", post(create_signup)) - .route("/signups_summary", get(get_waitlist_summary)) - .route("/user_invites", post(create_invite_from_code)) - .route("/unsent_invites", get(get_unsent_invites)) - .route("/sent_invites", post(record_sent_invites)) .layer( ServiceBuilder::new() .layer(Extension(state)) @@ -104,28 +94,6 @@ async fn get_authenticated_user( return Ok(Json(AuthenticatedUserResponse { user, metrics_id })); } -#[derive(Debug, Deserialize)] -struct GetUsersQueryParams { - query: Option, - page: Option, - limit: Option, -} - -async fn get_users( - Query(params): Query, - Extension(app): Extension>, -) -> Result>> { - let limit = params.limit.unwrap_or(100); - let users = if let Some(query) = params.query { - app.db.fuzzy_search_users(&query, limit).await? - } else { - app.db - .get_all_users(params.page.unwrap_or(0), limit) - .await? - }; - Ok(Json(users)) -} - #[derive(Deserialize, Debug)] struct CreateUserParams { github_user_id: i32, @@ -145,119 +113,6 @@ struct CreateUserResponse { metrics_id: String, } -async fn create_user( - Json(params): Json, - Extension(app): Extension>, - Extension(rpc_server): Extension>, -) -> Result>> { - let user = NewUserParams { - github_login: params.github_login, - github_user_id: params.github_user_id, - invite_count: params.invite_count, - }; - - // Creating a user via the normal signup process - let result = if let Some(email_confirmation_code) = params.email_confirmation_code { - if let Some(result) = app - .db - .create_user_from_invite( - &Invite { - email_address: params.email_address, - email_confirmation_code, - }, - user, - ) - .await? - { - result - } else { - return Ok(Json(None)); - } - } - // Creating a user as an admin - else if params.admin { - app.db - .create_user(¶ms.email_address, false, user) - .await? - } else { - Err(Error::Http( - StatusCode::UNPROCESSABLE_ENTITY, - "email confirmation code is required".into(), - ))? - }; - - if let Some(inviter_id) = result.inviting_user_id { - rpc_server - .invite_code_redeemed(inviter_id, result.user_id) - .await - .trace_err(); - } - - let user = app - .db - .get_user_by_id(result.user_id) - .await? - .ok_or_else(|| anyhow!("couldn't find the user we just created"))?; - - Ok(Json(Some(CreateUserResponse { - user, - metrics_id: result.metrics_id, - signup_device_id: result.signup_device_id, - }))) -} - -#[derive(Deserialize)] -struct UpdateUserParams { - admin: Option, - invite_count: Option, -} - -async fn update_user( - Path(user_id): Path, - Json(params): Json, - Extension(app): Extension>, - Extension(rpc_server): Extension>, -) -> Result<()> { - let user_id = UserId(user_id); - - if let Some(admin) = params.admin { - app.db.set_user_is_admin(user_id, admin).await?; - } - - if let Some(invite_count) = params.invite_count { - app.db - .set_invite_count_for_user(user_id, invite_count) - .await?; - rpc_server.invite_count_updated(user_id).await.trace_err(); - } - - Ok(()) -} - -async fn destroy_user( - Path(user_id): Path, - Extension(app): Extension>, -) -> Result<()> { - app.db.destroy_user(UserId(user_id)).await?; - Ok(()) -} - -#[derive(Debug, Deserialize)] -struct GetUsersWithNoInvites { - invited_by_another_user: bool, -} - -async fn get_users_with_no_invites( - Query(params): Query, - Extension(app): Extension>, -) -> Result>> { - Ok(Json( - app.db - .get_users_with_no_invites(params.invited_by_another_user) - .await?, - )) -} - #[derive(Debug, Deserialize)] struct Panic { version: String, @@ -327,69 +182,3 @@ async fn create_access_token( encrypted_access_token, })) } - -async fn get_user_for_invite_code( - Path(code): Path, - Extension(app): Extension>, -) -> Result> { - Ok(Json(app.db.get_user_for_invite_code(&code).await?)) -} - -async fn create_signup( - Json(params): Json, - Extension(app): Extension>, -) -> Result<()> { - app.db.create_signup(¶ms).await?; - Ok(()) -} - -async fn get_waitlist_summary( - Extension(app): Extension>, -) -> Result> { - Ok(Json(app.db.get_waitlist_summary().await?)) -} - -#[derive(Deserialize)] -pub struct CreateInviteFromCodeParams { - invite_code: String, - email_address: String, - device_id: Option, - #[serde(default)] - added_to_mailing_list: bool, -} - -async fn create_invite_from_code( - Json(params): Json, - Extension(app): Extension>, -) -> Result> { - Ok(Json( - app.db - .create_invite_from_code( - ¶ms.invite_code, - ¶ms.email_address, - params.device_id.as_deref(), - params.added_to_mailing_list, - ) - .await?, - )) -} - -#[derive(Deserialize)] -pub struct GetUnsentInvitesParams { - pub count: usize, -} - -async fn get_unsent_invites( - Query(params): Query, - Extension(app): Extension>, -) -> Result>> { - Ok(Json(app.db.get_unsent_invites(params.count).await?)) -} - -async fn record_sent_invites( - Json(params): Json>, - Extension(app): Extension>, -) -> Result<()> { - app.db.record_sent_invites(¶ms).await?; - Ok(()) -} diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b5d968ddf3b5e0b3302d03e8ad37c73df687d724..e60b7cc33dfb22f030c5600cfa0d8d0794438c1d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -14,13 +14,17 @@ use collections::{BTreeMap, HashMap, HashSet}; use dashmap::DashMap; use futures::StreamExt; use rand::{prelude::StdRng, Rng, SeedableRng}; -use rpc::{proto, ConnectionId}; +use rpc::{ + proto::{self}, + ConnectionId, +}; use sea_orm::{ - entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, - DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, - QueryOrder, QuerySelect, Statement, TransactionTrait, + entity::prelude::*, + sea_query::{Alias, Expr, OnConflict, Query}, + 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}, @@ -43,6 +47,8 @@ pub use ids::*; pub use sea_orm::ConnectOptions; pub use tables::user::Model as User; +use self::queries::channels::ChannelGraph; + pub struct Database { options: ConnectOptions, pool: DatabaseConnection, @@ -57,6 +63,7 @@ pub struct Database { // separate files in the `queries` folder. impl Database { pub async fn new(options: ConnectOptions, executor: Executor) -> Result { + sqlx::any::install_default_drivers(); Ok(Self { options: options.clone(), pool: sea_orm::Database::connect(options).await?, @@ -114,7 +121,7 @@ impl Database { Ok(new_migrations) } - async fn transaction(&self, f: F) -> Result + pub async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, Fut: Send + Future>, @@ -316,7 +323,7 @@ fn is_serialization_error(error: &Error) -> bool { } } -struct TransactionHandle(Arc>); +pub struct TransactionHandle(Arc>); impl Deref for TransactionHandle { type Target = DatabaseTransaction; @@ -421,18 +428,19 @@ pub struct NewUserResult { pub signup_device_id: Option, } -#[derive(FromQueryResult, Debug, PartialEq)] +#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)] pub struct Channel { pub id: ChannelId, pub name: String, - pub parent_id: Option, } #[derive(Debug, PartialEq)] pub struct ChannelsForUser { - pub channels: Vec, + pub channels: ChannelGraph, pub channel_participants: HashMap>, pub channels_with_admin_privileges: HashSet, + pub unseen_buffer_changes: Vec, + pub channel_messages: Vec, } #[derive(Debug)] @@ -506,7 +514,7 @@ pub struct RefreshedRoom { pub struct RefreshedChannelBuffer { pub connection_ids: Vec, - pub removed_collaborators: Vec, + pub collaborators: Vec, } pub struct Project { diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index b33ea57183b8771792ea50c6b3ab2b2631971194..23bb9e53bf9803ba64693f94605a8e87f904c571 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -1,6 +1,5 @@ use crate::Result; -use sea_orm::DbErr; -use sea_query::{Value, ValueTypeErr}; +use sea_orm::{entity::prelude::*, DbErr}; use serde::{Deserialize, Serialize}; macro_rules! id_type { @@ -17,6 +16,7 @@ macro_rules! id_type { Hash, Serialize, Deserialize, + DeriveValueType, )] #[serde(transparent)] pub struct $name(pub i32); @@ -42,40 +42,6 @@ macro_rules! id_type { } } - 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(|_| { @@ -88,7 +54,7 @@ macro_rules! id_type { } } - impl sea_query::Nullable for $name { + impl sea_orm::sea_query::Nullable for $name { fn null() -> Value { Value::Int(None) } @@ -96,24 +62,12 @@ macro_rules! id_type { }; } -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!(ChannelChatParticipantId); id_type!(ChannelId); id_type!(ChannelMemberId); +id_type!(MessageId); id_type!(ContactId); id_type!(FollowerId); id_type!(RoomId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 09a8f073b469f72773a0220750f5d65cf85629af..80bd8704b27704361241a93c56f5945ef51ef3cc 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -4,8 +4,8 @@ pub mod access_tokens; pub mod buffers; pub mod channels; pub mod contacts; +pub mod messages; pub mod projects; pub mod rooms; pub mod servers; -pub mod signups; pub mod users; diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 00de20140320640d98517556a5321433ad891f86..c85432f2bba1b62a1e346626171fb59bfb8ef7ac 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -2,6 +2,12 @@ use super::*; use prost::Message; use text::{EditOperation, UndoOperation}; +pub struct LeftChannelBuffer { + pub channel_id: ChannelId, + pub collaborators: Vec, + pub connections: Vec, +} + impl Database { pub async fn join_channel_buffer( &self, @@ -68,7 +74,32 @@ impl Database { .await?; collaborators.push(collaborator); - let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?; + let (base_text, operations, max_operation) = + self.get_buffer_state(&buffer, &tx).await?; + + // Save the last observed operation + if let Some(op) = max_operation { + observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { + user_id: ActiveValue::Set(user_id), + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(op.epoch), + lamport_timestamp: ActiveValue::Set(op.lamport_timestamp), + replica_id: ActiveValue::Set(op.replica_id), + }) + .on_conflict( + OnConflict::columns([ + observed_buffer_edits::Column::UserId, + observed_buffer_edits::Column::BufferId, + ]) + .update_columns([ + observed_buffer_edits::Column::Epoch, + observed_buffer_edits::Column::LamportTimestamp, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), @@ -204,23 +235,26 @@ impl Database { server_id: ServerId, ) -> Result { self.transaction(|tx| async move { - let collaborators = channel_buffer_collaborator::Entity::find() + let db_collaborators = channel_buffer_collaborator::Entity::find() .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) .all(&*tx) .await?; let mut connection_ids = Vec::new(); - let mut removed_collaborators = Vec::new(); + let mut collaborators = Vec::new(); let mut collaborator_ids_to_remove = Vec::new(); - for collaborator in &collaborators { - if !collaborator.connection_lost && collaborator.connection_server_id == server_id { - connection_ids.push(collaborator.connection()); + for db_collaborator in &db_collaborators { + if !db_collaborator.connection_lost + && db_collaborator.connection_server_id == server_id + { + connection_ids.push(db_collaborator.connection()); + collaborators.push(proto::Collaborator { + peer_id: Some(db_collaborator.connection().into()), + replica_id: db_collaborator.replica_id.0 as u32, + user_id: db_collaborator.user_id.to_proto(), + }) } else { - removed_collaborators.push(proto::RemoveChannelBufferCollaborator { - channel_id: channel_id.to_proto(), - peer_id: Some(collaborator.connection().into()), - }); - collaborator_ids_to_remove.push(collaborator.id); + collaborator_ids_to_remove.push(db_collaborator.id); } } @@ -231,7 +265,7 @@ impl Database { Ok(RefreshedChannelBuffer { connection_ids, - removed_collaborators, + collaborators, }) }) .await @@ -241,7 +275,7 @@ impl Database { &self, channel_id: ChannelId, connection: ConnectionId, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { self.leave_channel_buffer_internal(channel_id, connection, &*tx) .await @@ -249,10 +283,33 @@ impl Database { .await } + pub async fn channel_buffer_connection_lost( + &self, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result<()> { + 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(()) + } + pub async fn leave_channel_buffers( &self, connection: ConnectionId, - ) -> Result)>> { + ) -> Result> { self.transaction(|tx| async move { #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] enum QueryChannelIds { @@ -271,10 +328,10 @@ impl Database { let mut result = Vec::new(); for channel_id in channel_ids { - let collaborators = self + let left_channel_buffer = self .leave_channel_buffer_internal(channel_id, connection, &*tx) .await?; - result.push((channel_id, collaborators)); + result.push(left_channel_buffer); } Ok(result) @@ -287,7 +344,7 @@ impl Database { channel_id: ChannelId, connection: ConnectionId, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result { let result = channel_buffer_collaborator::Entity::delete_many() .filter( Condition::all() @@ -304,6 +361,7 @@ impl Database { Err(anyhow!("not a collaborator on this project"))?; } + let mut collaborators = Vec::new(); let mut connections = Vec::new(); let mut rows = channel_buffer_collaborator::Entity::find() .filter( @@ -313,19 +371,26 @@ impl Database { .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, + let connection = row.connection(); + connections.push(connection); + collaborators.push(proto::Collaborator { + peer_id: Some(connection.into()), + replica_id: row.replica_id.0 as u32, + user_id: row.user_id.to_proto(), }); } drop(rows); - if connections.is_empty() { + if collaborators.is_empty() { self.snapshot_channel_buffer(channel_id, &tx).await?; } - Ok(connections) + Ok(LeftChannelBuffer { + channel_id, + collaborators, + connections, + }) } pub async fn get_channel_buffer_collaborators( @@ -333,33 +398,46 @@ impl Database { 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) + self.get_channel_buffer_collaborators_internal(channel_id, &*tx) + .await }) .await } + async fn get_channel_buffer_collaborators_internal( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + #[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) + } + pub async fn update_channel_buffer( &self, channel_id: ChannelId, user: UserId, operations: &[proto::Operation], - ) -> Result> { + ) -> Result<( + Vec, + Vec, + i32, + Vec, + )> { self.transaction(move |tx| async move { self.check_user_is_channel_member(channel_id, user, &*tx) .await?; @@ -378,7 +456,38 @@ impl Database { .iter() .filter_map(|op| operation_to_storage(op, &buffer, serialization_version)) .collect::>(); + + let mut channel_members; + let max_version; + if !operations.is_empty() { + let max_operation = operations + .iter() + .max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref())) + .unwrap(); + + max_version = vec![proto::VectorClockEntry { + replica_id: *max_operation.replica_id.as_ref() as u32, + timestamp: *max_operation.lamport_timestamp.as_ref() as u32, + }]; + + // get current channel participants and save the max operation above + self.save_max_operation( + user, + buffer.id, + buffer.epoch, + *max_operation.replica_id.as_ref(), + *max_operation.lamport_timestamp.as_ref(), + &*tx, + ) + .await?; + + channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + let collaborators = self + .get_channel_buffer_collaborators_internal(channel_id, &*tx) + .await?; + channel_members.retain(|member| !collaborators.contains(member)); + buffer_operation::Entity::insert_many(operations) .on_conflict( OnConflict::columns([ @@ -392,6 +501,9 @@ impl Database { ) .exec(&*tx) .await?; + } else { + channel_members = Vec::new(); + max_version = Vec::new(); } let mut connections = Vec::new(); @@ -410,11 +522,53 @@ impl Database { }); } - Ok(connections) + Ok((connections, channel_members, buffer.epoch, max_version)) }) .await } + async fn save_max_operation( + &self, + user_id: UserId, + buffer_id: BufferId, + epoch: i32, + replica_id: i32, + lamport_timestamp: i32, + tx: &DatabaseTransaction, + ) -> Result<()> { + use observed_buffer_edits::Column; + + observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { + user_id: ActiveValue::Set(user_id), + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(epoch), + replica_id: ActiveValue::Set(replica_id), + lamport_timestamp: ActiveValue::Set(lamport_timestamp), + }) + .on_conflict( + OnConflict::columns([Column::UserId, Column::BufferId]) + .update_columns([Column::Epoch, Column::LamportTimestamp, Column::ReplicaId]) + .action_cond_where( + Condition::any().add(Column::Epoch.lt(epoch)).add( + Condition::all().add(Column::Epoch.eq(epoch)).add( + Condition::any() + .add(Column::LamportTimestamp.lt(lamport_timestamp)) + .add( + Column::LamportTimestamp + .eq(lamport_timestamp) + .and(Column::ReplicaId.lt(replica_id)), + ), + ), + ), + ) + .to_owned(), + ) + .exec_without_returning(tx) + .await?; + + Ok(()) + } + async fn get_buffer_operation_serialization_version( &self, buffer_id: BufferId, @@ -432,7 +586,7 @@ impl Database { .ok_or_else(|| anyhow!("missing buffer snapshot"))?) } - async fn get_channel_buffer( + pub async fn get_channel_buffer( &self, channel_id: ChannelId, tx: &DatabaseTransaction, @@ -451,7 +605,11 @@ impl Database { &self, buffer: &buffer::Model, tx: &DatabaseTransaction, - ) -> Result<(String, Vec)> { + ) -> Result<( + String, + Vec, + Option, + )> { let id = buffer.id; let (base_text, version) = if buffer.epoch > 0 { let snapshot = buffer_snapshot::Entity::find() @@ -476,16 +634,28 @@ impl Database { .eq(id) .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), ) + .order_by_asc(buffer_operation::Column::LamportTimestamp) + .order_by_asc(buffer_operation::Column::ReplicaId) .stream(&*tx) .await?; + let mut operations = Vec::new(); + let mut last_row = None; while let Some(row) = rows.next().await { + let row = row?; + last_row = Some(buffer_operation::Model { + buffer_id: row.buffer_id, + epoch: row.epoch, + lamport_timestamp: row.lamport_timestamp, + replica_id: row.lamport_timestamp, + value: Default::default(), + }); operations.push(proto::Operation { - variant: Some(operation_from_storage(row?, version)?), - }) + variant: Some(operation_from_storage(row, version)?), + }); } - Ok((base_text, operations)) + Ok((base_text, operations, last_row)) } async fn snapshot_channel_buffer( @@ -494,7 +664,7 @@ impl Database { tx: &DatabaseTransaction, ) -> Result<()> { let buffer = self.get_channel_buffer(channel_id, tx).await?; - let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + let (base_text, operations, _) = self.get_buffer_state(&buffer, tx).await?; if operations.is_empty() { return Ok(()); } @@ -527,6 +697,150 @@ impl Database { Ok(()) } + + pub async fn observe_buffer_version( + &self, + buffer_id: BufferId, + user_id: UserId, + epoch: i32, + version: &[proto::VectorClockEntry], + ) -> Result<()> { + self.transaction(|tx| async move { + // For now, combine concurrent operations. + let Some(component) = version.iter().max_by_key(|version| version.timestamp) else { + return Ok(()); + }; + self.save_max_operation( + user_id, + buffer_id, + epoch, + component.replica_id as i32, + component.timestamp as i32, + &*tx, + ) + .await?; + Ok(()) + }) + .await + } + + pub async fn unseen_channel_buffer_changes( + &self, + user_id: UserId, + channel_ids: &[ChannelId], + tx: &DatabaseTransaction, + ) -> Result> { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryIds { + ChannelId, + Id, + } + + let mut channel_ids_by_buffer_id = HashMap::default(); + let mut rows = buffer::Entity::find() + .filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied())) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + channel_ids_by_buffer_id.insert(row.id, row.channel_id); + } + drop(rows); + + let mut observed_edits_by_buffer_id = HashMap::default(); + let mut rows = observed_buffer_edits::Entity::find() + .filter(observed_buffer_edits::Column::UserId.eq(user_id)) + .filter( + observed_buffer_edits::Column::BufferId + .is_in(channel_ids_by_buffer_id.keys().copied()), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + observed_edits_by_buffer_id.insert(row.buffer_id, row); + } + drop(rows); + + let latest_operations = self + .get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx) + .await?; + + let mut changes = Vec::default(); + for latest in latest_operations { + if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) { + if ( + observed.epoch, + observed.lamport_timestamp, + observed.replica_id, + ) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id) + { + continue; + } + } + + if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) { + changes.push(proto::UnseenChannelBufferChange { + channel_id: channel_id.to_proto(), + epoch: latest.epoch as u64, + version: vec![proto::VectorClockEntry { + replica_id: latest.replica_id as u32, + timestamp: latest.lamport_timestamp as u32, + }], + }); + } + } + + Ok(changes) + } + + pub async fn get_latest_operations_for_buffers( + &self, + buffer_ids: impl IntoIterator, + tx: &DatabaseTransaction, + ) -> Result> { + let mut values = String::new(); + for id in buffer_ids { + if !values.is_empty() { + values.push_str(", "); + } + write!(&mut values, "({})", id).unwrap(); + } + + if values.is_empty() { + return Ok(Vec::default()); + } + + let sql = format!( + r#" + SELECT + * + FROM + ( + SELECT + *, + row_number() OVER ( + PARTITION BY buffer_id + ORDER BY + epoch DESC, + lamport_timestamp DESC, + replica_id DESC + ) as row_number + FROM buffer_operations + WHERE + buffer_id in ({values}) + ) AS last_operations + WHERE + row_number = 1 + "#, + ); + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + Ok(buffer_operation::Entity::find() + .from_raw_sql(stmt) + .all(&*tx) + .await?) + } } fn operation_to_storage( diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 5da4dd14646aa6fc1280ddcc608a530fad08f60d..ab31f59541887ac0c7a70db7ab60ade2bce79dbf 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,4 +1,8 @@ use super::*; +use rpc::proto::ChannelEdge; +use smallvec::SmallVec; + +type ChannelDescendants = HashMap>; impl Database { #[cfg(test)] @@ -46,7 +50,6 @@ impl Database { .insert(&*tx) .await?; - let channel_paths_stmt; if let Some(parent) = parent { let sql = r#" INSERT INTO channel_paths @@ -58,7 +61,7 @@ impl Database { WHERE channel_id = $3 "#; - channel_paths_stmt = Statement::from_sql_and_values( + let channel_paths_stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), sql, [ @@ -100,7 +103,7 @@ impl Database { .await } - pub async fn remove_channel( + pub async fn delete_channel( &self, channel_id: ChannelId, user_id: UserId, @@ -149,6 +152,19 @@ impl Database { .exec(&*tx) .await?; + // Delete any other paths that include this channel + let sql = r#" + DELETE FROM channel_paths + WHERE + id_path LIKE '%' || $1 || '%' + "#; + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [channel_id.to_proto().into()], + ); + tx.execute(channel_paths_stmt).await?; + Ok((channels_to_remove.into_keys().collect(), members_to_notify)) }) .await @@ -310,7 +326,6 @@ impl Database { .map(|channel| Channel { id: channel.id, name: channel.name, - parent_id: None, }) .collect(); @@ -319,6 +334,49 @@ impl Database { .await } + async fn get_channel_graph( + &self, + parents_by_child_id: ChannelDescendants, + trim_dangling_parents: bool, + tx: &DatabaseTransaction, + ) -> Result { + 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, + }) + } + } + + let mut edges = Vec::with_capacity(parents_by_child_id.len()); + for (channel, parents) in parents_by_child_id.iter() { + for parent in parents.into_iter() { + if trim_dangling_parents { + if parents_by_child_id.contains_key(parent) { + edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: parent.to_proto(), + }); + } + } else { + edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: parent.to_proto(), + }); + } + } + } + + Ok(ChannelGraph { channels, edges }) + } + pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -332,61 +390,94 @@ impl Database { .all(&*tx) .await?; - let parents_by_child_id = self - .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) + self.get_user_channels(user_id, channel_memberships, &tx) + .await + }) + .await + } + + pub async fn get_channel_for_user( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result { + self.transaction(|tx| async move { + let tx = tx; + + let channel_membership = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::ChannelId.eq(channel_id)) + .and(channel_member::Column::Accepted.eq(true)), + ) + .all(&*tx) .await?; - let channels_with_admin_privileges = channel_memberships - .iter() - .filter_map(|membership| membership.admin.then_some(membership.channel_id)) - .collect(); + self.get_user_channels(user_id, channel_membership, &tx) + .await + }) + .await + } - 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(), - }); - } - } + pub async fn get_user_channels( + &self, + user_id: UserId, + channel_memberships: Vec, + tx: &DatabaseTransaction, + ) -> Result { + let parents_by_child_id = self + .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) + .await?; - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryUserIdsAndChannelIds { - ChannelId, - UserId, - } + let channels_with_admin_privileges = channel_memberships + .iter() + .filter_map(|membership| membership.admin.then_some(membership.channel_id)) + .collect(); - 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) - } + let graph = self + .get_channel_graph(parents_by_child_id, true, &tx) + .await?; + + #[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(graph.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) } + } + + let channel_ids = graph.channels.iter().map(|c| c.id).collect::>(); + let channel_buffer_changes = self + .unseen_channel_buffer_changes(user_id, &channel_ids, &*tx) + .await?; - Ok(ChannelsForUser { - channels, - channel_participants, - channels_with_admin_privileges, - }) + let unseen_messages = self + .unseen_channel_messages(user_id, &channel_ids, &*tx) + .await?; + + Ok(ChannelsForUser { + channels: graph, + channel_participants, + channels_with_admin_privileges, + unseen_buffer_changes: channel_buffer_changes, + channel_messages: unseen_messages, }) - .await } pub async fn get_channel_members(&self, id: ChannelId) -> Result> { @@ -559,6 +650,7 @@ impl Database { Ok(()) } + /// Returns the channel ancestors, deepest first pub async fn get_channel_ancestors( &self, channel_id: ChannelId, @@ -566,6 +658,7 @@ impl Database { ) -> Result> { let paths = channel_path::Entity::find() .filter(channel_path::Column::ChannelId.eq(channel_id)) + .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) .all(tx) .await?; let mut channel_ids = Vec::new(); @@ -582,11 +675,25 @@ impl Database { Ok(channel_ids) } + /// Returns the channel descendants, + /// Structured as a map from child ids to their parent ids + /// For example, the descendants of 'a' in this DAG: + /// + /// /- b -\ + /// a -- c -- d + /// + /// would be: + /// { + /// a: [], + /// b: [a], + /// c: [a], + /// d: [a, c], + /// } async fn get_channel_descendants( &self, channel_ids: impl IntoIterator, tx: &DatabaseTransaction, - ) -> Result>> { + ) -> Result { let mut values = String::new(); for id in channel_ids { if !values.is_empty() { @@ -613,7 +720,7 @@ impl Database { let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - let mut parents_by_child_id = HashMap::default(); + let mut parents_by_child_id: ChannelDescendants = HashMap::default(); let mut paths = channel_path::Entity::find() .from_raw_sql(stmt) .stream(tx) @@ -632,7 +739,10 @@ impl Database { parent_id = Some(id); } } - parents_by_child_id.insert(path.channel_id, parent_id); + let entry = parents_by_child_id.entry(path.channel_id).or_default(); + if let Some(parent_id) = parent_id { + entry.insert(parent_id); + } } Ok(parents_by_child_id) @@ -677,7 +787,6 @@ impl Database { Channel { id: channel.id, name: channel.name, - parent_id: None, }, is_accepted, ))) @@ -703,9 +812,276 @@ impl Database { }) .await } + + // Insert an edge from the given channel to the given other channel. + pub async fn link_channel( + &self, + user: UserId, + channel: ChannelId, + to: ChannelId, + ) -> Result { + self.transaction(|tx| async move { + // Note that even with these maxed permissions, this linking operation + // is still insecure because you can't remove someone's permissions to a + // channel if they've linked the channel to one where they're an admin. + self.check_user_is_channel_admin(channel, user, &*tx) + .await?; + + self.link_channel_internal(user, channel, to, &*tx).await + }) + .await + } + + pub async fn link_channel_internal( + &self, + user: UserId, + channel: ChannelId, + to: ChannelId, + tx: &DatabaseTransaction, + ) -> Result { + self.check_user_is_channel_admin(to, user, &*tx).await?; + + let paths = channel_path::Entity::find() + .filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel))) + .all(tx) + .await?; + + let mut new_path_suffixes = HashSet::default(); + for path in paths { + if let Some(start_offset) = path.id_path.find(&format!("/{}/", channel)) { + new_path_suffixes.insert(( + path.channel_id, + path.id_path[(start_offset + 1)..].to_string(), + )); + } + } + + let paths_to_new_parent = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(to)) + .all(tx) + .await?; + + let mut new_paths = Vec::new(); + for path in paths_to_new_parent { + if path.id_path.contains(&format!("/{}/", channel)) { + Err(anyhow!("cycle"))?; + } + + new_paths.extend(new_path_suffixes.iter().map(|(channel_id, path_suffix)| { + channel_path::ActiveModel { + channel_id: ActiveValue::Set(*channel_id), + id_path: ActiveValue::Set(format!("{}{}", &path.id_path, path_suffix)), + } + })); + } + + channel_path::Entity::insert_many(new_paths) + .exec(&*tx) + .await?; + + // remove any root edges for the channel we just linked + { + channel_path::Entity::delete_many() + .filter(channel_path::Column::IdPath.like(&format!("/{}/%", channel))) + .exec(&*tx) + .await?; + } + + let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?; + if let Some(channel) = channel_descendants.get_mut(&channel) { + // Remove the other parents + channel.clear(); + channel.insert(to); + } + + let channels = self + .get_channel_graph(channel_descendants, false, &*tx) + .await?; + + Ok(channels) + } + + /// Unlink a channel from a given parent. This will add in a root edge if + /// the channel has no other parents after this operation. + pub async fn unlink_channel( + &self, + user: UserId, + channel: ChannelId, + from: ChannelId, + ) -> Result<()> { + self.transaction(|tx| async move { + // Note that even with these maxed permissions, this linking operation + // is still insecure because you can't remove someone's permissions to a + // channel if they've linked the channel to one where they're an admin. + self.check_user_is_channel_admin(channel, user, &*tx) + .await?; + + self.unlink_channel_internal(user, channel, from, &*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn unlink_channel_internal( + &self, + user: UserId, + channel: ChannelId, + from: ChannelId, + tx: &DatabaseTransaction, + ) -> Result<()> { + self.check_user_is_channel_admin(from, user, &*tx).await?; + + let sql = r#" + DELETE FROM channel_paths + WHERE + id_path LIKE '%/' || $1 || '/' || $2 || '/%' + RETURNING id_path, channel_id + "#; + + let paths = channel_path::Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [from.to_proto().into(), channel.to_proto().into()], + )) + .all(&*tx) + .await?; + + let is_stranded = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel)) + .count(&*tx) + .await? + == 0; + + // Make sure that there is always at least one path to the channel + if is_stranded { + let root_paths: Vec<_> = paths + .iter() + .map(|path| { + let start_offset = path.id_path.find(&format!("/{}/", channel)).unwrap(); + channel_path::ActiveModel { + channel_id: ActiveValue::Set(path.channel_id), + id_path: ActiveValue::Set(path.id_path[start_offset..].to_string()), + } + }) + .collect(); + channel_path::Entity::insert_many(root_paths) + .exec(&*tx) + .await?; + } + + Ok(()) + } + + /// Move a channel from one parent to another, returns the + /// Channels that were moved for notifying clients + pub async fn move_channel( + &self, + user: UserId, + channel: ChannelId, + from: ChannelId, + to: ChannelId, + ) -> Result { + if from == to { + return Ok(ChannelGraph { + channels: vec![], + edges: vec![], + }); + } + + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel, user, &*tx) + .await?; + + let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?; + + self.unlink_channel_internal(user, channel, from, &*tx) + .await?; + + Ok(moved_channels) + }) + .await + } } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIds { UserId, } + +#[derive(Debug)] +pub struct ChannelGraph { + pub channels: Vec, + pub edges: Vec, +} + +impl ChannelGraph { + pub fn is_empty(&self) -> bool { + self.channels.is_empty() && self.edges.is_empty() + } +} + +#[cfg(test)] +impl PartialEq for ChannelGraph { + fn eq(&self, other: &Self) -> bool { + // Order independent comparison for tests + let channels_set = self.channels.iter().collect::>(); + let other_channels_set = other.channels.iter().collect::>(); + let edges_set = self + .edges + .iter() + .map(|edge| (edge.channel_id, edge.parent_id)) + .collect::>(); + let other_edges_set = other + .edges + .iter() + .map(|edge| (edge.channel_id, edge.parent_id)) + .collect::>(); + + channels_set == other_channels_set && edges_set == other_edges_set + } +} + +#[cfg(not(test))] +impl PartialEq for ChannelGraph { + fn eq(&self, other: &Self) -> bool { + self.channels == other.channels && self.edges == other.edges + } +} + +struct SmallSet(SmallVec<[T; 1]>); + +impl Deref for SmallSet { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl Default for SmallSet { + fn default() -> Self { + Self(SmallVec::new()) + } +} + +impl SmallSet { + fn insert(&mut self, value: T) -> bool + where + T: Ord, + { + match self.binary_search(&value) { + Ok(_) => false, + Err(ix) => { + self.0.insert(ix, value); + true + } + } + } + + fn clear(&mut self) { + self.0.clear(); + } +} diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index a18958f035515450be2f9448b65c5f00dc6368fd..2171f1a6bf87354b8017fd35b47d77bba1e25af0 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -18,12 +18,12 @@ impl Database { 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) + Expr::col((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) + Expr::col((user_b_participant.clone(), room_participant::Column::Id)) .is_not_null(), "user_b_busy", ) diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs new file mode 100644 index 0000000000000000000000000000000000000000..83b5382cf588fceba9ffc2233747d6640d4eade0 --- /dev/null +++ b/crates/collab/src/db/queries/messages.rs @@ -0,0 +1,347 @@ +use super::*; +use time::OffsetDateTime; + +impl Database { + pub async fn join_channel_chat( + &self, + channel_id: ChannelId, + connection_id: ConnectionId, + user_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_member(channel_id, user_id, &*tx) + .await?; + channel_chat_participant::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(user_id), + connection_id: ActiveValue::Set(connection_id.id as i32), + connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)), + } + .insert(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn channel_chat_connection_lost( + &self, + connection_id: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result<()> { + channel_chat_participant::Entity::delete_many() + .filter( + Condition::all() + .add( + channel_chat_participant::Column::ConnectionServerId + .eq(connection_id.owner_id), + ) + .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)), + ) + .exec(tx) + .await?; + Ok(()) + } + + pub async fn leave_channel_chat( + &self, + channel_id: ChannelId, + connection_id: ConnectionId, + _user_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + channel_chat_participant::Entity::delete_many() + .filter( + Condition::all() + .add( + channel_chat_participant::Column::ConnectionServerId + .eq(connection_id.owner_id), + ) + .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)) + .add(channel_chat_participant::Column::ChannelId.eq(channel_id)), + ) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_channel_messages( + &self, + channel_id: ChannelId, + user_id: UserId, + count: usize, + before_message_id: Option, + ) -> Result> { + self.transaction(|tx| async move { + self.check_user_is_channel_member(channel_id, user_id, &*tx) + .await?; + + let mut condition = + Condition::all().add(channel_message::Column::ChannelId.eq(channel_id)); + + if let Some(before_message_id) = before_message_id { + condition = condition.add(channel_message::Column::Id.lt(before_message_id)); + } + + let mut rows = channel_message::Entity::find() + .filter(condition) + .order_by_asc(channel_message::Column::Id) + .limit(count as u64) + .stream(&*tx) + .await?; + + let mut messages = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + let nonce = row.nonce.as_u64_pair(); + messages.push(proto::ChannelMessage { + id: row.id.to_proto(), + sender_id: row.sender_id.to_proto(), + body: row.body, + timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, + nonce: Some(proto::Nonce { + upper_half: nonce.0, + lower_half: nonce.1, + }), + }); + } + drop(rows); + Ok(messages) + }) + .await + } + + pub async fn create_channel_message( + &self, + channel_id: ChannelId, + user_id: UserId, + body: &str, + timestamp: OffsetDateTime, + nonce: u128, + ) -> Result<(MessageId, Vec, Vec)> { + self.transaction(|tx| async move { + let mut rows = channel_chat_participant::Entity::find() + .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) + .stream(&*tx) + .await?; + + let mut is_participant = false; + let mut participant_connection_ids = Vec::new(); + let mut participant_user_ids = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + if row.user_id == user_id { + is_participant = true; + } + participant_user_ids.push(row.user_id); + participant_connection_ids.push(row.connection()); + } + drop(rows); + + if !is_participant { + Err(anyhow!("not a chat participant"))?; + } + + let timestamp = timestamp.to_offset(time::UtcOffset::UTC); + let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time()); + + let message = channel_message::Entity::insert(channel_message::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + sender_id: ActiveValue::Set(user_id), + body: ActiveValue::Set(body.to_string()), + sent_at: ActiveValue::Set(timestamp), + nonce: ActiveValue::Set(Uuid::from_u128(nonce)), + id: ActiveValue::NotSet, + }) + .on_conflict( + OnConflict::column(channel_message::Column::Nonce) + .update_column(channel_message::Column::Nonce) + .to_owned(), + ) + .exec(&*tx) + .await?; + + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryConnectionId { + ConnectionId, + } + + // Observe this message for the sender + self.observe_channel_message_internal( + channel_id, + user_id, + message.last_insert_id, + &*tx, + ) + .await?; + + let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + channel_members.retain(|member| !participant_user_ids.contains(member)); + + Ok(( + message.last_insert_id, + participant_connection_ids, + channel_members, + )) + }) + .await + } + + pub async fn observe_channel_message( + &self, + channel_id: ChannelId, + user_id: UserId, + message_id: MessageId, + ) -> Result<()> { + self.transaction(|tx| async move { + self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx) + .await?; + Ok(()) + }) + .await + } + + async fn observe_channel_message_internal( + &self, + channel_id: ChannelId, + user_id: UserId, + message_id: MessageId, + tx: &DatabaseTransaction, + ) -> Result<()> { + observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel { + user_id: ActiveValue::Set(user_id), + channel_id: ActiveValue::Set(channel_id), + channel_message_id: ActiveValue::Set(message_id), + }) + .on_conflict( + OnConflict::columns([ + observed_channel_messages::Column::ChannelId, + observed_channel_messages::Column::UserId, + ]) + .update_column(observed_channel_messages::Column::ChannelMessageId) + .action_cond_where(observed_channel_messages::Column::ChannelMessageId.lt(message_id)) + .to_owned(), + ) + // TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug + .exec_without_returning(&*tx) + .await?; + Ok(()) + } + + pub async fn unseen_channel_messages( + &self, + user_id: UserId, + channel_ids: &[ChannelId], + tx: &DatabaseTransaction, + ) -> Result> { + let mut observed_messages_by_channel_id = HashMap::default(); + let mut rows = observed_channel_messages::Entity::find() + .filter(observed_channel_messages::Column::UserId.eq(user_id)) + .filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied())) + .stream(&*tx) + .await?; + + while let Some(row) = rows.next().await { + let row = row?; + observed_messages_by_channel_id.insert(row.channel_id, row); + } + drop(rows); + 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(Default::default()); + } + + let sql = format!( + r#" + SELECT + * + FROM ( + SELECT + *, + row_number() OVER ( + PARTITION BY channel_id + ORDER BY id DESC + ) as row_number + FROM channel_messages + WHERE + channel_id in ({values}) + ) AS messages + WHERE + row_number = 1 + "#, + ); + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + let last_messages = channel_message::Model::find_by_statement(stmt) + .all(&*tx) + .await?; + + let mut changes = Vec::new(); + for last_message in last_messages { + if let Some(observed_message) = + observed_messages_by_channel_id.get(&last_message.channel_id) + { + if observed_message.channel_message_id == last_message.id { + continue; + } + } + changes.push(proto::UnseenChannelMessage { + channel_id: last_message.channel_id.to_proto(), + message_id: last_message.id.to_proto(), + }); + } + + Ok(changes) + } + + pub async fn remove_channel_message( + &self, + channel_id: ChannelId, + message_id: MessageId, + user_id: UserId, + ) -> Result> { + self.transaction(|tx| async move { + let mut rows = channel_chat_participant::Entity::find() + .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) + .stream(&*tx) + .await?; + + let mut is_participant = false; + let mut participant_connection_ids = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + if row.user_id == user_id { + is_participant = true; + } + participant_connection_ids.push(row.connection()); + } + drop(rows); + + if !is_participant { + Err(anyhow!("not a chat participant"))?; + } + + let result = channel_message::Entity::delete_by_id(message_id) + .filter(channel_message::Column::SenderId.eq(user_id)) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("no such message"))?; + } + + Ok(participant_connection_ids) + }) + .await + } +} diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 31c7cdae3e4837dd46487526fbf5c20a021d9b7d..3e2c00337823a91badeedf183a5598f94f8f2c2a 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -738,7 +738,7 @@ impl Database { Condition::any() .add( Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) + .add(follower::Column::ProjectId.eq(Some(project_id))) .add( follower::Column::LeaderConnectionServerId .eq(connection.owner_id), @@ -747,7 +747,7 @@ impl Database { ) .add( Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) + .add(follower::Column::ProjectId.eq(Some(project_id))) .add( follower::Column::FollowerConnectionServerId .eq(connection.owner_id), @@ -862,13 +862,46 @@ impl Database { .await } + pub async fn check_room_participants( + &self, + room_id: RoomId, + leader_id: ConnectionId, + follower_id: ConnectionId, + ) -> Result<()> { + self.transaction(|tx| async move { + use room_participant::Column; + + let count = room_participant::Entity::find() + .filter( + Condition::all().add(Column::RoomId.eq(room_id)).add( + Condition::any() + .add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and( + Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32), + )) + .add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and( + Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32), + )), + ), + ) + .count(&*tx) + .await?; + + if count < 2 { + Err(anyhow!("not room participants"))?; + } + + Ok(()) + }) + .await + } + pub async fn follow( &self, + room_id: RoomId, 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), @@ -894,15 +927,16 @@ impl Database { pub async fn unfollow( &self, + room_id: RoomId, 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::RoomId.eq(room_id)) .add(follower::Column::ProjectId.eq(project_id)) .add( follower::Column::LeaderConnectionServerId diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index e348b50beeedb47c609d455ad759f59adea01adf..b103ae1c737cfdd977418d528585c6fdd9ebb4b7 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -128,6 +128,7 @@ impl Database { calling_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), + participant_index: ActiveValue::set(Some(0)), ..Default::default() } .insert(&*tx) @@ -152,6 +153,7 @@ impl Database { room_id: ActiveValue::set(room_id), user_id: ActiveValue::set(called_user_id), answering_connection_lost: ActiveValue::set(false), + participant_index: ActiveValue::NotSet, 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( @@ -283,6 +285,26 @@ impl Database { .await? .ok_or_else(|| anyhow!("no such room"))?; + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryParticipantIndices { + ParticipantIndex, + } + let existing_participant_indices: Vec = room_participant::Entity::find() + .filter( + room_participant::Column::RoomId + .eq(room_id) + .and(room_participant::Column::ParticipantIndex.is_not_null()), + ) + .select_only() + .column(room_participant::Column::ParticipantIndex) + .into_values::<_, QueryParticipantIndices>() + .all(&*tx) + .await?; + let mut participant_index = 0; + while existing_participant_indices.contains(&participant_index) { + participant_index += 1; + } + if let Some(channel_id) = channel_id { self.check_user_is_channel_member(channel_id, user_id, &*tx) .await?; @@ -300,6 +322,7 @@ impl Database { calling_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), + participant_index: ActiveValue::Set(Some(participant_index)), ..Default::default() }]) .on_conflict( @@ -308,6 +331,7 @@ impl Database { room_participant::Column::AnsweringConnectionId, room_participant::Column::AnsweringConnectionServerId, room_participant::Column::AnsweringConnectionLost, + room_participant::Column::ParticipantIndex, ]) .to_owned(), ) @@ -322,6 +346,7 @@ impl Database { .add(room_participant::Column::AnsweringConnectionId.is_null()), ) .set(room_participant::ActiveModel { + participant_index: ActiveValue::Set(Some(participant_index)), answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, @@ -890,54 +915,43 @@ impl Database { 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) + self.room_connection_lost(connection, &*tx).await?; + self.channel_buffer_connection_lost(connection, &*tx) .await?; - + self.channel_chat_connection_lost(connection, &*tx).await?; Ok(()) }) .await } + pub async fn room_connection_lost( + &self, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result<()> { + 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?; + } + Ok(()) + } + fn build_incoming_call( room: &proto::Room, called_user_id: UserId, @@ -971,6 +985,39 @@ impl Database { Ok(room) } + pub async fn room_connection_ids( + &self, + room_id: RoomId, + connection_id: ConnectionId, + ) -> Result>> { + self.room_transaction(room_id, |tx| async move { + let mut participants = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .stream(&*tx) + .await?; + + let mut is_participant = false; + let mut connection_ids = HashSet::default(); + while let Some(participant) = participants.next().await { + let participant = participant?; + if let Some(answering_connection) = participant.answering_connection() { + if answering_connection == connection_id { + is_participant = true; + } else { + connection_ids.insert(answering_connection); + } + } + } + + if !is_participant { + Err(anyhow!("not a room participant"))?; + } + + Ok(connection_ids) + }) + .await + } + async fn get_channel_room( &self, room_id: RoomId, @@ -989,10 +1036,15 @@ impl Database { 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) - { + if let ( + Some(answering_connection_id), + Some(answering_connection_server_id), + Some(participant_index), + ) = ( + db_participant.answering_connection_id, + db_participant.answering_connection_server_id, + db_participant.participant_index, + ) { let location = match ( db_participant.location_kind, db_participant.location_project_id, @@ -1023,6 +1075,7 @@ impl Database { peer_id: Some(answering_connection.into()), projects: Default::default(), location: Some(proto::ParticipantLocation { variant: location }), + participant_index: participant_index as u32, }, ); } else { diff --git a/crates/collab/src/db/queries/signups.rs b/crates/collab/src/db/queries/signups.rs deleted file mode 100644 index 8cb8d866fb401cf20f6b29aac1deac84ea584ec7..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/queries/signups.rs +++ /dev/null @@ -1,349 +0,0 @@ -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 index 5cb1ef6ea39c6dbca8bb58131f36428580a0aa9d..27e64e25981ecdbd31e6aa337875e1ff81852b9c 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -123,27 +123,6 @@ impl Database { .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 { @@ -163,21 +142,6 @@ impl Database { .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() @@ -220,7 +184,7 @@ impl Database { Ok(user::Entity::find() .from_raw_sql(Statement::from_sql_and_values( self.pool.get_database_backend(), - query.into(), + query, vec![like_string.into(), name_query.into(), limit.into()], )) .all(&*tx) diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 1765cee065fb6c7ae31818568a229e3c3c0bd3f0..e19391da7dd513970b0fa593d7977fa7689c0510 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -4,12 +4,16 @@ pub mod buffer_operation; pub mod buffer_snapshot; pub mod channel; pub mod channel_buffer_collaborator; +pub mod channel_chat_participant; pub mod channel_member; +pub mod channel_message; pub mod channel_path; pub mod contact; pub mod feature_flag; pub mod follower; pub mod language_server; +pub mod observed_buffer_edits; +pub mod observed_channel_messages; pub mod project; pub mod project_collaborator; pub mod room; diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 05895ede4cf6b5080889cf281a1ce3651aebd1c2..54f12defc1b56570a0629e2e92a896ad167aa6d6 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -21,6 +21,8 @@ pub enum Relation { Member, #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] BufferCollaborators, + #[sea_orm(has_many = "super::channel_chat_participant::Entity")] + ChatParticipants, } impl Related for Entity { @@ -46,3 +48,9 @@ impl Related for Entity { Relation::BufferCollaborators.def() } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ChatParticipants.def() + } +} diff --git a/crates/collab/src/db/tables/channel_chat_participant.rs b/crates/collab/src/db/tables/channel_chat_participant.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3ef36c289f86e5f20411cf9b3f442698f6a4024 --- /dev/null +++ b/crates/collab/src/db/tables/channel_chat_participant.rs @@ -0,0 +1,41 @@ +use crate::db::{ChannelChatParticipantId, ChannelId, ServerId, UserId}; +use rpc::ConnectionId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_chat_participants")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelChatParticipantId, + pub channel_id: ChannelId, + pub user_id: UserId, + pub connection_id: i32, + pub connection_server_id: ServerId, +} + +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/tables/channel_message.rs b/crates/collab/src/db/tables/channel_message.rs new file mode 100644 index 0000000000000000000000000000000000000000..ff49c63ba71d675f20f542d3300d74d322d70722 --- /dev/null +++ b/crates/collab/src/db/tables/channel_message.rs @@ -0,0 +1,45 @@ +use crate::db::{ChannelId, MessageId, UserId}; +use sea_orm::entity::prelude::*; +use time::PrimitiveDateTime; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_messages")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: MessageId, + pub channel_id: ChannelId, + pub sender_id: UserId, + pub body: String, + pub sent_at: PrimitiveDateTime, + pub nonce: Uuid, +} + +impl ActiveModelBehavior for ActiveModel {} + +#[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, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::SenderId", + to = "super::user::Column::Id" + )] + Sender, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Sender.def() + } +} diff --git a/crates/collab/src/db/tables/observed_buffer_edits.rs b/crates/collab/src/db/tables/observed_buffer_edits.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8e7aafaa285cc6584577994b682bbc5d30a3a7c --- /dev/null +++ b/crates/collab/src/db/tables/observed_buffer_edits.rs @@ -0,0 +1,43 @@ +use crate::db::{BufferId, UserId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "observed_buffer_edits")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + pub buffer_id: BufferId, + pub epoch: i32, + pub lamport_timestamp: i32, + pub replica_id: 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, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/observed_channel_messages.rs b/crates/collab/src/db/tables/observed_channel_messages.rs new file mode 100644 index 0000000000000000000000000000000000000000..18259f844274750ebcb463c7d69d619457055d89 --- /dev/null +++ b/crates/collab/src/db/tables/observed_channel_messages.rs @@ -0,0 +1,41 @@ +use crate::db::{ChannelId, MessageId, UserId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "observed_channel_messages")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + pub channel_id: ChannelId, + pub channel_message_id: MessageId, +} + +#[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, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index 537cac9f14fcbe39e19915571e2833086c115888..4c5b8cc11c7a23532de3e7d0ea61f55fe3a4077f 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -1,4 +1,5 @@ use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; +use rpc::ConnectionId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] @@ -17,6 +18,16 @@ pub struct Model { pub calling_user_id: UserId, pub calling_connection_id: i32, pub calling_connection_server_id: Option, + pub participant_index: Option, +} + +impl Model { + pub fn answering_connection(&self) -> Option { + Some(ConnectionId { + owner_id: self.answering_connection_server_id?.0 as u32, + id: self.answering_connection_id? as u32, + }) + } } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index ee961006cbbf74b019141c0973aca18d73309012..75584ff90b68cf4fea0b4151a835c77e970daae5 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,10 +1,13 @@ mod buffer_tests; +mod channel_tests; mod db_tests; mod feature_flag_tests; +mod message_tests; use super::*; use gpui::executor::Background; use parking_lot::Mutex; +use rpc::proto::ChannelEdge; use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; use std::sync::Arc; @@ -36,7 +39,7 @@ impl TestDb { db.pool .execute(sea_orm::Statement::from_string( db.pool.get_database_backend(), - sql.into(), + sql, )) .await .unwrap(); @@ -131,7 +134,7 @@ impl Drop for TestDb { db.pool .execute(sea_orm::Statement::from_string( db.pool.get_database_backend(), - query.into(), + query, )) .await .log_err(); @@ -142,3 +145,27 @@ impl Drop for TestDb { } } } + +/// The second tuples are (channel_id, parent) +fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph { + let mut graph = ChannelGraph { + channels: vec![], + edges: vec![], + }; + + for (id, name) in channels { + graph.channels.push(Channel { + id: *id, + name: name.to_string(), + }) + } + + for (channel, parent) in edges { + graph.edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: parent.to_proto(), + }) + } + + graph +} diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index e71748b88b0571c2ccd7840924b8fc325cd368b7..f6e91b91f011046d6a20431ac10099b21e543774 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -1,6 +1,6 @@ use super::*; use crate::test_both_dbs; -use language::proto; +use language::proto::{self, serialize_version}; use text::Buffer; test_both_dbs!( @@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc) { let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); assert_eq!(zed_collaborats, &[a_id, b_id]); - let collaborators = db + let left_buffer = db .leave_channel_buffer(zed_id, connection_id_b) .await .unwrap(); - assert_eq!(collaborators, &[connection_id_a],); + assert_eq!(left_buffer.connections, &[connection_id_a],); let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); let _ = db @@ -163,3 +163,349 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(buffer_response_b.base_text, "hello, cruel world"); assert_eq!(buffer_response_b.operations, &[]); } + +test_both_dbs!( + test_channel_buffers_last_operations, + test_channel_buffers_last_operations_postgres, + test_channel_buffers_last_operations_sqlite +); + +async fn test_channel_buffers_last_operations(db: &Database) { + let user_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 observer_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; + let owner_id = db.create_server("production").await.unwrap().0 as u32; + let connection_id = ConnectionId { + owner_id, + id: user_id.0 as u32, + }; + + let mut buffers = Vec::new(); + let mut text_buffers = Vec::new(); + for i in 0..3 { + let channel = db + .create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id) + .await + .unwrap(); + + db.invite_channel_member(channel, observer_id, user_id, false) + .await + .unwrap(); + db.respond_to_channel_invite(channel, observer_id, true) + .await + .unwrap(); + + db.join_channel_buffer(channel, user_id, connection_id) + .await + .unwrap(); + + buffers.push( + db.transaction(|tx| async move { db.get_channel_buffer(channel, &*tx).await }) + .await + .unwrap(), + ); + + text_buffers.push(Buffer::new(0, 0, "".to_string())); + } + + let operations = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx) + .await + } + }) + .await + .unwrap(); + + assert!(operations.is_empty()); + + update_buffer( + buffers[0].channel_id, + user_id, + db, + vec![ + text_buffers[0].edit([(0..0, "a")]), + text_buffers[0].edit([(0..0, "b")]), + text_buffers[0].edit([(0..0, "c")]), + ], + ) + .await; + + update_buffer( + buffers[1].channel_id, + user_id, + db, + vec![ + text_buffers[1].edit([(0..0, "d")]), + text_buffers[1].edit([(1..1, "e")]), + text_buffers[1].edit([(2..2, "f")]), + ], + ) + .await; + + // cause buffer 1's epoch to increment. + db.leave_channel_buffer(buffers[1].channel_id, connection_id) + .await + .unwrap(); + db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id) + .await + .unwrap(); + text_buffers[1] = Buffer::new(1, 0, "def".to_string()); + update_buffer( + buffers[1].channel_id, + user_id, + db, + vec![ + text_buffers[1].edit([(0..0, "g")]), + text_buffers[1].edit([(0..0, "h")]), + ], + ) + .await; + + update_buffer( + buffers[2].channel_id, + user_id, + db, + vec![text_buffers[2].edit([(0..0, "i")])], + ) + .await; + + let operations = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx) + .await + } + }) + .await + .unwrap(); + assert_operations( + &operations, + &[ + (buffers[1].id, 1, &text_buffers[1]), + (buffers[2].id, 0, &text_buffers[2]), + ], + ); + + let operations = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx) + .await + } + }) + .await + .unwrap(); + assert_operations( + &operations, + &[ + (buffers[0].id, 0, &text_buffers[0]), + (buffers[1].id, 1, &text_buffers[1]), + ], + ); + + let buffer_changes = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.unseen_channel_buffer_changes( + observer_id, + &[ + buffers[0].channel_id, + buffers[1].channel_id, + buffers[2].channel_id, + ], + &*tx, + ) + .await + } + }) + .await + .unwrap(); + + pretty_assertions::assert_eq!( + buffer_changes, + [ + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[0].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[0].version()), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[1].channel_id.to_proto(), + epoch: 1, + version: serialize_version(&text_buffers[1].version()) + .into_iter() + .filter(|vector| vector.replica_id + == buffer_changes[1].version.first().unwrap().replica_id) + .collect::>(), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[2].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[2].version()), + }, + ] + ); + + db.observe_buffer_version( + buffers[1].id, + observer_id, + 1, + serialize_version(&text_buffers[1].version()).as_slice(), + ) + .await + .unwrap(); + + let buffer_changes = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.unseen_channel_buffer_changes( + observer_id, + &[ + buffers[0].channel_id, + buffers[1].channel_id, + buffers[2].channel_id, + ], + &*tx, + ) + .await + } + }) + .await + .unwrap(); + + assert_eq!( + buffer_changes, + [ + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[0].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[0].version()), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[2].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[2].version()), + }, + ] + ); + + // Observe an earlier version of the buffer. + db.observe_buffer_version( + buffers[1].id, + observer_id, + 1, + &[rpc::proto::VectorClockEntry { + replica_id: 0, + timestamp: 0, + }], + ) + .await + .unwrap(); + + let buffer_changes = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.unseen_channel_buffer_changes( + observer_id, + &[ + buffers[0].channel_id, + buffers[1].channel_id, + buffers[2].channel_id, + ], + &*tx, + ) + .await + } + }) + .await + .unwrap(); + + assert_eq!( + buffer_changes, + [ + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[0].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[0].version()), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[2].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[2].version()), + }, + ] + ); +} + +async fn update_buffer( + channel_id: ChannelId, + user_id: UserId, + db: &Database, + operations: Vec, +) { + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + db.update_channel_buffer(channel_id, user_id, &operations) + .await + .unwrap(); +} + +fn assert_operations( + operations: &[buffer_operation::Model], + expected: &[(BufferId, i32, &text::Buffer)], +) { + let actual = operations + .iter() + .map(|op| buffer_operation::Model { + buffer_id: op.buffer_id, + epoch: op.epoch, + lamport_timestamp: op.lamport_timestamp, + replica_id: op.replica_id, + value: vec![], + }) + .collect::>(); + let expected = expected + .iter() + .map(|(buffer_id, epoch, buffer)| buffer_operation::Model { + buffer_id: *buffer_id, + epoch: *epoch, + lamport_timestamp: buffer.lamport_clock.value as i32 - 1, + replica_id: buffer.replica_id() as i32, + value: vec![], + }) + .collect::>(); + assert_eq!(actual, expected, "unexpected operations") +} diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..429852d12870a232da68d165d75de68d3a7b9be0 --- /dev/null +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -0,0 +1,875 @@ +use collections::{HashMap, HashSet}; +use rpc::{ + proto::{self}, + ConnectionId, +}; + +use crate::{ + db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; + +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, + graph( + &[ + (zed_id, "zed"), + (crdb_id, "crdb"), + (livestreaming_id, "livestreaming"), + (replace_id, "replace"), + (rust_id, "rust"), + (cargo_id, "cargo"), + (cargo_ra_id, "cargo-ra") + ], + &[ + (crdb_id, zed_id), + (livestreaming_id, zed_id), + (replace_id, zed_id), + (cargo_id, rust_id), + (cargo_ra_id, cargo_id), + ] + ) + ); + + let result = db.get_channels_for_user(b_id).await.unwrap(); + assert_eq!( + result.channels, + graph( + &[ + (zed_id, "zed"), + (crdb_id, "crdb"), + (livestreaming_id, "livestreaming"), + (replace_id, "replace") + ], + &[ + (crdb_id, zed_id), + (livestreaming_id, zed_id), + (replace_id, 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, + graph( + &[ + (zed_id, "zed"), + (crdb_id, "crdb"), + (livestreaming_id, "livestreaming"), + (replace_id, "replace") + ], + &[ + (crdb_id, zed_id), + (livestreaming_id, zed_id), + (replace_id, zed_id) + ] + ) + ); + + // Remove a single channel + db.delete_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.delete_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()) +} + +test_both_dbs!( + test_db_channel_moving, + test_channels_moving_postgres, + test_channels_moving_sqlite +); + +async fn test_db_channel_moving(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 zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + let crdb_id = db + .create_channel("crdb", Some(zed_id), "2", a_id) + .await + .unwrap(); + + let gpui2_id = db + .create_channel("gpui2", Some(zed_id), "3", a_id) + .await + .unwrap(); + + let livestreaming_id = db + .create_channel("livestreaming", Some(crdb_id), "4", a_id) + .await + .unwrap(); + + let livestreaming_dag_id = db + .create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id) + .await + .unwrap(); + + // ======================================================================== + // sanity check + // Initial DAG: + // /- gpui2 + // zed -- crdb - livestreaming - livestreaming_dag + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + ], + ); + + // Attempt to make a cycle + assert!(db + .link_channel(a_id, zed_id, livestreaming_id) + .await + .is_err()); + + // ======================================================================== + // Make a link + db.link_channel(a_id, livestreaming_id, zed_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 + // zed -- crdb - livestreaming - livestreaming_dag + // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + ], + ); + + // ======================================================================== + // Create a new channel below a channel with multiple parents + let livestreaming_dag_sub_id = db + .create_channel( + "livestreaming_dag_sub", + Some(livestreaming_dag_id), + "6", + a_id, + ) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 + // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Test a complex DAG by making another link + let returned_channels = db + .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 /---------------------\ + // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // \--------/ + + // make sure we're getting just the new link + // Not using the assert_dag helper because we want to make sure we're returning the full data + pretty_assertions::assert_eq!( + returned_channels, + graph( + &[(livestreaming_dag_sub_id, "livestreaming_dag_sub")], + &[(livestreaming_dag_sub_id, livestreaming_id)] + ) + ); + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Test a complex DAG by making another link + let returned_channels = db + .link_channel(a_id, livestreaming_id, gpui2_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 -\ /---------------------\ + // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // \---------/ + + // Make sure that we're correctly getting the full sub-dag + pretty_assertions::assert_eq!( + returned_channels, + graph( + &[ + (livestreaming_id, "livestreaming"), + (livestreaming_dag_id, "livestreaming_dag"), + (livestreaming_dag_sub_id, "livestreaming_dag_sub"), + ], + &[ + (livestreaming_id, gpui2_id), + (livestreaming_dag_id, livestreaming_id), + (livestreaming_dag_sub_id, livestreaming_id), + (livestreaming_dag_sub_id, livestreaming_dag_id), + ] + ) + ); + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Test unlinking in a complex DAG by removing the inner link + db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 -\ + // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // \---------/ + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Test unlinking in a complex DAG by removing the inner link + db.unlink_channel(a_id, livestreaming_id, gpui2_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 + // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Test moving DAG nodes by moving livestreaming to be below gpui2 + db.move_channel(a_id, livestreaming_id, crdb_id, gpui2_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // zed - crdb / + // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Deleting a channel should not delete children that still have other parents + db.delete_channel(gpui2_id, a_id).await.unwrap(); + + // DAG is now: + // zed - crdb + // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Unlinking a channel from it's parent should automatically promote it to a root channel + db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap(); + + // DAG is now: + // crdb + // zed + // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, None), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // You should be able to move a root channel into a non-root channel + db.link_channel(a_id, crdb_id, zed_id).await.unwrap(); + + // DAG is now: + // zed - crdb + // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // ======================================================================== + // Prep for DAG deletion test + db.link_channel(a_id, livestreaming_id, crdb_id) + .await + .unwrap(); + + // DAG is now: + // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub + // \--------/ + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); + + // Deleting the parent of a DAG should delete the whole DAG: + db.delete_channel(zed_id, a_id).await.unwrap(); + let result = db.get_channels_for_user(a_id).await.unwrap(); + + assert!(result.channels.is_empty()) +} + +test_both_dbs!( + test_db_channel_moving_bugs, + test_db_channel_moving_bugs_postgres, + test_db_channel_moving_bugs_sqlite +); + +async fn test_db_channel_moving_bugs(db: &Arc) { + let user_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 zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap(); + + let projects_id = db + .create_channel("projects", Some(zed_id), "2", user_id) + .await + .unwrap(); + + let livestreaming_id = db + .create_channel("livestreaming", Some(projects_id), "3", user_id) + .await + .unwrap(); + + // Dag is: zed - projects - livestreaming + + // Move to same parent should be a no-op + assert!(db + .move_channel(user_id, projects_id, zed_id, zed_id) + .await + .unwrap() + .is_empty()); + + // Stranding a channel should retain it's sub channels + db.unlink_channel(user_id, projects_id, zed_id) + .await + .unwrap(); + + let result = db.get_channels_for_user(user_id).await.unwrap(); + assert_dag( + result.channels, + &[ + (zed_id, None), + (projects_id, None), + (livestreaming_id, Some(projects_id)), + ], + ); +} + +#[track_caller] +fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) { + let mut actual_map: HashMap> = HashMap::default(); + for channel in actual.channels { + actual_map.insert(channel.id, HashSet::default()); + } + for edge in actual.edges { + actual_map + .get_mut(&ChannelId::from_proto(edge.channel_id)) + .unwrap() + .insert(ChannelId::from_proto(edge.parent_id)); + } + + let mut expected_map: HashMap> = HashMap::default(); + + for (child, parent) in expected { + let entry = expected_map.entry(*child).or_default(); + if let Some(parent) = parent { + entry.insert(*parent); + } + } + + pretty_assertions::assert_eq!(actual_map, expected_map) +} diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index fc31ee7c4d4aee8dddc46bf6cc0e77fc89e4dd39..9a617166fead82ca5b538b84ec268329f1f8de22 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -575,999 +575,6 @@ async fn test_fuzzy_search_users() { } } -#[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/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..4966ef1bda4fc99647edf03daa8f9a09ff47474b --- /dev/null +++ b/crates/collab/src/db/tests/message_tests.rs @@ -0,0 +1,244 @@ +use crate::{ + db::{Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; +use time::OffsetDateTime; + +test_both_dbs!( + test_channel_message_nonces, + test_channel_message_nonces_postgres, + test_channel_message_nonces_sqlite +); + +async fn test_channel_message_nonces(db: &Arc) { + let user = db + .create_user( + "user@example.com", + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let channel = db + .create_channel("channel", None, "room", user) + .await + .unwrap(); + + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) + .await + .unwrap(); + + let msg1_id = db + .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1) + .await + .unwrap(); + let msg2_id = db + .create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2) + .await + .unwrap(); + let msg3_id = db + .create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1) + .await + .unwrap(); + let msg4_id = db + .create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2) + .await + .unwrap(); + + assert_ne!(msg1_id, msg2_id); + assert_eq!(msg1_id, msg3_id); + assert_eq!(msg2_id, msg4_id); +} + +test_both_dbs!( + test_channel_message_new_notification, + test_channel_message_new_notification_postgres, + test_channel_message_new_notification_sqlite +); + +async fn test_channel_message_new_notification(db: &Arc) { + let user = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let observer = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1 = db + .create_channel("channel", None, "room", user) + .await + .unwrap(); + + let channel_2 = db + .create_channel("channel-2", None, "room", user) + .await + .unwrap(); + + db.invite_channel_member(channel_1, observer, user, false) + .await + .unwrap(); + + db.respond_to_channel_invite(channel_1, observer, true) + .await + .unwrap(); + + db.invite_channel_member(channel_2, observer, user, false) + .await + .unwrap(); + + db.respond_to_channel_invite(channel_2, observer, true) + .await + .unwrap(); + + let owner_id = db.create_server("test").await.unwrap().0 as u32; + let user_connection_id = rpc::ConnectionId { owner_id, id: 0 }; + + db.join_channel_chat(channel_1, user_connection_id, user) + .await + .unwrap(); + + let _ = db + .create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1) + .await + .unwrap(); + + let (second_message, _, _) = db + .create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2) + .await + .unwrap(); + + let (third_message, _, _) = db + .create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3) + .await + .unwrap(); + + db.join_channel_chat(channel_2, user_connection_id, user) + .await + .unwrap(); + + let (fourth_message, _, _) = db + .create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4) + .await + .unwrap(); + + // Check that observer has new messages + let unseen_messages = db + .transaction(|tx| async move { + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) + .await + .unwrap(); + + assert_eq!( + unseen_messages, + [ + rpc::proto::UnseenChannelMessage { + channel_id: channel_1.to_proto(), + message_id: third_message.to_proto(), + }, + rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }, + ] + ); + + // Observe the second message + db.observe_channel_message(channel_1, observer, second_message) + .await + .unwrap(); + + // Make sure the observer still has a new message + let unseen_messages = db + .transaction(|tx| async move { + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) + .await + .unwrap(); + assert_eq!( + unseen_messages, + [ + rpc::proto::UnseenChannelMessage { + channel_id: channel_1.to_proto(), + message_id: third_message.to_proto(), + }, + rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }, + ] + ); + + // Observe the third message, + db.observe_channel_message(channel_1, observer, third_message) + .await + .unwrap(); + + // Make sure the observer does not have a new method + let unseen_messages = db + .transaction(|tx| async move { + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) + .await + .unwrap(); + + assert_eq!( + unseen_messages, + [rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }] + ); + + // Observe the second message again, should not regress our observed state + db.observe_channel_message(channel_1, observer, second_message) + .await + .unwrap(); + + // Make sure the observer does not have a new message + let unseen_messages = db + .transaction(|tx| async move { + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) + .await + .unwrap(); + assert_eq!( + unseen_messages, + [rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }] + ); +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e454fcbb9e7a4f2202602ca1ef7947ea6d6b6c9b..5eb434e167cc115c7ec9f08dd24bc7b12f04e30a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,10 @@ mod connection_pool; use crate::{ auth, - db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{ + self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, + ServerId, User, UserId, + }, executor::Executor, AppState, Result, }; @@ -35,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, - LiveKitConnectionInfo, RequestMessage, + self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -56,6 +59,7 @@ use std::{ }, time::{Duration, Instant}, }; +use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{info_span, instrument, Instrument}; @@ -63,6 +67,9 @@ use tracing::{info_span, instrument, Instrument}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +const MESSAGE_COUNT_PER_PAGE: usize = 100; +const MAX_MESSAGE_LEN: usize = 1024; + lazy_static! { static ref METRIC_CONNECTIONS: IntGauge = register_int_gauge!("connections", "number of connections").unwrap(); @@ -243,7 +250,7 @@ impl Server { .add_request_handler(remove_contact) .add_request_handler(respond_to_contact_request) .add_request_handler(create_channel) - .add_request_handler(remove_channel) + .add_request_handler(delete_channel) .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) @@ -255,11 +262,21 @@ impl Server { .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) + .add_request_handler(join_channel_chat) + .add_message_handler(leave_channel_chat) + .add_request_handler(send_channel_message) + .add_request_handler(remove_channel_message) + .add_request_handler(get_channel_messages) + .add_request_handler(link_channel) + .add_request_handler(unlink_channel) + .add_request_handler(move_channel) .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) .add_message_handler(update_diff_base) - .add_request_handler(get_private_user_info); + .add_request_handler(get_private_user_info) + .add_message_handler(acknowledge_channel_message) + .add_message_handler(acknowledge_buffer_version); Arc::new(server) } @@ -298,9 +315,16 @@ impl Server { .trace_err() { for connection_id in refreshed_channel_buffer.connection_ids { - for message in &refreshed_channel_buffer.removed_collaborators { - peer.send(connection_id, message.clone()).trace_err(); - } + peer.send( + connection_id, + proto::UpdateChannelBufferCollaborators { + channel_id: channel_id.to_proto(), + collaborators: refreshed_channel_buffer + .collaborators + .clone(), + }, + ) + .trace_err(); } } } @@ -553,9 +577,8 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4( + let (contacts, channels_for_user, channel_invites) = future::try_join3( this.app_state.db.get_contacts(user_id), - this.app_state.db.get_invite_code_for_user(user_id), this.app_state.db.get_channels_for_user(user_id), this.app_state.db.get_channel_invites_for_user(user_id) ).await?; @@ -568,13 +591,6 @@ impl Server { channels_for_user, channel_invites ))?; - - if let Some((code, count)) = invite_code { - this.peer.send(connection_id, proto::UpdateInviteInfo { - url: format!("{}{}", this.app_state.config.invite_link_prefix, code), - count: count as u32, - })?; - } } if let Some(incoming_call) = this.app_state.db.incoming_call_for_user(user_id).await? { @@ -893,9 +909,8 @@ async fn connection_lost( room_updated(&room, &session.peer); } } - update_user_contacts(session.user_id, &session).await?; - + update_user_contacts(session.user_id, &session).await?; } _ = teardown.changed().fuse() => {} } @@ -1877,94 +1892,94 @@ async fn follow( response: Response, session: Session, ) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); + let room_id = RoomId::from_proto(request.room_id); + let project_id = request.project_id.map(ProjectId::from_proto); let leader_id = request .leader_id .ok_or_else(|| anyhow!("invalid leader id"))? .into(); let follower_id = session.connection_id; - { - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - - if !project_connection_ids.contains(&leader_id) { - Err(anyhow!("no such peer"))?; - } - } + session + .db() + .await + .check_room_participants(room_id, leader_id, session.connection_id) + .await?; - let mut response_payload = session + let response_payload = session .peer .forward_request(session.connection_id, leader_id, request) .await?; - response_payload - .views - .retain(|view| view.leader_id != Some(follower_id.into())); response.send(response_payload)?; - let room = session - .db() - .await - .follow(project_id, leader_id, follower_id) - .await?; - room_updated(&room, &session.peer); + if let Some(project_id) = project_id { + let room = session + .db() + .await + .follow(room_id, project_id, leader_id, follower_id) + .await?; + room_updated(&room, &session.peer); + } Ok(()) } async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); + let room_id = RoomId::from_proto(request.room_id); + let project_id = request.project_id.map(ProjectId::from_proto); let leader_id = request .leader_id .ok_or_else(|| anyhow!("invalid leader id"))? .into(); let follower_id = session.connection_id; - if !session + session .db() .await - .project_connection_ids(project_id, session.connection_id) - .await? - .contains(&leader_id) - { - Err(anyhow!("no such peer"))?; - } + .check_room_participants(room_id, leader_id, session.connection_id) + .await?; session .peer .forward_send(session.connection_id, leader_id, request)?; - let room = session - .db() - .await - .unfollow(project_id, leader_id, follower_id) - .await?; - room_updated(&room, &session.peer); + if let Some(project_id) = project_id { + let room = session + .db() + .await + .unfollow(room_id, project_id, leader_id, follower_id) + .await?; + room_updated(&room, &session.peer); + } Ok(()) } async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db - .lock() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; + let room_id = RoomId::from_proto(request.room_id); + let database = session.db.lock().await; + + let connection_ids = if let Some(project_id) = request.project_id { + let project_id = ProjectId::from_proto(project_id); + database + .project_connection_ids(project_id, session.connection_id) + .await? + } else { + database + .room_connection_ids(room_id, session.connection_id) + .await? + }; - let leader_id = request.variant.as_ref().and_then(|variant| match variant { - proto::update_followers::Variant::CreateView(payload) => payload.leader_id, + // For now, don't send view update messages back to that view's current leader. + let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant { proto::update_followers::Variant::UpdateView(payload) => payload.leader_id, - proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id, + _ => None, }); + for follower_peer_id in request.follower_ids.iter().copied() { let follower_connection_id = follower_peer_id.into(); - if project_connection_ids.contains(&follower_connection_id) - && Some(follower_peer_id) != leader_id + if Some(follower_peer_id) != connection_id_to_omit + && connection_ids.contains(&follower_connection_id) { session.peer.forward_send( session.connection_id, @@ -2194,56 +2209,58 @@ async fn create_channel( let channel = proto::Channel { id: id.to_proto(), name: request.name, - parent_id: request.parent_id, }; - response.send(proto::ChannelResponse { + response.send(proto::CreateChannelResponse { channel: Some(channel.clone()), + parent_id: request.parent_id, })?; - let mut update = proto::UpdateChannels::default(); - update.channels.push(channel); + let Some(parent_id) = parent_id else { + return Ok(()); + }; - let user_ids_to_notify = if let Some(parent_id) = parent_id { - db.get_channel_members(parent_id).await? - } else { - vec![session.user_id] + let update = proto::UpdateChannels { + channels: vec![channel], + insert_edge: vec![ChannelEdge { + parent_id: parent_id.to_proto(), + channel_id: id.to_proto(), + }], + ..Default::default() }; + let user_ids_to_notify = db.get_channel_members(parent_id).await?; + let connection_pool = session.connection_pool().await; for user_id in user_ids_to_notify { for connection_id in connection_pool.user_connection_ids(user_id) { - let mut update = update.clone(); if user_id == session.user_id { - update.channel_permissions.push(proto::ChannelPermission { - channel_id: id.to_proto(), - is_admin: true, - }); + continue; } - session.peer.send(connection_id, update)?; + session.peer.send(connection_id, update.clone())?; } } Ok(()) } -async fn remove_channel( - request: proto::RemoveChannel, - response: Response, +async fn delete_channel( + request: proto::DeleteChannel, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; let channel_id = request.channel_id; let (removed_channels, member_ids) = db - .remove_channel(ChannelId::from_proto(channel_id), session.user_id) + .delete_channel(ChannelId::from_proto(channel_id), session.user_id) .await?; response.send(proto::Ack {})?; // Notify members of removed channels let mut update = proto::UpdateChannels::default(); update - .remove_channels + .delete_channels .extend(removed_channels.into_iter().map(|id| id.to_proto())); let connection_pool = session.connection_pool().await; @@ -2276,7 +2293,6 @@ async fn invite_channel_member( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - parent_id: None, }); for connection_id in session .connection_pool() @@ -2303,7 +2319,7 @@ async fn remove_channel_member( .await?; let mut update = proto::UpdateChannels::default(); - update.remove_channels.push(channel_id.to_proto()); + update.delete_channels.push(channel_id.to_proto()); for connection_id in session .connection_pool() @@ -2367,9 +2383,8 @@ async fn rename_channel( let channel = proto::Channel { id: request.channel_id, name: new_name, - parent_id: None, }; - response.send(proto::ChannelResponse { + response.send(proto::RenameChannelResponse { channel: Some(channel.clone()), })?; let mut update = proto::UpdateChannels::default(); @@ -2387,6 +2402,132 @@ async fn rename_channel( Ok(()) } +async fn link_channel( + request: proto::LinkChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let to = ChannelId::from_proto(request.to); + let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?; + + let members = db.get_channel_members(to).await?; + let connection_pool = session.connection_pool().await; + let update = proto::UpdateChannels { + channels: channels_to_send + .channels + .into_iter() + .map(|channel| proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + }) + .collect(), + insert_edge: channels_to_send.edges, + ..Default::default() + }; + for member_id in members { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + response.send(Ack {})?; + + Ok(()) +} + +async fn unlink_channel( + request: proto::UnlinkChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let from = ChannelId::from_proto(request.from); + + db.unlink_channel(session.user_id, channel_id, from).await?; + + let members = db.get_channel_members(from).await?; + + let update = proto::UpdateChannels { + delete_edge: vec![proto::ChannelEdge { + channel_id: channel_id.to_proto(), + parent_id: from.to_proto(), + }], + ..Default::default() + }; + let connection_pool = session.connection_pool().await; + for member_id in members { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + response.send(Ack {})?; + + Ok(()) +} + +async fn move_channel( + request: proto::MoveChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let from_parent = ChannelId::from_proto(request.from); + let to = ChannelId::from_proto(request.to); + + let channels_to_send = db + .move_channel(session.user_id, channel_id, from_parent, to) + .await?; + + if channels_to_send.is_empty() { + response.send(Ack {})?; + return Ok(()); + } + + let members_from = db.get_channel_members(from_parent).await?; + let members_to = db.get_channel_members(to).await?; + + let update = proto::UpdateChannels { + delete_edge: vec![proto::ChannelEdge { + channel_id: channel_id.to_proto(), + parent_id: from_parent.to_proto(), + }], + ..Default::default() + }; + let connection_pool = session.connection_pool().await; + for member_id in members_from { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + let update = proto::UpdateChannels { + channels: channels_to_send + .channels + .into_iter() + .map(|channel| proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + }) + .collect(), + insert_edge: channels_to_send.edges, + ..Default::default() + }; + for member_id in members_to { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + response.send(Ack {})?; + + Ok(()) +} + async fn get_channel_members( request: proto::GetChannelMembers, response: Response, @@ -2416,14 +2557,22 @@ async fn respond_to_channel_invite( .remove_channel_invitations .push(channel_id.to_proto()); if request.accept { - let result = db.get_channels_for_user(session.user_id).await?; + let result = db.get_channel_for_user(channel_id, session.user_id).await?; update .channels - .extend(result.channels.into_iter().map(|channel| proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - parent_id: channel.parent_id.map(ChannelId::to_proto), - })); + .extend( + result + .channels + .channels + .into_iter() + .map(|channel| proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + }), + ); + update.unseen_channel_messages = result.channel_messages; + update.unseen_channel_buffer_changes = result.unseen_buffer_changes; + update.insert_edge = result.channels.edges; update .channel_participants .extend( @@ -2520,18 +2669,12 @@ async fn join_channel_buffer( .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 { + let update = UpdateChannelBufferCollaborators { 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, - }), + collaborators: collaborators.clone(), }; channel_buffer_updated( session.connection_id, @@ -2552,7 +2695,7 @@ async fn update_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let collaborators = db + let (collaborators, non_collaborators, epoch, version) = db .update_channel_buffer(channel_id, session.user_id, &request.operations) .await?; @@ -2565,6 +2708,29 @@ async fn update_channel_buffer( }, &session.peer, ); + + let pool = &*session.connection_pool().await; + + broadcast( + None, + non_collaborators + .iter() + .flat_map(|user_id| pool.user_connection_ids(*user_id)), + |peer_id| { + session.peer.send( + peer_id.into(), + proto::UpdateChannels { + unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange { + channel_id: channel_id.to_proto(), + epoch: epoch as u64, + version: version.clone(), + }], + ..Default::default() + }, + ) + }, + ); + Ok(()) } @@ -2578,8 +2744,8 @@ async fn rejoin_channel_buffers( .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id) .await?; - for buffer in &buffers { - let collaborators_to_notify = buffer + for rejoined_buffer in &buffers { + let collaborators_to_notify = rejoined_buffer .buffer .collaborators .iter() @@ -2587,10 +2753,9 @@ async fn rejoin_channel_buffers( channel_buffer_updated( session.connection_id, collaborators_to_notify, - &proto::UpdateChannelBufferCollaborator { - channel_id: buffer.buffer.channel_id, - old_peer_id: Some(buffer.old_connection_id.into()), - new_peer_id: Some(session.connection_id.into()), + &proto::UpdateChannelBufferCollaborators { + channel_id: rejoined_buffer.buffer.channel_id, + collaborators: rejoined_buffer.buffer.collaborators.clone(), }, &session.peer, ); @@ -2611,7 +2776,7 @@ async fn leave_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let collaborators_to_notify = db + let left_buffer = db .leave_channel_buffer(channel_id, session.connection_id) .await?; @@ -2619,10 +2784,10 @@ async fn leave_channel_buffer( channel_buffer_updated( session.connection_id, - collaborators_to_notify, - &proto::RemoveChannelBufferCollaborator { + left_buffer.connections, + &proto::UpdateChannelBufferCollaborators { channel_id: channel_id.to_proto(), - peer_id: Some(session.connection_id.into()), + collaborators: left_buffer.collaborators, }, &session.peer, ); @@ -2641,6 +2806,184 @@ fn channel_buffer_updated( }); } +async fn send_channel_message( + request: proto::SendChannelMessage, + response: Response, + session: Session, +) -> Result<()> { + // Validate the message body. + let body = request.body.trim().to_string(); + if body.len() > MAX_MESSAGE_LEN { + return Err(anyhow!("message is too long"))?; + } + if body.is_empty() { + return Err(anyhow!("message can't be blank"))?; + } + + let timestamp = OffsetDateTime::now_utc(); + let nonce = request + .nonce + .ok_or_else(|| anyhow!("nonce can't be blank"))?; + + let channel_id = ChannelId::from_proto(request.channel_id); + let (message_id, connection_ids, non_participants) = session + .db() + .await + .create_channel_message( + channel_id, + session.user_id, + &body, + timestamp, + nonce.clone().into(), + ) + .await?; + let message = proto::ChannelMessage { + sender_id: session.user_id.to_proto(), + id: message_id.to_proto(), + body, + timestamp: timestamp.unix_timestamp() as u64, + nonce: Some(nonce), + }; + broadcast(Some(session.connection_id), connection_ids, |connection| { + session.peer.send( + connection, + proto::ChannelMessageSent { + channel_id: channel_id.to_proto(), + message: Some(message.clone()), + }, + ) + }); + response.send(proto::SendChannelMessageResponse { + message: Some(message), + })?; + + let pool = &*session.connection_pool().await; + broadcast( + None, + non_participants + .iter() + .flat_map(|user_id| pool.user_connection_ids(*user_id)), + |peer_id| { + session.peer.send( + peer_id.into(), + proto::UpdateChannels { + unseen_channel_messages: vec![proto::UnseenChannelMessage { + channel_id: channel_id.to_proto(), + message_id: message_id.to_proto(), + }], + ..Default::default() + }, + ) + }, + ); + + Ok(()) +} + +async fn remove_channel_message( + request: proto::RemoveChannelMessage, + response: Response, + session: Session, +) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + let message_id = MessageId::from_proto(request.message_id); + let connection_ids = session + .db() + .await + .remove_channel_message(channel_id, message_id, session.user_id) + .await?; + broadcast(Some(session.connection_id), connection_ids, |connection| { + session.peer.send(connection, request.clone()) + }); + response.send(proto::Ack {})?; + Ok(()) +} + +async fn acknowledge_channel_message( + request: proto::AckChannelMessage, + session: Session, +) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + let message_id = MessageId::from_proto(request.message_id); + session + .db() + .await + .observe_channel_message(channel_id, session.user_id, message_id) + .await?; + Ok(()) +} + +async fn acknowledge_buffer_version( + request: proto::AckBufferOperation, + session: Session, +) -> Result<()> { + let buffer_id = BufferId::from_proto(request.buffer_id); + session + .db() + .await + .observe_buffer_version( + buffer_id, + session.user_id, + request.epoch as i32, + &request.version, + ) + .await?; + Ok(()) +} + +async fn join_channel_chat( + request: proto::JoinChannelChat, + response: Response, + session: Session, +) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + + let db = session.db().await; + db.join_channel_chat(channel_id, session.connection_id, session.user_id) + .await?; + let messages = db + .get_channel_messages(channel_id, session.user_id, MESSAGE_COUNT_PER_PAGE, None) + .await?; + response.send(proto::JoinChannelChatResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + })?; + Ok(()) +} + +async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + session + .db() + .await + .leave_channel_chat(channel_id, session.connection_id, session.user_id) + .await?; + Ok(()) +} + +async fn get_channel_messages( + request: proto::GetChannelMessages, + response: Response, + session: Session, +) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + let messages = session + .db() + .await + .get_channel_messages( + channel_id, + session.user_id, + MESSAGE_COUNT_PER_PAGE, + Some(MessageId::from_proto(request.before_message_id)), + ) + .await?; + response.send(proto::GetChannelMessagesResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + })?; + Ok(()) +} + 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 @@ -2716,14 +3059,17 @@ fn build_initial_channels_update( ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); - for channel in channels.channels { + for channel in channels.channels.channels { update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - parent_id: channel.parent_id.map(|id| id.to_proto()), }); } + update.unseen_channel_buffer_changes = channels.unseen_buffer_changes; + update.unseen_channel_messages = channels.channel_messages; + update.insert_edge = channels.channels.edges; + for (channel_id, participants) in channels.channel_participants { update .channel_participants @@ -2749,7 +3095,6 @@ fn build_initial_channels_update( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - parent_id: None, }); } @@ -2972,13 +3317,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { .leave_channel_buffers(session.connection_id) .await?; - for (channel_id, connections) in left_channel_buffers { + for left_buffer 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()), + left_buffer.connections, + &proto::UpdateChannelBufferCollaborators { + channel_id: left_buffer.channel_id.to_proto(), + collaborators: left_buffer.collaborators, }, &session.peer, ); diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 3000f0d8c351d2c5e3cc77ca89bd2ba344191ed8..e78bbe3466318cfc44fbcf298cef65a86350a0b8 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -2,7 +2,9 @@ use call::Room; use gpui::{ModelHandle, TestAppContext}; mod channel_buffer_tests; +mod channel_message_tests; mod channel_tests; +mod following_tests; mod integration_tests; mod random_channel_buffer_tests; mod random_project_collaboration_tests; diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index fe286895b4ada34c697a6587b0243c130d0f328e..a0b9b524841f19e4eb6537348317ae38d23627c2 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -3,15 +3,17 @@ use crate::{ tests::TestServer, }; use call::ActiveCall; -use channel::Channel; -use client::UserId; +use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; +use client::ParticipantIndex; +use client::{Collaborator, UserId}; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use editor::{Anchor, Editor, ToOffset}; use futures::future; -use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; -use rpc::{proto, RECEIVE_TIMEOUT}; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; #[gpui::test] async fn test_core_channel_buffers( @@ -25,7 +27,7 @@ async fn test_core_channel_buffers( 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)]) + .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Client A joins the channel buffer @@ -100,7 +102,7 @@ async fn test_core_channel_buffers( channel_buffer_b.read_with(cx_b, |buffer, _| { assert_collaborators( &buffer.collaborators(), - &[client_b.user_id(), client_a.user_id()], + &[client_a.user_id(), client_b.user_id()], ); }); @@ -120,10 +122,10 @@ async fn test_core_channel_buffers( } #[gpui::test] -async fn test_channel_buffer_replica_ids( +async fn test_channel_notes_participant_indices( deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); @@ -132,154 +134,195 @@ async fn test_channel_buffer_replica_ids( let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + let channel_id = server .make_channel( "the-channel", + None, (&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, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + client_a + .fs() + .insert_tree("/root", json!({"file.txt": "123"})) + .await; + let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; + let project_b = client_b.build_empty_local_project(cx_b); + let project_c = client_c.build_empty_local_project(cx_c); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + + // Clients A, B, and C open the channel notes + let channel_view_a = cx_a + .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) .await .unwrap(); - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), 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)) + let channel_view_c = cx_c + .update(|cx| ChannelView::open(channel_id, workspace_c.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); + // Clients A, B, and C all insert and select some text + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.insert("a", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); }); - 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); + deterministic.run_until_parked(); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("b", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![1..2]); + }); + }); }); - 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); + deterministic.run_until_parked(); + channel_view_c.update(cx_c, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("c", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); }); - 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) + // Client A sees clients B and C without assigned colors, because they aren't + // in a call together. + deterministic.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], 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); + // Clients A and B join the same call. + for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { + call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + } - // 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::>() - ); + // Clients A and B see each other with two different assigned colors. Client C + // still doesn't have a color. + deterministic.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], + cx, + ); + }); }); - 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::>(), - ) + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], + cx, + ); + }); }); - // 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 A shares a project, and client B joins. + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - // 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)) + // Clients A and B open the same file. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) .await + .unwrap() + .downcast::() .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); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); }); + deterministic.run_until_parked(); - // 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::>() - ); + // Clients A and B see each other with the same colors as in the channel notes. + editor_a.update(cx_a, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); }); - 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::>(), - ) + editor_b.update(cx_b, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); }); } +#[track_caller] +fn assert_remote_selections( + editor: &mut Editor, + expected_selections: &[(Option, Range)], + cx: &mut ViewContext, +) { + let snapshot = editor.snapshot(cx); + let range = Anchor::min()..Anchor::max(); + let remote_selections = snapshot + .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) + .map(|s| { + let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); + let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); + (s.participant_index, start..end) + }) + .collect::>(); + assert_eq!( + remote_selections, expected_selections, + "incorrect remote selections" + ); +} + #[gpui::test] -async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mut TestAppContext) { +async fn test_multiple_handles_to_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 channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut []) + .make_channel("the-channel", None, (&client_a, cx_a), &mut []) .await; let channel_buffer_1 = client_a @@ -341,7 +384,12 @@ async fn test_channel_buffer_disconnect( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) .await; let channel_buffer_a = client_a @@ -362,10 +410,7 @@ async fn test_channel_buffer_disconnect( channel_buffer_a.update(cx_a, |buffer, _| { assert_eq!( buffer.channel().as_ref(), - &Channel { - id: channel_id, - name: "the-channel".to_string() - } + &channel(channel_id, "the-channel") ); assert!(!buffer.is_connected()); }); @@ -390,15 +435,21 @@ async fn test_channel_buffer_disconnect( channel_buffer_b.update(cx_b, |buffer, _| { assert_eq!( buffer.channel().as_ref(), - &Channel { - id: channel_id, - name: "the-channel".to_string() - } + &channel(channel_id, "the-channel") ); assert!(!buffer.is_connected()); }); } +fn channel(id: u64, name: &'static str) -> Channel { + Channel { + id, + name: name.to_string(), + unseen_note_version: None, + unseen_message_id: None, + } +} + #[gpui::test] async fn test_rejoin_channel_buffer( deterministic: Arc, @@ -411,7 +462,12 @@ async fn test_rejoin_channel_buffer( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) .await; let channel_buffer_a = client_a @@ -491,6 +547,7 @@ async fn test_channel_buffers_and_server_restarts( let channel_id = server .make_channel( "the-channel", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -553,26 +610,284 @@ async fn test_channel_buffers_and_server_restarts( channel_buffer_a.read_with(cx_a, |buffer_a, _| { channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_eq!( - buffer_a - .collaborators() - .iter() - .map(|c| c.user_id) - .collect::>(), - vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()] + assert_collaborators( + buffer_a.collaborators(), + &[client_a.user_id(), client_b.user_id()], ); assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); }); }); } +#[gpui::test(iterations = 10)] +async fn test_following_to_channel_notes_without_a_shared_project( + deterministic: Arc, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, + mut 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; + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + cx_a.update(collab_ui::channel_view::init); + cx_b.update(collab_ui::channel_view::init); + cx_c.update(collab_ui::channel_view::init); + + let channel_1_id = server + .make_channel( + "channel-1", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + let channel_2_id = server + .make_channel( + "channel-2", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + // Clients A, B, and C join a channel. + 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); + for (call, cx) in [ + (&active_call_a, &mut cx_a), + (&active_call_b, &mut cx_b), + (&active_call_c, &mut cx_c), + ] { + call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) + .await + .unwrap(); + } + deterministic.run_until_parked(); + + // Clients A, B, and C all open their own unshared projects. + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; + client_c.fs().insert_tree("/c", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (project_b, _) = client_b.build_local_project("/b", cx_b).await; + let (project_c, _) = client_b.build_local_project("/c", cx_c).await; + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + // Client A opens the notes for channel 1. + let channel_view_1_a = cx_a + .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_1_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-1"); + notes.editor.update(cx, |editor, cx| { + editor.insert("Hello from A.", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![3..4]); + }); + }); + }); + + // Client B follows client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // Client B is taken to the notes for channel 1, with the same + // text selected as client A. + deterministic.run_until_parked(); + let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_1_b.read_with(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-1"); + let editor = notes.editor.read(cx); + assert_eq!(editor.text(cx), "Hello from A."); + assert_eq!(editor.selections.ranges::(cx), &[3..4]); + }); + + // Client A opens the notes for channel 2. + let channel_view_2_a = cx_a + .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_2_a.read_with(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-2"); + }); + + // Client B is taken to the notes for channel 2. + deterministic.run_until_parked(); + let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_2_b.read_with(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-2"); + }); +} + +#[gpui::test] +async fn test_channel_buffer_changes( + 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( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + // Client A makes an edit, and client B should see that the note has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); + + // Opening the buffer should clear the changed flag. + let project_b = client_b.build_empty_local_project(cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Editing the channel while the buffer is open should not show that the buffer has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "2")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); + + // Test that the server is tracking things correctly, and we retain our 'not changed' + // state across a disconnect + server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Closing the buffer should re-enable change tracking + cx_b.update(|cx| { + workspace_b.update(cx, |workspace, cx| { + workspace.close_all_items_and_panes(&Default::default(), cx) + }); + + drop(channel_view_b) + }); + + deterministic.run_until_parked(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "3")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); +} + #[track_caller] -fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { +fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { + let mut user_ids = collaborators + .values() + .map(|collaborator| collaborator.user_id) + .collect::>(); + user_ids.sort(); assert_eq!( - collaborators - .into_iter() - .map(|collaborator| collaborator.user_id) - .collect::>(), + user_ids, ids.into_iter().map(|id| id.unwrap()).collect::>() ); } diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..0fc3b085edde00cbfe552352fe5590753f686134 --- /dev/null +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -0,0 +1,360 @@ +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use channel::{ChannelChat, ChannelMessageId}; +use collab_ui::chat_panel::ChatPanel; +use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext}; +use std::sync::Arc; +use workspace::dock::Panel; + +#[gpui::test] +async fn test_basic_channel_messages( + 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( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + let channel_chat_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_a + .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + channel_chat_a.update(cx_a, |c, _| { + assert_eq!( + c.messages() + .iter() + .map(|m| m.body.as_str()) + .collect::>(), + vec!["one", "two", "three"] + ); + }) +} + +#[gpui::test] +async fn test_rejoin_channel_chat( + 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( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + let channel_chat_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + + // While client A is disconnected, clients A and B both send new messages. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap_err(); + channel_chat_a + .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) + .await + .unwrap_err(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("five".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("six".into(), cx).unwrap()) + .await + .unwrap(); + + // Client A reconnects. + server.allow_connections(); + deterministic.advance_clock(RECONNECT_TIMEOUT); + + // Client A fetches the messages that were sent while they were disconnected + // and resends their own messages which failed to send. + let expected_messages = &["one", "two", "five", "six", "three", "four"]; + assert_messages(&channel_chat_a, expected_messages, cx_a); + assert_messages(&channel_chat_b, expected_messages, cx_b); +} + +#[gpui::test] +async fn test_remove_channel_message( + 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( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + let channel_chat_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + // Client A sends some messages. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_a + .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_a + .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap(); + + // Clients A and B see all of the messages. + deterministic.run_until_parked(); + let expected_messages = &["one", "two", "three"]; + assert_messages(&channel_chat_a, expected_messages, cx_a); + assert_messages(&channel_chat_b, expected_messages, cx_b); + + // Client A deletes one of their messages. + channel_chat_a + .update(cx_a, |c, cx| { + let ChannelMessageId::Saved(id) = c.message(1).id else { + panic!("message not saved") + }; + c.remove_message(id, cx) + }) + .await + .unwrap(); + + // Client B sees that the message is gone. + deterministic.run_until_parked(); + let expected_messages = &["one", "three"]; + assert_messages(&channel_chat_a, expected_messages, cx_a); + assert_messages(&channel_chat_b, expected_messages, cx_b); + + // Client C joins the channel chat, and does not see the deleted message. + let channel_chat_c = client_c + .channel_store() + .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + assert_messages(&channel_chat_c, expected_messages, cx_c); +} + +#[track_caller] +fn assert_messages(chat: &ModelHandle, messages: &[&str], cx: &mut TestAppContext) { + assert_eq!( + chat.read_with(cx, |chat, _| chat + .messages() + .iter() + .map(|m| m.body.clone()) + .collect::>(),), + messages + ); +} + +#[gpui::test] +async fn test_channel_message_changes( + 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( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + // Client A sends a message, client B should see that there is a new message. + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); + + // Opening the chat should clear the changed flag. + cx_b.update(|cx| { + collab_ui::init(&client_b.app_state, cx); + }); + let project_b = client_b.build_empty_local_project(cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx)); + chat_panel_b + .update(cx_b, |chat_panel, cx| { + chat_panel.set_active(true, cx); + chat_panel.select_channel(channel_id, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(!b_has_messages); + + // Sending a message while the chat is open should not change the flag. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(!b_has_messages); + + // Sending a message while the chat is closed should change the flag. + chat_panel_b.update(cx_b, |chat_panel, cx| { + chat_panel.set_active(false, cx); + }); + + // Sending a message while the chat is open should not change the flag. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); + + // Closing the chat should re-enable change tracking + cx_b.update(|_| drop(chat_panel_b)); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); +} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index b54b4d349ba54e5c23e048cd81b292a10566445d..6bdcee6af3eebc5a885f7a60b780b9f13e7ca0a3 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -56,7 +56,10 @@ async fn test_core_channels( ); client_b.channel_store().read_with(cx_b, |channels, _| { - assert!(channels.channels().collect::>().is_empty()) + assert!(channels + .channel_dag_entries() + .collect::>() + .is_empty()) }); // Invite client B to channel A as client A. @@ -326,7 +329,7 @@ async fn test_joining_channel_ancestor_member( let client_b = server.create_client(cx_b, "user_b").await; let parent_id = server - .make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let sub_id = client_a @@ -361,6 +364,7 @@ async fn test_channel_room( let zed_id = server .make_channel( "zed", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -544,9 +548,11 @@ async fn test_channel_jumping(deterministic: Arc, cx_a: &mut Test 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 zed_id = server + .make_channel("zed", None, (&client_a, cx_a), &mut []) + .await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut []) + .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; let active_call_a = cx_a.read(ActiveCall::global); @@ -597,7 +603,7 @@ async fn test_permissions_update_while_invited( let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut []) + .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; client_a @@ -658,7 +664,7 @@ async fn test_channel_rename( let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Rename the channel @@ -716,6 +722,7 @@ async fn test_call_from_channel( let channel_id = server .make_channel( "x", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -786,7 +793,9 @@ async fn test_lost_channel_creation( .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await; + let channel_id = server + .make_channel("x", None, (&client_a, cx_a), &mut []) + .await; // Invite a member client_a @@ -874,6 +883,253 @@ async fn test_lost_channel_creation( ); } +#[gpui::test] +async fn test_channel_moving( + 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 channels = server + .make_channel_tree( + &[ + ("channel-a", None), + ("channel-b", Some("channel-a")), + ("channel-c", Some("channel-b")), + ("channel-d", Some("channel-c")), + ], + (&client_a, cx_a), + ) + .await; + let channel_a_id = channels[0]; + let channel_b_id = channels[1]; + let channel_c_id = channels[2]; + let channel_d_id = channels[3]; + + // Current shape: + // a - b - c - d + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[ + (channel_a_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + ], + ); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.move_channel(channel_d_id, channel_c_id, channel_b_id, cx) + }) + .await + .unwrap(); + + // Current shape: + // /- d + // a - b -- c + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[ + (channel_a_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 2), + ], + ); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.link_channel(channel_d_id, channel_c_id, cx) + }) + .await + .unwrap(); + + // Current shape for A: + // /------\ + // a - b -- c -- d + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[ + (channel_a_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), + ], + ); + + let b_channels = server + .make_channel_tree( + &[ + ("channel-mu", None), + ("channel-gamma", Some("channel-mu")), + ("channel-epsilon", Some("channel-mu")), + ], + (&client_b, cx_b), + ) + .await; + let channel_mu_id = b_channels[0]; + let channel_ga_id = b_channels[1]; + let channel_ep_id = b_channels[2]; + + // Current shape for B: + // /- ep + // mu -- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)], + ); + + client_a + .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a) + .await; + + // Current shape for B: + // /- ep + // mu -- ga + // /---------\ + // b -- c -- d + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + // New channels from a + (channel_b_id, 0), + (channel_c_id, 1), + (channel_d_id, 2), + (channel_d_id, 1), + // B's old channels + (channel_mu_id, 0), + (channel_ep_id, 1), + (channel_ga_id, 1), + ], + ); + + client_b + .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) + .await; + + // Current shape for C: + // - ep + assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]); + + client_b + .channel_store() + .update(cx_b, |channel_store, cx| { + channel_store.link_channel(channel_b_id, channel_ep_id, cx) + }) + .await + .unwrap(); + + // Current shape for B: + // /---------\ + // /- ep -- b -- c -- d + // mu -- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (channel_mu_id, 0), + (channel_ep_id, 1), + (channel_b_id, 2), + (channel_c_id, 3), + (channel_d_id, 4), + (channel_d_id, 3), + (channel_ga_id, 1), + ], + ); + + // Current shape for C: + // /---------\ + // ep -- b -- c -- d + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[ + (channel_ep_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), + ], + ); + + client_b + .channel_store() + .update(cx_b, |channel_store, cx| { + channel_store.link_channel(channel_ga_id, channel_b_id, cx) + }) + .await + .unwrap(); + + // Current shape for B: + // /---------\ + // /- ep -- b -- c -- d + // / \ + // mu ---------- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (channel_mu_id, 0), + (channel_ep_id, 1), + (channel_b_id, 2), + (channel_c_id, 3), + (channel_d_id, 4), + (channel_d_id, 3), + (channel_ga_id, 3), + (channel_ga_id, 1), + ], + ); + + // Current shape for A: + // /------\ + // a - b -- c -- d + // \-- ga + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[ + (channel_a_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), + (channel_ga_id, 2), + ], + ); + + // Current shape for C: + // /-------\ + // ep -- b -- c -- d + // \-- ga + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[ + (channel_ep_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), + (channel_ga_id, 2), + ], + ); +} + #[derive(Debug, PartialEq)] struct ExpectedChannel { depth: usize, @@ -911,7 +1167,7 @@ fn assert_channels( ) { let actual = channel_store.read_with(cx, |store, _| { store - .channels() + .channel_dag_entries() .map(|(depth, channel)| ExpectedChannel { depth, name: channel.name.clone(), @@ -920,5 +1176,22 @@ fn assert_channels( }) .collect::>() }); - assert_eq!(actual, expected_channels); + pretty_assertions::assert_eq!(actual, expected_channels); +} + +#[track_caller] +fn assert_channels_list_shape( + channel_store: &ModelHandle, + cx: &TestAppContext, + expected_channels: &[(u64, usize)], +) { + cx.foreground().run_until_parked(); + + let actual = channel_store.read_with(cx, |store, _| { + store + .channel_dag_entries() + .map(|(depth, channel)| (channel.id, depth)) + .collect::>() + }); + pretty_assertions::assert_eq!(actual, expected_channels); } diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..3a489b9ac32e82be4ce33733fd8fa3efdc7f72a5 --- /dev/null +++ b/crates/collab/src/tests/following_tests.rs @@ -0,0 +1,1699 @@ +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use call::ActiveCall; +use collab_ui::project_shared_notification::ProjectSharedNotification; +use editor::{Editor, ExcerptRange, MultiBuffer}; +use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; +use live_kit_client::MacOSDisplay; +use rpc::proto::PeerId; +use serde_json::json; +use std::{borrow::Cow, sync::Arc}; +use workspace::{ + dock::{test::TestPanel, DockPosition}, + item::{test::TestItem, ItemHandle as _}, + shared_screen::SharedScreen, + SplitDirection, Workspace, +}; + +#[gpui::test(iterations = 10)] +async fn test_basic_following( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_d: &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 client_d = server.create_client(cx_d, "user_d").await; + server + .create_room(&mut [ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + (&client_d, cx_d), + ]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let window_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = window_a.root(cx_a); + let window_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = window_b.root(cx_b); + + // Client A opens some editors. + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + let editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let peer_id_a = client_a.peer_id().unwrap(); + let peer_id_b = client_b.peer_id().unwrap(); + let peer_id_c = client_c.peer_id().unwrap(); + let peer_id_d = client_d.peer_id().unwrap(); + + // Client A updates their selections in those editors + editor_a1.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_a2.update(cx_a, |editor, cx| { + editor.handle_input("d", cx); + editor.handle_input("e", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![2..1]); + }); + + // When client B starts following client A, all visible view states are replicated to client B. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + cx_c.foreground().run_until_parked(); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_b2.project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![2..1] + ); + assert_eq!( + editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![3..2] + ); + + cx_c.foreground().run_until_parked(); + let active_call_c = cx_c.read(ActiveCall::global); + let project_c = client_c.build_remote_project(project_id, cx_c).await; + let window_c = client_c.build_workspace(&project_c, cx_c); + let workspace_c = window_c.root(cx_c); + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + drop(project_c); + + // Client C also follows client A. + workspace_c + .update(cx_c, |workspace, cx| { + workspace.follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + cx_d.foreground().run_until_parked(); + let active_call_d = cx_d.read(ActiveCall::global); + let project_d = client_d.build_remote_project(project_id, cx_d).await; + let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); + active_call_d + .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) + .await + .unwrap(); + drop(project_d); + + // All clients see that clients B and C are following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b, peer_id_c], + "checking followers for A as {name}" + ); + }); + } + + // Client C unfollows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx); + }); + + // All clients see that clients B is following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b], + "checking followers for A as {name}" + ); + }); + } + + // Client C re-follows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.follow(peer_id_a, cx); + }); + + // All clients see that clients B and C are following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b, peer_id_c], + "checking followers for A as {name}" + ); + }); + } + + // Client D follows client C. + workspace_d + .update(cx_d, |workspace, cx| { + workspace.follow(peer_id_c, cx).unwrap() + }) + .await + .unwrap(); + + // All clients see that D is following C + cx_d.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_c, project_id), + &[peer_id_d], + "checking followers for C as {name}" + ); + }); + } + + // Client C closes the project. + window_c.remove(cx_c); + cx_c.drop_last(workspace_c); + + // Clients A and B see that client B is following A, and client C is not present in the followers. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b], + "checking followers for A as {name}" + ); + }); + } + + // All clients see that no-one is following C + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_c, project_id), + &[], + "checking followers for C as {name}" + ); + }); + } + + // When client A activates a different editor, client B does so as well. + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a1, cx) + }); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + // When client A opens a multibuffer, client B does so as well. + let multibuffer_a = cx_a.add_model(|cx| { + let buffer_a1 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "1.txt").into(), cx) + .unwrap() + }); + let buffer_a2 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "2.txt").into(), cx) + .unwrap() + }); + let mut result = MultiBuffer::new(0); + result.push_excerpts( + buffer_a1, + [ExcerptRange { + context: 0..3, + primary: None, + }], + cx, + ); + result.push_excerpts( + buffer_a2, + [ExcerptRange { + context: 4..7, + primary: None, + }], + cx, + ); + result + }); + let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { + let editor = + cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor + }); + deterministic.run_until_parked(); + let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), + multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), + ); + + // When client A navigates back and forth, client B does so as well. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_forward(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + // Changes to client A's editor are reflected on client B. + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); + }); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); + }); + + editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); + + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.set_scroll_position(vec2f(0., 100.), cx); + }); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[3..3]); + }); + + // After unfollowing, client B stops receiving updates from client A. + workspace_b.update(cx_b, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx) + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a2, cx) + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_b1.id() + ); + + // Client A starts following client B. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.follow(peer_id_b, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + Some(peer_id_b) + ); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_a1.id() + ); + + // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + let display = MacOSDisplay::new(); + active_call_b + .update(cx_b, |call, cx| call.set_location(None, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_display_sources(vec![display.clone()]); + room.share_screen(cx) + }) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item isn't a shared screen") + }); + + // Client B activates Zed again, which causes the previous editor to become focused again. + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) + }); + + // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_item(&multibuffer_editor_b, cx) + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); + + // Client B activates a panel, and the previously-opened screen-sharing item gets activated. + let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.add_panel(panel, cx); + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + shared_screen.id() + ); + + // Toggling the focus back to the pane causes client A to return to the multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); + + // Client B activates an item that doesn't implement following, + // so the previously-opened screen-sharing item gets activated. + let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + }) + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + shared_screen.id() + ); + + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()); + deterministic.advance_clock(RECONNECT_TIMEOUT); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); +} + +#[gpui::test] +async fn test_following_tab_order( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + 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; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + //Open 1, 3 in that order on client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); + + let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { + pane.update(cx, |pane, cx| { + pane.items() + .map(|item| { + item.project_path(cx) + .unwrap() + .path + .to_str() + .unwrap() + .to_owned() + }) + .collect::>() + }) + }; + + //Verify that the tabs opened in the order we expect + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); + + //Follow client B as client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.follow(client_b_id, cx).unwrap() + }) + .await + .unwrap(); + + //Open just 2 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Verify that newly opened followed file is at the end + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); + + //Open just 1 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); + deterministic.run_until_parked(); + + // Verify that following into 1 did not reorder + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_following_each_other( + 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; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + "4.txt": "four", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + // Client A opens a file. + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens a different file. + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Clients A and B follow each other in split panes + workspace_a.update(cx_a, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.follow(client_b.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // Clients A and B return focus to the original files they had open + workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + deterministic.run_until_parked(); + + // Both clients see the other client's focused file in their right pane. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(true, "1.txt".into())] + }, + PaneSummary { + active: false, + leader: client_b.peer_id(), + items: vec![(false, "1.txt".into()), (true, "2.txt".into())] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(true, "2.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![(false, "2.txt".into()), (true, "1.txt".into())] + }, + ] + ); + + // Clients A and B each open a new file. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "4.txt"), None, true, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Both client's see the other client open the new file, but keep their + // focus on their own active pane. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: false, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (true, "3.txt".into()) + ] + }, + ] + ); + + // Client A focuses their right pane, in which they're following client B. + workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + deterministic.run_until_parked(); + + // Client B sees that client A is now looking at the same file as them. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (false, "3.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + + // Client B focuses their right pane, in which they're following client A, + // who is following them. + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + deterministic.run_until_parked(); + + // Client A sees that client B is now looking at the same file as them. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (false, "3.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + + // Client B focuses a file that they previously followed A to, breaking + // the follow. + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + deterministic.run_until_parked(); + + // Both clients see that client B is looking at that previous file. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (true, "3.txt".into()), + (false, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (true, "3.txt".into()), + ] + }, + ] + ); + + // Client B closes tabs, some of which were originally opened by client A, + // and some of which were originally opened by client B. + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_inactive_items(&Default::default(), cx) + .unwrap() + .detach(); + }); + }); + + deterministic.run_until_parked(); + + // Both clients see that Client B is looking at the previous tab. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![(true, "3.txt".into()),] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (true, "3.txt".into()), + ] + }, + ] + ); + + // Client B follows client A again. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // Client A cycles through some tabs. + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + deterministic.run_until_parked(); + + // Client B follows client A into those tabs. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![(false, "3.txt".into()), (true, "4.txt".into())] + }, + ] + ); + + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + deterministic.run_until_parked(); + + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "1.txt".into()), + (true, "2.txt".into()), + (false, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "3.txt".into()), + (false, "4.txt".into()), + (true, "2.txt".into()) + ] + }, + ] + ); + + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + deterministic.run_until_parked(); + + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (true, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "3.txt".into()), + (false, "4.txt".into()), + (false, "2.txt".into()), + (true, "1.txt".into()), + ] + }, + ] + ); +} + +#[gpui::test(iterations = 10)] +async fn test_auto_unfollowing( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + // 2 clients connect to a server. + 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; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B starts following client A. + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + + // When client B moves, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B edits, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B scrolls, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| { + editor.set_scroll_position(vec2f(0., 3.), cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different pane, it continues following client A in the original pane. + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different item in the original pane, it automatically stops following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_simultaneously_following_each_other( + 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; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a.fs().insert_tree("/a", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + deterministic.run_until_parked(); + let client_a_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { + workspace.follow(client_b_id, cx).unwrap() + }); + let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a_id, cx).unwrap() + }); + + futures::try_join!(a_follow_b, b_follow_a).unwrap(); + workspace_a.read_with(cx_a, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_b_id) + ); + }); + workspace_b.read_with(cx_b, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a_id) + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_following_across_workspaces( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + // a and b join a channel/call + // a shares project 1 + // b shares project 2 + // + // b follows a: causes project 2 to be joined, and b to follow a. + // b opens a different file in project 2, a follows b + // b opens a different file in project 1, a cannot follow b + // b shares the project, a joins the project and follows b + 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; + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "w.rs": "", + "x.rs": "", + }), + ) + .await; + + client_b + .fs() + .insert_tree( + "/b", + json!({ + "y.rs": "", + "z.rs": "", + }), + ) + .await; + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; + let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); + cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); + + active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + assert_eq!(visible_push_notifications(cx_b).len(), 1); + + workspace_b.update(cx_b, |workspace, cx| { + workspace + .follow(client_a.peer_id().unwrap(), cx) + .unwrap() + .detach() + }); + + deterministic.run_until_parked(); + let workspace_b_project_a = cx_b + .windows() + .iter() + .max_by_key(|window| window.id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_b); + + // assert that b is following a in project a in w.rs + workspace_b_project_a.update(cx_b, |workspace, cx| { + assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); + assert_eq!( + client_a.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs")); + }); + + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b_project_a.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + + // assert that there are no share notifications open + assert_eq!(visible_push_notifications(cx_b).len(), 0); + + // b moves to x.rs in a's project, and a follows + workspace_b_project_a + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + workspace_b_project_a.update(cx_b, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); + }); + + workspace_a.update(cx_a, |workspace, cx| { + workspace + .follow(client_b.peer_id().unwrap(), cx) + .unwrap() + .detach() + }); + + deterministic.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_pane().read(cx).active_item().unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); + }); + + // b moves to y.rs in b's project, a is still following but can't yet see + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) + }) + .await + .unwrap(); + + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + assert_eq!(visible_push_notifications(cx_a).len(), 1); + cx_a.update(|cx| { + workspace::join_remote_project( + project_b_id, + client_b.user_id().unwrap(), + client_a.app_state.clone(), + cx, + ) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + assert_eq!(visible_push_notifications(cx_a).len(), 0); + let workspace_a_project_b = cx_a + .windows() + .iter() + .max_by_key(|window| window.id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_a); + + workspace_a_project_b.update(cx_a, |workspace, cx| { + assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs")); + }); +} + +fn visible_push_notifications( + cx: &mut TestAppContext, +) -> Vec> { + let mut ret = Vec::new(); + for window in cx.windows() { + window.read_with(cx, |window| { + if let Some(handle) = window + .root_view() + .clone() + .downcast::() + { + ret.push(handle) + } + }); + } + ret +} + +#[derive(Debug, PartialEq, Eq)] +struct PaneSummary { + active: bool, + leader: Option, + items: Vec<(bool, String)>, +} + +fn pane_summaries(workspace: &ViewHandle, cx: &mut TestAppContext) -> Vec { + workspace.read_with(cx, |workspace, cx| { + let active_pane = workspace.active_pane(); + workspace + .panes() + .iter() + .map(|pane| { + let leader = workspace.leader_for_pane(pane); + let active = pane == active_pane; + let pane = pane.read(cx); + let active_ix = pane.active_item_index(); + PaneSummary { + active, + leader, + items: pane + .items() + .enumerate() + .map(|(ix, item)| { + ( + ix == active_ix, + item.tab_description(0, cx) + .map_or(String::new(), |s| s.to_string()), + ) + }) + .collect(), + } + }) + .collect() + }) +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 8121b0ac91d5021e2830236c1694a24a20cff3b5..4008a941dd2e76be691e8a9d54b5cb66f1f8c5a2 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7,14 +7,11 @@ use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, - ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, + ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, }; use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; -use gpui::{ - executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, - TestAppContext, ViewHandle, -}; +use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, TestAppContext}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, @@ -38,12 +35,7 @@ use std::{ }, }; use unindent::Unindent as _; -use workspace::{ - dock::{test::TestPanel, DockPosition}, - item::{test::TestItem, ItemHandle as _}, - shared_screen::SharedScreen, - SplitDirection, Workspace, -}; +use workspace::Workspace; #[ctor::ctor] fn init_logger() { @@ -3146,6 +3138,7 @@ async fn test_local_settings( ) .await; let (project_a, _) = client_a.build_local_project("/dir", cx_a).await; + deterministic.run_until_parked(); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await @@ -4824,7 +4817,7 @@ async fn test_project_search( let mut results = HashMap::default(); let mut search_rx = project_b.update(cx_b, |project, cx| { project.search( - SearchQuery::text("world", false, false, Vec::new(), Vec::new()), + SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(), cx, ) }); @@ -6387,455 +6380,49 @@ async fn test_contact_requests( } #[gpui::test(iterations = 10)] -async fn test_basic_following( +async fn test_join_call_after_screen_was_shared( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, - cx_d: &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 client_d = server.create_client(cx_d, "user_d").await; server - .create_room(&mut [ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - (&client_d, cx_d), - ]) + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one\none\none", - "2.txt": "two\ntwo\ntwo", - "3.txt": "three\nthree\nthree", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + // Call users B and C from client A. active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let window_a = client_a.build_workspace(&project_a, cx_a); - let workspace_a = window_a.root(cx_a); - let window_b = client_b.build_workspace(&project_b, cx_b); - let workspace_b = window_b.root(cx_b); - - // Client A opens some editors. - let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let editor_a2 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B opens an editor. - let editor_b1 = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let peer_id_a = client_a.peer_id().unwrap(); - let peer_id_b = client_b.peer_id().unwrap(); - let peer_id_c = client_c.peer_id().unwrap(); - let peer_id_d = client_d.peer_id().unwrap(); - - // Client A updates their selections in those editors - editor_a1.update(cx_a, |editor, cx| { - editor.handle_input("a", cx); - editor.handle_input("b", cx); - editor.handle_input("c", cx); - editor.select_left(&Default::default(), cx); - assert_eq!(editor.selections.ranges(cx), vec![3..2]); - }); - editor_a2.update(cx_a, |editor, cx| { - editor.handle_input("d", cx); - editor.handle_input("e", cx); - editor.select_left(&Default::default(), cx); - assert_eq!(editor.selections.ranges(cx), vec![2..1]); - }); - - // When client B starts following client A, all visible view states are replicated to client B. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); - - cx_c.foreground().run_until_parked(); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - assert_eq!( - cx_b.read(|cx| editor_b2.project_path(cx)), - Some((worktree_id, "2.txt").into()) - ); - assert_eq!( - editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![2..1] - ); - assert_eq!( - editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![3..2] - ); - - cx_c.foreground().run_until_parked(); - let active_call_c = cx_c.read(ActiveCall::global); - let project_c = client_c.build_remote_project(project_id, cx_c).await; - let window_c = client_c.build_workspace(&project_c, cx_c); - let workspace_c = window_c.root(cx_c); - active_call_c - .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) - .await - .unwrap(); - drop(project_c); - - // Client C also follows client A. - workspace_c - .update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); - - cx_d.foreground().run_until_parked(); - let active_call_d = cx_d.read(ActiveCall::global); - let project_d = client_d.build_remote_project(project_id, cx_d).await; - let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); - active_call_d - .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) - .await - .unwrap(); - drop(project_d); - - // All clients see that clients B and C are following client A. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); - } - - // Client C unfollows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); - }); - - // All clients see that clients B is following client A. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); - } - - // Client C re-follows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); - }); - - // All clients see that clients B and C are following client A. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); - } - - // Client D follows client C. - workspace_d - .update(cx_d, |workspace, cx| { - workspace.toggle_follow(peer_id_c, cx).unwrap() - }) - .await - .unwrap(); - - // All clients see that D is following C - cx_d.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[peer_id_d], - "checking followers for C as {name}" - ); - }); - } - - // Client C closes the project. - window_c.remove(cx_c); - cx_c.drop_last(workspace_c); - - // Clients A and B see that client B is following A, and client C is not present in the followers. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); - } - - // All clients see that no-one is following C - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[], - "checking followers for C as {name}" - ); - }); - } - - // When client A activates a different editor, client B does so as well. - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a1, cx) - }); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - // When client A opens a multibuffer, client B does so as well. - let multibuffer_a = cx_a.add_model(|cx| { - let buffer_a1 = project_a.update(cx, |project, cx| { - project - .get_open_buffer(&(worktree_id, "1.txt").into(), cx) - .unwrap() - }); - let buffer_a2 = project_a.update(cx, |project, cx| { - project - .get_open_buffer(&(worktree_id, "2.txt").into(), cx) - .unwrap() - }); - let mut result = MultiBuffer::new(0); - result.push_excerpts( - buffer_a1, - [ExcerptRange { - context: 0..3, - primary: None, - }], - cx, - ); - result.push_excerpts( - buffer_a2, - [ExcerptRange { - context: 4..7, - primary: None, - }], - cx, - ); - result - }); - let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { - let editor = - cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); - workspace.add_item(Box::new(editor.clone()), cx); - editor - }); - deterministic.run_until_parked(); - let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - assert_eq!( - multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), - multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), - ); - - // When client A navigates back and forth, client B does so as well. - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_forward(workspace.active_pane().downgrade(), cx) + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - // Changes to client A's editor are reflected on client B. - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); - }); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); - }); - - editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); - - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([3..3])); - editor.set_scroll_position(vec2f(0., 100.), cx); - }); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[3..3]); - }); - - // After unfollowing, client B stops receiving updates from client A. - workspace_b.update(cx_b, |workspace, cx| { - workspace.unfollow(&workspace.active_pane().clone(), cx) - }); - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a2, cx) - }); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); deterministic.run_until_parked(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - editor_b1.id() + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: vec!["user_b".to_string()] + } ); - // Client A starts following client B. - workspace_a - .update(cx_a, |workspace, cx| { - workspace.toggle_follow(peer_id_b, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - Some(peer_id_b) - ); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - editor_a1.id() - ); + // User B receives the call. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + let call_b = incoming_call_b.next().await.unwrap().unwrap(); + assert_eq!(call_b.calling_user.github_login, "user_a"); - // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + // User A shares their screen let display = MacOSDisplay::new(); - active_call_b - .update(cx_b, |call, cx| call.set_location(None, cx)) - .await - .unwrap(); - active_call_b - .update(cx_b, |call, cx| { + active_call_a + .update(cx_a, |call, cx| { call.room().unwrap().update(cx, |room, cx| { room.set_display_sources(vec![display.clone()]); room.share_screen(cx) @@ -6843,153 +6430,18 @@ async fn test_basic_following( }) .await .unwrap(); - deterministic.run_until_parked(); - let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() + + client_b.user_store().update(cx_b, |user_store, _| { + user_store.clear_cache(); }); - // Client B activates Zed again, which causes the previous editor to become focused again. + // User B joins the room active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) - }); - - // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_item(&multibuffer_editor_b, cx) - }); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() - ) - }); - - // Client B activates a panel, and the previously-opened screen-sharing item gets activated. - let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); - workspace_b.update(cx_b, |workspace, cx| { - workspace.add_panel(panel, cx); - workspace.toggle_panel_focus::(cx); - }); - deterministic.run_until_parked(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - shared_screen.id() - ); - - // Toggling the focus back to the pane causes client A to return to the multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_panel_focus::(cx); - }); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() - ) - }); - - // Client B activates an item that doesn't implement following, - // so the previously-opened screen-sharing item gets activated. - let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); - workspace_b.update(cx_b, |workspace, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(unfollowable_item), true, true, None, cx) - }) - }); - deterministic.run_until_parked(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - shared_screen.id() - ); - - // Following interrupts when client B disconnects. - client_b.disconnect(&cx_b.to_async()); - deterministic.advance_clock(RECONNECT_TIMEOUT); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - None - ); -} - -#[gpui::test(iterations = 10)] -async fn test_join_call_after_screen_was_shared( - 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; - server - .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - // Call users B and C from client A. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: vec!["user_b".to_string()] - } - ); - - // User B receives the call. - let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); - let call_b = incoming_call_b.next().await.unwrap().unwrap(); - assert_eq!(call_b.calling_user.github_login, "user_a"); - - // User A shares their screen - let display = MacOSDisplay::new(); - active_call_a - .update(cx_a, |call, cx| { - call.room().unwrap().update(cx, |room, cx| { - room.set_display_sources(vec![display.clone()]); - room.share_screen(cx) - }) - }) - .await - .unwrap(); - - client_b.user_store().update(cx_b, |user_store, _| { - user_store.clear_cache(); - }); - - // User B joins the room - active_call_b - .update(cx_b, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); - assert!(incoming_call_b.next().await.unwrap().is_none()); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + assert!(incoming_call_b.next().await.unwrap().is_none()); deterministic.run_until_parked(); assert_eq!( @@ -7020,526 +6472,6 @@ async fn test_join_call_after_screen_was_shared( }); } -#[gpui::test] -async fn test_following_tab_order( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - 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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - - let client_b_id = project_a.read_with(cx_a, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - - //Open 1, 3 in that order on client A - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap(); - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "3.txt"), None, true, cx) - }) - .await - .unwrap(); - - let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { - pane.update(cx, |pane, cx| { - pane.items() - .map(|item| { - item.project_path(cx) - .unwrap() - .path - .to_str() - .unwrap() - .to_owned() - }) - .collect::>() - }) - }; - - //Verify that the tabs opened in the order we expect - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); - - //Follow client B as client A - workspace_a - .update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() - }) - .await - .unwrap(); - - //Open just 2 on client B - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - - // Verify that newly opened followed file is at the end - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); - - //Open just 1 on client B - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap(); - assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); - deterministic.run_until_parked(); - - // Verify that following into 1 did not reorder - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); -} - -#[gpui::test(iterations = 10)] -async fn test_peers_following_each_other( - 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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - // Client A shares a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - "4.txt": "four", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Client B joins the project. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let _editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B opens an editor. - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - let _editor_b1 = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Clients A and B follow each other in split panes - workspace_a.update(cx_a, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); - workspace_a - .update(cx_a, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_a1); - let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); - workspace_b - .update(cx_b, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_b1); - let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_next_pane(cx); - }); - // Wait for focus effects to be fully flushed - workspace_a.update(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "3.txt"), None, true, cx) - }) - .await - .unwrap(); - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_next_pane(cx); - }); - - workspace_b - .update(cx_b, |workspace, cx| { - assert_eq!(*workspace.active_pane(), pane_b1); - workspace.open_path((worktree_id, "4.txt"), None, true, cx) - }) - .await - .unwrap(); - cx_a.foreground().run_until_parked(); - - // Ensure leader updates don't change the active pane of followers - workspace_a.read_with(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); - workspace_b.read_with(cx_b, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_b1); - }); - - // Ensure peers following each other doesn't cause an infinite loop. - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .project_path(cx)), - Some((worktree_id, "3.txt").into()) - ); - workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); - workspace.activate_next_pane(cx); - }); - - workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); - }); - - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); - workspace.activate_next_pane(cx); - }); - - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_auto_unfollowing( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - - // 2 clients connect to a server. - 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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - // Client A shares a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let _editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B starts following client A. - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - let leader_id = project_b.read_with(cx_b, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - - // When client B moves, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B edits, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B scrolls, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| { - editor.set_scroll_position(vec2f(0., 3.), cx) - }); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B activates a different pane, it continues following client A in the original pane. - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) - }); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B activates a different item in the original pane, it automatically stops following client A. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); -} - -#[gpui::test(iterations = 10)] -async fn test_peers_simultaneously_following_each_other( - 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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a.fs().insert_tree("/a", json!({})).await; - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - let project_b = client_b.build_remote_project(project_id, cx_b).await; - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - - deterministic.run_until_parked(); - let client_a_id = project_b.read_with(cx_b, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - let client_b_id = project_a.read_with(cx_a, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - - let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() - }); - let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_follow(client_a_id, cx).unwrap() - }); - - futures::try_join!(a_follow_b, b_follow_a).unwrap(); - workspace_a.read_with(cx_a, |workspace, _| { - assert_eq!( - workspace.leader_for_pane(workspace.active_pane()), - Some(client_b_id) - ); - }); - workspace_b.read_with(cx_b, |workspace, _| { - assert_eq!( - workspace.leader_for_pane(workspace.active_pane()), - Some(client_a_id) - ); - }); -} - #[gpui::test(iterations = 10)] async fn test_on_input_format_from_host_to_guest( deterministic: Arc, diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index a60d3d7d7d6c14d54fb7e7129ba414e148ff90c1..ad0181602c9ac3bd5ab25d6029ad84d7ba74ce3e 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -86,7 +86,7 @@ impl RandomizedTest for RandomChannelBufferTest { match rng.gen_range(0..100_u32) { 0..=29 => { let channel_name = client.channel_store().read_with(cx, |store, cx| { - store.channels().find_map(|(_, channel)| { + store.channel_dag_entries().find_map(|(_, channel)| { if store.has_open_channel_buffer(channel.id, cx) { None } else { @@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest { ChannelBufferOperation::JoinChannelNotes { channel_name } => { let buffer = client.channel_store().update(cx, |store, cx| { let channel_id = store - .channels() + .channel_dag_entries() .find(|(_, c)| c.name == channel_name) .unwrap() .1 @@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest { // channel buffer. let collaborators = channel_buffer.collaborators(); let mut user_ids = - collaborators.iter().map(|c| c.user_id).collect::>(); + collaborators.values().map(|c| c.user_id).collect::>(); user_ids.sort(); assert_eq!( user_ids, diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 7570768249c4d820da8d1c248b71fef52d181464..6f9513c3253ebece9aaa553d8839b80ded113fff 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -869,7 +869,7 @@ impl RandomizedTest for ProjectCollaborationTest { let mut search = project.update(cx, |project, cx| { project.search( - SearchQuery::text(query, false, false, Vec::new(), Vec::new()), + SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(), cx, ) }); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index eef1dde96755701cf39a78094aeb19f737f95d3e..e10ded7d953f3872a3056a29940a7610db73de41 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -1,12 +1,12 @@ use crate::{ db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, - rpc::{Server, CLEANUP_TIMEOUT}, + rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, AppState, }; use anyhow::anyhow; use call::ActiveCall; -use channel::{channel_buffer::ChannelBuffer, ChannelStore}; +use channel::{ChannelBuffer, ChannelStore}; use client::{ self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; @@ -17,6 +17,7 @@ use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHan use language::LanguageRegistry; use parking_lot::Mutex; use project::{Project, WorktreeId}; +use rpc::RECEIVE_TIMEOUT; use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, @@ -29,7 +30,7 @@ use std::{ }, }; use util::http::FakeHttpClient; -use workspace::Workspace; +use workspace::{Workspace, WorkspaceStore}; pub struct TestServer { pub app_state: Arc, @@ -151,12 +152,12 @@ impl TestServer { Arc::get_mut(&mut client) .unwrap() - .set_id(user_id.0 as usize) + .set_id(user_id.to_proto()) .override_authenticate(move |cx| { cx.spawn(|_| async move { let access_token = "the-token".to_string(); Ok(Credentials { - user_id: user_id.0 as u64, + user_id: user_id.to_proto(), access_token, }) }) @@ -204,13 +205,17 @@ impl TestServer { let fs = FakeFs::new(cx.background()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + let mut language_registry = LanguageRegistry::test(); + language_registry.set_executor(cx.background()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), + workspace_store, channel_store: channel_store.clone(), - languages: Arc::new(LanguageRegistry::test()), + languages: Arc::new(language_registry), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), @@ -251,6 +256,19 @@ impl TestServer { .store(true, SeqCst); } + pub fn simulate_long_connection_interruption( + &self, + peer_id: PeerId, + deterministic: &Arc, + ) { + self.forbid_connections(); + self.disconnect_client(peer_id); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + self.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + deterministic.run_until_parked(); + } + pub fn forbid_connections(&self) { self.forbid_connections.store(true, SeqCst); } @@ -288,6 +306,7 @@ impl TestServer { pub async fn make_channel( &self, channel: &str, + parent: Option, admin: (&TestClient, &mut TestAppContext), members: &mut [(&TestClient, &mut TestAppContext)], ) -> u64 { @@ -296,7 +315,7 @@ impl TestServer { .app_state .channel_store .update(admin_cx, |channel_store, cx| { - channel_store.create_channel(channel, None, cx) + channel_store.create_channel(channel, parent, cx) }) .await .unwrap(); @@ -331,6 +350,39 @@ impl TestServer { channel_id } + pub async fn make_channel_tree( + &self, + channels: &[(&str, Option<&str>)], + creator: (&TestClient, &mut TestAppContext), + ) -> Vec { + let mut observed_channels = HashMap::default(); + let mut result = Vec::new(); + for (channel, parent) in channels { + let id; + if let Some(parent) = parent { + if let Some(parent_id) = observed_channels.get(parent) { + id = self + .make_channel(channel, Some(*parent_id), (creator.0, creator.1), &mut []) + .await; + } else { + panic!( + "Edge {}->{} referenced before {} was created", + parent, channel, parent + ) + } + } else { + id = self + .make_channel(channel, None, (creator.0, creator.1), &mut []) + .await; + } + + observed_channels.insert(channel, id); + result.push(id); + } + + result + } + pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { self.make_contacts(clients).await; @@ -502,15 +554,7 @@ impl TestClient { root_path: impl AsRef, cx: &mut TestAppContext, ) -> (ModelHandle, WorktreeId) { - let project = cx.update(|cx| { - Project::local( - self.client().clone(), - self.app_state.user_store.clone(), - self.app_state.languages.clone(), - self.app_state.fs.clone(), - cx, - ) - }); + let project = self.build_empty_local_project(cx); let (worktree, _) = project .update(cx, |p, cx| { p.find_or_create_local_worktree(root_path, true, cx) @@ -523,6 +567,18 @@ impl TestClient { (project, worktree.read_with(cx, |tree, _| tree.id())) } + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle { + cx.update(|cx| { + Project::local( + self.client().clone(), + self.app_state.user_store.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), + cx, + ) + }) + } + pub async fn build_remote_project( &self, host_project_id: u64, @@ -549,6 +605,34 @@ impl TestClient { ) -> WindowHandle { cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } + + pub async fn add_admin_to_channel( + &self, + user: (&TestClient, &mut TestAppContext), + channel: u64, + cx_self: &mut TestAppContext, + ) { + let (other_client, other_cx) = user; + + self.app_state + .channel_store + .update(cx_self, |channel_store, cx| { + channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx) + }) + .await + .unwrap(); + + cx_self.foreground().run_until_parked(); + + other_client + .app_state + .channel_store + .update(other_cx, |channels, _| { + channels.respond_to_channel_invite(channel, true) + }) + .await + .unwrap(); + } } impl Drop for TestClient { diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index da32308558f7c7e8279c420961f8d42d9356d37b..98790778c98d69afa90743f8e40d94aa397cf886 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -30,12 +30,14 @@ channel = { path = "../channel" } clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } +drag_and_drop = { path = "../drag_and_drop" } editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } +rich_text = { path = "../rich_text" } picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} @@ -55,6 +57,7 @@ schemars.workspace = true postage.workspace = true serde.workspace = true serde_derive.workspace = true +time.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index a09073c55d35fed13b72f82df359609a492f6819..a95576805074d63a5a74de8d1d107899cb956714 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,12 +1,12 @@ use anyhow::{anyhow, Result}; -use channel::{ - channel_buffer::{self, ChannelBuffer}, - ChannelId, +use call::report_call_event_for_channel; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; +use client::{ + proto::{self, PeerId}, + Collaborator, ParticipantIndex, }; -use client::proto; -use clock::ReplicaId; use collections::HashMap; -use editor::Editor; +use editor::{CollaborationHub, Editor}; use gpui::{ actions, elements::{ChildView, Label}, @@ -15,7 +15,11 @@ use gpui::{ ViewContext, ViewHandle, }; use project::Project; -use std::any::{Any, TypeId}; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use util::ResultExt; use workspace::{ item::{FollowableItem, Item, ItemHandle}, register_followable_item, @@ -25,13 +29,14 @@ use workspace::{ actions!(channel_view, [Deploy]); -pub(crate) fn init(cx: &mut AppContext) { +pub fn init(cx: &mut AppContext) { register_followable_item::(cx) } pub struct ChannelView { pub editor: ViewHandle, project: ModelHandle, + channel_store: ModelHandle, channel_buffer: ModelHandle, remote_id: Option, _editor_event_subscription: Subscription, @@ -39,6 +44,28 @@ pub struct ChannelView { impl ChannelView { pub fn open( + channel_id: ChannelId, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); + cx.spawn(|mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + report_call_event_for_channel( + "open channel notes", + channel_id, + &workspace.read(cx).app_state().client, + cx, + ); + pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); + }); + anyhow::Ok(channel_view) + }) + } + + pub fn open_in_pane( channel_id: ChannelId, pane: ViewHandle, workspace: ViewHandle, @@ -56,17 +83,25 @@ impl ChannelView { 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); - }) - }); + + if let Some(markdown) = markdown.await.log_err() { + 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))) + .unwrap_or_else(|| { + cx.add_view(|cx| { + let mut this = Self::new(project, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this + }) + }) }) .ok_or_else(|| anyhow!("pane was dropped")) }) @@ -74,96 +109,79 @@ impl ChannelView { pub fn new( project: ModelHandle, + channel_store: 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 = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor + }); 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 { + Self { editor, project, + channel_store, 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); + pub fn channel(&self, cx: &AppContext) -> Arc { + self.channel_buffer.read(cx).channel() } fn handle_channel_buffer_event( &mut self, _: ModelHandle, - event: &channel_buffer::Event, + event: &ChannelBufferEvent, 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| { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { editor.set_read_only(true); cx.notify(); }), + ChannelBufferEvent::BufferEdited => { + if cx.is_self_focused() || self.editor.is_focused(cx) { + self.acknowledge_buffer_version(cx); + } else { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.notes_changed( + channel_buffer.channel().id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + } + } + _ => {} } } - /// 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) + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.acknowledge_notes_version( + channel_buffer.channel().id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + self.channel_buffer.update(cx, |buffer, cx| { + buffer.acknowledge_buffer_version(cx); }); } } @@ -183,6 +201,7 @@ impl View for ChannelView { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { + self.acknowledge_buffer_version(cx); cx.focus(self.editor.as_any()) } } @@ -222,6 +241,7 @@ impl Item for ChannelView { fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { Some(Self::new( self.project.clone(), + self.channel_store.clone(), self.channel_buffer.clone(), cx, )) @@ -294,7 +314,7 @@ impl FollowableItem for ChannelView { unreachable!() }; - let open = ChannelView::open(state.channel_id, pane, workspace, cx); + let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); Some(cx.spawn(|mut cx| async move { let this = open.await?; @@ -354,17 +374,32 @@ impl FollowableItem for ChannelView { }) } - fn set_leader_replica_id( - &mut self, - leader_replica_id: Option, - cx: &mut ViewContext, - ) { + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| { - editor.set_leader_replica_id(leader_replica_id, cx) + editor.set_leader_peer_id(leader_peer_id, cx) }) } fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { Editor::should_unfollow_on_event(event, cx) } + + fn is_project_item(&self, _cx: &AppContext) -> bool { + false + } +} + +struct ChannelBufferCollaborationHub(ModelHandle); + +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).participant_indices() + } } diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..b446521c5ab6120840ec311425171d0dc231e1f3 --- /dev/null +++ b/crates/collab_ui/src/chat_panel.rs @@ -0,0 +1,885 @@ +use crate::{channel_view::ChannelView, ChatPanelSettings}; +use anyhow::Result; +use call::ActiveCall; +use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; +use client::Client; +use collections::HashMap; +use db::kvp::KEY_VALUE_STORE; +use editor::Editor; +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; +use gpui::{ + actions, + elements::*, + platform::{CursorStyle, MouseButton}, + serde_json, + views::{ItemType, Select, SelectStyle}, + AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task, + View, ViewContext, ViewHandle, WeakViewHandle, +}; +use language::{language_settings::SoftWrap, LanguageRegistry}; +use menu::Confirm; +use project::Fs; +use rich_text::RichText; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; +use std::sync::Arc; +use theme::{IconButton, Theme}; +use time::{OffsetDateTime, UtcOffset}; +use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + Workspace, +}; + +const MESSAGE_LOADING_THRESHOLD: usize = 50; +const CHAT_PANEL_KEY: &'static str = "ChatPanel"; + +pub struct ChatPanel { + client: Arc, + channel_store: ModelHandle, + languages: Arc, + active_chat: Option<(ModelHandle, Subscription)>, + message_list: ListState, + input_editor: ViewHandle, + channel_select: ViewHandle, + ) -> AnyElement