diff --git a/.cargo/config.toml b/.cargo/config.toml index 9da6b3be08..32fdb271ad 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,3 @@ -[alias] -xtask = "run --package xtask --" - [build] # v0 mangling scheme provides more detailed backtraces around closures rustflags = ["-C", "symbol-mangling-version=v0"] diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 5063d414ec..b7e6a0321e 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -2,8 +2,8 @@ name: Release Nightly on: schedule: - # Fire every day at 1:00pm and 1:00am - - cron: "0 1,13 * * *" + # Fire every day at 7:00am UTC (Roughly before EU workday and after US workday) + - cron: "0 7 * * *" push: tags: - "nightly" @@ -92,7 +92,7 @@ jobs: run: script/generate-licenses - name: Create app bundle - run: script/bundle -2 + run: script/bundle - name: Upload Zed Nightly run: script/upload-nightly diff --git a/Cargo.lock b/Cargo.lock index 68919dffbc..c6e7ecebc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 3 name = "activity_indicator" version = "0.1.0" dependencies = [ + "anyhow", "auto_update", "editor", "futures 0.3.28", @@ -15,45 +16,18 @@ dependencies = [ "settings", "smallvec", "theme", + "ui", "util", "workspace", ] -[[package]] -name = "activity_indicator2" -version = "0.1.0" -dependencies = [ - "anyhow", - "auto_update2", - "editor2", - "futures 0.3.28", - "gpui2", - "language2", - "project2", - "settings2", - "smallvec", - "theme2", - "ui2", - "util", - "workspace2", -] - -[[package]] -name = "addr2line" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" -dependencies = [ - "gimli 0.26.2", -] - [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli 0.28.0", + "gimli", ] [[package]] @@ -127,33 +101,6 @@ dependencies = [ "util", ] -[[package]] -name = "ai2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "bincode", - "futures 0.3.28", - "gpui2", - "isahc", - "language2", - "lazy_static", - "log", - "matrixmultiply", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "parse_duration", - "postage", - "rand 0.8.5", - "regex", - "rusqlite", - "serde", - "serde_json", - "tiktoken-rs", - "util", -] - [[package]] name = "alacritty_config" version = "0.1.2-dev" @@ -182,7 +129,7 @@ dependencies = [ "alacritty_config", "alacritty_config_derive", "base64 0.13.1", - "bitflags 2.4.0", + "bitflags 2.4.1", "home", "libc", "log", @@ -237,12 +184,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "ambient-authority" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -306,6 +247,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "any_ascii" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea50b14b7a4b9343f8c627a7a53c52076482bd4bdad0a24fd3ec533ed616cc2c" + [[package]] name = "anyhow" version = "1.0.75" @@ -345,6 +292,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "assets" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "rust-embed", +] + [[package]] name = "assistant" version = "0.1.0" @@ -356,7 +312,7 @@ dependencies = [ "collections", "ctor", "editor", - "env_logger 0.9.3", + "env_logger", "fs", "futures 0.3.28", "gpui", @@ -380,52 +336,12 @@ dependencies = [ "smol", "theme", "tiktoken-rs", + "ui", "util", "uuid 1.4.1", "workspace", ] -[[package]] -name = "assistant2" -version = "0.1.0" -dependencies = [ - "ai2", - "anyhow", - "chrono", - "client2", - "collections", - "ctor", - "editor2", - "env_logger 0.9.3", - "fs2", - "futures 0.3.28", - "gpui2", - "indoc", - "isahc", - "language2", - "log", - "menu2", - "multi_buffer2", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "project2", - "rand 0.8.5", - "regex", - "schemars", - "search2", - "semantic_index2", - "serde", - "serde_json", - "settings2", - "smol", - "theme2", - "tiktoken-rs", - "ui2", - "util", - "uuid 1.4.1", - "workspace2", -] - [[package]] name = "async-broadcast" version = "0.4.1" @@ -669,8 +585,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.0.3" -source = "git+https://github.com/zed-industries/async-task?rev=341b57d6de98cdfd7b418567b8de2022ca993a6e#341b57d6de98cdfd7b418567b8de2022ca993a6e" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-tls" @@ -745,24 +662,11 @@ dependencies = [ [[package]] name = "audio" version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "gpui", - "log", - "parking_lot 0.11.2", - "rodio", - "util", -] - -[[package]] -name = "audio2" -version = "0.1.0" dependencies = [ "anyhow", "collections", "futures 0.3.28", - "gpui2", + "gpui", "log", "parking_lot 0.11.2", "rodio", @@ -782,6 +686,7 @@ dependencies = [ "log", "menu", "project", + "schemars", "serde", "serde_derive", "serde_json", @@ -793,30 +698,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "auto_update2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "db2", - "gpui2", - "isahc", - "lazy_static", - "log", - "menu2", - "project2", - "serde", - "serde_derive", - "serde_json", - "settings2", - "smol", - "tempdir", - "theme2", - "util", - "workspace2", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -901,12 +782,12 @@ version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ - "addr2line 0.21.0", + "addr2line", "cc", "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", - "object 0.32.1", + "object", "rustc-demangle", ] @@ -1025,9 +906,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" dependencies = [ "serde", ] @@ -1142,27 +1023,10 @@ dependencies = [ "search", "settings", "theme", + "ui", "workspace", ] -[[package]] -name = "breadcrumbs2" -version = "0.1.0" -dependencies = [ - "collections", - "editor2", - "gpui2", - "itertools 0.10.5", - "language2", - "outline2", - "project2", - "search2", - "settings2", - "theme2", - "ui2", - "workspace2", -] - [[package]] name = "bromberg_sl2" version = "0.6.0" @@ -1253,6 +1117,7 @@ dependencies = [ "fs", "futures 0.3.28", "gpui", + "image", "language", "live_kit_client", "log", @@ -1264,103 +1129,10 @@ dependencies = [ "serde_derive", "serde_json", "settings", - "util", -] - -[[package]] -name = "call2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-broadcast", - "audio2", - "client2", - "collections", - "fs2", - "futures 0.3.28", - "gpui2", - "image", - "language2", - "live_kit_client2", - "log", - "media", - "postage", - "project2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings2", "smallvec", "util", ] -[[package]] -name = "cap-fs-ext" -version = "0.24.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54b86398b5852ddd45784b1d9b196b98beb39171821bad4b8b44534a1e87927" -dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes 0.5.3", - "winapi 0.3.9", -] - -[[package]] -name = "cap-primitives" -version = "0.24.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8fca3e81fae1d91a36e9784ca22a39ef623702b5f7904d89dc31f10184a178" -dependencies = [ - "ambient-authority", - "errno 0.2.8", - "fs-set-times", - "io-extras", - "io-lifetimes 0.5.3", - "ipnet", - "maybe-owned", - "rustix 0.33.7", - "winapi 0.3.9", - "winapi-util", - "winx", -] - -[[package]] -name = "cap-rand" -version = "0.24.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3b27294116983d706f4c8168f6d10c84f9f5daed0c28bc7d0296cf16bcf971" -dependencies = [ - "ambient-authority", - "rand 0.8.5", -] - -[[package]] -name = "cap-std" -version = "0.24.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2247568946095c7765ad2b441a56caffc08027734c634a6d5edda648f04e32eb" -dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes 0.5.3", - "ipnet", - "rustix 0.33.7", -] - -[[package]] -name = "cap-time-ext" -version = "0.24.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50472b6ebc302af0401fa3fb939694cd8ff00e0d4c9182001e434fc822ab83a" -dependencies = [ - "cap-primitives", - "once_cell", - "rustix 0.33.7", - "winx", -] - [[package]] name = "castaway" version = "0.1.2" @@ -1459,43 +1231,6 @@ dependencies = [ "uuid 1.4.1", ] -[[package]] -name = "channel2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "clock", - "collections", - "db2", - "feature_flags2", - "futures 0.3.28", - "gpui2", - "image", - "language2", - "lazy_static", - "log", - "parking_lot 0.11.2", - "postage", - "rand 0.8.5", - "rpc2", - "schemars", - "serde", - "serde_derive", - "settings2", - "smallvec", - "smol", - "sum_tree", - "tempfile", - "text2", - "thiserror", - "time", - "tiny_http", - "url", - "util", - "uuid 1.4.1", -] - [[package]] name = "chrono" version = "0.4.31" @@ -1655,6 +1390,7 @@ dependencies = [ "schemars", "serde", "serde_derive", + "serde_json", "settings", "smol", "sum_tree", @@ -1669,43 +1405,6 @@ dependencies = [ "uuid 1.4.1", ] -[[package]] -name = "client2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-recursion 0.3.2", - "async-tungstenite", - "chrono", - "collections", - "db2", - "feature_flags2", - "futures 0.3.28", - "gpui2", - "image", - "lazy_static", - "log", - "parking_lot 0.11.2", - "postage", - "rand 0.8.5", - "rpc2", - "schemars", - "serde", - "serde_derive", - "settings2", - "smol", - "sum_tree", - "sysinfo", - "tempfile", - "text2", - "thiserror", - "time", - "tiny_http", - "url", - "util", - "uuid 1.4.1", -] - [[package]] name = "clock" version = "0.1.0" @@ -1753,7 +1452,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.31.0" +version = "0.35.0" dependencies = [ "anyhow", "async-trait", @@ -1772,7 +1471,7 @@ dependencies = [ "ctor", "dashmap", "editor", - "env_logger 0.9.3", + "env_logger", "envy", "fs", "futures 0.3.28", @@ -1824,79 +1523,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "collab2" -version = "0.28.0" -dependencies = [ - "anyhow", - "async-trait", - "async-tungstenite", - "audio2", - "axum", - "axum-extra", - "base64 0.13.1", - "call2", - "channel2", - "clap 3.2.25", - "client2", - "clock", - "collab_ui2", - "collections", - "ctor", - "dashmap", - "editor2", - "env_logger 0.9.3", - "envy", - "fs2", - "futures 0.3.28", - "git3", - "gpui2", - "hyper", - "indoc", - "language2", - "lazy_static", - "lipsum", - "live_kit_client2", - "live_kit_server", - "log", - "lsp2", - "nanoid", - "node_runtime", - "notifications2", - "parking_lot 0.11.2", - "pretty_assertions", - "project2", - "prometheus", - "prost 0.8.0", - "rand 0.8.5", - "reqwest", - "rpc2", - "scrypt", - "sea-orm", - "serde", - "serde_derive", - "serde_json", - "settings2", - "sha-1 0.9.8", - "smallvec", - "sqlx", - "text2", - "theme2", - "time", - "tokio", - "tokio-tungstenite", - "toml 0.5.11", - "tonic", - "tower", - "tracing", - "tracing-log", - "tracing-subscriber", - "unindent", - "util", - "uuid 1.4.1", - "workspace2", -] - [[package]] name = "collab_ui" version = "0.1.0" @@ -1908,9 +1534,7 @@ dependencies = [ "client", "clock", "collections", - "context_menu", "db", - "drag_and_drop", "editor", "feature_flags", "feedback", @@ -1932,59 +1556,19 @@ dependencies = [ "schemars", "serde", "serde_derive", + "serde_json", "settings", "smallvec", + "story", "theme", "theme_selector", "time", "tree-sitter-markdown", + "ui", "util", "vcs_menu", "workspace", - "zed-actions", -] - -[[package]] -name = "collab_ui2" -version = "0.1.0" -dependencies = [ - "anyhow", - "call2", - "channel2", - "client2", - "clock", - "collections", - "db2", - "editor2", - "feature_flags2", - "futures 0.3.28", - "fuzzy2", - "gpui2", - "language2", - "lazy_static", - "log", - "menu2", - "notifications2", - "picker2", - "postage", - "pretty_assertions", - "project2", - "recent_projects2", - "rich_text2", - "rpc2", - "schemars", - "serde", - "serde_derive", - "settings2", - "smallvec", - "theme2", - "time", - "tree-sitter-markdown", - "ui2", - "util", - "vcs_menu2", - "workspace2", - "zed_actions2", + "zed_actions", ] [[package]] @@ -2020,60 +1604,26 @@ dependencies = [ name = "command_palette" version = "0.1.0" dependencies = [ + "anyhow", "collections", "ctor", "editor", - "env_logger 0.9.3", + "env_logger", "fuzzy", + "go_to_line", "gpui", "language", + "menu", "picker", "project", - "serde_json", - "settings", - "theme", - "util", - "workspace", - "zed-actions", -] - -[[package]] -name = "command_palette2" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "ctor", - "editor2", - "env_logger 0.9.3", - "fuzzy2", - "go_to_line2", - "gpui2", - "language2", - "menu2", - "picker2", - "project2", "serde", "serde_json", - "settings2", - "theme2", - "ui2", - "util", - "workspace2", - "zed_actions2", -] - -[[package]] -name = "component_test" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", - "project", "settings", "theme", + "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -2110,17 +1660,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" -[[package]] -name = "context_menu" -version = "0.1.0" -dependencies = [ - "gpui", - "menu", - "settings", - "smallvec", - "theme", -] - [[package]] name = "convert_case" version = "0.4.0" @@ -2145,7 +1684,6 @@ dependencies = [ "async-tar", "clock", "collections", - "context_menu", "fs", "futures 0.3.28", "gpui", @@ -2164,38 +1702,10 @@ dependencies = [ ] [[package]] -name = "copilot2" +name = "copilot_ui" version = "0.1.0" dependencies = [ "anyhow", - "async-compression", - "async-tar", - "clock", - "collections", - "fs", - "futures 0.3.28", - "gpui2", - "language2", - "log", - "lsp2", - "node_runtime", - "parking_lot 0.11.2", - "rpc2", - "serde", - "serde_derive", - "settings2", - "smol", - "theme2", - "ui2", - "util", -] - -[[package]] -name = "copilot_button" -version = "0.1.0" -dependencies = [ - "anyhow", - "context_menu", "copilot", "editor", "fs", @@ -2205,27 +1715,10 @@ dependencies = [ "settings", "smol", "theme", + "ui", "util", "workspace", -] - -[[package]] -name = "copilot_button2" -version = "0.1.0" -dependencies = [ - "anyhow", - "copilot2", - "editor2", - "fs2", - "futures 0.3.28", - "gpui2", - "language2", - "settings2", - "smol", - "theme2", - "util", - "workspace2", - "zed_actions2", + "zed_actions", ] [[package]] @@ -2338,15 +1831,6 @@ dependencies = [ "windows 0.46.0", ] -[[package]] -name = "cpp_demangle" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "cpufeatures" version = "0.2.9" @@ -2356,102 +1840,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-bforest" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749d0d6022c9038dccf480bdde2a38d435937335bf2bb0f14e815d94517cdce8" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-codegen" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94370cc7b37bf652ccd8bb8f09bd900997f7ccf97520edfc75554bb5c4abbea" -dependencies = [ - "cranelift-bforest", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-entity", - "cranelift-isle", - "gimli 0.26.2", - "log", - "regalloc2", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a3cea8fdab90e44018c5b9a1dfd460d8ee265ac354337150222a354628bdb6" -dependencies = [ - "cranelift-codegen-shared", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac72f76f2698598951ab26d8c96eaa854810e693e7dd52523958b5909fde6b2" - -[[package]] -name = "cranelift-entity" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09eaeacfcd2356fe0e66b295e8f9d59fdd1ac3ace53ba50de14d628ec902f72d" -dependencies = [ - "serde", -] - -[[package]] -name = "cranelift-frontend" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba69c9980d5ffd62c18a2bde927855fcd7c8dc92f29feaf8636052662cbd99c" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-isle" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2920dc1e05cac40304456ed3301fde2c09bd6a9b0210bcfa2f101398d628d5b" - -[[package]] -name = "cranelift-native" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04dfa45f9b2a6f587c564d6b63388e00cd6589d2df6ea2758cf79e1a13285e6" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon", -] - -[[package]] -name = "cranelift-wasm" -version = "0.85.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31a46513ae6f26f3f267d8d75b5373d555fbbd1e68681f348d99df43f747ec54" -dependencies = [ - "cranelift-codegen", - "cranelift-entity", - "cranelift-frontend", - "itertools 0.10.5", - "log", - "smallvec", - "wasmparser", - "wasmtime-types", -] - [[package]] name = "crc" version = "3.0.1" @@ -2551,11 +1939,12 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.20" -source = "git+https://github.com/zed-industries/rust-ctor?rev=7f824cf6a7943885a649b579f33f9ac53f0d1db6#7f824cf6a7943885a649b579f33f9ac53f0d1db6" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] @@ -2623,7 +2012,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "env_logger 0.9.3", + "env_logger", "gpui", "indoc", "lazy_static", @@ -2638,28 +2027,6 @@ dependencies = [ "util", ] -[[package]] -name = "db2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "env_logger 0.9.3", - "gpui2", - "indoc", - "lazy_static", - "log", - "parking_lot 0.11.2", - "serde", - "serde_derive", - "smol", - "sqlez", - "sqlez_macros", - "tempdir", - "util", -] - [[package]] name = "deflate" version = "0.8.6" @@ -2761,39 +2128,12 @@ dependencies = [ "settings", "smallvec", "theme", + "ui", "unindent", "util", "workspace", ] -[[package]] -name = "diagnostics2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "collections", - "editor2", - "futures 0.3.28", - "gpui2", - "language2", - "log", - "lsp2", - "postage", - "project2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings2", - "smallvec", - "theme2", - "ui2", - "unindent", - "util", - "workspace2", -] - [[package]] name = "dialoguer" version = "0.11.0" @@ -2835,16 +2175,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", -] - [[package]] name = "dirs" version = "3.0.2" @@ -2910,14 +2240,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "drag_and_drop" -version = "0.1.0" -dependencies = [ - "collections", - "gpui", -] - [[package]] name = "dwrote" version = "0.11.0" @@ -2945,13 +2267,11 @@ dependencies = [ "client", "clock", "collections", - "context_menu", "convert_case 0.6.0", "copilot", "ctor", "db", - "drag_and_drop", - "env_logger 0.9.3", + "env_logger", "futures 0.3.28", "fuzzy", "git", @@ -2973,6 +2293,7 @@ dependencies = [ "schemars", "serde", "serde_derive", + "serde_json", "settings", "smallvec", "smol", @@ -2985,65 +2306,12 @@ dependencies = [ "tree-sitter-html", "tree-sitter-rust", "tree-sitter-typescript", + "ui", "unindent", "util", "workspace", ] -[[package]] -name = "editor2" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "client2", - "clock", - "collections", - "convert_case 0.6.0", - "copilot2", - "ctor", - "db2", - "env_logger 0.9.3", - "futures 0.3.28", - "fuzzy2", - "git3", - "gpui2", - "indoc", - "itertools 0.10.5", - "language2", - "lazy_static", - "log", - "lsp2", - "multi_buffer2", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "postage", - "project2", - "rand 0.8.5", - "rich_text2", - "rpc2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings2", - "smallvec", - "smol", - "snippet", - "sqlez", - "sum_tree", - "text2", - "theme2", - "tree-sitter", - "tree-sitter-html", - "tree-sitter-rust", - "tree-sitter-typescript", - "ui2", - "unindent", - "util", - "workspace2", -] - [[package]] name = "either" version = "1.9.0" @@ -3081,19 +2349,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "env_logger" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "humantime", - "is-terminal 0.4.9", - "log", - "regex", - "termcolor", -] - [[package]] name = "envy" version = "0.4.2" @@ -3118,17 +2373,6 @@ dependencies = [ "serde", ] -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi 0.3.9", -] - [[package]] name = "errno" version = "0.3.3" @@ -3237,20 +2481,14 @@ dependencies = [ "gpui", ] -[[package]] -name = "feature_flags2" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui2", -] - [[package]] name = "feedback" version = "0.1.0" dependencies = [ "anyhow", + "bitflags 2.4.1", "client", + "db", "editor", "futures 0.3.28", "gpui", @@ -3259,64 +2497,23 @@ dependencies = [ "language", "lazy_static", "log", + "menu", "postage", "project", "regex", - "search", "serde", "serde_derive", + "serde_json", "settings", "smallvec", - "sysinfo", - "theme", - "tree-sitter-markdown", - "urlencoding", - "util", - "workspace", -] - -[[package]] -name = "feedback2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "db2", - "editor2", - "futures 0.3.28", - "gpui2", - "human_bytes", - "isahc", - "language2", - "lazy_static", - "log", - "menu2", - "postage", - "project2", - "regex", - "search2", - "serde", - "serde_derive", - "settings2", - "smallvec", "smol", "sysinfo", - "theme2", + "theme", "tree-sitter-markdown", - "ui2", + "ui", "urlencoding", "util", - "workspace2", -] - -[[package]] -name = "file-per-thread-logger" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f2e425d9790201ba4af4630191feac6dcc98765b118d4d18e91d23c2353866" -dependencies = [ - "env_logger 0.10.0", - "log", + "workspace", ] [[package]] @@ -3326,7 +2523,7 @@ dependencies = [ "collections", "ctor", "editor", - "env_logger 0.9.3", + "env_logger", "fuzzy", "gpui", "language", @@ -3334,39 +2531,16 @@ dependencies = [ "picker", "postage", "project", + "serde", "serde_json", "settings", "text", "theme", + "ui", "util", "workspace", ] -[[package]] -name = "file_finder2" -version = "0.1.0" -dependencies = [ - "collections", - "ctor", - "editor2", - "env_logger 0.9.3", - "fuzzy2", - "gpui2", - "language2", - "menu2", - "picker2", - "postage", - "project2", - "serde", - "serde_json", - "settings2", - "text2", - "theme2", - "ui2", - "util", - "workspace2", -] - [[package]] name = "filetime" version = "0.2.22" @@ -3538,45 +2712,6 @@ dependencies = [ "util", ] -[[package]] -name = "fs-set-times" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df62ee66ee2d532ea8d567b5a3f0d03ecd64636b98bad5be1e93dcc918b92aa" -dependencies = [ - "io-lifetimes 0.5.3", - "rustix 0.33.7", - "winapi 0.3.9", -] - -[[package]] -name = "fs2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "fsevent", - "futures 0.3.28", - "git2", - "gpui2", - "lazy_static", - "libc", - "log", - "parking_lot 0.11.2", - "regex", - "rope2", - "serde", - "serde_derive", - "serde_json", - "smol", - "sum_tree", - "tempfile", - "text2", - "time", - "util", -] - [[package]] name = "fsevent" version = "2.0.2" @@ -3764,23 +2899,6 @@ dependencies = [ "thread_local", ] -[[package]] -name = "fuzzy2" -version = "0.1.0" -dependencies = [ - "gpui2", - "util", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3823,17 +2941,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "gimli" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" -dependencies = [ - "fallible-iterator", - "indexmap 1.9.3", - "stable_deref_trait", -] - [[package]] name = "gimli" version = "0.28.0" @@ -3873,26 +2980,6 @@ dependencies = [ "url", ] -[[package]] -name = "git3" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "clock", - "collections", - "futures 0.3.28", - "git2", - "lazy_static", - "log", - "parking_lot 0.11.2", - "smol", - "sum_tree", - "text2", - "unindent", - "util", -] - [[package]] name = "glob" version = "0.3.1" @@ -3932,30 +3019,15 @@ dependencies = [ "gpui", "menu", "postage", + "serde", "settings", "text", "theme", + "ui", "util", "workspace", ] -[[package]] -name = "go_to_line2" -version = "0.1.0" -dependencies = [ - "editor2", - "gpui2", - "menu2", - "postage", - "serde", - "settings2", - "text2", - "theme2", - "ui2", - "util", - "workspace2", -] - [[package]] name = "gpui" version = "0.1.0" @@ -3964,69 +3036,7 @@ dependencies = [ "async-task", "backtrace", "bindgen 0.65.1", - "block", - "cc", - "cocoa", - "collections", - "core-foundation", - "core-graphics", - "core-text", - "ctor", - "derive_more", - "dhat", - "env_logger 0.9.3", - "etagere", - "font-kit", - "foreign-types", - "futures 0.3.28", - "gpui_macros", - "image", - "itertools 0.10.5", - "lazy_static", - "log", - "media", - "metal", - "num_cpus", - "objc", - "ordered-float 2.10.0", - "parking", - "parking_lot 0.11.2", - "pathfinder_color", - "pathfinder_geometry", - "png", - "postage", - "rand 0.8.5", - "refineable", - "resvg", - "schemars", - "seahash", - "serde", - "serde_derive", - "serde_json", - "simplelog", - "smallvec", - "smol", - "sqlez", - "sum_tree", - "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e)", - "thiserror", - "time", - "tiny-skia", - "usvg", - "util", - "uuid 1.4.1", - "waker-fn", -] - -[[package]] -name = "gpui2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-task", - "backtrace", - "bindgen 0.65.1", - "bitflags 2.4.0", + "bitflags 2.4.1", "block", "cbindgen", "cocoa", @@ -4037,12 +3047,12 @@ dependencies = [ "ctor", "derive_more", "dhat", - "env_logger 0.9.3", + "env_logger", "etagere", "font-kit", "foreign-types", "futures 0.3.28", - "gpui2_macros", + "gpui_macros", "image", "itertools 0.10.5", "lazy_static", @@ -4056,7 +3066,6 @@ dependencies = [ "parking", "parking_lot 0.11.2", "pathfinder_geometry", - "plane-split", "png", "postage", "rand 0.8.5", @@ -4071,9 +3080,8 @@ dependencies = [ "slotmap", "smallvec", "smol", - "sqlez", "sum_tree", - "taffy 0.3.11 (git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b)", + "taffy", "thiserror", "time", "tiny-skia", @@ -4083,31 +3091,15 @@ dependencies = [ "waker-fn", ] -[[package]] -name = "gpui2_macros" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "gpui_macros" version = "0.1.0" dependencies = [ - "lazy_static", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "grid" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" - [[package]] name = "grid" version = "0.11.0" @@ -4133,15 +3125,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash 0.7.6", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -4230,15 +3213,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.3" @@ -4513,18 +3487,6 @@ dependencies = [ "util", ] -[[package]] -name = "install_cli2" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui2", - "log", - "serde", - "smol", - "util", -] - [[package]] name = "instant" version = "0.1.12" @@ -4534,26 +3496,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "io-extras" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c937cc9891c12eaa8c63ad347e4a288364b1328b924886970b47a14ab8f8f8" -dependencies = [ - "io-lifetimes 0.5.3", - "winapi 0.3.9", -] - -[[package]] -name = "io-lifetimes" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec58677acfea8a15352d42fc87d11d63596ade9239e0a7c9352914417515dbe6" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "io-lifetimes" version = "1.0.11" @@ -4599,29 +3541,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-terminal" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c89a757e762896bdbdfadf2860d0f8b0cea5e363d8cf3e7bdfeb63d1d976352" -dependencies = [ - "hermit-abi 0.2.6", - "io-lifetimes 0.5.3", - "rustix 0.33.7", - "winapi 0.3.9", -] - -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi 0.3.3", - "rustix 0.38.14", - "windows-sys 0.48.0", -] - [[package]] name = "isahc" version = "1.7.2" @@ -4673,15 +3592,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "ittapi-rs" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f712648a1ad72fbfb7adc2772c331e8d90f022f8cf30cbabefba2878dd3172b0" -dependencies = [ - "cc", -] - [[package]] name = "jni" version = "0.19.0" @@ -4734,24 +3644,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "journal2" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "dirs 4.0.0", - "editor2", - "gpui2", - "log", - "schemars", - "serde", - "settings2", - "shellexpand", - "util", - "workspace2", -] - [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -4830,8 +3722,7 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger 0.9.3", - "fs", + "env_logger", "futures 0.3.28", "fuzzy", "git", @@ -4874,60 +3765,6 @@ dependencies = [ "util", ] -[[package]] -name = "language2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-broadcast", - "async-trait", - "client2", - "clock", - "collections", - "ctor", - "env_logger 0.9.3", - "futures 0.3.28", - "fuzzy2", - "git3", - "globset", - "gpui2", - "indoc", - "lazy_static", - "log", - "lsp2", - "parking_lot 0.11.2", - "postage", - "pulldown-cmark", - "rand 0.8.5", - "regex", - "rpc2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings2", - "similar", - "smallvec", - "smol", - "sum_tree", - "text2", - "theme2", - "tree-sitter", - "tree-sitter-elixir", - "tree-sitter-embedded-template", - "tree-sitter-heex", - "tree-sitter-html", - "tree-sitter-json 0.20.0", - "tree-sitter-markdown", - "tree-sitter-python", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-typescript", - "unicase", - "unindent", - "util", -] - [[package]] name = "language_selector" version = "0.1.0" @@ -4941,28 +3778,11 @@ dependencies = [ "project", "settings", "theme", + "ui", "util", "workspace", ] -[[package]] -name = "language_selector2" -version = "0.1.0" -dependencies = [ - "anyhow", - "editor2", - "fuzzy2", - "gpui2", - "language2", - "picker2", - "project2", - "settings2", - "theme2", - "ui2", - "util", - "workspace2", -] - [[package]] name = "language_tools" version = "0.1.0" @@ -4971,16 +3791,18 @@ dependencies = [ "client", "collections", "editor", - "env_logger 0.9.3", + "env_logger", "futures 0.3.28", "gpui", "language", "lsp", "project", "serde", + "serde_json", "settings", "theme", "tree-sitter", + "ui", "unindent", "util", "workspace", @@ -5001,12 +3823,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - [[package]] name = "libc" version = "0.2.148" @@ -5109,12 +3925,6 @@ dependencies = [ "syn 2.0.37", ] -[[package]] -name = "linux-raw-sys" -version = "0.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5284f00d480e1c39af34e72f8ad60b94f47007e3481cd3b731c1d67190ddc7b7" - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -5123,9 +3933,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lipsum" @@ -5170,39 +3980,6 @@ dependencies = [ "simplelog", ] -[[package]] -name = "live_kit_client2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-broadcast", - "async-trait", - "block", - "byteorder", - "bytes 1.5.0", - "cocoa", - "collections", - "core-foundation", - "core-graphics", - "foreign-types", - "futures 0.3.28", - "gpui2", - "hmac 0.12.1", - "jwt", - "live_kit_server", - "log", - "media", - "nanoid", - "objc", - "parking_lot 0.11.2", - "postage", - "serde", - "serde_derive", - "serde_json", - "sha2 0.10.7", - "simplelog", -] - [[package]] name = "live_kit_server" version = "0.1.0" @@ -5250,7 +4027,7 @@ dependencies = [ "async-pipe", "collections", "ctor", - "env_logger 0.9.3", + "env_logger", "futures 0.3.28", "gpui", "log", @@ -5277,38 +4054,6 @@ dependencies = [ "url", ] -[[package]] -name = "lsp2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-pipe", - "collections", - "ctor", - "env_logger 0.9.3", - "futures 0.3.28", - "gpui2", - "log", - "lsp-types", - "parking_lot 0.11.2", - "postage", - "serde", - "serde_derive", - "serde_json", - "smol", - "unindent", - "util", -] - -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - [[package]] name = "mach2" version = "0.4.1" @@ -5358,12 +4103,6 @@ dependencies = [ "rawpointer", ] -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - [[package]] name = "md-5" version = "0.10.5" @@ -5393,15 +4132,6 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" -[[package]] -name = "memfd" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6627dc657574b49d6ad27105ed671822be56e0d2547d413bfbf3e8d8fa92e7a" -dependencies = [ - "libc", -] - [[package]] name = "memmap2" version = "0.2.3" @@ -5434,13 +4164,6 @@ name = "menu" version = "0.1.0" dependencies = [ "gpui", -] - -[[package]] -name = "menu2" -version = "0.1.0" -dependencies = [ - "gpui2", "serde", ] @@ -5595,12 +4318,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "more-asserts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" - [[package]] name = "multi_buffer" version = "0.1.0" @@ -5610,11 +4327,10 @@ dependencies = [ "client", "clock", "collections", - "context_menu", "convert_case 0.6.0", "copilot", "ctor", - "env_logger 0.9.3", + "env_logger", "futures 0.3.28", "git", "gpui", @@ -5647,54 +4363,6 @@ dependencies = [ "tree-sitter-typescript", "unindent", "util", - "workspace", -] - -[[package]] -name = "multi_buffer2" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "client2", - "clock", - "collections", - "convert_case 0.6.0", - "copilot2", - "ctor", - "env_logger 0.9.3", - "futures 0.3.28", - "git3", - "gpui2", - "indoc", - "itertools 0.10.5", - "language2", - "lazy_static", - "log", - "lsp2", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "postage", - "project2", - "pulldown-cmark", - "rand 0.8.5", - "rich_text2", - "schemars", - "serde", - "serde_derive", - "settings2", - "smallvec", - "smol", - "snippet", - "sum_tree", - "text2", - "theme2", - "tree-sitter", - "tree-sitter-html", - "tree-sitter-rust", - "tree-sitter-typescript", - "unindent", - "util", ] [[package]] @@ -5866,26 +4534,6 @@ dependencies = [ "util", ] -[[package]] -name = "notifications2" -version = "0.1.0" -dependencies = [ - "anyhow", - "channel2", - "client2", - "clock", - "collections", - "db2", - "feature_flags2", - "gpui2", - "rpc2", - "settings2", - "sum_tree", - "text2", - "time", - "util", -] - [[package]] name = "ntapi" version = "0.3.7" @@ -6133,18 +4781,6 @@ dependencies = [ "cc", ] -[[package]] -name = "object" -version = "0.28.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" -dependencies = [ - "crc32fast", - "hashbrown 0.11.2", - "indexmap 1.9.3", - "memchr", -] - [[package]] name = "object" version = "0.32.1" @@ -6195,7 +4831,7 @@ version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if 1.0.0", "foreign-types", "libc", @@ -6296,27 +4932,9 @@ dependencies = [ "smol", "text", "theme", - "workspace", -] - -[[package]] -name = "outline2" -version = "0.1.0" -dependencies = [ - "editor2", - "fuzzy2", - "gpui2", - "language2", - "ordered-float 2.10.0", - "picker2", - "postage", - "settings2", - "smol", - "text2", - "theme2", - "ui2", + "ui", "util", - "workspace2", + "workspace", ] [[package]] @@ -6528,35 +5146,18 @@ version = "0.1.0" dependencies = [ "ctor", "editor", - "env_logger 0.9.3", + "env_logger", "gpui", "menu", "parking_lot 0.11.2", "serde_json", "settings", "theme", + "ui", "util", "workspace", ] -[[package]] -name = "picker2" -version = "0.1.0" -dependencies = [ - "ctor", - "editor2", - "env_logger 0.9.3", - "gpui2", - "menu2", - "parking_lot 0.11.2", - "serde_json", - "settings2", - "theme2", - "ui2", - "util", - "workspace2", -] - [[package]] name = "pico-args" version = "0.4.2" @@ -6628,17 +5229,6 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "plane-split" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f7d82649829ecdef8e258790b0587acf0a8403f0ce963473d8e918acc1643" -dependencies = [ - "euclid", - "log", - "smallvec", -] - [[package]] name = "plist" version = "1.5.0" @@ -6675,22 +5265,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "plugin_runtime" -version = "0.1.0" -dependencies = [ - "anyhow", - "bincode", - "pollster", - "serde", - "serde_derive", - "serde_json", - "smol", - "wasi-common", - "wasmtime", - "wasmtime-wasi", -] - [[package]] name = "png" version = "0.16.8" @@ -6769,27 +5343,6 @@ dependencies = [ "util", ] -[[package]] -name = "prettier2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "collections", - "fs2", - "futures 0.3.28", - "gpui2", - "language2", - "log", - "lsp2", - "node_runtime", - "parking_lot 0.11.2", - "serde", - "serde_derive", - "serde_json", - "util", -] - [[package]] name = "pretty_assertions" version = "1.4.0" @@ -6887,7 +5440,7 @@ dependencies = [ "copilot", "ctor", "db", - "env_logger 0.9.3", + "env_logger", "fs", "fsevent", "futures 0.3.28", @@ -6928,61 +5481,6 @@ dependencies = [ "util", ] -[[package]] -name = "project2" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "async-trait", - "backtrace", - "client2", - "clock", - "collections", - "copilot2", - "ctor", - "db2", - "env_logger 0.9.3", - "fs2", - "fsevent", - "futures 0.3.28", - "fuzzy2", - "git2", - "git3", - "globset", - "gpui2", - "ignore", - "itertools 0.10.5", - "language2", - "lazy_static", - "log", - "lsp2", - "node_runtime", - "parking_lot 0.11.2", - "postage", - "prettier2", - "pretty_assertions", - "rand 0.8.5", - "regex", - "rpc2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings2", - "sha2 0.10.7", - "similar", - "smol", - "sum_tree", - "tempdir", - "terminal2", - "text2", - "thiserror", - "toml 0.5.11", - "unindent", - "util", -] - [[package]] name = "project_panel" version = "0.1.0" @@ -6990,9 +5488,7 @@ dependencies = [ "anyhow", "client", "collections", - "context_menu", "db", - "drag_and_drop", "editor", "futures 0.3.28", "gpui", @@ -7002,45 +5498,19 @@ dependencies = [ "pretty_assertions", "project", "schemars", + "search", "serde", "serde_derive", "serde_json", "settings", + "smallvec", "theme", + "ui", "unicase", "util", "workspace", ] -[[package]] -name = "project_panel2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "collections", - "db2", - "editor2", - "futures 0.3.28", - "gpui2", - "language2", - "menu2", - "postage", - "pretty_assertions", - "project2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings2", - "smallvec", - "theme2", - "ui2", - "unicase", - "util", - "workspace2", -] - [[package]] name = "project_symbols" version = "0.1.0" @@ -7056,6 +5526,7 @@ dependencies = [ "picker", "postage", "project", + "serde_json", "settings", "smol", "text", @@ -7064,29 +5535,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "project_symbols2" -version = "0.1.0" -dependencies = [ - "anyhow", - "editor2", - "futures 0.3.28", - "fuzzy2", - "gpui2", - "language2", - "lsp2", - "ordered-float 2.10.0", - "picker2", - "postage", - "project2", - "settings2", - "smol", - "text2", - "theme2", - "util", - "workspace2", -] - [[package]] name = "prometheus" version = "0.13.3" @@ -7194,15 +5642,6 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -[[package]] -name = "psm" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" -dependencies = [ - "cc", -] - [[package]] name = "ptr_meta" version = "0.1.4" @@ -7251,22 +5690,10 @@ dependencies = [ "editor", "gpui", "search", - "theme", + "ui", "workspace", ] -[[package]] -name = "quick_action_bar2" -version = "0.1.0" -dependencies = [ - "assistant2", - "editor2", - "gpui2", - "search2", - "ui2", - "workspace2", -] - [[package]] name = "quote" version = "1.0.33" @@ -7432,7 +5859,6 @@ dependencies = [ name = "recent_projects" version = "0.1.0" dependencies = [ - "db", "editor", "futures 0.3.28", "fuzzy", @@ -7445,32 +5871,11 @@ dependencies = [ "smol", "text", "theme", + "ui", "util", "workspace", ] -[[package]] -name = "recent_projects2" -version = "0.1.0" -dependencies = [ - "db", - "editor2", - "futures 0.3.28", - "fuzzy2", - "gpui2", - "language2", - "ordered-float 2.10.0", - "picker2", - "postage", - "settings2", - "smol", - "text2", - "theme2", - "ui2", - "util", - "workspace2", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -7510,18 +5915,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "regalloc2" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a8d23b35d7177df3b9d31ed8a9ab4bf625c668be77a319d4f5efd4a5257701c" -dependencies = [ - "fxhash", - "log", - "slice-group-by", - "smallvec", -] - [[package]] name = "regex" version = "1.9.5" @@ -7566,18 +5959,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" -[[package]] -name = "region" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877e54ea2adcd70d80e9179344c97f93ef0dffd6b03e1f4529e6e83ab2fa9ae0" -dependencies = [ - "bitflags 1.3.2", - "libc", - "mach", - "winapi 0.3.9", -] - [[package]] name = "remove_dir_all" version = "0.5.3" @@ -7676,24 +6057,6 @@ dependencies = [ "util", ] -[[package]] -name = "rich_text2" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "futures 0.3.28", - "gpui2", - "language2", - "lazy_static", - "pulldown-cmark", - "smallvec", - "smol", - "sum_tree", - "theme2", - "util", -] - [[package]] name = "ring" version = "0.16.20" @@ -7782,20 +6145,6 @@ dependencies = [ "util", ] -[[package]] -name = "rope2" -version = "0.1.0" -dependencies = [ - "arrayvec 0.7.4", - "bromberg_sl2", - "gpui2", - "log", - "rand 0.8.5", - "smallvec", - "sum_tree", - "util", -] - [[package]] name = "roxmltree" version = "0.14.1" @@ -7816,7 +6165,7 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger 0.9.3", + "env_logger", "futures 0.3.28", "gpui", "parking_lot 0.11.2", @@ -7836,37 +6185,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "rpc2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-lock", - "async-tungstenite", - "base64 0.13.1", - "clock", - "collections", - "ctor", - "env_logger 0.9.3", - "futures 0.3.28", - "gpui2", - "parking_lot 0.11.2", - "prost 0.8.0", - "prost-build", - "rand 0.8.5", - "rsa 0.4.0", - "serde", - "serde_derive", - "serde_json", - "smol", - "smol-timeout", - "strum", - "tempdir", - "tracing", - "util", - "zstd", -] - [[package]] name = "rsa" version = "0.4.0" @@ -7915,7 +6233,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -7995,22 +6313,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.33.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938a344304321a9da4973b9ff4f9f8db9caf4597dfd9dda6a60b523340a0fff0" -dependencies = [ - "bitflags 1.3.2", - "errno 0.2.8", - "io-lifetimes 0.5.3", - "itoa", - "libc", - "linux-raw-sys 0.0.42", - "once_cell", - "winapi 0.3.9", -] - [[package]] name = "rustix" version = "0.37.23" @@ -8018,8 +6320,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", - "errno 0.3.3", - "io-lifetimes 1.0.11", + "errno", + "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", @@ -8027,14 +6329,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.14" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.4.0", - "errno 0.3.3", + "bitflags 2.4.1", + "errno", "libc", - "linux-raw-sys 0.4.7", + "linux-raw-sys 0.4.12", "windows-sys 0.48.0", ] @@ -8339,41 +6641,12 @@ dependencies = [ "smallvec", "smol", "theme", + "ui", "unindent", "util", "workspace", ] -[[package]] -name = "search2" -version = "0.1.0" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "client2", - "collections", - "editor2", - "futures 0.3.28", - "gpui2", - "language2", - "log", - "menu2", - "postage", - "project2", - "semantic_index2", - "serde", - "serde_derive", - "serde_json", - "settings2", - "smallvec", - "smol", - "theme2", - "ui2", - "unindent", - "util", - "workspace2", -] - [[package]] name = "security-framework" version = "2.9.2" @@ -8407,8 +6680,7 @@ dependencies = [ "client", "collections", "ctor", - "editor", - "env_logger 0.9.3", + "env_logger", "futures 0.3.28", "globset", "gpui", @@ -8419,7 +6691,6 @@ dependencies = [ "node_runtime", "ordered-float 2.10.0", "parking_lot 0.11.2", - "picker", "postage", "pretty_assertions", "project", @@ -8434,7 +6705,6 @@ dependencies = [ "sha1", "smol", "tempdir", - "theme", "tiktoken-rs", "tree-sitter", "tree-sitter-cpp", @@ -8451,57 +6721,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "semantic_index2" -version = "0.1.0" -dependencies = [ - "ai2", - "anyhow", - "async-trait", - "client2", - "collections", - "ctor", - "env_logger 0.9.3", - "futures 0.3.28", - "globset", - "gpui2", - "language2", - "lazy_static", - "log", - "ndarray", - "node_runtime", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "postage", - "pretty_assertions", - "project2", - "rand 0.8.5", - "rpc2", - "rusqlite", - "rust-embed", - "schemars", - "serde", - "serde_json", - "settings2", - "sha1", - "smol", - "tempdir", - "tiktoken-rs", - "tree-sitter", - "tree-sitter-cpp", - "tree-sitter-elixir", - "tree-sitter-json 0.20.0", - "tree-sitter-lua", - "tree-sitter-php", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-toml", - "tree-sitter-typescript", - "unindent", - "util", - "workspace2", -] - [[package]] name = "semver" version = "1.0.18" @@ -8643,36 +6862,6 @@ dependencies = [ "serde_json", "serde_json_lenient", "smallvec", - "sqlez", - "toml 0.5.11", - "tree-sitter", - "tree-sitter-json 0.19.0", - "unindent", - "util", -] - -[[package]] -name = "settings2" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "feature_flags2", - "fs2", - "futures 0.3.28", - "gpui2", - "indoc", - "lazy_static", - "postage", - "pretty_assertions", - "rust-embed", - "schemars", - "serde", - "serde_derive", - "serde_json", - "serde_json_lenient", - "smallvec", - "sqlez", "toml 0.5.11", "tree-sitter", "tree-sitter-json 0.19.0", @@ -8869,12 +7058,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slice-group-by" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" - [[package]] name = "slotmap" version = "1.0.6" @@ -9134,7 +7317,7 @@ dependencies = [ "atoi", "base64 0.21.4", "bigdecimal", - "bitflags 2.4.0", + "bitflags 2.4.1", "byteorder", "bytes 1.5.0", "chrono", @@ -9181,7 +7364,7 @@ dependencies = [ "atoi", "base64 0.21.4", "bigdecimal", - "bitflags 2.4.0", + "bitflags 2.4.1", "byteorder", "chrono", "crc", @@ -9242,12 +7425,6 @@ dependencies = [ "uuid 1.4.1", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -9258,36 +7435,39 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" name = "story" version = "0.1.0" dependencies = [ - "gpui2", + "gpui", + "itertools 0.10.5", + "smallvec", ] [[package]] -name = "storybook2" +name = "storybook" version = "0.1.0" dependencies = [ "anyhow", "backtrace-on-stack-overflow", "chrono", "clap 4.4.4", + "collab_ui", "dialoguer", - "editor2", - "fuzzy2", - "gpui2", + "editor", + "fuzzy", + "gpui", + "indoc", "itertools 0.11.0", - "language2", + "language", "log", - "menu2", - "picker2", + "menu", + "picker", "rust-embed", "serde", - "settings2", + "settings", "simplelog", "smallvec", "story", "strum", "theme", - "theme2", - "ui2", + "ui", "util", ] @@ -9342,7 +7522,7 @@ version = "0.1.0" dependencies = [ "arrayvec 0.7.4", "ctor", - "env_logger 0.9.3", + "env_logger", "log", "rand 0.8.5", ] @@ -9494,40 +7674,13 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "system-interface" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e09bb3fb4e02ec4b87e182ea9718fadbc0fa3e50085b40a9af9690572b67f9e" -dependencies = [ - "atty", - "bitflags 1.3.2", - "cap-fs-ext", - "cap-std", - "io-lifetimes 0.5.3", - "rustix 0.33.7", - "winapi 0.3.9", - "winx", -] - [[package]] name = "taffy" version = "0.3.11" source = "git+https://github.com/DioxusLabs/taffy?rev=1876f72bee5e376023eaa518aa7b8a34c769bd1b#1876f72bee5e376023eaa518aa7b8a34c769bd1b" dependencies = [ "arrayvec 0.7.4", - "grid 0.11.0", - "num-traits", - "slotmap", -] - -[[package]] -name = "taffy" -version = "0.3.11" -source = "git+https://github.com/DioxusLabs/taffy?rev=4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e#4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" -dependencies = [ - "arrayvec 0.7.4", - "grid 0.10.0", + "grid", "num-traits", "slotmap", ] @@ -9544,12 +7697,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" - [[package]] name = "tempdir" version = "0.3.7" @@ -9569,7 +7716,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.14", + "rustix 0.38.21", "windows-sys 0.48.0", ] @@ -9611,42 +7758,12 @@ dependencies = [ "util", ] -[[package]] -name = "terminal2" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "db2", - "dirs 4.0.0", - "futures 0.3.28", - "gpui2", - "itertools 0.10.5", - "lazy_static", - "libc", - "mio-extras", - "ordered-float 2.10.0", - "procinfo", - "rand 0.8.5", - "schemars", - "serde", - "serde_derive", - "settings2", - "shellexpand", - "smallvec", - "smol", - "theme2", - "thiserror", - "util", -] - [[package]] name = "terminal_view" version = "0.1.0" dependencies = [ "anyhow", "client", - "context_menu", "db", "dirs 4.0.0", "editor", @@ -9664,6 +7781,7 @@ dependencies = [ "search", "serde", "serde_derive", + "serde_json", "settings", "shellexpand", "smallvec", @@ -9671,44 +7789,11 @@ dependencies = [ "terminal", "theme", "thiserror", + "ui", "util", "workspace", ] -[[package]] -name = "terminal_view2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "db2", - "dirs 4.0.0", - "editor2", - "futures 0.3.28", - "gpui2", - "itertools 0.10.5", - "language2", - "lazy_static", - "libc", - "mio-extras", - "ordered-float 2.10.0", - "procinfo", - "project2", - "rand 0.8.5", - "serde", - "serde_derive", - "settings2", - "shellexpand", - "smallvec", - "smol", - "terminal2", - "theme2", - "thiserror", - "ui2", - "util", - "workspace2", -] - [[package]] name = "text" version = "0.1.0" @@ -9718,7 +7803,7 @@ dependencies = [ "collections", "ctor", "digest 0.9.0", - "env_logger 0.9.3", + "env_logger", "gpui", "lazy_static", "log", @@ -9732,29 +7817,6 @@ dependencies = [ "util", ] -[[package]] -name = "text2" -version = "0.1.0" -dependencies = [ - "anyhow", - "clock", - "collections", - "ctor", - "digest 0.9.0", - "env_logger 0.9.3", - "gpui2", - "lazy_static", - "log", - "parking_lot 0.11.2", - "postage", - "rand 0.8.5", - "regex", - "rope2", - "smallvec", - "sum_tree", - "util", -] - [[package]] name = "textwrap" version = "0.16.0" @@ -9769,24 +7831,6 @@ dependencies = [ "fs", "gpui", "indexmap 1.9.3", - "parking_lot 0.11.2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings", - "toml 0.5.11", - "util", -] - -[[package]] -name = "theme2" -version = "0.1.0" -dependencies = [ - "anyhow", - "fs2", - "gpui2", - "indexmap 1.9.3", "itertools 0.11.0", "parking_lot 0.11.2", "refineable", @@ -9794,7 +7838,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "settings2", + "settings", "story", "toml 0.5.11", "util", @@ -9805,18 +7849,23 @@ dependencies = [ name = "theme_importer" version = "0.1.0" dependencies = [ + "any_ascii", "anyhow", + "clap 4.4.4", "convert_case 0.6.0", - "gpui2", + "gpui", "indexmap 1.9.3", + "indoc", "json_comments", "log", "palette", + "pathfinder_color", "rust-embed", "serde", + "serde_json", "simplelog", "strum", - "theme2", + "theme", "uuid 1.4.1", ] @@ -9824,6 +7873,7 @@ dependencies = [ name = "theme_selector" version = "0.1.0" dependencies = [ + "client", "editor", "feature_flags", "fs", @@ -9836,31 +7886,11 @@ dependencies = [ "settings", "smol", "theme", + "ui", "util", "workspace", ] -[[package]] -name = "theme_selector2" -version = "0.1.0" -dependencies = [ - "editor2", - "feature_flags2", - "fs2", - "fuzzy2", - "gpui2", - "log", - "parking_lot 0.11.2", - "picker2", - "postage", - "settings2", - "smol", - "theme2", - "ui2", - "util", - "workspace2", -] - [[package]] name = "thiserror" version = "1.0.48" @@ -10320,7 +8350,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=b5f461a69bf3df7298b1903574d506179e6390b0#b5f461a69bf3df7298b1903574d506179e6390b0" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=31c40449749c4263a91a43593831b82229049a4c#31c40449749c4263a91a43593831b82229049a4c" dependencies = [ "cc", "regex", @@ -10478,7 +8508,7 @@ dependencies = [ [[package]] name = "tree-sitter-nu" version = "0.0.1" -source = "git+https://github.com/nushell/tree-sitter-nu?rev=786689b0562b9799ce53e824cb45a1a2a04dc673#786689b0562b9799ce53e824cb45a1a2a04dc673" +source = "git+https://github.com/nushell/tree-sitter-nu?rev=26bbaecda0039df4067861ab38ea8ea169f7f5aa#26bbaecda0039df4067861ab38ea8ea169f7f5aa" dependencies = [ "cc", "tree-sitter", @@ -10658,21 +8688,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "ui2" +name = "ui" version = "0.1.0" dependencies = [ "anyhow", "chrono", - "gpui2", + "gpui", "itertools 0.11.0", - "menu2", + "menu", "rand 0.8.5", "serde", - "settings2", + "settings", "smallvec", "story", "strum", - "theme2", + "theme", ] [[package]] @@ -10918,25 +8948,11 @@ dependencies = [ "fuzzy", "gpui", "picker", - "theme", + "ui", "util", "workspace", ] -[[package]] -name = "vcs_menu2" -version = "0.1.0" -dependencies = [ - "anyhow", - "fs2", - "fuzzy2", - "gpui2", - "picker2", - "ui2", - "util", - "workspace2", -] - [[package]] name = "version_check" version = "0.9.4" @@ -10959,7 +8975,6 @@ dependencies = [ "indoc", "itertools 0.10.5", "language", - "language_selector", "log", "lsp", "nvim-rs", @@ -10972,43 +8987,10 @@ dependencies = [ "settings", "theme", "tokio", + "ui", "util", "workspace", - "zed-actions", -] - -[[package]] -name = "vim2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compat", - "async-trait", - "collections", - "command_palette2", - "diagnostics2", - "editor2", - "futures 0.3.28", - "gpui2", - "indoc", - "itertools 0.10.5", - "language2", - "log", - "lsp2", - "nvim-rs", - "parking_lot 0.11.2", - "project2", - "search2", - "serde", - "serde_derive", - "serde_json", - "settings2", - "theme2", - "tokio", - "ui2", - "util", - "workspace2", - "zed_actions2", + "zed_actions", ] [[package]] @@ -11070,48 +9052,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasi-cap-std-sync" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f086c5026d2fc3b268d138e65373f46422cc810f46d6e0776859c5027cb18728" -dependencies = [ - "anyhow", - "async-trait", - "cap-fs-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "io-extras", - "io-lifetimes 0.5.3", - "is-terminal 0.1.0", - "lazy_static", - "rustix 0.33.7", - "system-interface", - "tracing", - "wasi-common", - "winapi 0.3.9", -] - -[[package]] -name = "wasi-common" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8844fede1c3787cc08853872f47e8bd91f6c939c7406bc7a5dba496b260c08" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "cap-rand", - "cap-std", - "io-extras", - "rustix 0.33.7", - "thiserror", - "tracing", - "wiggle", - "winapi 0.3.9", -] - [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -11178,251 +9118,6 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" -[[package]] -name = "wasm-encoder" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39de0723a53d3c8f54bed106cfbc0d06b3e4d945c5c5022115a61e3b29183ae" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasmparser" -version = "0.85.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "570460c58b21e9150d2df0eaaedbb7816c34bcec009ae0dcc976e40ba81463e7" -dependencies = [ - "indexmap 1.9.3", -] - -[[package]] -name = "wasmtime" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f50eadf868ab6a04b7b511460233377d0bfbb92e417b2f6a98b98fef2e098f5" -dependencies = [ - "anyhow", - "async-trait", - "backtrace", - "bincode", - "cfg-if 1.0.0", - "indexmap 1.9.3", - "lazy_static", - "libc", - "log", - "object 0.28.4", - "once_cell", - "paste", - "psm", - "rayon", - "region", - "serde", - "target-lexicon", - "wasmparser", - "wasmtime-cache", - "wasmtime-cranelift", - "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit", - "wasmtime-runtime", - "wat", - "winapi 0.3.9", -] - -[[package]] -name = "wasmtime-cache" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1df23c642e1376892f3b72f311596976979cbf8b85469680cdd3a8a063d12a2" -dependencies = [ - "anyhow", - "base64 0.13.1", - "bincode", - "directories-next", - "file-per-thread-logger", - "log", - "rustix 0.33.7", - "serde", - "sha2 0.9.9", - "toml 0.5.11", - "winapi 0.3.9", - "zstd", -] - -[[package]] -name = "wasmtime-cranelift" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f264ff6b4df247d15584f2f53d009fbc90032cfdc2605b52b961bffc71b6eccd" -dependencies = [ - "anyhow", - "cranelift-codegen", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "cranelift-wasm", - "gimli 0.26.2", - "log", - "more-asserts", - "object 0.28.4", - "target-lexicon", - "thiserror", - "wasmparser", - "wasmtime-environ", -] - -[[package]] -name = "wasmtime-environ" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839d2820e4b830f4b9e7aa08d4c0acabf4a5036105d639f6dfa1c6891c73bdc6" -dependencies = [ - "anyhow", - "cranelift-entity", - "gimli 0.26.2", - "indexmap 1.9.3", - "log", - "more-asserts", - "object 0.28.4", - "serde", - "target-lexicon", - "thiserror", - "wasmparser", - "wasmtime-types", -] - -[[package]] -name = "wasmtime-fiber" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3248be3c4911233535356025f6562193614a40155ee9094bb6a2b43f0dc82803" -dependencies = [ - "cc", - "rustix 0.33.7", - "winapi 0.3.9", -] - -[[package]] -name = "wasmtime-jit" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef0a0bcbfa18b946d890078ba0e1bc76bcc53eccfb40806c0020ec29dcd1bd49" -dependencies = [ - "addr2line 0.17.0", - "anyhow", - "bincode", - "cfg-if 1.0.0", - "cpp_demangle", - "gimli 0.26.2", - "ittapi-rs", - "log", - "object 0.28.4", - "region", - "rustc-demangle", - "rustix 0.33.7", - "serde", - "target-lexicon", - "thiserror", - "wasmtime-environ", - "wasmtime-jit-debug", - "wasmtime-runtime", - "winapi 0.3.9", -] - -[[package]] -name = "wasmtime-jit-debug" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4779d976206c458edd643d1ac622b6c37e4a0800a8b1d25dfbf245ac2f2cac" -dependencies = [ - "lazy_static", - "object 0.28.4", - "rustix 0.33.7", -] - -[[package]] -name = "wasmtime-runtime" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7eb6ffa169eb5dcd18ac9473c817358cd57bc62c244622210566d473397954a" -dependencies = [ - "anyhow", - "backtrace", - "cc", - "cfg-if 1.0.0", - "indexmap 1.9.3", - "libc", - "log", - "mach", - "memfd", - "memoffset 0.6.5", - "more-asserts", - "rand 0.8.5", - "region", - "rustix 0.33.7", - "thiserror", - "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-debug", - "winapi 0.3.9", -] - -[[package]] -name = "wasmtime-types" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d932b0ac5336f7308d869703dd225610a6a3aeaa8e968c52b43eed96cefb1c2" -dependencies = [ - "cranelift-entity", - "serde", - "thiserror", - "wasmparser", -] - -[[package]] -name = "wasmtime-wasi" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b68b7d77fb6f2975a6fe6cc4d0015d6b0cebb65c39fce1dd4cc00880dbf7789c" -dependencies = [ - "anyhow", - "wasi-cap-std-sync", - "wasi-common", - "wasmtime", - "wiggle", -] - -[[package]] -name = "wast" -version = "35.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" -dependencies = [ - "leb128", -] - -[[package]] -name = "wast" -version = "65.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd8c1cbadf94a0b0d1071c581d3cfea1b7ed5192c79808dd15406e508dd0afb" -dependencies = [ - "leb128", - "memchr", - "unicode-width", - "wasm-encoder", -] - -[[package]] -name = "wat" -version = "1.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3209e35eeaf483714f4c6be93f4a03e69aad5f304e3fa66afa7cb90fe1c8051f" -dependencies = [ - "wast 65.0.1", -] - [[package]] name = "web-sys" version = "0.3.64" @@ -11487,37 +9182,12 @@ dependencies = [ "settings", "theme", "theme_selector", + "ui", "util", "vim", "workspace", ] -[[package]] -name = "welcome2" -version = "0.1.0" -dependencies = [ - "anyhow", - "client2", - "db2", - "editor2", - "fs2", - "fuzzy2", - "gpui2", - "install_cli2", - "log", - "picker2", - "project2", - "schemars", - "serde", - "settings2", - "theme2", - "theme_selector2", - "ui2", - "util", - "vim2", - "workspace2", -] - [[package]] name = "which" version = "4.4.2" @@ -11527,7 +9197,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.14", + "rustix 0.38.21", ] [[package]] @@ -11536,48 +9206,6 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -[[package]] -name = "wiggle" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dadac11343d2aabc8a906a0db0aaf7cb5046ec3d6fffccdaf2847dccdef8d6" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 1.3.2", - "thiserror", - "tracing", - "wasmtime", - "wiggle-macro", -] - -[[package]] -name = "wiggle-generate" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a1dccd6b3fbd9a27417f5d30ce9aa3ee9cf529aad453abbf88a49c5d605b79" -dependencies = [ - "anyhow", - "heck 0.4.1", - "proc-macro2", - "quote", - "shellexpand", - "syn 1.0.109", - "witx", -] - -[[package]] -name = "wiggle-macro" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c368d57d9560c34deaa67e06b0953ccf65edb906c525e5a2c866c849b48ec2" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "wiggle-generate", -] - [[package]] name = "winapi" version = "0.2.8" @@ -11790,17 +9418,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winx" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d5973cb8cd94a77d03ad7e23bbe14889cb29805da1cec0e4aff75e21aebded" -dependencies = [ - "bitflags 1.3.2", - "io-lifetimes 0.5.3", - "winapi 0.3.9", -] - [[package]] name = "wio" version = "0.2.2" @@ -11810,18 +9427,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "witx" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" -dependencies = [ - "anyhow", - "log", - "thiserror", - "wast 35.0.2", -] - [[package]] name = "workspace" version = "0.1.0" @@ -11832,10 +9437,8 @@ dependencies = [ "call", "client", "collections", - "context_menu", "db", - "drag_and_drop", - "env_logger 0.9.3", + "env_logger", "fs", "futures 0.3.28", "gpui", @@ -11845,7 +9448,6 @@ dependencies = [ "language", "lazy_static", "log", - "menu", "node_runtime", "parking_lot 0.11.2", "postage", @@ -11856,46 +9458,10 @@ dependencies = [ "serde_json", "settings", "smallvec", + "sqlez", "terminal", "theme", - "util", - "uuid 1.4.1", -] - -[[package]] -name = "workspace2" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-recursion 1.0.5", - "bincode", - "call2", - "client2", - "collections", - "db2", - "env_logger 0.9.3", - "fs2", - "futures 0.3.28", - "gpui2", - "indoc", - "install_cli2", - "itertools 0.10.5", - "language2", - "lazy_static", - "log", - "node_runtime", - "parking_lot 0.11.2", - "postage", - "project2", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings2", - "smallvec", - "terminal2", - "theme2", - "ui2", + "ui", "util", "uuid 1.4.1", ] @@ -11940,17 +9506,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xtask" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap 4.4.4", - "schemars", - "serde_json", - "theme", -] - [[package]] name = "yaml-rust" version = "0.4.5" @@ -11980,11 +9535,12 @@ dependencies = [ [[package]] name = "zed" -version = "0.118.0" +version = "0.120.0" dependencies = [ "activity_indicator", "ai", "anyhow", + "assets", "assistant", "async-compression", "async-recursion 0.3.2", @@ -11999,26 +9555,22 @@ dependencies = [ "chrono", "cli", "client", - "clock", "collab_ui", "collections", "command_palette", - "component_test", - "context_menu", "copilot", - "copilot_button", + "copilot_ui", "ctor", "db", "diagnostics", "editor", - "env_logger 0.9.3", + "env_logger", "feature_flags", "feedback", "file_finder", "fs", "fsevent", "futures 0.3.28", - "fuzzy", "go_to_line", "gpui", "ignore", @@ -12034,12 +9586,12 @@ dependencies = [ "libc", "log", "lsp", + "menu", "node_runtime", "notifications", "num_cpus", "outline", "parking_lot 0.11.2", - "plugin_runtime", "postage", "project", "project_panel", @@ -12048,6 +9600,7 @@ dependencies = [ "rand 0.8.5", "recent_projects", "regex", + "rope", "rpc", "rsa 0.4.0", "rust-embed", @@ -12108,154 +9661,17 @@ dependencies = [ "vim", "welcome", "workspace", - "zed-actions", + "zed_actions", ] [[package]] -name = "zed-actions" +name = "zed_actions" version = "0.1.0" dependencies = [ "gpui", "serde", ] -[[package]] -name = "zed2" -version = "2.0.0" -dependencies = [ - "activity_indicator2", - "ai2", - "anyhow", - "assistant2", - "async-compression", - "async-recursion 0.3.2", - "async-tar", - "async-trait", - "audio2", - "auto_update2", - "backtrace", - "breadcrumbs2", - "call2", - "channel2", - "chrono", - "cli", - "client2", - "collab_ui2", - "collections", - "command_palette2", - "copilot2", - "copilot_button2", - "ctor", - "db2", - "diagnostics2", - "editor2", - "env_logger 0.9.3", - "feature_flags2", - "feedback2", - "file_finder2", - "fs2", - "fsevent", - "futures 0.3.28", - "go_to_line2", - "gpui2", - "ignore", - "image", - "indexmap 1.9.3", - "install_cli2", - "isahc", - "journal2", - "language2", - "language_selector2", - "lazy_static", - "libc", - "log", - "lsp2", - "menu2", - "node_runtime", - "notifications2", - "num_cpus", - "outline2", - "parking_lot 0.11.2", - "postage", - "project2", - "project_panel2", - "project_symbols2", - "quick_action_bar2", - "rand 0.8.5", - "recent_projects2", - "regex", - "rope2", - "rpc2", - "rsa 0.4.0", - "rust-embed", - "schemars", - "search2", - "semantic_index2", - "serde", - "serde_derive", - "serde_json", - "settings2", - "shellexpand", - "simplelog", - "smallvec", - "smol", - "sum_tree", - "tempdir", - "terminal_view2", - "text2", - "theme2", - "theme_selector2", - "thiserror", - "tiny_http", - "toml 0.5.11", - "tree-sitter", - "tree-sitter-bash", - "tree-sitter-c", - "tree-sitter-cpp", - "tree-sitter-css", - "tree-sitter-elixir", - "tree-sitter-elm", - "tree-sitter-embedded-template", - "tree-sitter-glsl", - "tree-sitter-go", - "tree-sitter-heex", - "tree-sitter-html", - "tree-sitter-json 0.20.0", - "tree-sitter-lua", - "tree-sitter-markdown", - "tree-sitter-nix", - "tree-sitter-nu", - "tree-sitter-php", - "tree-sitter-python", - "tree-sitter-racket", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-scheme", - "tree-sitter-svelte", - "tree-sitter-toml", - "tree-sitter-typescript", - "tree-sitter-uiua", - "tree-sitter-vue", - "tree-sitter-yaml", - "unindent", - "url", - "urlencoding", - "util", - "uuid 1.4.1", - "vim2", - "welcome2", - "workspace2", - "zed_actions2", -] - -[[package]] -name = "zed_actions2" -version = "0.1.0" -dependencies = [ - "gpui2", - "serde", -] - [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 3b453527b8..79d28821d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,137 +1,90 @@ [workspace] members = [ + "crates/assets", "crates/activity_indicator", - "crates/activity_indicator2", "crates/ai", "crates/assistant", - "crates/assistant2", "crates/audio", - "crates/audio2", "crates/auto_update", - "crates/auto_update2", "crates/breadcrumbs", - "crates/breadcrumbs2", "crates/call", - "crates/call2", "crates/channel", - "crates/channel2", "crates/cli", "crates/client", - "crates/client2", "crates/clock", "crates/collab", - "crates/collab2", "crates/collab_ui", - "crates/collab_ui2", "crates/collections", "crates/command_palette", - "crates/command_palette2", - "crates/component_test", - "crates/context_menu", "crates/copilot", - "crates/copilot2", - "crates/copilot_button", + "crates/copilot_ui", "crates/db", - "crates/db2", "crates/refineable", "crates/refineable/derive_refineable", "crates/diagnostics", - "crates/diagnostics2", - "crates/drag_and_drop", "crates/editor", "crates/feature_flags", - "crates/feature_flags2", "crates/feedback", "crates/file_finder", "crates/fs", - "crates/fs2", "crates/fsevent", "crates/fuzzy", - "crates/fuzzy2", "crates/git", "crates/go_to_line", - "crates/go_to_line2", "crates/gpui", "crates/gpui_macros", - "crates/gpui2", - "crates/gpui2_macros", + "crates/gpui", + "crates/gpui_macros", "crates/install_cli", - "crates/install_cli2", "crates/journal", - "crates/journal2", + "crates/journal", "crates/language", - "crates/language2", "crates/language_selector", - "crates/language_selector2", "crates/language_tools", "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", - "crates/lsp2", "crates/media", "crates/menu", - "crates/menu2", "crates/multi_buffer", - "crates/multi_buffer2", "crates/node_runtime", "crates/notifications", - "crates/notifications2", "crates/outline", - "crates/outline2", "crates/picker", - "crates/picker2", "crates/plugin", "crates/plugin_macros", - "crates/plugin_runtime", "crates/prettier", - "crates/prettier2", "crates/project", - "crates/project2", "crates/project_panel", - "crates/project_panel2", "crates/project_symbols", - "crates/project_symbols2", - "crates/quick_action_bar2", + "crates/quick_action_bar", "crates/recent_projects", - "crates/recent_projects2", "crates/rope", "crates/rpc", - "crates/rpc2", "crates/search", - "crates/search2", "crates/semantic_index", - "crates/semantic_index2", "crates/settings", - "crates/settings2", "crates/snippet", "crates/sqlez", "crates/sqlez_macros", "crates/rich_text", - "crates/storybook2", + "crates/storybook", "crates/sum_tree", "crates/terminal", - "crates/terminal2", - "crates/terminal_view2", + "crates/terminal_view", "crates/text", "crates/theme", - "crates/theme2", "crates/theme_importer", "crates/theme_selector", - "crates/theme_selector2", - "crates/ui2", + "crates/ui", "crates/util", "crates/story", "crates/vim", "crates/vcs_menu", - "crates/vcs_menu2", - "crates/workspace2", + "crates/workspace", "crates/welcome", - "crates/welcome2", - "crates/xtask", "crates/zed", - "crates/zed2", - "crates/zed-actions", - "crates/zed_actions2" + "crates/zed_actions", ] default-members = ["crates/zed"] resolver = "2" @@ -140,10 +93,7 @@ resolver = "2" anyhow = { version = "1.0.57" } async-trait = { version = "0.1" } async-compression = { version = "0.4", features = ["gzip", "futures-io"] } -# TODO: Switch back to the published version of `ctor` once: -# 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295 -# 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20). -ctor = { git = "https://github.com/zed-industries/rust-ctor", rev = "7f824cf6a7943885a649b579f33f9ac53f0d1db6" } +ctor = "0.2.6" derive_more = { version = "0.99.17" } env_logger = { version = "0.9" } futures = { version = "0.3" } @@ -175,7 +125,7 @@ thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } toml = { version = "0.5" } tiktoken-rs = "0.5.7" -tree-sitter = "0.20" +tree-sitter = { version = "0.20" } unindent = { version = "0.1.7" } pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} @@ -206,13 +156,13 @@ tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-rack tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930"} tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } -tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} +tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"} tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "b5f461a69bf3df7298b1903574d506179e6390b0" } -async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "31c40449749c4263a91a43593831b82229049a4c" } +# wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" } diff --git a/Dockerfile b/Dockerfile index f3d0b601b9..ed6e4fac8e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.73-bullseye as builder +FROM rust:1.75-bullseye as builder WORKDIR app COPY . . diff --git a/Procfile.zed2 b/Procfile.zed2 deleted file mode 100644 index 51a509209b..0000000000 --- a/Procfile.zed2 +++ /dev/null @@ -1,4 +0,0 @@ -web: cd ../zed.dev && PORT=3000 npm run dev -collab: cd crates/collab2 && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve -livekit: livekit-server --dev -postgrest: postgrest crates/collab2/admin_api.conf diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg new file mode 100644 index 0000000000..90e352bdea --- /dev/null +++ b/assets/icons/arrow_circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/at-sign.svg b/assets/icons/at_sign.svg similarity index 85% rename from assets/icons/at-sign.svg rename to assets/icons/at_sign.svg index 5adac38f62..4cf8cd468f 100644 --- a/assets/icons/at-sign.svg +++ b/assets/icons/at_sign.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/backspace.svg b/assets/icons/backspace.svg new file mode 100644 index 0000000000..f7f1cf107a --- /dev/null +++ b/assets/icons/backspace.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/bell-off.svg b/assets/icons/bell_off.svg similarity index 100% rename from assets/icons/bell-off.svg rename to assets/icons/bell_off.svg diff --git a/assets/icons/bell-ring.svg b/assets/icons/bell_ring.svg similarity index 100% rename from assets/icons/bell-ring.svg rename to assets/icons/bell_ring.svg diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg new file mode 100644 index 0000000000..a7edbb6158 --- /dev/null +++ b/assets/icons/delete.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/escape.svg b/assets/icons/escape.svg new file mode 100644 index 0000000000..00c772a2ad --- /dev/null +++ b/assets/icons/escape.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/github.svg b/assets/icons/github.svg new file mode 100644 index 0000000000..28148b9894 --- /dev/null +++ b/assets/icons/github.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/magic-wand.svg b/assets/icons/magic_wand.svg similarity index 100% rename from assets/icons/magic-wand.svg rename to assets/icons/magic_wand.svg diff --git a/assets/icons/mail-open.svg b/assets/icons/mail_open.svg similarity index 100% rename from assets/icons/mail-open.svg rename to assets/icons/mail_open.svg diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index f37f6a2087..b3504b5701 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg index 060caeecbf..6598697ff8 100644 --- a/assets/icons/menu.svg +++ b/assets/icons/menu.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/mic-mute.svg b/assets/icons/mic_mute.svg similarity index 100% rename from assets/icons/mic-mute.svg rename to assets/icons/mic_mute.svg diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index ec78f152e1..0451233cc9 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/page_down.svg b/assets/icons/page_down.svg new file mode 100644 index 0000000000..765f36b26a --- /dev/null +++ b/assets/icons/page_down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/page_up.svg b/assets/icons/page_up.svg new file mode 100644 index 0000000000..f555165d2d --- /dev/null +++ b/assets/icons/page_up.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/quote.svg b/assets/icons/quote.svg index 50205479c3..b970db1430 100644 --- a/assets/icons/quote.svg +++ b/assets/icons/quote.svg @@ -1,8 +1 @@ - - - + diff --git a/assets/icons/return.svg b/assets/icons/return.svg index 683519c306..16cfeeda2e 100644 --- a/assets/icons/return.svg +++ b/assets/icons/return.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/select-all.svg b/assets/icons/select_all.svg similarity index 100% rename from assets/icons/select-all.svg rename to assets/icons/select_all.svg diff --git a/assets/icons/snip.svg b/assets/icons/snip.svg new file mode 100644 index 0000000000..03ae4ce039 --- /dev/null +++ b/assets/icons/snip.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/space.svg b/assets/icons/space.svg new file mode 100644 index 0000000000..63718fb4aa --- /dev/null +++ b/assets/icons/space.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/speaker-loud.svg b/assets/icons/speaker_loud.svg similarity index 100% rename from assets/icons/speaker-loud.svg rename to assets/icons/speaker_loud.svg diff --git a/assets/icons/speaker-off.svg b/assets/icons/speaker_off.svg similarity index 100% rename from assets/icons/speaker-off.svg rename to assets/icons/speaker_off.svg diff --git a/assets/icons/split_message.svg b/assets/icons/split_message.svg deleted file mode 100644 index 54d9e81224..0000000000 --- a/assets/icons/split_message.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/tab.svg b/assets/icons/tab.svg new file mode 100644 index 0000000000..49a3536bed --- /dev/null +++ b/assets/icons/tab.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index b8d3711132..3ff0db1a16 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -402,7 +402,7 @@ "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", "alt-cmd-y": "workspace::CloseAllDocks", - "cmd-shift-f": "workspace::NewSearch", + "cmd-shift-f": "workspace::DeploySearch", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", "cmd-t": "project_symbols::Toggle", @@ -502,7 +502,7 @@ } }, { - "context": "Editor", + "context": "Editor && mode == full", "bindings": { "alt-enter": "editor::OpenExcerpts", "cmd-f8": "editor::GoToHunk", diff --git a/assets/settings/default.json b/assets/settings/default.json index 8217f1675a..bd157c3e61 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -76,7 +76,7 @@ // or waits for a `copilot::Toggle` "show_copilot_suggestions": true, // Whether to show tabs and spaces in the editor. - // This setting can take two values: + // This setting can take three values: // // 1. Draw tabs and spaces only for the selected text (default): // "selection" @@ -183,7 +183,7 @@ // Default height when the assistant is docked to the bottom. "default_height": 320, // The default OpenAI model to use when starting new conversations. This - // setting can take two values: + // setting can take three values: // // 1. "gpt-3.5-turbo-0613"" // 2. "gpt-4-0613"" @@ -351,7 +351,7 @@ // } "working_directory": "current_project_directory", // Set the cursor blinking behavior in the terminal. - // May take 4 values: + // May take 3 values: // 1. Never blink the cursor, ignoring the terminal mode // "blinking": "off", // 2. Default the cursor blink to off, but allow the terminal to diff --git a/assets/themes/src/vscode/noctis/family.json b/assets/themes/src/vscode/noctis/family.json index 525cb160f5..623be89c90 100644 --- a/assets/themes/src/vscode/noctis/family.json +++ b/assets/themes/src/vscode/noctis/family.json @@ -13,7 +13,7 @@ "appearance": "dark" }, { - "name": "Noctus Hibernus", + "name": "Noctis Hibernus", "file_name": "hibernus.json", "appearance": "light" }, diff --git a/assets/themes/src/vscode/rose-pine/family.json b/assets/themes/src/vscode/rose-pine/family.json index 3f7b149db8..517b20c652 100644 --- a/assets/themes/src/vscode/rose-pine/family.json +++ b/assets/themes/src/vscode/rose-pine/family.json @@ -1,19 +1,19 @@ { - "name": "Rose Pine", + "name": "Rosé Pine", "author": "Rosé Pine", "themes": [ { - "name": "Rose Pine", + "name": "Rosé Pine", "file_name": "rose-pine.json", "appearance": "dark" }, { - "name": "Rose Pine Moon", + "name": "Rosé Pine Moon", "file_name": "rose-pine-moon.json", "appearance": "dark" }, { - "name": "Rose Pine Dawn", + "name": "Rosé Pine Dawn", "file_name": "rose-pine-dawn.json", "appearance": "light" } diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 43d16e6b9b..55672d0956 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -15,10 +15,12 @@ language = { path = "../language" } gpui = { path = "../gpui" } project = { path = "../project" } settings = { path = "../settings" } +ui = { path = "../ui" } util = { path = "../util" } theme = { path = "../theme" } -workspace = { path = "../workspace" } +workspace = { path = "../workspace", package = "workspace" } +anyhow.workspace = true futures.workspace = true smallvec.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index f9b34add9a..4b990fa430 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -2,19 +2,19 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; use editor::Editor; use futures::StreamExt; use gpui::{ - actions, anyhow, - elements::*, - platform::{CursorStyle, MouseButton}, - AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle, + actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model, + ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext as _, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use project::{LanguageServerProgress, Project}; use smallvec::SmallVec; use std::{cmp::Reverse, fmt::Write, sync::Arc}; +use ui::prelude::*; use util::ResultExt; use workspace::{item::ItemHandle, StatusItemView, Workspace}; -actions!(lsp_status, [ShowErrorMessage]); +actions!(activity_indicator, [ShowErrorMessage]); const DOWNLOAD_ICON: &str = "icons/download.svg"; const WARNING_ICON: &str = "icons/warning.svg"; @@ -25,8 +25,8 @@ pub enum Event { pub struct ActivityIndicator { statuses: Vec, - project: ModelHandle, - auto_updater: Option>, + project: Model, + auto_updater: Option>, } struct LspStatus { @@ -47,20 +47,15 @@ struct Content { on_click: Option)>>, } -pub fn init(cx: &mut AppContext) { - cx.add_action(ActivityIndicator::show_error_message); - cx.add_action(ActivityIndicator::dismiss_error_message); -} - impl ActivityIndicator { pub fn new( workspace: &mut Workspace, languages: Arc, cx: &mut ViewContext, - ) -> ViewHandle { + ) -> View { let project = workspace.project().clone(); let auto_updater = AutoUpdater::get(cx); - let this = cx.add_view(|cx: &mut ViewContext| { + let this = cx.new_view(|cx: &mut ViewContext| { let mut status_events = languages.language_server_binary_statuses(); cx.spawn(|this, mut cx| async move { while let Some((language, event)) = status_events.next().await { @@ -77,11 +72,13 @@ impl ActivityIndicator { }) .detach(); cx.observe(&project, |_, _, cx| cx.notify()).detach(); + if let Some(auto_updater) = auto_updater.as_ref() { cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); } - cx.observe_active_labeled_tasks(|_, cx| cx.notify()) - .detach(); + + // cx.observe_active_labeled_tasks(|_, cx| cx.notify()) + // .detach(); Self { statuses: Default::default(), @@ -89,6 +86,7 @@ impl ActivityIndicator { auto_updater, } }); + cx.subscribe(&this, move |workspace, _, event, cx| match event { Event::ShowError { lsp_name, error } => { if let Some(buffer) = project @@ -104,7 +102,7 @@ impl ActivityIndicator { }); workspace.add_item( Box::new( - cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), + cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), ), cx, ); @@ -290,71 +288,41 @@ impl ActivityIndicator { }; } - if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { - return Content { - icon: None, - message: most_recent_active_task.to_string(), - on_click: None, - }; - } + // todo!(show active tasks) + // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { + // return Content { + // icon: None, + // message: most_recent_active_task.to_string(), + // on_click: None, + // }; + // } Default::default() } } -impl Entity for ActivityIndicator { - type Event = Event; -} +impl EventEmitter for ActivityIndicator {} -impl View for ActivityIndicator { - fn ui_name() -> &'static str { - "ActivityIndicator" - } +impl Render for ActivityIndicator { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let content = self.content_to_render(cx); - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let Content { - icon, - message, - on_click, - } = self.content_to_render(cx); + let mut result = h_stack() + .id("activity-indicator") + .on_action(cx.listener(Self::show_error_message)) + .on_action(cx.listener(Self::dismiss_error_message)); - let mut element = MouseEventHandler::new::(0, cx, |state, cx| { - let theme = &theme::current(cx).workspace.status_bar.lsp_status; - let style = if state.hovered() && on_click.is_some() { - theme.hovered.as_ref().unwrap_or(&theme.default) - } else { - &theme.default - }; - Flex::row() - .with_children(icon.map(|path| { - Svg::new(path) - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_width) - .contained() - .with_margin_right(style.icon_spacing) - .aligned() - .into_any_named("activity-icon") + if let Some(on_click) = content.on_click { + result = result + .cursor(CursorStyle::PointingHand) + .on_click(cx.listener(move |this, _, cx| { + on_click(this, cx); })) - .with_child( - Text::new(message, style.message.clone()) - .with_soft_wrap(false) - .aligned(), - ) - .constrained() - .with_height(style.height) - .contained() - .with_style(style.container) - .aligned() - }); - - if let Some(on_click) = on_click.clone() { - element = element - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx)); } - element.into_any() + result + .children(content.icon.map(|icon| svg().path(icon))) + .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small)) } } diff --git a/crates/activity_indicator2/Cargo.toml b/crates/activity_indicator2/Cargo.toml deleted file mode 100644 index 400869d2fd..0000000000 --- a/crates/activity_indicator2/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "activity_indicator2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/activity_indicator.rs" -doctest = false - -[dependencies] -auto_update = { path = "../auto_update2", package = "auto_update2" } -editor = { path = "../editor2", package = "editor2" } -language = { path = "../language2", package = "language2" } -gpui = { path = "../gpui2", package = "gpui2" } -project = { path = "../project2", package = "project2" } -settings = { path = "../settings2", package = "settings2" } -ui = { path = "../ui2", package = "ui2" } -util = { path = "../util" } -theme = { path = "../theme2", package = "theme2" } -workspace = { path = "../workspace2", package = "workspace2" } - -anyhow.workspace = true -futures.workspace = true -smallvec.workspace = true - -[dev-dependencies] -editor = { path = "../editor2", package = "editor2", features = ["test-support"] } diff --git a/crates/activity_indicator2/src/activity_indicator.rs b/crates/activity_indicator2/src/activity_indicator.rs deleted file mode 100644 index e4a5b01ba6..0000000000 --- a/crates/activity_indicator2/src/activity_indicator.rs +++ /dev/null @@ -1,333 +0,0 @@ -use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; -use editor::Editor; -use futures::StreamExt; -use gpui::{ - actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model, - ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View, - ViewContext, VisualContext as _, -}; -use language::{LanguageRegistry, LanguageServerBinaryStatus}; -use project::{LanguageServerProgress, Project}; -use smallvec::SmallVec; -use std::{cmp::Reverse, fmt::Write, sync::Arc}; -use ui::h_stack; -use util::ResultExt; -use workspace::{item::ItemHandle, StatusItemView, Workspace}; - -actions!(activity_indicator, [ShowErrorMessage]); - -const DOWNLOAD_ICON: &str = "icons/download.svg"; -const WARNING_ICON: &str = "icons/warning.svg"; - -pub enum Event { - ShowError { lsp_name: Arc, error: String }, -} - -pub struct ActivityIndicator { - statuses: Vec, - project: Model, - auto_updater: Option>, -} - -struct LspStatus { - name: Arc, - status: LanguageServerBinaryStatus, -} - -struct PendingWork<'a> { - language_server_name: &'a str, - progress_token: &'a str, - progress: &'a LanguageServerProgress, -} - -#[derive(Default)] -struct Content { - icon: Option<&'static str>, - message: String, - on_click: Option)>>, -} - -impl ActivityIndicator { - pub fn new( - workspace: &mut Workspace, - languages: Arc, - cx: &mut ViewContext, - ) -> View { - let project = workspace.project().clone(); - let auto_updater = AutoUpdater::get(cx); - let this = cx.build_view(|cx: &mut ViewContext| { - let mut status_events = languages.language_server_binary_statuses(); - cx.spawn(|this, mut cx| async move { - while let Some((language, event)) = status_events.next().await { - this.update(&mut cx, |this, cx| { - this.statuses.retain(|s| s.name != language.name()); - this.statuses.push(LspStatus { - name: language.name(), - status: event, - }); - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach(); - cx.observe(&project, |_, _, cx| cx.notify()).detach(); - - if let Some(auto_updater) = auto_updater.as_ref() { - cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); - } - - // cx.observe_active_labeled_tasks(|_, cx| cx.notify()) - // .detach(); - - Self { - statuses: Default::default(), - project: project.clone(), - auto_updater, - } - }); - - cx.subscribe(&this, move |workspace, _, event, cx| match event { - Event::ShowError { lsp_name, error } => { - if let Some(buffer) = project - .update(cx, |project, cx| project.create_buffer(error, None, cx)) - .log_err() - { - buffer.update(cx, |buffer, cx| { - buffer.edit( - [(0..0, format!("Language server error: {}\n\n", lsp_name))], - None, - cx, - ); - }); - workspace.add_item( - Box::new(cx.build_view(|cx| { - Editor::for_buffer(buffer, Some(project.clone()), cx) - })), - cx, - ); - } - } - }) - .detach(); - this - } - - fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext) { - self.statuses.retain(|status| { - if let LanguageServerBinaryStatus::Failed { error } = &status.status { - cx.emit(Event::ShowError { - lsp_name: status.name.clone(), - error: error.clone(), - }); - false - } else { - true - } - }); - - cx.notify(); - } - - fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { - if let Some(updater) = &self.auto_updater { - updater.update(cx, |updater, cx| { - updater.dismiss_error(cx); - }); - } - cx.notify(); - } - - fn pending_language_server_work<'a>( - &self, - cx: &'a AppContext, - ) -> impl Iterator> { - self.project - .read(cx) - .language_server_statuses() - .rev() - .filter_map(|status| { - if status.pending_work.is_empty() { - None - } else { - let mut pending_work = status - .pending_work - .iter() - .map(|(token, progress)| PendingWork { - language_server_name: status.name.as_str(), - progress_token: token.as_str(), - progress, - }) - .collect::>(); - pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); - Some(pending_work) - } - }) - .flatten() - } - - fn content_to_render(&mut self, cx: &mut ViewContext) -> Content { - // Show any language server has pending activity. - let mut pending_work = self.pending_language_server_work(cx); - if let Some(PendingWork { - language_server_name, - progress_token, - progress, - }) = pending_work.next() - { - let mut message = language_server_name.to_string(); - - message.push_str(": "); - if let Some(progress_message) = progress.message.as_ref() { - message.push_str(progress_message); - } else { - message.push_str(progress_token); - } - - if let Some(percentage) = progress.percentage { - write!(&mut message, " ({}%)", percentage).unwrap(); - } - - let additional_work_count = pending_work.count(); - if additional_work_count > 0 { - write!(&mut message, " + {} more", additional_work_count).unwrap(); - } - - return Content { - icon: None, - message, - on_click: None, - }; - } - - // Show any language server installation info. - let mut downloading = SmallVec::<[_; 3]>::new(); - let mut checking_for_update = SmallVec::<[_; 3]>::new(); - let mut failed = SmallVec::<[_; 3]>::new(); - for status in &self.statuses { - let name = status.name.clone(); - match status.status { - LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name), - LanguageServerBinaryStatus::Downloading => downloading.push(name), - LanguageServerBinaryStatus::Failed { .. } => failed.push(name), - LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} - } - } - - if !downloading.is_empty() { - return Content { - icon: Some(DOWNLOAD_ICON), - message: format!( - "Downloading {} language server{}...", - downloading.join(", "), - if downloading.len() > 1 { "s" } else { "" } - ), - on_click: None, - }; - } else if !checking_for_update.is_empty() { - return Content { - icon: Some(DOWNLOAD_ICON), - message: format!( - "Checking for updates to {} language server{}...", - checking_for_update.join(", "), - if checking_for_update.len() > 1 { - "s" - } else { - "" - } - ), - on_click: None, - }; - } else if !failed.is_empty() { - return Content { - icon: Some(WARNING_ICON), - message: format!( - "Failed to download {} language server{}. Click to show error.", - failed.join(", "), - if failed.len() > 1 { "s" } else { "" } - ), - on_click: Some(Arc::new(|this, cx| { - this.show_error_message(&Default::default(), cx) - })), - }; - } - - // Show any application auto-update info. - if let Some(updater) = &self.auto_updater { - return match &updater.read(cx).status() { - AutoUpdateStatus::Checking => Content { - icon: Some(DOWNLOAD_ICON), - message: "Checking for Zed updates…".to_string(), - on_click: None, - }, - AutoUpdateStatus::Downloading => Content { - icon: Some(DOWNLOAD_ICON), - message: "Downloading Zed update…".to_string(), - on_click: None, - }, - AutoUpdateStatus::Installing => Content { - icon: Some(DOWNLOAD_ICON), - message: "Installing Zed update…".to_string(), - on_click: None, - }, - AutoUpdateStatus::Updated => Content { - icon: None, - message: "Click to restart and update Zed".to_string(), - on_click: Some(Arc::new(|_, cx| { - workspace::restart(&Default::default(), cx) - })), - }, - AutoUpdateStatus::Errored => Content { - icon: Some(WARNING_ICON), - message: "Auto update failed".to_string(), - on_click: Some(Arc::new(|this, cx| { - this.dismiss_error_message(&Default::default(), cx) - })), - }, - AutoUpdateStatus::Idle => Default::default(), - }; - } - - // todo!(show active tasks) - // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { - // return Content { - // icon: None, - // message: most_recent_active_task.to_string(), - // on_click: None, - // }; - // } - - Default::default() - } -} - -impl EventEmitter for ActivityIndicator {} - -impl Render for ActivityIndicator { - type Element = Stateful
; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let content = self.content_to_render(cx); - - let mut result = h_stack() - .id("activity-indicator") - .on_action(cx.listener(Self::show_error_message)) - .on_action(cx.listener(Self::dismiss_error_message)); - - if let Some(on_click) = content.on_click { - result = result - .cursor(CursorStyle::PointingHand) - .on_click(cx.listener(move |this, _, cx| { - on_click(this, cx); - })) - } - - result - .children(content.icon.map(|icon| svg().path(icon))) - .child(SharedString::from(content.message)) - } -} - -impl StatusItemView for ActivityIndicator { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} -} diff --git a/crates/ai/src/auth.rs b/crates/ai/src/auth.rs index c6256df216..1ea49bd615 100644 --- a/crates/ai/src/auth.rs +++ b/crates/ai/src/auth.rs @@ -9,7 +9,7 @@ pub enum ProviderCredential { pub trait CredentialProvider: Send + Sync { fn has_credentials(&self) -> bool; - fn retrieve_credentials(&self, cx: &AppContext) -> ProviderCredential; - fn save_credentials(&self, cx: &AppContext, credential: ProviderCredential); - fn delete_credentials(&self, cx: &AppContext); + fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential; + fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential); + fn delete_credentials(&self, cx: &mut AppContext); } diff --git a/crates/ai/src/prompts/repository_context.rs b/crates/ai/src/prompts/repository_context.rs index c21b0f995c..0d831c2cb2 100644 --- a/crates/ai/src/prompts/repository_context.rs +++ b/crates/ai/src/prompts/repository_context.rs @@ -2,7 +2,7 @@ use crate::prompts::base::{PromptArguments, PromptTemplate}; use std::fmt::Write; use std::{ops::Range, path::PathBuf}; -use gpui::{AsyncAppContext, ModelHandle}; +use gpui::{AsyncAppContext, Model}; use language::{Anchor, Buffer}; #[derive(Clone)] @@ -13,8 +13,12 @@ pub struct PromptCodeSnippet { } impl PromptCodeSnippet { - pub fn new(buffer: ModelHandle, range: Range, cx: &AsyncAppContext) -> Self { - let (content, language_name, file_path) = buffer.read_with(cx, |buffer, _| { + pub fn new( + buffer: Model, + range: Range, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let (content, language_name, file_path) = buffer.update(cx, |buffer, _| { let snapshot = buffer.snapshot(); let content = snapshot.text_for_range(range.clone()).collect::(); @@ -27,13 +31,13 @@ impl PromptCodeSnippet { .and_then(|file| Some(file.path().to_path_buf())); (content, language_name, file_path) - }); + })?; - PromptCodeSnippet { + anyhow::Ok(PromptCodeSnippet { path: file_path, language_name, content, - } + }) } } diff --git a/crates/ai/src/providers/open_ai/completion.rs b/crates/ai/src/providers/open_ai/completion.rs index 94685fd233..c9a2abd0c8 100644 --- a/crates/ai/src/providers/open_ai/completion.rs +++ b/crates/ai/src/providers/open_ai/completion.rs @@ -3,7 +3,7 @@ use futures::{ future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt, Stream, StreamExt, }; -use gpui::{executor::Background, AppContext}; +use gpui::{AppContext, BackgroundExecutor}; use isahc::{http::StatusCode, Request, RequestExt}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; @@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent { pub async fn stream_completion( credential: ProviderCredential, - executor: Arc, + executor: BackgroundExecutor, request: Box, ) -> Result>> { let api_key = match credential { @@ -197,11 +197,11 @@ pub async fn stream_completion( pub struct OpenAICompletionProvider { model: OpenAILanguageModel, credential: Arc>, - executor: Arc, + executor: BackgroundExecutor, } impl OpenAICompletionProvider { - pub fn new(model_name: &str, executor: Arc) -> Self { + pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self { let model = OpenAILanguageModel::load(model_name); let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials)); Self { @@ -219,46 +219,45 @@ impl CredentialProvider for OpenAICompletionProvider { _ => false, } } - fn retrieve_credentials(&self, cx: &AppContext) -> ProviderCredential { - let mut credential = self.credential.write(); - match *credential { - ProviderCredential::Credentials { .. } => { - return credential.clone(); - } + + fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { + let existing_credential = self.credential.read().clone(); + let retrieved_credential = match existing_credential { + ProviderCredential::Credentials { .. } => existing_credential.clone(), _ => { - if let Ok(api_key) = env::var("OPENAI_API_KEY") { - *credential = ProviderCredential::Credentials { api_key }; - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() + if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { + ProviderCredential::Credentials { api_key } + } else if let Some(Some((_, api_key))) = + cx.read_credentials(OPENAI_API_URL).log_err() { if let Some(api_key) = String::from_utf8(api_key).log_err() { - *credential = ProviderCredential::Credentials { api_key }; + ProviderCredential::Credentials { api_key } + } else { + ProviderCredential::NoCredentials } } else { - }; + ProviderCredential::NoCredentials + } } - } - - credential.clone() + }; + *self.credential.write() = retrieved_credential.clone(); + retrieved_credential } - fn save_credentials(&self, cx: &AppContext, credential: ProviderCredential) { - match credential.clone() { + fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { + *self.credential.write() = credential.clone(); + let credential = credential.clone(); + match credential { ProviderCredential::Credentials { api_key } => { - cx.platform() - .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) + cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) .log_err(); } _ => {} } - - *self.credential.write() = credential; } - fn delete_credentials(&self, cx: &AppContext) { - cx.platform().delete_credentials(OPENAI_API_URL).log_err(); + + fn delete_credentials(&self, cx: &mut AppContext) { + cx.delete_credentials(OPENAI_API_URL).log_err(); *self.credential.write() = ProviderCredential::NoCredentials; } } diff --git a/crates/ai/src/providers/open_ai/embedding.rs b/crates/ai/src/providers/open_ai/embedding.rs index fbfd0028f9..0a9b6ba969 100644 --- a/crates/ai/src/providers/open_ai/embedding.rs +++ b/crates/ai/src/providers/open_ai/embedding.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::AsyncReadExt; -use gpui::executor::Background; -use gpui::{serde_json, AppContext}; +use gpui::AppContext; +use gpui::BackgroundExecutor; use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; @@ -11,6 +11,7 @@ use parking_lot::{Mutex, RwLock}; use parse_duration::parse; use postage::watch; use serde::{Deserialize, Serialize}; +use serde_json; use std::env; use std::ops::Add; use std::sync::Arc; @@ -35,7 +36,7 @@ pub struct OpenAIEmbeddingProvider { model: OpenAILanguageModel, credential: Arc>, pub client: Arc, - pub executor: Arc, + pub executor: BackgroundExecutor, rate_limit_count_rx: watch::Receiver>, rate_limit_count_tx: Arc>>>, } @@ -66,7 +67,7 @@ struct OpenAIEmbeddingUsage { } impl OpenAIEmbeddingProvider { - pub fn new(client: Arc, executor: Arc) -> Self { + pub fn new(client: Arc, executor: BackgroundExecutor) -> Self { let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None); let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx)); @@ -153,46 +154,45 @@ impl CredentialProvider for OpenAIEmbeddingProvider { _ => false, } } - fn retrieve_credentials(&self, cx: &AppContext) -> ProviderCredential { - let mut credential = self.credential.write(); - match *credential { - ProviderCredential::Credentials { .. } => { - return credential.clone(); - } + fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { + let existing_credential = self.credential.read().clone(); + + let retrieved_credential = match existing_credential { + ProviderCredential::Credentials { .. } => existing_credential.clone(), _ => { - if let Ok(api_key) = env::var("OPENAI_API_KEY") { - *credential = ProviderCredential::Credentials { api_key }; - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() + if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { + ProviderCredential::Credentials { api_key } + } else if let Some(Some((_, api_key))) = + cx.read_credentials(OPENAI_API_URL).log_err() { if let Some(api_key) = String::from_utf8(api_key).log_err() { - *credential = ProviderCredential::Credentials { api_key }; + ProviderCredential::Credentials { api_key } + } else { + ProviderCredential::NoCredentials } } else { - }; + ProviderCredential::NoCredentials + } } - } + }; - credential.clone() + *self.credential.write() = retrieved_credential.clone(); + retrieved_credential } - fn save_credentials(&self, cx: &AppContext, credential: ProviderCredential) { - match credential.clone() { + fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { + *self.credential.write() = credential.clone(); + match credential { ProviderCredential::Credentials { api_key } => { - cx.platform() - .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) + cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) .log_err(); } _ => {} } - - *self.credential.write() = credential; } - fn delete_credentials(&self, cx: &AppContext) { - cx.platform().delete_credentials(OPENAI_API_URL).log_err(); + + fn delete_credentials(&self, cx: &mut AppContext) { + cx.delete_credentials(OPENAI_API_URL).log_err(); *self.credential.write() = ProviderCredential::NoCredentials; } } diff --git a/crates/ai/src/test.rs b/crates/ai/src/test.rs index 3f331da117..3d59febbe9 100644 --- a/crates/ai/src/test.rs +++ b/crates/ai/src/test.rs @@ -104,11 +104,11 @@ impl CredentialProvider for FakeEmbeddingProvider { fn has_credentials(&self) -> bool { true } - fn retrieve_credentials(&self, _cx: &AppContext) -> ProviderCredential { + fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { ProviderCredential::NotNeeded } - fn save_credentials(&self, _cx: &AppContext, _credential: ProviderCredential) {} - fn delete_credentials(&self, _cx: &AppContext) {} + fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} + fn delete_credentials(&self, _cx: &mut AppContext) {} } #[async_trait] @@ -153,17 +153,10 @@ impl FakeCompletionProvider { pub fn send_completion(&self, completion: impl Into) { let mut tx = self.last_completion_tx.lock(); - - println!("COMPLETION TX: {:?}", &tx); - - let a = tx.as_mut().unwrap(); - a.try_send(completion.into()).unwrap(); - - // tx.as_mut().unwrap().try_send(completion.into()).unwrap(); + tx.as_mut().unwrap().try_send(completion.into()).unwrap(); } pub fn finish_completion(&self) { - println!("FINISHING COMPLETION"); self.last_completion_tx.lock().take().unwrap(); } } @@ -172,11 +165,11 @@ impl CredentialProvider for FakeCompletionProvider { fn has_credentials(&self) -> bool { true } - fn retrieve_credentials(&self, _cx: &AppContext) -> ProviderCredential { + fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { ProviderCredential::NotNeeded } - fn save_credentials(&self, _cx: &AppContext, _credential: ProviderCredential) {} - fn delete_credentials(&self, _cx: &AppContext) {} + fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} + fn delete_credentials(&self, _cx: &mut AppContext) {} } impl CompletionProvider for FakeCompletionProvider { @@ -188,10 +181,8 @@ impl CompletionProvider for FakeCompletionProvider { &self, _prompt: Box, ) -> BoxFuture<'static, anyhow::Result>>> { - println!("COMPLETING"); let (tx, rx) = mpsc::channel(1); *self.last_completion_tx.lock() = Some(tx); - println!("TX: {:?}", *self.last_completion_tx.lock()); async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed() } fn box_clone(&self) -> Box { diff --git a/crates/ai2/Cargo.toml b/crates/ai2/Cargo.toml deleted file mode 100644 index 25c9965915..0000000000 --- a/crates/ai2/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "ai2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/ai2.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -gpui = { package = "gpui2", path = "../gpui2" } -util = { path = "../util" } -language = { package = "language2", path = "../language2" } -async-trait.workspace = true -anyhow.workspace = true -futures.workspace = true -lazy_static.workspace = true -ordered-float.workspace = true -parking_lot.workspace = true -isahc.workspace = true -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -postage.workspace = true -rand.workspace = true -log.workspace = true -parse_duration = "2.1.1" -tiktoken-rs.workspace = true -matrixmultiply = "0.3.7" -rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } -bincode = "1.3.3" - -[dev-dependencies] -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } diff --git a/crates/ai2/src/ai2.rs b/crates/ai2/src/ai2.rs deleted file mode 100644 index dda22d2a1d..0000000000 --- a/crates/ai2/src/ai2.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod auth; -pub mod completion; -pub mod embedding; -pub mod models; -pub mod prompts; -pub mod providers; -#[cfg(any(test, feature = "test-support"))] -pub mod test; diff --git a/crates/ai2/src/auth.rs b/crates/ai2/src/auth.rs deleted file mode 100644 index 1ea49bd615..0000000000 --- a/crates/ai2/src/auth.rs +++ /dev/null @@ -1,15 +0,0 @@ -use gpui::AppContext; - -#[derive(Clone, Debug)] -pub enum ProviderCredential { - Credentials { api_key: String }, - NoCredentials, - NotNeeded, -} - -pub trait CredentialProvider: Send + Sync { - fn has_credentials(&self) -> bool; - fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential; - fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential); - fn delete_credentials(&self, cx: &mut AppContext); -} diff --git a/crates/ai2/src/completion.rs b/crates/ai2/src/completion.rs deleted file mode 100644 index 30a60fcf1d..0000000000 --- a/crates/ai2/src/completion.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Result; -use futures::{future::BoxFuture, stream::BoxStream}; - -use crate::{auth::CredentialProvider, models::LanguageModel}; - -pub trait CompletionRequest: Send + Sync { - fn data(&self) -> serde_json::Result; -} - -pub trait CompletionProvider: CredentialProvider { - fn base_model(&self) -> Box; - fn complete( - &self, - prompt: Box, - ) -> BoxFuture<'static, Result>>>; - fn box_clone(&self) -> Box; -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.box_clone() - } -} diff --git a/crates/ai2/src/embedding.rs b/crates/ai2/src/embedding.rs deleted file mode 100644 index 6768b7ce7b..0000000000 --- a/crates/ai2/src/embedding.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::time::Instant; - -use anyhow::Result; -use async_trait::async_trait; -use ordered_float::OrderedFloat; -use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef}; -use rusqlite::ToSql; - -use crate::auth::CredentialProvider; -use crate::models::LanguageModel; - -#[derive(Debug, PartialEq, Clone)] -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) - } -} - -impl Embedding { - pub fn similarity(&self, other: &Self) -> OrderedFloat { - let len = self.0.len(); - assert_eq!(len, other.0.len()); - - let mut result = 0.0; - unsafe { - matrixmultiply::sgemm( - 1, - len, - 1, - 1.0, - self.0.as_ptr(), - len as isize, - 1, - other.0.as_ptr(), - 1, - len as isize, - 0.0, - &mut result as *mut f32, - 1, - 1, - ); - } - OrderedFloat(result) - } -} - -#[async_trait] -pub trait EmbeddingProvider: CredentialProvider { - fn base_model(&self) -> Box; - async fn embed_batch(&self, spans: Vec) -> Result>; - fn max_tokens_per_batch(&self) -> usize; - fn rate_limit_expiration(&self) -> Option; -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - - #[gpui::test] - fn test_similarity(mut rng: StdRng) { - assert_eq!( - Embedding::from(vec![1., 0., 0., 0., 0.]) - .similarity(&Embedding::from(vec![0., 1., 0., 0., 0.])), - 0. - ); - assert_eq!( - Embedding::from(vec![2., 0., 0., 0., 0.]) - .similarity(&Embedding::from(vec![3., 1., 0., 0., 0.])), - 6. - ); - - for _ in 0..100 { - let size = 1536; - let mut a = vec![0.; size]; - let mut b = vec![0.; size]; - for (a, b) in a.iter_mut().zip(b.iter_mut()) { - *a = rng.gen(); - *b = rng.gen(); - } - let a = Embedding::from(a); - let b = Embedding::from(b); - - assert_eq!( - round_to_decimals(a.similarity(&b), 1), - round_to_decimals(reference_dot(&a.0, &b.0), 1) - ); - } - - 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]) -> OrderedFloat { - OrderedFloat(a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()) - } - } -} diff --git a/crates/ai2/src/models.rs b/crates/ai2/src/models.rs deleted file mode 100644 index 1db3d58c6f..0000000000 --- a/crates/ai2/src/models.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub enum TruncationDirection { - Start, - End, -} - -pub trait LanguageModel { - fn name(&self) -> String; - fn count_tokens(&self, content: &str) -> anyhow::Result; - fn truncate( - &self, - content: &str, - length: usize, - direction: TruncationDirection, - ) -> anyhow::Result; - fn capacity(&self) -> anyhow::Result; -} diff --git a/crates/ai2/src/prompts/base.rs b/crates/ai2/src/prompts/base.rs deleted file mode 100644 index 75bad00154..0000000000 --- a/crates/ai2/src/prompts/base.rs +++ /dev/null @@ -1,330 +0,0 @@ -use std::cmp::Reverse; -use std::ops::Range; -use std::sync::Arc; - -use language::BufferSnapshot; -use util::ResultExt; - -use crate::models::LanguageModel; -use crate::prompts::repository_context::PromptCodeSnippet; - -pub(crate) enum PromptFileType { - Text, - Code, -} - -// TODO: Set this up to manage for defaults well -pub struct PromptArguments { - pub model: Arc, - pub user_prompt: Option, - pub language_name: Option, - pub project_name: Option, - pub snippets: Vec, - pub reserved_tokens: usize, - pub buffer: Option, - pub selected_range: Option>, -} - -impl PromptArguments { - pub(crate) fn get_file_type(&self) -> PromptFileType { - if self - .language_name - .as_ref() - .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str()))) - .unwrap_or(true) - { - PromptFileType::Code - } else { - PromptFileType::Text - } - } -} - -pub trait PromptTemplate { - fn generate( - &self, - args: &PromptArguments, - max_token_length: Option, - ) -> anyhow::Result<(String, usize)>; -} - -#[repr(i8)] -#[derive(PartialEq, Eq, Ord)] -pub enum PromptPriority { - Mandatory, // Ignores truncation - Ordered { order: usize }, // Truncates based on priority -} - -impl PartialOrd for PromptPriority { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Mandatory, Self::Mandatory) => Some(std::cmp::Ordering::Equal), - (Self::Mandatory, Self::Ordered { .. }) => Some(std::cmp::Ordering::Greater), - (Self::Ordered { .. }, Self::Mandatory) => Some(std::cmp::Ordering::Less), - (Self::Ordered { order: a }, Self::Ordered { order: b }) => b.partial_cmp(a), - } - } -} - -pub struct PromptChain { - args: PromptArguments, - templates: Vec<(PromptPriority, Box)>, -} - -impl PromptChain { - pub fn new( - args: PromptArguments, - templates: Vec<(PromptPriority, Box)>, - ) -> Self { - PromptChain { args, templates } - } - - pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> { - // Argsort based on Prompt Priority - let seperator = "\n"; - let seperator_tokens = self.args.model.count_tokens(seperator)?; - let mut sorted_indices = (0..self.templates.len()).collect::>(); - sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0)); - - // If Truncate - let mut tokens_outstanding = if truncate { - Some(self.args.model.capacity()? - self.args.reserved_tokens) - } else { - None - }; - - let mut prompts = vec!["".to_string(); sorted_indices.len()]; - for idx in sorted_indices { - let (_, template) = &self.templates[idx]; - - if let Some((template_prompt, prompt_token_count)) = - template.generate(&self.args, tokens_outstanding).log_err() - { - if template_prompt != "" { - prompts[idx] = template_prompt; - - if let Some(remaining_tokens) = tokens_outstanding { - let new_tokens = prompt_token_count + seperator_tokens; - tokens_outstanding = if remaining_tokens > new_tokens { - Some(remaining_tokens - new_tokens) - } else { - Some(0) - }; - } - } - } - } - - prompts.retain(|x| x != ""); - - let full_prompt = prompts.join(seperator); - let total_token_count = self.args.model.count_tokens(&full_prompt)?; - anyhow::Ok((prompts.join(seperator), total_token_count)) - } -} - -#[cfg(test)] -pub(crate) mod tests { - use crate::models::TruncationDirection; - use crate::test::FakeLanguageModel; - - use super::*; - - #[test] - pub fn test_prompt_chain() { - struct TestPromptTemplate {} - impl PromptTemplate for TestPromptTemplate { - fn generate( - &self, - args: &PromptArguments, - max_token_length: Option, - ) -> anyhow::Result<(String, usize)> { - let mut content = "This is a test prompt template".to_string(); - - let mut token_count = args.model.count_tokens(&content)?; - if let Some(max_token_length) = max_token_length { - if token_count > max_token_length { - content = args.model.truncate( - &content, - max_token_length, - TruncationDirection::End, - )?; - token_count = max_token_length; - } - } - - anyhow::Ok((content, token_count)) - } - } - - struct TestLowPriorityTemplate {} - impl PromptTemplate for TestLowPriorityTemplate { - fn generate( - &self, - args: &PromptArguments, - max_token_length: Option, - ) -> anyhow::Result<(String, usize)> { - let mut content = "This is a low priority test prompt template".to_string(); - - let mut token_count = args.model.count_tokens(&content)?; - if let Some(max_token_length) = max_token_length { - if token_count > max_token_length { - content = args.model.truncate( - &content, - max_token_length, - TruncationDirection::End, - )?; - token_count = max_token_length; - } - } - - anyhow::Ok((content, token_count)) - } - } - - let model: Arc = Arc::new(FakeLanguageModel { capacity: 100 }); - let args = PromptArguments { - model: model.clone(), - language_name: None, - project_name: None, - snippets: Vec::new(), - reserved_tokens: 0, - buffer: None, - selected_range: None, - user_prompt: None, - }; - - let templates: Vec<(PromptPriority, Box)> = vec![ - ( - PromptPriority::Ordered { order: 0 }, - Box::new(TestPromptTemplate {}), - ), - ( - PromptPriority::Ordered { order: 1 }, - Box::new(TestLowPriorityTemplate {}), - ), - ]; - let chain = PromptChain::new(args, templates); - - let (prompt, token_count) = chain.generate(false).unwrap(); - - assert_eq!( - prompt, - "This is a test prompt template\nThis is a low priority test prompt template" - .to_string() - ); - - assert_eq!(model.count_tokens(&prompt).unwrap(), token_count); - - // Testing with Truncation Off - // Should ignore capacity and return all prompts - let model: Arc = Arc::new(FakeLanguageModel { capacity: 20 }); - let args = PromptArguments { - model: model.clone(), - language_name: None, - project_name: None, - snippets: Vec::new(), - reserved_tokens: 0, - buffer: None, - selected_range: None, - user_prompt: None, - }; - - let templates: Vec<(PromptPriority, Box)> = vec![ - ( - PromptPriority::Ordered { order: 0 }, - Box::new(TestPromptTemplate {}), - ), - ( - PromptPriority::Ordered { order: 1 }, - Box::new(TestLowPriorityTemplate {}), - ), - ]; - let chain = PromptChain::new(args, templates); - - let (prompt, token_count) = chain.generate(false).unwrap(); - - assert_eq!( - prompt, - "This is a test prompt template\nThis is a low priority test prompt template" - .to_string() - ); - - assert_eq!(model.count_tokens(&prompt).unwrap(), token_count); - - // Testing with Truncation Off - // Should ignore capacity and return all prompts - let capacity = 20; - let model: Arc = Arc::new(FakeLanguageModel { capacity }); - let args = PromptArguments { - model: model.clone(), - language_name: None, - project_name: None, - snippets: Vec::new(), - reserved_tokens: 0, - buffer: None, - selected_range: None, - user_prompt: None, - }; - - let templates: Vec<(PromptPriority, Box)> = vec![ - ( - PromptPriority::Ordered { order: 0 }, - Box::new(TestPromptTemplate {}), - ), - ( - PromptPriority::Ordered { order: 1 }, - Box::new(TestLowPriorityTemplate {}), - ), - ( - PromptPriority::Ordered { order: 2 }, - Box::new(TestLowPriorityTemplate {}), - ), - ]; - let chain = PromptChain::new(args, templates); - - let (prompt, token_count) = chain.generate(true).unwrap(); - - assert_eq!(prompt, "This is a test promp".to_string()); - assert_eq!(token_count, capacity); - - // Change Ordering of Prompts Based on Priority - let capacity = 120; - let reserved_tokens = 10; - let model: Arc = Arc::new(FakeLanguageModel { capacity }); - let args = PromptArguments { - model: model.clone(), - language_name: None, - project_name: None, - snippets: Vec::new(), - reserved_tokens, - buffer: None, - selected_range: None, - user_prompt: None, - }; - let templates: Vec<(PromptPriority, Box)> = vec![ - ( - PromptPriority::Mandatory, - Box::new(TestLowPriorityTemplate {}), - ), - ( - PromptPriority::Ordered { order: 0 }, - Box::new(TestPromptTemplate {}), - ), - ( - PromptPriority::Ordered { order: 1 }, - Box::new(TestLowPriorityTemplate {}), - ), - ]; - let chain = PromptChain::new(args, templates); - - let (prompt, token_count) = chain.generate(true).unwrap(); - - assert_eq!( - prompt, - "This is a low priority test prompt template\nThis is a test prompt template\nThis is a low priority test prompt " - .to_string() - ); - assert_eq!(token_count, capacity - reserved_tokens); - } -} diff --git a/crates/ai2/src/prompts/file_context.rs b/crates/ai2/src/prompts/file_context.rs deleted file mode 100644 index f108a62f6f..0000000000 --- a/crates/ai2/src/prompts/file_context.rs +++ /dev/null @@ -1,164 +0,0 @@ -use anyhow::anyhow; -use language::BufferSnapshot; -use language::ToOffset; - -use crate::models::LanguageModel; -use crate::models::TruncationDirection; -use crate::prompts::base::PromptArguments; -use crate::prompts::base::PromptTemplate; -use std::fmt::Write; -use std::ops::Range; -use std::sync::Arc; - -fn retrieve_context( - buffer: &BufferSnapshot, - selected_range: &Option>, - model: Arc, - max_token_count: Option, -) -> anyhow::Result<(String, usize, bool)> { - let mut prompt = String::new(); - let mut truncated = false; - if let Some(selected_range) = selected_range { - let start = selected_range.start.to_offset(buffer); - let end = selected_range.end.to_offset(buffer); - - let start_window = buffer.text_for_range(0..start).collect::(); - - let mut selected_window = String::new(); - if start == end { - write!(selected_window, "<|START|>").unwrap(); - } else { - write!(selected_window, "<|START|").unwrap(); - } - - write!( - selected_window, - "{}", - buffer.text_for_range(start..end).collect::() - ) - .unwrap(); - - if start != end { - write!(selected_window, "|END|>").unwrap(); - } - - let end_window = buffer.text_for_range(end..buffer.len()).collect::(); - - if let Some(max_token_count) = max_token_count { - let selected_tokens = model.count_tokens(&selected_window)?; - if selected_tokens > max_token_count { - return Err(anyhow!( - "selected range is greater than model context window, truncation not possible" - )); - }; - - let mut remaining_tokens = max_token_count - selected_tokens; - let start_window_tokens = model.count_tokens(&start_window)?; - let end_window_tokens = model.count_tokens(&end_window)?; - let outside_tokens = start_window_tokens + end_window_tokens; - if outside_tokens > remaining_tokens { - let (start_goal_tokens, end_goal_tokens) = - if start_window_tokens < end_window_tokens { - let start_goal_tokens = (remaining_tokens / 2).min(start_window_tokens); - remaining_tokens -= start_goal_tokens; - let end_goal_tokens = remaining_tokens.min(end_window_tokens); - (start_goal_tokens, end_goal_tokens) - } else { - let end_goal_tokens = (remaining_tokens / 2).min(end_window_tokens); - remaining_tokens -= end_goal_tokens; - let start_goal_tokens = remaining_tokens.min(start_window_tokens); - (start_goal_tokens, end_goal_tokens) - }; - - let truncated_start_window = - model.truncate(&start_window, start_goal_tokens, TruncationDirection::Start)?; - let truncated_end_window = - model.truncate(&end_window, end_goal_tokens, TruncationDirection::End)?; - writeln!( - prompt, - "{truncated_start_window}{selected_window}{truncated_end_window}" - ) - .unwrap(); - truncated = true; - } else { - writeln!(prompt, "{start_window}{selected_window}{end_window}").unwrap(); - } - } else { - // If we dont have a selected range, include entire file. - writeln!(prompt, "{}", &buffer.text()).unwrap(); - - // Dumb truncation strategy - if let Some(max_token_count) = max_token_count { - if model.count_tokens(&prompt)? > max_token_count { - truncated = true; - prompt = model.truncate(&prompt, max_token_count, TruncationDirection::End)?; - } - } - } - } - - let token_count = model.count_tokens(&prompt)?; - anyhow::Ok((prompt, token_count, truncated)) -} - -pub struct FileContext {} - -impl PromptTemplate for FileContext { - fn generate( - &self, - args: &PromptArguments, - max_token_length: Option, - ) -> anyhow::Result<(String, usize)> { - if let Some(buffer) = &args.buffer { - let mut prompt = String::new(); - // Add Initial Preamble - // TODO: Do we want to add the path in here? - writeln!( - prompt, - "The file you are currently working on has the following content:" - ) - .unwrap(); - - let language_name = args - .language_name - .clone() - .unwrap_or("".to_string()) - .to_lowercase(); - - let (context, _, truncated) = retrieve_context( - buffer, - &args.selected_range, - args.model.clone(), - max_token_length, - )?; - writeln!(prompt, "```{language_name}\n{context}\n```").unwrap(); - - if truncated { - writeln!(prompt, "Note the content has been truncated and only represents a portion of the file.").unwrap(); - } - - if let Some(selected_range) = &args.selected_range { - let start = selected_range.start.to_offset(buffer); - let end = selected_range.end.to_offset(buffer); - - if start == end { - writeln!(prompt, "In particular, the user's cursor is currently on the '<|START|>' span in the above content, with no text selected.").unwrap(); - } else { - writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); - } - } - - // Really dumb truncation strategy - if let Some(max_tokens) = max_token_length { - prompt = args - .model - .truncate(&prompt, max_tokens, TruncationDirection::End)?; - } - - let token_count = args.model.count_tokens(&prompt)?; - anyhow::Ok((prompt, token_count)) - } else { - Err(anyhow!("no buffer provided to retrieve file context from")) - } - } -} diff --git a/crates/ai2/src/prompts/generate.rs b/crates/ai2/src/prompts/generate.rs deleted file mode 100644 index c7be620107..0000000000 --- a/crates/ai2/src/prompts/generate.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::prompts::base::{PromptArguments, PromptFileType, PromptTemplate}; -use anyhow::anyhow; -use std::fmt::Write; - -pub fn capitalize(s: &str) -> String { - let mut c = s.chars(); - match c.next() { - None => String::new(), - Some(f) => f.to_uppercase().collect::() + c.as_str(), - } -} - -pub struct GenerateInlineContent {} - -impl PromptTemplate for GenerateInlineContent { - fn generate( - &self, - args: &PromptArguments, - max_token_length: Option, - ) -> anyhow::Result<(String, usize)> { - let Some(user_prompt) = &args.user_prompt else { - return Err(anyhow!("user prompt not provided")); - }; - - let file_type = args.get_file_type(); - let content_type = match &file_type { - PromptFileType::Code => "code", - PromptFileType::Text => "text", - }; - - let mut prompt = String::new(); - - if let Some(selected_range) = &args.selected_range { - if selected_range.start == selected_range.end { - writeln!( - prompt, - "Assume the cursor is located where the `<|START|>` span is." - ) - .unwrap(); - writeln!( - prompt, - "{} can't be replaced, so assume your answer will be inserted at the cursor.", - capitalize(content_type) - ) - .unwrap(); - writeln!( - prompt, - "Generate {content_type} based on the users prompt: {user_prompt}", - ) - .unwrap(); - } else { - writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap(); - writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap(); - writeln!(prompt, "Double check that you only return code and not the '<|START|' and '|END|'> spans").unwrap(); - } - } else { - writeln!( - prompt, - "Generate {content_type} based on the users prompt: {user_prompt}" - ) - .unwrap(); - } - - if let Some(language_name) = &args.language_name { - writeln!( - prompt, - "Your answer MUST always and only be valid {}.", - language_name - ) - .unwrap(); - } - writeln!(prompt, "Never make remarks about the output.").unwrap(); - writeln!( - prompt, - "Do not return anything else, except the generated {content_type}." - ) - .unwrap(); - - match file_type { - PromptFileType::Code => { - // writeln!(prompt, "Always wrap your code in a Markdown block.").unwrap(); - } - _ => {} - } - - // Really dumb truncation strategy - if let Some(max_tokens) = max_token_length { - prompt = args.model.truncate( - &prompt, - max_tokens, - crate::models::TruncationDirection::End, - )?; - } - - let token_count = args.model.count_tokens(&prompt)?; - - anyhow::Ok((prompt, token_count)) - } -} diff --git a/crates/ai2/src/prompts/mod.rs b/crates/ai2/src/prompts/mod.rs deleted file mode 100644 index 0025269a44..0000000000 --- a/crates/ai2/src/prompts/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod base; -pub mod file_context; -pub mod generate; -pub mod preamble; -pub mod repository_context; diff --git a/crates/ai2/src/prompts/preamble.rs b/crates/ai2/src/prompts/preamble.rs deleted file mode 100644 index 92e0edeb78..0000000000 --- a/crates/ai2/src/prompts/preamble.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::prompts::base::{PromptArguments, PromptFileType, PromptTemplate}; -use std::fmt::Write; - -pub struct EngineerPreamble {} - -impl PromptTemplate for EngineerPreamble { - fn generate( - &self, - args: &PromptArguments, - max_token_length: Option, - ) -> anyhow::Result<(String, usize)> { - let mut prompts = Vec::new(); - - match args.get_file_type() { - PromptFileType::Code => { - prompts.push(format!( - "You are an expert {}engineer.", - args.language_name.clone().unwrap_or("".to_string()) + " " - )); - } - PromptFileType::Text => { - prompts.push("You are an expert engineer.".to_string()); - } - } - - if let Some(project_name) = args.project_name.clone() { - prompts.push(format!( - "You are currently working inside the '{project_name}' project in code editor Zed." - )); - } - - if let Some(mut remaining_tokens) = max_token_length { - let mut prompt = String::new(); - let mut total_count = 0; - for prompt_piece in prompts { - let prompt_token_count = - args.model.count_tokens(&prompt_piece)? + args.model.count_tokens("\n")?; - if remaining_tokens > prompt_token_count { - writeln!(prompt, "{prompt_piece}").unwrap(); - remaining_tokens -= prompt_token_count; - total_count += prompt_token_count; - } - } - - anyhow::Ok((prompt, total_count)) - } else { - let prompt = prompts.join("\n"); - let token_count = args.model.count_tokens(&prompt)?; - anyhow::Ok((prompt, token_count)) - } - } -} diff --git a/crates/ai2/src/prompts/repository_context.rs b/crates/ai2/src/prompts/repository_context.rs deleted file mode 100644 index 0d831c2cb2..0000000000 --- a/crates/ai2/src/prompts/repository_context.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::prompts::base::{PromptArguments, PromptTemplate}; -use std::fmt::Write; -use std::{ops::Range, path::PathBuf}; - -use gpui::{AsyncAppContext, Model}; -use language::{Anchor, Buffer}; - -#[derive(Clone)] -pub struct PromptCodeSnippet { - path: Option, - language_name: Option, - content: String, -} - -impl PromptCodeSnippet { - pub fn new( - buffer: Model, - range: Range, - cx: &mut AsyncAppContext, - ) -> anyhow::Result { - let (content, language_name, file_path) = buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let content = snapshot.text_for_range(range.clone()).collect::(); - - let language_name = buffer - .language() - .and_then(|language| Some(language.name().to_string().to_lowercase())); - - let file_path = buffer - .file() - .and_then(|file| Some(file.path().to_path_buf())); - - (content, language_name, file_path) - })?; - - anyhow::Ok(PromptCodeSnippet { - path: file_path, - language_name, - content, - }) - } -} - -impl ToString for PromptCodeSnippet { - fn to_string(&self) -> String { - let path = self - .path - .as_ref() - .and_then(|path| Some(path.to_string_lossy().to_string())) - .unwrap_or("".to_string()); - let language_name = self.language_name.clone().unwrap_or("".to_string()); - let content = self.content.clone(); - - format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```") - } -} - -pub struct RepositoryContext {} - -impl PromptTemplate for RepositoryContext { - fn generate( - &self, - args: &PromptArguments, - max_token_length: Option, - ) -> anyhow::Result<(String, usize)> { - const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; - let template = "You are working inside a large repository, here are a few code snippets that may be useful."; - let mut prompt = String::new(); - - let mut remaining_tokens = max_token_length.clone(); - let seperator_token_length = args.model.count_tokens("\n")?; - for snippet in &args.snippets { - let mut snippet_prompt = template.to_string(); - let content = snippet.to_string(); - writeln!(snippet_prompt, "{content}").unwrap(); - - let token_count = args.model.count_tokens(&snippet_prompt)?; - if token_count <= MAXIMUM_SNIPPET_TOKEN_COUNT { - if let Some(tokens_left) = remaining_tokens { - if tokens_left >= token_count { - writeln!(prompt, "{snippet_prompt}").unwrap(); - remaining_tokens = if tokens_left >= (token_count + seperator_token_length) - { - Some(tokens_left - token_count - seperator_token_length) - } else { - Some(0) - }; - } - } else { - writeln!(prompt, "{snippet_prompt}").unwrap(); - } - } - } - - let total_token_count = args.model.count_tokens(&prompt)?; - anyhow::Ok((prompt, total_token_count)) - } -} diff --git a/crates/ai2/src/providers/mod.rs b/crates/ai2/src/providers/mod.rs deleted file mode 100644 index acd0f9d910..0000000000 --- a/crates/ai2/src/providers/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod open_ai; diff --git a/crates/ai2/src/providers/open_ai/completion.rs b/crates/ai2/src/providers/open_ai/completion.rs deleted file mode 100644 index c9a2abd0c8..0000000000 --- a/crates/ai2/src/providers/open_ai/completion.rs +++ /dev/null @@ -1,297 +0,0 @@ -use anyhow::{anyhow, Result}; -use futures::{ - future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt, - Stream, StreamExt, -}; -use gpui::{AppContext, BackgroundExecutor}; -use isahc::{http::StatusCode, Request, RequestExt}; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use std::{ - env, - fmt::{self, Display}, - io, - sync::Arc, -}; -use util::ResultExt; - -use crate::{ - auth::{CredentialProvider, ProviderCredential}, - completion::{CompletionProvider, CompletionRequest}, - models::LanguageModel, -}; - -use crate::providers::open_ai::{OpenAILanguageModel, OPENAI_API_URL}; - -#[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, - pub stop: Vec, - pub temperature: f32, -} - -impl CompletionRequest for OpenAIRequest { - fn data(&self) -> serde_json::Result { - serde_json::to_string(self) - } -} - -#[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( - credential: ProviderCredential, - executor: BackgroundExecutor, - request: Box, -) -> Result>> { - let api_key = match credential { - ProviderCredential::Credentials { api_key } => api_key, - _ => { - return Err(anyhow!("no credentials provider for completion")); - } - }; - - let (tx, rx) = futures::channel::mpsc::unbounded::>(); - - let json_data = request.data()?; - 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, - )), - } - } -} - -#[derive(Clone)] -pub struct OpenAICompletionProvider { - model: OpenAILanguageModel, - credential: Arc>, - executor: BackgroundExecutor, -} - -impl OpenAICompletionProvider { - pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self { - let model = OpenAILanguageModel::load(model_name); - let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials)); - Self { - model, - credential, - executor, - } - } -} - -impl CredentialProvider for OpenAICompletionProvider { - fn has_credentials(&self) -> bool { - match *self.credential.read() { - ProviderCredential::Credentials { .. } => true, - _ => false, - } - } - - fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { - let existing_credential = self.credential.read().clone(); - let retrieved_credential = match existing_credential { - ProviderCredential::Credentials { .. } => existing_credential.clone(), - _ => { - if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { - ProviderCredential::Credentials { api_key } - } else if let Some(Some((_, api_key))) = - cx.read_credentials(OPENAI_API_URL).log_err() - { - if let Some(api_key) = String::from_utf8(api_key).log_err() { - ProviderCredential::Credentials { api_key } - } else { - ProviderCredential::NoCredentials - } - } else { - ProviderCredential::NoCredentials - } - } - }; - *self.credential.write() = retrieved_credential.clone(); - retrieved_credential - } - - fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { - *self.credential.write() = credential.clone(); - let credential = credential.clone(); - match credential { - ProviderCredential::Credentials { api_key } => { - cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) - .log_err(); - } - _ => {} - } - } - - fn delete_credentials(&self, cx: &mut AppContext) { - cx.delete_credentials(OPENAI_API_URL).log_err(); - *self.credential.write() = ProviderCredential::NoCredentials; - } -} - -impl CompletionProvider for OpenAICompletionProvider { - fn base_model(&self) -> Box { - let model: Box = Box::new(self.model.clone()); - model - } - fn complete( - &self, - prompt: Box, - ) -> BoxFuture<'static, Result>>> { - // Currently the CompletionRequest for OpenAI, includes a 'model' parameter - // This means that the model is determined by the CompletionRequest and not the CompletionProvider, - // which is currently model based, due to the langauge model. - // At some point in the future we should rectify this. - let credential = self.credential.read().clone(); - let request = stream_completion(credential, 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() - } - fn box_clone(&self) -> Box { - Box::new((*self).clone()) - } -} diff --git a/crates/ai2/src/providers/open_ai/embedding.rs b/crates/ai2/src/providers/open_ai/embedding.rs deleted file mode 100644 index d5fe4e8c58..0000000000 --- a/crates/ai2/src/providers/open_ai/embedding.rs +++ /dev/null @@ -1,305 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use futures::AsyncReadExt; -use gpui::BackgroundExecutor; -use gpui::{serde_json, AppContext}; -use isahc::http::StatusCode; -use isahc::prelude::Configurable; -use isahc::{AsyncBody, Response}; -use lazy_static::lazy_static; -use parking_lot::{Mutex, RwLock}; -use parse_duration::parse; -use postage::watch; -use serde::{Deserialize, Serialize}; -use std::env; -use std::ops::Add; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tiktoken_rs::{cl100k_base, CoreBPE}; -use util::http::{HttpClient, Request}; -use util::ResultExt; - -use crate::auth::{CredentialProvider, ProviderCredential}; -use crate::embedding::{Embedding, EmbeddingProvider}; -use crate::models::LanguageModel; -use crate::providers::open_ai::OpenAILanguageModel; - -use crate::providers::open_ai::OPENAI_API_URL; - -lazy_static! { - static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap(); -} - -#[derive(Clone)] -pub struct OpenAIEmbeddingProvider { - model: OpenAILanguageModel, - credential: Arc>, - pub client: Arc, - pub executor: BackgroundExecutor, - rate_limit_count_rx: watch::Receiver>, - rate_limit_count_tx: Arc>>>, -} - -#[derive(Serialize)] -struct OpenAIEmbeddingRequest<'a> { - model: &'static str, - input: Vec<&'a str>, -} - -#[derive(Deserialize)] -struct OpenAIEmbeddingResponse { - data: Vec, - usage: OpenAIEmbeddingUsage, -} - -#[derive(Debug, Deserialize)] -struct OpenAIEmbedding { - embedding: Vec, - index: usize, - object: String, -} - -#[derive(Deserialize)] -struct OpenAIEmbeddingUsage { - prompt_tokens: usize, - total_tokens: usize, -} - -impl OpenAIEmbeddingProvider { - pub fn new(client: Arc, executor: BackgroundExecutor) -> Self { - let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None); - let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx)); - - let model = OpenAILanguageModel::load("text-embedding-ada-002"); - let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials)); - - OpenAIEmbeddingProvider { - model, - credential, - client, - executor, - rate_limit_count_rx, - rate_limit_count_tx, - } - } - - fn get_api_key(&self) -> Result { - match self.credential.read().clone() { - ProviderCredential::Credentials { api_key } => Ok(api_key), - _ => Err(anyhow!("api credentials not provided")), - } - } - - fn resolve_rate_limit(&self) { - let reset_time = *self.rate_limit_count_tx.lock().borrow(); - - if let Some(reset_time) = reset_time { - if Instant::now() >= reset_time { - *self.rate_limit_count_tx.lock().borrow_mut() = None - } - } - - log::trace!( - "resolving reset time: {:?}", - *self.rate_limit_count_tx.lock().borrow() - ); - } - - fn update_reset_time(&self, reset_time: Instant) { - let original_time = *self.rate_limit_count_tx.lock().borrow(); - - let updated_time = if let Some(original_time) = original_time { - if reset_time < original_time { - Some(reset_time) - } else { - Some(original_time) - } - } else { - Some(reset_time) - }; - - log::trace!("updating rate limit time: {:?}", updated_time); - - *self.rate_limit_count_tx.lock().borrow_mut() = updated_time; - } - async fn send_request( - &self, - api_key: &str, - spans: Vec<&str>, - request_timeout: u64, - ) -> Result> { - let request = Request::post("https://api.openai.com/v1/embeddings") - .redirect_policy(isahc::config::RedirectPolicy::Follow) - .timeout(Duration::from_secs(request_timeout)) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .body( - serde_json::to_string(&OpenAIEmbeddingRequest { - input: spans.clone(), - model: "text-embedding-ada-002", - }) - .unwrap() - .into(), - )?; - - Ok(self.client.send(request).await?) - } -} - -impl CredentialProvider for OpenAIEmbeddingProvider { - fn has_credentials(&self) -> bool { - match *self.credential.read() { - ProviderCredential::Credentials { .. } => true, - _ => false, - } - } - fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential { - let existing_credential = self.credential.read().clone(); - - let retrieved_credential = match existing_credential { - ProviderCredential::Credentials { .. } => existing_credential.clone(), - _ => { - if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() { - ProviderCredential::Credentials { api_key } - } else if let Some(Some((_, api_key))) = - cx.read_credentials(OPENAI_API_URL).log_err() - { - if let Some(api_key) = String::from_utf8(api_key).log_err() { - ProviderCredential::Credentials { api_key } - } else { - ProviderCredential::NoCredentials - } - } else { - ProviderCredential::NoCredentials - } - } - }; - - *self.credential.write() = retrieved_credential.clone(); - retrieved_credential - } - - fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) { - *self.credential.write() = credential.clone(); - match credential { - ProviderCredential::Credentials { api_key } => { - cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) - .log_err(); - } - _ => {} - } - } - - fn delete_credentials(&self, cx: &mut AppContext) { - cx.delete_credentials(OPENAI_API_URL).log_err(); - *self.credential.write() = ProviderCredential::NoCredentials; - } -} - -#[async_trait] -impl EmbeddingProvider for OpenAIEmbeddingProvider { - fn base_model(&self) -> Box { - let model: Box = Box::new(self.model.clone()); - model - } - - fn max_tokens_per_batch(&self) -> usize { - 50000 - } - - fn rate_limit_expiration(&self) -> Option { - *self.rate_limit_count_rx.borrow() - } - - async fn embed_batch(&self, spans: Vec) -> Result> { - const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45]; - const MAX_RETRIES: usize = 4; - - let api_key = self.get_api_key()?; - - let mut request_number = 0; - let mut rate_limiting = false; - let mut request_timeout: u64 = 15; - let mut response: Response; - while request_number < MAX_RETRIES { - response = self - .send_request( - &api_key, - spans.iter().map(|x| &**x).collect(), - request_timeout, - ) - .await?; - - request_number += 1; - - match response.status() { - StatusCode::REQUEST_TIMEOUT => { - request_timeout += 5; - } - StatusCode::OK => { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?; - - log::trace!( - "openai embedding completed. tokens: {:?}", - response.usage.total_tokens - ); - - // If we complete a request successfully that was previously rate_limited - // resolve the rate limit - if rate_limiting { - self.resolve_rate_limit() - } - - return Ok(response - .data - .into_iter() - .map(|embedding| Embedding::from(embedding.embedding)) - .collect()); - } - StatusCode::TOO_MANY_REQUESTS => { - rate_limiting = true; - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - let delay_duration = { - let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64); - if let Some(time_to_reset) = - response.headers().get("x-ratelimit-reset-tokens") - { - if let Ok(time_str) = time_to_reset.to_str() { - parse(time_str).unwrap_or(delay) - } else { - delay - } - } else { - delay - } - }; - - // If we've previously rate limited, increment the duration but not the count - let reset_time = Instant::now().add(delay_duration); - self.update_reset_time(reset_time); - - log::trace!( - "openai rate limiting: waiting {:?} until lifted", - &delay_duration - ); - - self.executor.timer(delay_duration).await; - } - _ => { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - return Err(anyhow!( - "open ai bad request: {:?} {:?}", - &response.status(), - body - )); - } - } - } - Err(anyhow!("openai max retries")) - } -} diff --git a/crates/ai2/src/providers/open_ai/mod.rs b/crates/ai2/src/providers/open_ai/mod.rs deleted file mode 100644 index 7d2f86045d..0000000000 --- a/crates/ai2/src/providers/open_ai/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod completion; -pub mod embedding; -pub mod model; - -pub use completion::*; -pub use embedding::*; -pub use model::OpenAILanguageModel; - -pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; diff --git a/crates/ai2/src/providers/open_ai/model.rs b/crates/ai2/src/providers/open_ai/model.rs deleted file mode 100644 index 6e306c80b9..0000000000 --- a/crates/ai2/src/providers/open_ai/model.rs +++ /dev/null @@ -1,57 +0,0 @@ -use anyhow::anyhow; -use tiktoken_rs::CoreBPE; -use util::ResultExt; - -use crate::models::{LanguageModel, TruncationDirection}; - -#[derive(Clone)] -pub struct OpenAILanguageModel { - name: String, - bpe: Option, -} - -impl OpenAILanguageModel { - pub fn load(model_name: &str) -> Self { - let bpe = tiktoken_rs::get_bpe_from_model(model_name).log_err(); - OpenAILanguageModel { - name: model_name.to_string(), - bpe, - } - } -} - -impl LanguageModel for OpenAILanguageModel { - fn name(&self) -> String { - self.name.clone() - } - fn count_tokens(&self, content: &str) -> anyhow::Result { - if let Some(bpe) = &self.bpe { - anyhow::Ok(bpe.encode_with_special_tokens(content).len()) - } else { - Err(anyhow!("bpe for open ai model was not retrieved")) - } - } - fn truncate( - &self, - content: &str, - length: usize, - direction: TruncationDirection, - ) -> anyhow::Result { - if let Some(bpe) = &self.bpe { - let tokens = bpe.encode_with_special_tokens(content); - if tokens.len() > length { - match direction { - TruncationDirection::End => bpe.decode(tokens[..length].to_vec()), - TruncationDirection::Start => bpe.decode(tokens[length..].to_vec()), - } - } else { - bpe.decode(tokens) - } - } else { - Err(anyhow!("bpe for open ai model was not retrieved")) - } - } - fn capacity(&self) -> anyhow::Result { - anyhow::Ok(tiktoken_rs::model::get_context_size(&self.name)) - } -} diff --git a/crates/ai2/src/providers/open_ai/new.rs b/crates/ai2/src/providers/open_ai/new.rs deleted file mode 100644 index c7d67f2ba1..0000000000 --- a/crates/ai2/src/providers/open_ai/new.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub trait LanguageModel { - fn name(&self) -> String; - fn count_tokens(&self, content: &str) -> anyhow::Result; - fn truncate( - &self, - content: &str, - length: usize, - direction: TruncationDirection, - ) -> anyhow::Result; - fn capacity(&self) -> anyhow::Result; -} diff --git a/crates/ai2/src/test.rs b/crates/ai2/src/test.rs deleted file mode 100644 index 3d59febbe9..0000000000 --- a/crates/ai2/src/test.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{ - sync::atomic::{self, AtomicUsize, Ordering}, - time::Instant, -}; - -use async_trait::async_trait; -use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; -use gpui::AppContext; -use parking_lot::Mutex; - -use crate::{ - auth::{CredentialProvider, ProviderCredential}, - completion::{CompletionProvider, CompletionRequest}, - embedding::{Embedding, EmbeddingProvider}, - models::{LanguageModel, TruncationDirection}, -}; - -#[derive(Clone)] -pub struct FakeLanguageModel { - pub capacity: usize, -} - -impl LanguageModel for FakeLanguageModel { - fn name(&self) -> String { - "dummy".to_string() - } - fn count_tokens(&self, content: &str) -> anyhow::Result { - anyhow::Ok(content.chars().collect::>().len()) - } - fn truncate( - &self, - content: &str, - length: usize, - direction: TruncationDirection, - ) -> anyhow::Result { - println!("TRYING TO TRUNCATE: {:?}", length.clone()); - - if length > self.count_tokens(content)? { - println!("NOT TRUNCATING"); - return anyhow::Ok(content.to_string()); - } - - anyhow::Ok(match direction { - TruncationDirection::End => content.chars().collect::>()[..length] - .into_iter() - .collect::(), - TruncationDirection::Start => content.chars().collect::>()[length..] - .into_iter() - .collect::(), - }) - } - fn capacity(&self) -> anyhow::Result { - anyhow::Ok(self.capacity) - } -} - -pub struct FakeEmbeddingProvider { - pub embedding_count: AtomicUsize, -} - -impl Clone for FakeEmbeddingProvider { - fn clone(&self) -> Self { - FakeEmbeddingProvider { - embedding_count: AtomicUsize::new(self.embedding_count.load(Ordering::SeqCst)), - } - } -} - -impl Default for FakeEmbeddingProvider { - fn default() -> Self { - FakeEmbeddingProvider { - embedding_count: AtomicUsize::default(), - } - } -} - -impl FakeEmbeddingProvider { - pub fn embedding_count(&self) -> usize { - self.embedding_count.load(atomic::Ordering::SeqCst) - } - - pub fn embed_sync(&self, span: &str) -> Embedding { - let mut result = vec![1.0; 26]; - for letter in span.chars() { - let letter = letter.to_ascii_lowercase(); - if letter as u32 >= 'a' as u32 { - let ix = (letter as u32) - ('a' as u32); - if ix < 26 { - result[ix as usize] += 1.0; - } - } - } - - let norm = result.iter().map(|x| x * x).sum::().sqrt(); - for x in &mut result { - *x /= norm; - } - - result.into() - } -} - -impl CredentialProvider for FakeEmbeddingProvider { - fn has_credentials(&self) -> bool { - true - } - fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { - ProviderCredential::NotNeeded - } - fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} - fn delete_credentials(&self, _cx: &mut AppContext) {} -} - -#[async_trait] -impl EmbeddingProvider for FakeEmbeddingProvider { - fn base_model(&self) -> Box { - Box::new(FakeLanguageModel { capacity: 1000 }) - } - fn max_tokens_per_batch(&self) -> usize { - 1000 - } - - fn rate_limit_expiration(&self) -> Option { - None - } - - async fn embed_batch(&self, spans: Vec) -> anyhow::Result> { - self.embedding_count - .fetch_add(spans.len(), atomic::Ordering::SeqCst); - - anyhow::Ok(spans.iter().map(|span| self.embed_sync(span)).collect()) - } -} - -pub struct FakeCompletionProvider { - last_completion_tx: Mutex>>, -} - -impl Clone for FakeCompletionProvider { - fn clone(&self) -> Self { - Self { - last_completion_tx: Mutex::new(None), - } - } -} - -impl FakeCompletionProvider { - pub fn new() -> Self { - Self { - last_completion_tx: Mutex::new(None), - } - } - - pub fn send_completion(&self, completion: impl Into) { - let mut tx = self.last_completion_tx.lock(); - tx.as_mut().unwrap().try_send(completion.into()).unwrap(); - } - - pub fn finish_completion(&self) { - self.last_completion_tx.lock().take().unwrap(); - } -} - -impl CredentialProvider for FakeCompletionProvider { - fn has_credentials(&self) -> bool { - true - } - fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential { - ProviderCredential::NotNeeded - } - fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {} - fn delete_credentials(&self, _cx: &mut AppContext) {} -} - -impl CompletionProvider for FakeCompletionProvider { - fn base_model(&self) -> Box { - let model: Box = Box::new(FakeLanguageModel { capacity: 8190 }); - model - } - fn complete( - &self, - _prompt: Box, - ) -> BoxFuture<'static, anyhow::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 box_clone(&self) -> Box { - Box::new((*self).clone()) - } -} diff --git a/crates/zed_actions2/Cargo.toml b/crates/assets/Cargo.toml similarity index 71% rename from crates/zed_actions2/Cargo.toml rename to crates/assets/Cargo.toml index b3b5b4ce57..7ebae21d7d 100644 --- a/crates/zed_actions2/Cargo.toml +++ b/crates/assets/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "zed_actions2" +name = "assets" version = "0.1.0" edition = "2021" publish = false @@ -7,5 +7,6 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -gpui = { package = "gpui2", path = "../gpui2" } -serde.workspace = true +gpui = {path = "../gpui"} +rust-embed.workspace = true +anyhow.workspace = true diff --git a/crates/zed2/src/assets.rs b/crates/assets/src/lib.rs similarity index 82% rename from crates/zed2/src/assets.rs rename to crates/assets/src/lib.rs index 5d5e81a60e..010b7ebda3 100644 --- a/crates/zed2/src/assets.rs +++ b/crates/assets/src/lib.rs @@ -1,3 +1,4 @@ +// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build. use anyhow::anyhow; use gpui::{AssetSource, Result, SharedString}; diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 876e5e0b76..9588932c25 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -18,13 +18,14 @@ gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } multi_buffer = { path = "../multi_buffer" } +project = { path = "../project" } search = { path = "../search" } +semantic_index = { path = "../semantic_index" } settings = { path = "../settings" } theme = { path = "../theme" } +ui = { path = "../ui" } util = { path = "../util" } workspace = { path = "../workspace" } -semantic_index = { path = "../semantic_index" } -project = { path = "../project" } uuid.workspace = true log.workspace = true @@ -43,9 +44,9 @@ smol.workspace = true tiktoken-rs.workspace = true [dev-dependencies] +ai = { path = "../ai", features = ["test-support"]} editor = { path = "../editor", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } -ai = { path = "../ai", features = ["test-support"]} ctor.workspace = true env_logger.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 91d61a19f9..743c8b22e6 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -12,12 +12,28 @@ use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; use futures::StreamExt; -use gpui::AppContext; +use gpui::{actions, AppContext, SharedString}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; use util::paths::CONVERSATIONS_DIR; +actions!( + assistant, + [ + NewConversation, + Assist, + Split, + CycleMessageRole, + QuoteSelection, + ToggleFocus, + ResetKey, + InlineAssist, + ToggleIncludeConversation, + ToggleRetrieveContext, + ] +); + #[derive( Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] @@ -34,7 +50,7 @@ struct MessageMetadata { enum MessageStatus { Pending, Done, - Error(Arc), + Error(SharedString), } #[derive(Serialize, Deserialize)] diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e472e8c8df..d4743afb71 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2,8 +2,9 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, prompts::generate_content_prompt, - MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, - SavedMessage, + Assist, CycleMessageRole, InlineAssist, MessageId, MessageMetadata, MessageStatus, + NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext, }; use ai::{ @@ -15,35 +16,31 @@ use ai::{ use ai::prompts::repository_context::PromptCodeSnippet; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use client::{telemetry::AssistantKind, TelemetrySettings}; +use client::telemetry::AssistantKind; 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, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, + ToOffset, ToPoint, }; use fs::Fs; use futures::StreamExt; use gpui::{ - actions, - elements::{ - ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable, - Stack, Svg, Text, UniformList, UniformListState, - }, - fonts::HighlightStyle, - geometry::vector::{vec2f, Vector2F}, - platform::{CursorStyle, MouseButton, PromptLevel}, - Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, - ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, - WeakModelHandle, WeakViewHandle, WindowContext, + canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, + AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, FocusHandle, + FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, + ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, + StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, + View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; -use search::BufferSearchBar; +use search::{buffer_search::DivRegistrar, BufferSearchBar}; use semantic_index::{SemanticIndex, SemanticIndexStatus}; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use std::{ cell::Cell, cmp, @@ -55,105 +52,60 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use theme::{ - components::{action_button::Button, ComponentExt}, - AssistantStyle, +use theme::ThemeSettings; +use ui::{ + prelude::*, + utils::{DateTimeType, FormatDistance}, + ButtonLike, Tab, TabBar, Tooltip, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; use workspace::{ - dock::{DockPosition, Panel}, + dock::{DockPosition, Panel, PanelEvent}, searchable::Direction, Save, Toast, ToggleZoom, Toolbar, Workspace, }; -actions!( - assistant, - [ - NewConversation, - Assist, - Split, - CycleMessageRole, - QuoteSelection, - ToggleFocus, - ResetKey, - InlineAssist, - ToggleIncludeConversation, - ToggleRetrieveContext, - ] -); - pub fn init(cx: &mut AppContext) { - settings::register::(cx); - cx.add_action( - |this: &mut AssistantPanel, - _: &workspace::NewFile, - cx: &mut ViewContext| { - this.new_conversation(cx); + AssistantSettings::register(cx); + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace + .register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }) + .register_action(AssistantPanel::inline_assist) + .register_action(AssistantPanel::cancel_last_inline_assist) + .register_action(ConversationEditor::quote_selection); }, - ); - cx.add_action(ConversationEditor::assist); - cx.capture_action(ConversationEditor::cancel_last_assist); - cx.capture_action(ConversationEditor::save); - cx.add_action(ConversationEditor::quote_selection); - cx.capture_action(ConversationEditor::copy); - cx.add_action(ConversationEditor::split); - cx.capture_action(ConversationEditor::cycle_message_role); - cx.add_action(AssistantPanel::save_credentials); - cx.add_action(AssistantPanel::reset_credentials); - cx.add_action(AssistantPanel::toggle_zoom); - cx.add_action(AssistantPanel::deploy); - cx.add_action(AssistantPanel::select_next_match); - cx.add_action(AssistantPanel::select_prev_match); - cx.add_action(AssistantPanel::handle_editor_cancel); - cx.add_action( - |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_panel_focus::(cx); - }, - ); - cx.add_action(AssistantPanel::inline_assist); - cx.add_action(AssistantPanel::cancel_last_inline_assist); - cx.add_action(InlineAssistant::confirm); - cx.add_action(InlineAssistant::cancel); - cx.add_action(InlineAssistant::toggle_include_conversation); - cx.add_action(InlineAssistant::toggle_retrieve_context); - cx.add_action(InlineAssistant::move_up); - cx.add_action(InlineAssistant::move_down); -} - -#[derive(Debug)] -pub enum AssistantPanelEvent { - ZoomIn, - ZoomOut, - Focus, - Close, - DockPositionChanged, + ) + .detach(); } pub struct AssistantPanel { - workspace: WeakViewHandle, - width: Option, - height: Option, + workspace: WeakView, + width: Option, + height: Option, active_editor_index: Option, prev_active_editor_index: Option, - editors: Vec>, + editors: Vec>, saved_conversations: Vec, - saved_conversations_list_state: UniformListState, + saved_conversations_scroll_handle: UniformListScrollHandle, zoomed: bool, - has_focus: bool, - toolbar: ViewHandle, + focus_handle: FocusHandle, + toolbar: View, completion_provider: Arc, - api_key_editor: Option>, + api_key_editor: Option>, languages: Arc, fs: Arc, subscriptions: Vec, next_inline_assist_id: usize, pending_inline_assists: HashMap, - pending_inline_assist_ids_by_editor: HashMap, Vec>, + pending_inline_assist_ids_by_editor: HashMap, Vec>, include_conversation_in_next_inline_assist: bool, inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, - semantic_index: Option>, + semantic_index: Option>, retrieve_context_in_next_inline_assist: bool, } @@ -161,11 +113,11 @@ impl AssistantPanel { const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; pub fn load( - workspace: WeakViewHandle, - cx: AsyncAppContext, - ) -> Task>> { + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { cx.spawn(|mut cx| async move { - let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; let saved_conversations = SavedConversationMetadata::list(fs.clone()) .await .log_err() @@ -174,7 +126,7 @@ impl AssistantPanel { // TODO: deserialize state. let workspace_handle = workspace.clone(); workspace.update(&mut cx, |workspace, cx| { - cx.add_view::(|cx| { + cx.new_view::(|cx| { const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { let mut events = fs @@ -195,10 +147,10 @@ impl AssistantPanel { anyhow::Ok(()) }); - let toolbar = cx.add_view(|cx| { + let toolbar = cx.new_view(|cx| { let mut toolbar = Toolbar::new(); toolbar.set_can_navigate(false, cx); - toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); + toolbar.add_item(cx.new_view(|cx| BufferSearchBar::new(cx)), cx); toolbar }); @@ -206,18 +158,22 @@ impl AssistantPanel { // Defaulting currently to GPT4, allow for this to be set via config. let completion_provider = Arc::new(OpenAICompletionProvider::new( "gpt-4", - cx.background().clone(), + cx.background_executor().clone(), )); + let focus_handle = cx.focus_handle(); + cx.on_focus_in(&focus_handle, Self::focus_in).detach(); + cx.on_focus_out(&focus_handle, Self::focus_out).detach(); + let mut this = Self { workspace: workspace_handle, active_editor_index: Default::default(), prev_active_editor_index: Default::default(), editors: Default::default(), saved_conversations, - saved_conversations_list_state: Default::default(), + saved_conversations_scroll_handle: Default::default(), zoomed: false, - has_focus: false, + focus_handle, toolbar, completion_provider, api_key_editor: None, @@ -238,11 +194,11 @@ impl AssistantPanel { let mut old_dock_position = this.position(cx); this.subscriptions = - vec![cx.observe_global::(move |this, cx| { + vec![cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; - cx.emit(AssistantPanelEvent::DockPositionChanged); + cx.emit(PanelEvent::ChangePosition); } cx.notify(); })]; @@ -253,6 +209,25 @@ impl AssistantPanel { }) } + fn focus_in(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); + cx.notify(); + if self.focus_handle.is_focused(cx) { + if let Some(editor) = self.active_editor() { + cx.focus_view(editor); + } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { + cx.focus_view(api_key_editor); + } + } + } + + fn focus_out(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); + cx.notify(); + } + pub fn inline_assist( workspace: &mut Workspace, _: &InlineAssist, @@ -293,9 +268,9 @@ impl AssistantPanel { fn new_inline_assist( &mut self, - editor: &ViewHandle, + editor: &View, cx: &mut ViewContext, - project: &ModelHandle, + project: &Model, ) { let selection = editor.read(cx).selections.newest_anchor().clone(); if selection.start.excerpt_id != selection.end.excerpt_id { @@ -331,7 +306,7 @@ impl AssistantPanel { // Retrieve Credentials Authenticates the Provider provider.retrieve_credentials(cx); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); @@ -341,14 +316,14 @@ impl AssistantPanel { let previously_indexed = semantic_index .update(&mut cx, |index, cx| { index.project_previously_indexed(&project, cx) - }) + })? .await .unwrap_or(false); if previously_indexed { let _ = semantic_index .update(&mut cx, |index, cx| { index.index_project(project.clone(), cx) - }) + })? .await; } anyhow::Ok(()) @@ -357,8 +332,8 @@ impl AssistantPanel { } let measurements = Rc::new(Cell::new(BlockMeasurements::default())); - let inline_assistant = cx.add_view(|cx| { - let assistant = InlineAssistant::new( + let inline_assistant = cx.new_view(|cx| { + InlineAssistant::new( inline_assist_id, measurements.clone(), self.include_conversation_in_next_inline_assist, @@ -369,9 +344,7 @@ impl AssistantPanel { self.retrieve_context_in_next_inline_assist, self.semantic_index.clone(), project.clone(), - ); - cx.focus_self(); - assistant + ) }); let block_id = editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |selections| { @@ -389,7 +362,7 @@ impl AssistantPanel { anchor_x: cx.anchor_x, gutter_width: cx.gutter_width, }); - ChildView::new(&inline_assistant, cx).into_any() + inline_assistant.clone().into_any_element() } }), disposition: if selection.reversed { @@ -415,10 +388,12 @@ impl AssistantPanel { cx.subscribe(editor, { let inline_assistant = inline_assistant.downgrade(); move |_, editor, event, cx| { - if let Some(inline_assistant) = inline_assistant.upgrade(cx) { - if let editor::Event::SelectionsChanged { local } = event { - if *local && inline_assistant.read(cx).has_focus { - cx.focus(&editor); + if let Some(inline_assistant) = inline_assistant.upgrade() { + if let EditorEvent::SelectionsChanged { local } = event { + if *local + && inline_assistant.focus_handle(cx).contains_focused(cx) + { + cx.focus_view(&editor); } } } @@ -427,7 +402,7 @@ impl AssistantPanel { cx.observe(&codegen, { let editor = editor.downgrade(); move |this, _, cx| { - if let Some(editor) = editor.upgrade(cx) { + if let Some(editor) = editor.upgrade() { this.update_highlights_for_editor(&editor, cx); } } @@ -451,7 +426,7 @@ impl AssistantPanel { .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) { + if let Some(workspace) = this.workspace.upgrade() { workspace.update(cx, |workspace, cx| { workspace.show_toast( Toast::new(inline_assist_id, error), @@ -479,7 +454,7 @@ impl AssistantPanel { fn handle_inline_assistant_event( &mut self, - inline_assistant: ViewHandle, + inline_assistant: View, event: &InlineAssistantEvent, cx: &mut ViewContext, ) { @@ -543,7 +518,7 @@ impl AssistantPanel { } } - cx.propagate_action(); + cx.propagate(); } fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { @@ -552,7 +527,7 @@ impl AssistantPanel { if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { if let hash_map::Entry::Occupied(mut entry) = self .pending_inline_assist_ids_by_editor - .entry(pending_assist.editor) + .entry(pending_assist.editor.clone()) { entry.get_mut().retain(|id| *id != assist_id); if entry.get().is_empty() { @@ -560,7 +535,7 @@ impl AssistantPanel { } } - if let Some(editor) = pending_assist.editor.upgrade(cx) { + if let Some(editor) = pending_assist.editor.upgrade() { self.update_highlights_for_editor(&editor, cx); if undo { @@ -574,10 +549,13 @@ impl AssistantPanel { fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { - if let Some(editor) = pending_assist.editor.upgrade(cx) { - if let Some((block_id, _)) = pending_assist.inline_assistant.take() { + if let Some(editor) = pending_assist.editor.upgrade() { + if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() { editor.update(cx, |editor, cx| { editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + if inline_assistant.focus_handle(cx).contains_focused(cx) { + editor.focus(cx); + } }); } } @@ -606,7 +584,7 @@ impl AssistantPanel { return; }; - let editor = if let Some(editor) = pending_assist.editor.upgrade(cx) { + let editor = if let Some(editor) = pending_assist.editor.upgrade() { editor } else { return; @@ -614,7 +592,7 @@ impl AssistantPanel { let project = pending_assist.project.clone(); - let project_name = if let Some(project) = project.upgrade(cx) { + let project_name = if let Some(project) = project.upgrade() { Some( project .read(cx) @@ -679,7 +657,7 @@ impl AssistantPanel { let user_prompt = user_prompt.to_string(); let snippets = if retrieve_context { - let Some(project) = project.upgrade(cx) else { + let Some(project) = project.upgrade() else { return; }; @@ -688,31 +666,35 @@ impl AssistantPanel { this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) }); - cx.background() + cx.background_executor() .spawn(async move { search_results.await.unwrap_or_default() }) } else { Task::ready(Vec::new()) }; - let snippets = cx.spawn(|_, cx| async move { + let snippets = cx.spawn(|_, mut cx| async move { let mut snippets = Vec::new(); for result in search_results.await { - snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &cx)); + snippets.push(PromptCodeSnippet::new( + result.buffer, + result.range, + &mut cx, + )?); } - snippets + anyhow::Ok(snippets) }); snippets } else { - Task::ready(Vec::new()) + Task::ready(Ok(Vec::new())) }; - let mut model = settings::get::(cx) + let mut model = AssistantSettings::get_global(cx) .default_open_ai_model .clone(); let model_name = model.full_name(); - let prompt = cx.background().spawn(async move { - let snippets = snippets.await; + let prompt = cx.background_executor().spawn(async move { + let snippets = snippets.await?; let language_name = language_name.as_deref(); generate_content_prompt( @@ -755,17 +737,13 @@ impl AssistantPanel { temperature, }); - codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?; anyhow::Ok(()) }) .detach(); } - fn update_highlights_for_editor( - &self, - editor: &ViewHandle, - cx: &mut ViewContext, - ) { + fn update_highlights_for_editor(&self, editor: &View, cx: &mut ViewContext) { let mut background_ranges = Vec::new(); let mut foreground_ranges = Vec::new(); let empty_inline_assist_ids = Vec::new(); @@ -791,7 +769,7 @@ impl AssistantPanel { } else { editor.highlight_background::( background_ranges, - |theme| theme.assistant.inline.pending_edit_background, + |theme| theme.editor_active_line_background, // todo!("use the appropriate color") cx, ); } @@ -811,8 +789,8 @@ impl AssistantPanel { }); } - fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { - let editor = cx.add_view(|cx| { + fn new_conversation(&mut self, cx: &mut ViewContext) -> View { + let editor = cx.new_view(|cx| { ConversationEditor::new( self.completion_provider.clone(), self.languages.clone(), @@ -825,11 +803,7 @@ impl AssistantPanel { editor } - fn add_conversation( - &mut self, - editor: ViewHandle, - cx: &mut ViewContext, - ) { + fn add_conversation(&mut self, editor: View, cx: &mut ViewContext) { self.subscriptions .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); @@ -850,8 +824,8 @@ impl AssistantPanel { self.toolbar.update(cx, |toolbar, cx| { toolbar.set_active_item(Some(&editor), cx); }); - if self.has_focus(cx) { - cx.focus(&editor); + if self.focus_handle.contains_focused(cx) { + cx.focus_view(&editor); } } else { self.toolbar.update(cx, |toolbar, cx| { @@ -864,7 +838,7 @@ impl AssistantPanel { fn handle_conversation_editor_event( &mut self, - _: ViewHandle, + _: View, event: &ConversationEditorEvent, cx: &mut ViewContext, ) { @@ -887,45 +861,46 @@ impl AssistantPanel { self.completion_provider.save_credentials(cx, credential); self.api_key_editor.take(); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } } else { - cx.propagate_action(); + cx.propagate(); } } fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { self.completion_provider.delete_credentials(cx); self.api_key_editor = Some(build_api_key_editor(cx)); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { if self.zoomed { - cx.emit(AssistantPanelEvent::ZoomOut) + cx.emit(PanelEvent::ZoomOut) } else { - cx.emit(AssistantPanelEvent::ZoomIn) + cx.emit(PanelEvent::ZoomIn) } } fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { - let mut propagate_action = true; + let mut propagate = true; if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { if search_bar.show(cx) { search_bar.search_suggested(cx); if action.focus { + let focus_handle = search_bar.focus_handle(cx); search_bar.select_query(cx); - cx.focus_self(); + cx.focus(&focus_handle); } - propagate_action = false + propagate = false } }); } - if propagate_action { - cx.propagate_action(); + if propagate { + cx.propagate(); } } @@ -938,7 +913,7 @@ impl AssistantPanel { return; } } - cx.propagate_action(); + cx.propagate(); } fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { @@ -953,201 +928,125 @@ impl AssistantPanel { } } - fn active_editor(&self) -> Option<&ViewHandle> { + fn active_editor(&self) -> Option<&View> { self.editors.get(self.active_editor_index?) } - fn render_hamburger_button(cx: &mut ViewContext) -> impl Element { - enum History {} - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.hamburger_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if this.active_editor().is_some() { - this.set_active_editor_index(None, cx); - } else { - this.set_active_editor_index(this.prev_active_editor_index, cx); - } - }) - .with_tooltip::(1, "History", None, tooltip_style, cx) + fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("hamburger_button", IconName::Menu) + .on_click(cx.listener(|this, _event, cx| { + if this.active_editor().is_some() { + this.set_active_editor_index(None, cx); + } else { + this.set_active_editor_index(this.prev_active_editor_index, cx); + } + })) + .tooltip(|cx| Tooltip::text("Conversation History", cx)) } - fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec> { + fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { if self.active_editor().is_some() { vec![ - Self::render_split_button(cx).into_any(), - Self::render_quote_button(cx).into_any(), - Self::render_assist_button(cx).into_any(), + Self::render_split_button(cx).into_any_element(), + Self::render_quote_button(cx).into_any_element(), + Self::render_assist_button(cx).into_any_element(), ] } else { Default::default() } } - fn render_split_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.split_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); - } - }) - .with_tooltip::( - 1, - "Split Message", - Some(Box::new(Split)), - tooltip_style, - cx, - ) + fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("split_button", IconName::Snip) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); + } + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx)) } - fn render_assist_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.assist_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); - } - }) - .with_tooltip::(1, "Assist", Some(Box::new(Assist)), tooltip_style, cx) + fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("assist_button", IconName::MagicWand) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); + } + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx)) } - fn render_quote_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.quote_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - ConversationEditor::quote_selection(workspace, &Default::default(), cx) + fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("quote_button", IconName::Quote) + .on_click(cx.listener(|this, _event, cx| { + if let Some(workspace) = this.workspace.upgrade() { + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + ConversationEditor::quote_selection(workspace, &Default::default(), cx) + }); }); - }); - } - }) - .with_tooltip::( - 1, - "Quote Selection", - Some(Box::new(QuoteSelection)), - tooltip_style, - cx, - ) + } + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("Quote Selection", &QuoteSelection, cx)) } - fn render_plus_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.plus_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - this.new_conversation(cx); - }) - .with_tooltip::( - 1, - "New Conversation", - Some(Box::new(NewConversation)), - tooltip_style, - cx, - ) + fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("plus_button", IconName::Plus) + .on_click(cx.listener(|this, _event, cx| { + this.new_conversation(cx); + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx)) } - fn render_zoom_button(&self, cx: &mut ViewContext) -> impl Element { - enum ToggleZoomButton {} - - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - let style = if self.zoomed { - &theme.assistant.zoom_out_button - } else { - &theme.assistant.zoom_in_button - }; - - MouseEventHandler::new::(0, cx, |state, _| { - let style = style.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.toggle_zoom(&ToggleZoom, cx); - }) - .with_tooltip::( - 0, - if self.zoomed { "Zoom Out" } else { "Zoom In" }, - Some(Box::new(ToggleZoom)), - tooltip_style, - cx, - ) + fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { + let zoomed = self.zoomed; + IconButton::new("zoom_button", IconName::Maximize) + .on_click(cx.listener(|this, _event, cx| { + this.toggle_zoom(&ToggleZoom, cx); + })) + .selected(zoomed) + .selected_icon(IconName::Minimize) + .icon_size(IconSize::Small) + .tooltip(move |cx| { + Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) + }) } fn render_saved_conversation( &mut self, index: usize, cx: &mut ViewContext, - ) -> impl Element { + ) -> impl IntoElement { let conversation = &self.saved_conversations[index]; let path = conversation.path.clone(); - MouseEventHandler::new::(index, cx, move |state, cx| { - let style = &theme::current(cx).assistant.saved_conversation; - Flex::row() - .with_child( - Label::new( - conversation.mtime.format("%F %I:%M%p").to_string(), - style.saved_at.text.clone(), + + ButtonLike::new(index) + .on_click(cx.listener(move |this, _, cx| { + this.open_conversation(path.clone(), cx) + .detach_and_log_err(cx) + })) + .full_width() + .child( + div() + .flex() + .w_full() + .gap_2() + .child( + Label::new(conversation.mtime.format("%F %I:%M%p").to_string()) + .color(Color::Muted) + .size(LabelSize::Small), ) - .aligned() - .contained() - .with_style(style.saved_at.container), - ) - .with_child( - Label::new(conversation.title.clone(), style.title.text.clone()) - .aligned() - .contained() - .with_style(style.title.container), - ) - .contained() - .with_style(*style.container.style_for(state)) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.open_conversation(path.clone(), cx) - .detach_and_log_err(cx) - }) + .child(Label::new(conversation.title.clone()).size(LabelSize::Small)), + ) } fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { + cx.focus(&self.focus_handle); + if let Some(ix) = self.editor_index_for_path(&path, cx) { self.set_active_editor_index(Some(ix), cx); return Task::ready(Ok(())); @@ -1159,16 +1058,16 @@ impl AssistantPanel { cx.spawn(|this, mut cx| async move { let saved_conversation = fs.load(&path).await?; let saved_conversation = serde_json::from_str(&saved_conversation)?; - let conversation = cx.add_model(|cx| { + let conversation = cx.new_model(|cx| { Conversation::deserialize(saved_conversation, path.clone(), languages, cx) - }); + })?; this.update(&mut cx, |this, cx| { // If, by the time we've loaded the conversation, the user has already opened // the same conversation, we don't want to open it again. 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| { + let editor = cx.new_view(|cx| { ConversationEditor::for_conversation(conversation, fs, workspace, cx) }); this.add_conversation(editor, cx); @@ -1193,168 +1092,142 @@ impl AssistantPanel { } } -fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { - cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), - cx, - ); +fn build_api_key_editor(cx: &mut ViewContext) -> View { + cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); editor }) } -impl Entity for AssistantPanel { - type Event = AssistantPanelEvent; -} - -impl View for AssistantPanel { - fn ui_name() -> &'static str { - "AssistantPanel" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx); - let style = &theme.assistant; - if let Some(api_key_editor) = self.api_key_editor.as_ref() { - Flex::column() - .with_child( - Text::new( - "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", - style.api_key_prompt.text.clone(), - ), - ) - .with_child( - Text::new( - " - Having a subscription for another service like GitHub Copilot won't work.", - style.api_key_prompt.text.clone(), - ), - ) - .with_child( - Text::new( - " - You can create a api key at: platform.openai.com/api-keys", - style.api_key_prompt.text.clone(), - ), - ) - .with_child( - Text::new( - " ", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .with_child( - Text::new( - "Paste your OpenAI API key and press Enter to use the assistant", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .with_child( - ChildView::new(api_key_editor, cx) - .contained() - .with_style(style.api_key_editor.container) - .aligned(), - ) - .with_child( - Text::new( - " ", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .with_child( - Text::new( - "Click on the Z button in the status bar to close this panel.", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .contained() - .with_style(style.api_key_prompt.container) - .aligned() - .into_any() +impl Render for AssistantPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + if let Some(api_key_editor) = self.api_key_editor.clone() { + v_stack() + .on_action(cx.listener(AssistantPanel::save_credentials)) + .track_focus(&self.focus_handle) + .child(Label::new( + "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", + )) + .child(Label::new( + " - Having a subscription for another service like GitHub Copilot won't work." + )) + .child(Label::new( + " - You can create a api key at: platform.openai.com/api-keys" + )) + .child(Label::new( + " " + )) + .child(Label::new( + "Paste your OpenAI API key and press Enter to use the assistant" + )) + .child(api_key_editor) + .child(Label::new( + "Click on the Z button in the status bar to close this panel." + )) } else { - let title = self.active_editor().map(|editor| { - Label::new(editor.read(cx).title(cx), style.title.text.clone()) - .contained() - .with_style(style.title.container) - .aligned() - .left() - .flex(1., false) - }); - let mut header = Flex::row() - .with_child(Self::render_hamburger_button(cx).aligned()) - .with_children(title); - if self.has_focus { - header.add_children( - self.render_editor_tools(cx) - .into_iter() - .map(|tool| tool.aligned().flex_float()), - ); - header.add_child(Self::render_plus_button(cx).aligned().flex_float()); - header.add_child(self.render_zoom_button(cx).aligned()); - } - - Flex::column() - .with_child( - header - .contained() - .with_style(theme.workspace.tab_bar.container) - .expanded() - .constrained() - .with_height(theme.workspace.tab_bar.height), + let header = TabBar::new("assistant_header") + .start_child( + h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), ) - .with_children(if self.toolbar.read(cx).hidden() { + .children(self.active_editor().map(|editor| { + h_stack() + .h(rems(Tab::HEIGHT_IN_REMS)) + .flex_1() + .px_2() + .child(Label::new(editor.read(cx).title(cx)).into_element()) + })) + .end_child(if self.focus_handle.contains_focused(cx) { + h_stack() + .gap_2() + .child(h_stack().gap_1().children(self.render_editor_tools(cx))) + .child( + ui::Divider::vertical() + .inset() + .color(ui::DividerColor::Border), + ) + .child( + h_stack() + .gap_1() + .child(Self::render_plus_button(cx)) + .child(self.render_zoom_button(cx)), + ) + } else { + div() + }); + + let contents = if self.active_editor().is_some() { + let mut registrar = DivRegistrar::new( + |panel, cx| panel.toolbar.read(cx).item_of_type::(), + cx, + ); + BufferSearchBar::register_inner(&mut registrar); + registrar.into_div() + } else { + div() + }; + v_stack() + .key_context("AssistantPanel") + .size_full() + .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { + this.new_conversation(cx); + })) + .on_action(cx.listener(AssistantPanel::reset_credentials)) + .on_action(cx.listener(AssistantPanel::toggle_zoom)) + .on_action(cx.listener(AssistantPanel::deploy)) + .on_action(cx.listener(AssistantPanel::select_next_match)) + .on_action(cx.listener(AssistantPanel::select_prev_match)) + .on_action(cx.listener(AssistantPanel::handle_editor_cancel)) + .track_focus(&self.focus_handle) + .child(header) + .children(if self.toolbar.read(cx).hidden() { None } else { - Some(ChildView::new(&self.toolbar, cx).expanded()) + Some(self.toolbar.clone()) }) - .with_child(if let Some(editor) = self.active_editor() { - ChildView::new(editor, cx).flex(1., true).into_any() - } else { - UniformList::new( - self.saved_conversations_list_state.clone(), - self.saved_conversations.len(), - cx, - |this, range, items, cx| { - for ix in range { - items.push(this.render_saved_conversation(ix, cx).into_any()); - } - }, - ) - .flex(1., true) - .into_any() - }) - .into_any() + .child( + contents + .flex_1() + .child(if let Some(editor) = self.active_editor() { + editor.clone().into_any_element() + } else { + let view = cx.view().clone(); + let scroll_handle = self.saved_conversations_scroll_handle.clone(); + let conversation_count = self.saved_conversations.len(); + canvas(move |bounds, cx| { + uniform_list( + view, + "saved_conversations", + conversation_count, + |this, range, cx| { + range + .map(|ix| this.render_saved_conversation(ix, cx)) + .collect() + }, + ) + .track_scroll(scroll_handle) + .into_any_element() + .draw( + bounds.origin, + bounds.size.map(AvailableSpace::Definite), + cx, + ); + }) + .size_full() + .into_any_element() + }), + ) } } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); - cx.notify(); - if cx.is_self_focused() { - if let Some(editor) = self.active_editor() { - cx.focus(editor); - } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { - cx.focus(api_key_editor); - } - } - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = false; - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); - cx.notify(); - } } impl Panel for AssistantPanel { + fn persistent_name() -> &'static str { + "AssistantPanel" + } + fn position(&self, cx: &WindowContext) -> DockPosition { - match settings::get::(cx).dock { + match AssistantSettings::get_global(cx).dock { AssistantDockPosition::Left => DockPosition::Left, AssistantDockPosition::Bottom => DockPosition::Bottom, AssistantDockPosition::Right => DockPosition::Right, @@ -1376,8 +1249,8 @@ impl Panel for AssistantPanel { }); } - fn size(&self, cx: &WindowContext) -> f32 { - let settings = settings::get::(cx); + fn size(&self, cx: &WindowContext) -> Pixels { + let settings = AssistantSettings::get_global(cx); match self.position(cx) { DockPosition::Left | DockPosition::Right => { self.width.unwrap_or_else(|| settings.default_width) @@ -1386,7 +1259,7 @@ impl Panel for AssistantPanel { } } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { match self.position(cx) { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, @@ -1394,14 +1267,6 @@ impl Panel for AssistantPanel { cx.notify(); } - fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool { - matches!(event, AssistantPanelEvent::ZoomIn) - } - - fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool { - matches!(event, AssistantPanelEvent::ZoomOut) - } - fn is_zoomed(&self, _: &WindowContext) -> bool { self.zoomed } @@ -1421,34 +1286,24 @@ impl Panel for AssistantPanel { } } - fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { - settings::get::(cx) - .button - .then(|| "icons/ai.svg") + fn icon(&self, cx: &WindowContext) -> Option { + Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button) } - fn icon_tooltip(&self) -> (String, Option>) { - ("Assistant Panel".into(), Some(Box::new(ToggleFocus))) + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + Some("Assistant Panel") } - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, AssistantPanelEvent::DockPositionChanged) + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) } +} - fn should_activate_on_event(_: &Self::Event) -> bool { - false - } +impl EventEmitter for AssistantPanel {} - fn should_close_on_event(event: &AssistantPanelEvent) -> bool { - matches!(event, AssistantPanelEvent::Close) - } - - fn has_focus(&self, _: &WindowContext) -> bool { - self.has_focus - } - - fn is_focus_event(event: &Self::Event) -> bool { - matches!(event, AssistantPanelEvent::Focus) +impl FocusableView for AssistantPanel { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } @@ -1466,7 +1321,7 @@ struct Summary { struct Conversation { id: Option, - buffer: ModelHandle, + buffer: Model, message_anchors: Vec, messages_metadata: HashMap, next_message_id: MessageId, @@ -1484,9 +1339,7 @@ struct Conversation { completion_provider: Arc, } -impl Entity for Conversation { - type Event = ConversationEvent; -} +impl EventEmitter for Conversation {} impl Conversation { fn new( @@ -1495,24 +1348,21 @@ impl Conversation { completion_provider: Arc, ) -> Self { let markdown = language_registry.language_for_name("Markdown"); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); + let buffer = cx.new_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), ""); buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); buffer }); - let settings = settings::get::(cx); + let settings = AssistantSettings::get_global(cx); let model = settings.default_open_ai_model.clone(); let mut this = Self { @@ -1587,14 +1437,14 @@ impl Conversation { }; let model = saved_conversation.model; let completion_provider: Arc = Arc::new( - OpenAICompletionProvider::new(model.full_name(), cx.background().clone()), + OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()), ); completion_provider.retrieve_credentials(cx); let markdown = language_registry.language_for_name("Markdown"); let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); + let buffer = cx.new_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -1603,14 +1453,11 @@ impl Conversation { next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); } buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -1645,7 +1492,7 @@ impl Conversation { fn handle_buffer_event( &mut self, - _: ModelHandle, + _: Model, event: &language::Event, cx: &mut ModelContext, ) { @@ -1681,24 +1528,24 @@ impl Conversation { }) .collect::>(); let model = self.model.clone(); - self.pending_token_count = cx.spawn_weak(|this, mut cx| { + self.pending_token_count = cx.spawn(|this, mut cx| { async move { - cx.background().timer(Duration::from_millis(200)).await; + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; let token_count = cx - .background() + .background_executor() .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) }) .await?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.max_token_count = - tiktoken_rs::model::get_context_size(&this.model.full_name()); - this.token_count = Some(token_count); - cx.notify() - }); + this.update(&mut cx, |this, cx| { + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); + this.token_count = Some(token_count); + cx.notify() + })?; anyhow::Ok(()) } .log_err() @@ -1785,7 +1632,7 @@ impl Conversation { .unwrap(); user_messages.push(user_message); - let task = cx.spawn_weak({ + let task = cx.spawn({ |this, mut cx| async move { let assistant_message_id = assistant_message.id; let stream_completion = async { @@ -1794,59 +1641,56 @@ impl Conversation { while let Some(message) = messages.next().await { let text = message?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - let message_ix = this - .message_anchors + this.update(&mut cx, |this, cx| { + let message_ix = this + .message_anchors + .iter() + .position(|message| message.id == assistant_message_id)?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] .iter() - .position(|message| message.id == assistant_message_id)?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message.start.to_offset(buffer).saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message.start.to_offset(buffer).saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + })?; smol::future::yield_now().await; } - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - }); + this.update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + })?; anyhow::Ok(()) }; let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = - MessageStatus::Error(error.to_string().trim().into()); - } + + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = MessageStatus::Error(SharedString::from( + error.to_string().trim().to_string(), + )); } - cx.notify(); } - }); - } + cx.notify(); + } + }) + .ok(); } }); @@ -2069,7 +1913,7 @@ impl Conversation { .text .push_str(&text); cx.emit(ConversationEvent::SummaryChanged); - }); + })?; } this.update(&mut cx, |this, cx| { @@ -2077,7 +1921,7 @@ impl Conversation { summary.done = true; cx.emit(ConversationEvent::SummaryChanged); } - }); + })?; anyhow::Ok(()) } @@ -2166,7 +2010,7 @@ impl Conversation { ) { self.pending_save = cx.spawn(|this, mut cx| async move { if let Some(debounce) = debounce { - cx.background().timer(debounce).await; + cx.background_executor().timer(debounce).await; } let (old_path, summary) = this.read_with(&cx, |this, _| { @@ -2181,10 +2025,10 @@ impl Conversation { None }; (path, summary) - }); + })?; if let Some(summary) = summary { - let conversation = this.read_with(&cx, |this, cx| this.serialize(cx)); + let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?; let path = if let Some(old_path) = old_path { old_path } else { @@ -2208,7 +2052,7 @@ impl Conversation { fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) .await?; - this.update(&mut cx, |this, _| this.path = Some(path)); + this.update(&mut cx, |this, _| this.path = Some(path))?; } Ok(()) @@ -2227,15 +2071,15 @@ enum ConversationEditorEvent { #[derive(Copy, Clone, Debug, PartialEq)] struct ScrollPosition { - offset_before_cursor: Vector2F, + offset_before_cursor: gpui::Point, cursor: Anchor, } struct ConversationEditor { - conversation: ModelHandle, + conversation: Model, fs: Arc, - workspace: WeakViewHandle, - editor: ViewHandle, + workspace: WeakView, + editor: View, blocks: HashSet, scroll_position: Option, _subscriptions: Vec, @@ -2246,21 +2090,21 @@ impl ConversationEditor { completion_provider: Arc, language_registry: Arc, fs: Arc, - workspace: WeakViewHandle, + workspace: WeakView, cx: &mut ViewContext, ) -> Self { let conversation = - cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider)); + cx.new_model(|cx| Conversation::new(language_registry, cx, completion_provider)); Self::for_conversation(conversation, fs, workspace, cx) } fn for_conversation( - conversation: ModelHandle, + conversation: Model, fs: Arc, - workspace: WeakViewHandle, + workspace: WeakView, cx: &mut ViewContext, ) -> Self { - let editor = cx.add_view(|cx| { + let editor = cx.new_view(|cx| { let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); @@ -2332,7 +2176,7 @@ impl ConversationEditor { .conversation .update(cx, |conversation, _| conversation.cancel_last_assist()) { - cx.propagate_action(); + cx.propagate(); } } @@ -2358,7 +2202,7 @@ impl ConversationEditor { fn handle_conversation_event( &mut self, - _: ModelHandle, + _: Model, event: &ConversationEvent, cx: &mut ViewContext, ) { @@ -2381,9 +2225,9 @@ impl ConversationEditor { let snapshot = editor.snapshot(cx); let cursor_point = scroll_position.cursor.to_display_point(&snapshot); let scroll_top = - cursor_point.row() as f32 - scroll_position.offset_before_cursor.y(); + cursor_point.row() as f32 - scroll_position.offset_before_cursor.y; editor.set_scroll_position( - vec2f(scroll_position.offset_before_cursor.x(), scroll_top), + point(scroll_position.offset_before_cursor.x, scroll_top), cx, ); } @@ -2394,12 +2238,12 @@ impl ConversationEditor { fn handle_editor_event( &mut self, - _: ViewHandle, - event: &editor::Event, + _: View, + event: &EditorEvent, cx: &mut ViewContext, ) { match event { - editor::Event::ScrollPositionChanged { autoscroll, .. } => { + EditorEvent::ScrollPositionChanged { autoscroll, .. } => { let cursor_scroll_position = self.cursor_scroll_position(cx); if *autoscroll { self.scroll_position = cursor_scroll_position; @@ -2407,7 +2251,7 @@ impl ConversationEditor { self.scroll_position = None; } } - editor::Event::SelectionsChanged { .. } => { + EditorEvent::SelectionsChanged { .. } => { self.scroll_position = self.cursor_scroll_position(cx); } _ => {} @@ -2424,14 +2268,11 @@ impl ConversationEditor { .anchor() .scroll_position(&snapshot.display_snapshot); - let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); - if (scroll_position.y()..scroll_bottom).contains(&cursor_row) { + let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); + if (scroll_position.y..scroll_bottom).contains(&cursor_row) { Some(ScrollPosition { cursor, - offset_before_cursor: vec2f( - scroll_position.x(), - cursor_row - scroll_position.y(), - ), + offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), }) } else { None @@ -2454,90 +2295,67 @@ impl ConversationEditor { style: BlockStyle::Sticky, render: Arc::new({ let conversation = self.conversation.clone(); - // let metadata = message.metadata.clone(); - // let message = message.clone(); - move |cx| { - enum Sender {} - enum ErrorTooltip {} - - let theme = theme::current(cx); - let style = &theme.assistant; + move |_cx| { let message_id = message.id; - let sender = MouseEventHandler::new::( - message_id.0, - cx, - |state, _| match message.role { - Role::User => { - let style = style.user_sender.style_for(state); - Label::new("You", style.text.clone()) - .contained() - .with_style(style.container) - } - Role::Assistant => { - let style = style.assistant_sender.style_for(state); - Label::new("Assistant", style.text.clone()) - .contained() - .with_style(style.container) - } - Role::System => { - let style = style.system_sender.style_for(state); - Label::new("System", style.text.clone()) - .contained() - .with_style(style.container) - } - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, { - let conversation = conversation.clone(); - move |_, _, cx| { - conversation.update(cx, |conversation, cx| { - conversation.cycle_message_roles( - HashSet::from_iter(Some(message_id)), - cx, - ) - }) - } - }); - - Flex::row() - .with_child(sender.aligned()) - .with_child( - Label::new( - message.sent_at.format("%I:%M%P").to_string(), - style.sent_at.text.clone(), + let sender = ButtonLike::new("role") + .style(ButtonStyle::Filled) + .child(match message.role { + Role::User => Label::new("You").color(Color::Default), + Role::Assistant => Label::new("Assistant").color(Color::Info), + Role::System => Label::new("System").color(Color::Warning), + }) + .tooltip(|cx| { + Tooltip::with_meta( + "Toggle message role", + None, + "Available roles: You (User), Assistant, System", + cx, ) - .contained() - .with_style(style.sent_at.container) - .aligned(), + }) + .on_click({ + let conversation = conversation.clone(); + move |_, cx| { + conversation.update(cx, |conversation, cx| { + conversation.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) + }) + } + }); + + div() + .h_flex() + .id(("message_header", message_id.0)) + .h_11() + .relative() + .gap_1() + .child(sender) + // TODO: Only show this if the message if the message has been sent + .child( + Label::new( + FormatDistance::from_now(DateTimeType::Local( + message.sent_at, + )) + .hide_prefix(true) + .add_suffix(true) + .to_string(), + ) + .color(Color::Muted), ) - .with_children( - if let MessageStatus::Error(error) = &message.status { + .children( + if let MessageStatus::Error(error) = message.status.clone() { Some( - Svg::new("icons/error.svg") - .with_color(style.error_icon.color) - .constrained() - .with_width(style.error_icon.width) - .contained() - .with_style(style.error_icon.container) - .with_tooltip::( - message_id.0, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), + div() + .id("error") + .tooltip(move |cx| Tooltip::text(error.clone(), cx)) + .child(Icon::new(IconName::XCircle)), ) } else { None }, ) - .aligned() - .left() - .contained() - .with_style(style.message_header) - .into_any() + .into_any_element() } }), disposition: BlockDisposition::Above, @@ -2565,36 +2383,35 @@ impl ConversationEditor { return; }; - let text = editor.read_with(cx, |editor, cx| { - let range = editor.selections.newest::(cx).range(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + let editor = editor.read(cx); + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); - let selected_text = buffer.text_for_range(range).collect::(); - if selected_text.is_empty() { - None + let selected_text = buffer.text_for_range(range).collect::(); + let text = if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") } else { - Some(if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - format!("```{language_name}\n{selected_text}\n```") - }) - } - }); + format!("```{language_name}\n{selected_text}\n```") + }) + }; // Activate the panel - if !panel.read(cx).has_focus(cx) { + if !panel.focus_handle(cx).contains_focused(cx) { workspace.toggle_panel_focus::(cx); } @@ -2638,13 +2455,12 @@ impl ConversationEditor { } if spanned_messages > 1 { - cx.platform() - .write_to_clipboard(ClipboardItem::new(copied_text)); + cx.write_to_clipboard(ClipboardItem::new(copied_text)); return; } } - cx.propagate_action(); + cx.propagate(); } fn split(&mut self, _: &Split, cx: &mut ViewContext) { @@ -2682,80 +2498,65 @@ impl ConversationEditor { .unwrap_or_else(|| "New Conversation".into()) } - fn render_current_model( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> impl Element { - enum Model {} - - MouseEventHandler::new::(0, cx, |state, cx| { - let style = style.model.style_for(state); - let model_display_name = self.conversation.read(cx).model.short_name(); - Label::new(model_display_name, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) - } - - fn render_remaining_tokens( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> Option> { - let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; - let remaining_tokens_style = if remaining_tokens <= 0 { - &style.no_remaining_tokens - } else if remaining_tokens <= 500 { - &style.low_remaining_tokens - } else { - &style.remaining_tokens - }; - Some( - Label::new( - remaining_tokens.to_string(), - remaining_tokens_style.text.clone(), - ) - .contained() - .with_style(remaining_tokens_style.container), + fn render_current_model(&self, cx: &mut ViewContext) -> impl IntoElement { + Button::new( + "current_model", + self.conversation.read(cx).model.short_name(), ) + .style(ButtonStyle::Filled) + .tooltip(move |cx| Tooltip::text("Change Model", cx)) + .on_click(cx.listener(|this, _, cx| this.cycle_model(cx))) + } + + fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { + let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; + let remaining_tokens_color = if remaining_tokens <= 0 { + Color::Error + } else if remaining_tokens <= 500 { + Color::Warning + } else { + Color::Default + }; + Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)) } } -impl Entity for ConversationEditor { - type Event = ConversationEditorEvent; +impl EventEmitter for ConversationEditor {} + +impl Render for ConversationEditor { + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + div() + .key_context("ConversationEditor") + .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) + .capture_action(cx.listener(ConversationEditor::save)) + .capture_action(cx.listener(ConversationEditor::copy)) + .capture_action(cx.listener(ConversationEditor::cycle_message_role)) + .on_action(cx.listener(ConversationEditor::assist)) + .on_action(cx.listener(ConversationEditor::split)) + .size_full() + .relative() + .child( + div() + .size_full() + .pl_4() + .bg(cx.theme().colors().editor_background) + .child(self.editor.clone()), + ) + .child( + h_stack() + .absolute() + .gap_1() + .top_3() + .right_5() + .child(self.render_current_model(cx)) + .children(self.render_remaining_tokens(cx)), + ) + } } -impl View for ConversationEditor { - fn ui_name() -> &'static str { - "ConversationEditor" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).assistant; - Stack::new() - .with_child( - ChildView::new(&self.editor, cx) - .contained() - .with_style(theme.container), - ) - .with_child( - Flex::row() - .with_child(self.render_current_model(theme, cx)) - .with_children(self.render_remaining_tokens(theme, cx)) - .aligned() - .top() - .right(), - ) - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.editor); - } +impl FocusableView for ConversationEditor { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.editor.focus_handle(cx) } } @@ -2806,142 +2607,104 @@ enum InlineAssistantEvent { struct InlineAssistant { id: usize, - prompt_editor: ViewHandle, - workspace: WeakViewHandle, + prompt_editor: View, + workspace: WeakView, confirmed: bool, - has_focus: bool, include_conversation: bool, measurements: Rc>, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, - codegen: ModelHandle, + codegen: Model, _subscriptions: Vec, retrieve_context: bool, - semantic_index: Option>, + semantic_index: Option>, semantic_permissioned: Option, - project: WeakModelHandle, + project: WeakModel, maintain_rate_limit: Option>, } -impl Entity for InlineAssistant { - type Event = InlineAssistantEvent; -} +impl EventEmitter for InlineAssistant {} -impl View for InlineAssistant { - fn ui_name() -> &'static str { - "InlineAssistant" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum ErrorIcon {} - let theme = theme::current(cx); - - Flex::row() - .with_children([Flex::row() - .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) - .element() - .aligned(), - ) - .with_children(if SemanticIndex::enabled(cx) { - Some( - Button::action(ToggleRetrieveContext) - .with_tooltip("Retrieve Context", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new( - "icons/magnifying_glass.svg", - )) - .toggleable(self.retrieve_context) - .with_style(theme.assistant.inline.retrieve_context.clone()) - .element() - .aligned(), +impl Render for InlineAssistant { + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let measurements = self.measurements.get(); + h_stack() + .w_full() + .py_2() + .border_y_1() + .border_color(cx.theme().colors().border) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::toggle_include_conversation)) + .on_action(cx.listener(Self::toggle_retrieve_context)) + .on_action(cx.listener(Self::move_up)) + .on_action(cx.listener(Self::move_down)) + .child( + h_stack() + .justify_center() + .w(measurements.gutter_width) + .child( + IconButton::new("include_conversation", IconName::Ai) + .on_click(cx.listener(|this, _, cx| { + this.toggle_include_conversation(&ToggleIncludeConversation, cx) + })) + .selected(self.include_conversation) + .tooltip(|cx| { + Tooltip::for_action( + "Include Conversation", + &ToggleIncludeConversation, + cx, + ) + }), ) - } else { - None - }) - .with_children(if let Some(error) = self.codegen.read(cx).error() { - Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), - ) - } else { - None - }) - .aligned() - .constrained() - .dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f(measurements.gutter_width, constraint.min.y()), - max: vec2f(measurements.gutter_width, constraint.max.y()), - } - } - })]) - .with_child(Empty::new().constrained().dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.min.y(), - ), - max: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.max.y(), - ), - } - } - })) - .with_child( - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() - .flex(1., true), + .children(if SemanticIndex::enabled(cx) { + Some( + IconButton::new("retrieve_context", IconName::MagnifyingGlass) + .on_click(cx.listener(|this, _, cx| { + this.toggle_retrieve_context(&ToggleRetrieveContext, cx) + })) + .selected(self.retrieve_context) + .tooltip(|cx| { + Tooltip::for_action( + "Retrieve Context", + &ToggleRetrieveContext, + cx, + ) + }), + ) + } else { + None + }) + .children(if let Some(error) = self.codegen.read(cx).error() { + let error_message = SharedString::from(error.to_string()); + Some( + div() + .id("error") + .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) + .child(Icon::new(IconName::XCircle).color(Color::Error)), + ) + } else { + None + }), ) - .with_children(if self.retrieve_context { - Some( - Flex::row() - .with_children(self.retrieve_context_status(cx)) - .flex(1., true) - .aligned(), - ) + .child( + h_stack() + .w_full() + .ml(measurements.anchor_x - measurements.gutter_width) + .child(self.render_prompt_editor(cx)), + ) + .children(if self.retrieve_context { + self.retrieve_context_status(cx) } else { None }) - .contained() - .with_style(theme.assistant.inline.container) - .into_any() - .into_any() } +} - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - cx.focus(&self.prompt_editor); - self.has_focus = true; - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; +impl FocusableView for InlineAssistant { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.prompt_editor.focus_handle(cx) } } @@ -2951,18 +2714,15 @@ impl InlineAssistant { measurements: Rc>, include_conversation: bool, prompt_history: VecDeque, - codegen: ModelHandle, - workspace: WeakViewHandle, + codegen: Model, + workspace: WeakView, cx: &mut ViewContext, retrieve_context: bool, - semantic_index: Option>, - project: ModelHandle, + semantic_index: Option>, + project: Model, ) -> Self { - let prompt_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); + let prompt_editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); let placeholder = match codegen.read(cx).kind() { CodegenKind::Transform { .. } => "Enter transformation prompt…", CodegenKind::Generate { .. } => "Enter generation prompt…", @@ -2970,6 +2730,8 @@ impl InlineAssistant { editor.set_placeholder_text(placeholder, cx); editor }); + cx.focus_view(&prompt_editor); + let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), @@ -2984,7 +2746,6 @@ impl InlineAssistant { prompt_editor, workspace, confirmed: false, - has_focus: false, include_conversation, measurements, prompt_history, @@ -3009,7 +2770,7 @@ impl InlineAssistant { return Task::ready(Ok(value)); } - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return Task::ready(Err(anyhow!("project was dropped"))); }; @@ -3023,11 +2784,11 @@ impl InlineAssistant { fn handle_prompt_editor_events( &mut self, - _: ViewHandle, - event: &editor::Event, + _: View, + event: &EditorEvent, cx: &mut ViewContext, ) { - if let editor::Event::Edited = event { + if let EditorEvent::Edited = event { self.pending_prompt = self.prompt_editor.read(cx).text(cx); cx.notify(); } @@ -3035,10 +2796,10 @@ impl InlineAssistant { fn semantic_index_changed( &mut self, - semantic_index: ModelHandle, + semantic_index: Model, cx: &mut ViewContext, ) { - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return; }; @@ -3051,7 +2812,7 @@ impl InlineAssistant { if self.maintain_rate_limit.is_none() { self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { loop { - cx.background().timer(Duration::from_secs(1)).await; + cx.background_executor().timer(Duration::from_secs(1)).await; this.update(&mut cx, |_, cx| cx.notify()).log_err(); } })); @@ -3064,26 +2825,16 @@ impl InlineAssistant { } } - fn handle_codegen_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + fn handle_codegen_changed(&mut self, _: Model, 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(); + let was_read_only = editor.read_only(cx); 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, - ); } } }); @@ -3101,15 +2852,8 @@ impl InlineAssistant { 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); - editor.set_field_editor_style( - Some(Arc::new(|theme| { - theme.assistant.inline.disabled_editor.clone() - })), - cx, - ); - }); + self.prompt_editor + .update(cx, |editor, _cx| editor.set_read_only(true)); cx.emit(InlineAssistantEvent::Confirmed { prompt, include_conversation: self.include_conversation, @@ -3123,7 +2867,7 @@ impl InlineAssistant { fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { let semantic_permissioned = self.semantic_permissioned(cx); - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return; }; @@ -3141,7 +2885,7 @@ impl InlineAssistant { cx.spawn(|this, mut cx| async move { // If Necessary prompt user if !semantic_permissioned.await.unwrap_or(false) { - let mut answer = this.update(&mut cx, |_, cx| { + let answer = this.update(&mut cx, |_, cx| { cx.prompt( PromptLevel::Info, prompt_text.as_str(), @@ -3149,7 +2893,7 @@ impl InlineAssistant { ) })?; - if answer.next().await == Some(0) { + if answer.await? == 0 { this.update(&mut cx, |this, _| { this.semantic_permissioned = Some(true); })?; @@ -3179,7 +2923,7 @@ impl InlineAssistant { } fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return Err(anyhow!("project was dropped!")); }; @@ -3189,7 +2933,7 @@ impl InlineAssistant { // This has to be updated to accomodate for semantic_permissions if semantic_permissioned.await.unwrap_or(false) { semantic_index - .update(&mut cx, |index, cx| index.index_project(project, cx)) + .update(&mut cx, |index, cx| index.index_project(project, cx))? .await } else { Err(anyhow!("project is not permissioned for semantic indexing")) @@ -3201,153 +2945,72 @@ impl InlineAssistant { anyhow::Ok(()) } - fn retrieve_context_status( - &self, - cx: &mut ViewContext, - ) -> Option> { - enum ContextStatusIcon {} - - let Some(project) = self.project.upgrade(cx) else { + fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { + let Some(project) = self.project.upgrade() else { return None; }; - if let Some(semantic_index) = SemanticIndex::global(cx) { - let status = semantic_index.update(cx, |index, _| index.status(&project)); - let theme = theme::current(cx); - match status { - SemanticIndexStatus::NotAuthenticated {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ), - SemanticIndexStatus::NotIndexed {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.inline.context_status.error_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.error_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.error_icon.container) - .with_tooltip::( - self.id, - "Not Indexed", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ), - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { + let semantic_index = SemanticIndex::global(cx)?; + let status = semantic_index.update(cx, |index, _| index.status(&project)); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) + .child(Icon::new(IconName::XCircle)) + .into_any_element() + ), - let mut status_text = if remaining_files == 0 { - "Indexing...".to_string() - } else { - format!("Remaining files to index: {remaining_files}") - }; + SemanticIndexStatus::NotIndexed {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Indexed", cx)) + .child(Icon::new(IconName::XCircle)) + .into_any_element() + ), - if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { - write!( - status_text, - " (rate limit expires in {}s)", - remaining_seconds.as_secs() - ) - .unwrap(); - } + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; + + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); } - Some( - Svg::new("icons/update.svg") - .with_color(theme.assistant.inline.context_status.in_progress_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.in_progress_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.in_progress_icon.container) - .with_tooltip::( - self.id, - status_text, - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ) } - SemanticIndexStatus::Indexed {} => Some( - Svg::new("icons/check.svg") - .with_color(theme.assistant.inline.context_status.complete_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.complete_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.complete_icon.container) - .with_tooltip::( - self.id, - "Index up to date", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ), + + let status_text = SharedString::from(status_text); + Some( + div() + .id("update") + .tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) + .child(Icon::new(IconName::Update).color(Color::Info)) + .into_any_element() + ) } - } else { - None + + SemanticIndexStatus::Indexed {} => Some( + div() + .id("check") + .tooltip(|cx| Tooltip::text("Index up to date", cx)) + .child(Icon::new(IconName::Check).color(Color::Success)) + .into_any_element() + ), } } - // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { - // let project = self.project.clone(); - // if let Some(semantic_index) = self.semantic_index.clone() { - // let status = semantic_index.update(cx, |index, cx| index.status(&project)); - // return match status { - // // This theoretically shouldnt be a valid code path - // // As the inline assistant cant be launched without an API key - // // We keep it here for safety - // semantic_index::SemanticIndexStatus::NotAuthenticated => { - // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexed => { - // "Indexing Complete!".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { - - // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); - - // if let Some(rate_limit_expiry) = rate_limit_expiry { - // let remaining_seconds = - // rate_limit_expiry.duration_since(Instant::now()); - // if remaining_seconds > Duration::from_secs(0) { - // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); - // } - // } - // status - // } - // semantic_index::SemanticIndexStatus::NotIndexed => { - // "Not Indexed for Context Retrieval".to_string() - // } - // }; - // } - - // "".to_string() - // } - fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, @@ -3396,21 +3059,50 @@ impl InlineAssistant { }); }); } + + fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.prompt_editor.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.3).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + EditorElement::new( + &self.prompt_editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } } // This wouldn't need to exist if we could pass parameters when rendering child views. #[derive(Copy, Clone, Default)] struct BlockMeasurements { - anchor_x: f32, - gutter_width: f32, + anchor_x: Pixels, + gutter_width: Pixels, } struct PendingInlineAssist { - editor: WeakViewHandle, - inline_assistant: Option<(BlockId, ViewHandle)>, - codegen: ModelHandle, + editor: WeakView, + inline_assistant: Option<(BlockId, View)>, + codegen: Model, _subscriptions: Vec, - project: WeakModelHandle, + project: WeakModel, } fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { @@ -3444,12 +3136,13 @@ mod tests { #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3574,12 +3267,13 @@ mod tests { #[gpui::test] fn test_message_splitting(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3672,11 +3366,12 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3740,7 +3435,7 @@ mod tests { ); fn message_ids_for_offsets( - conversation: &ModelHandle, + conversation: &Model, offsets: &[usize], cx: &AppContext, ) -> Vec { @@ -3755,12 +3450,13 @@ mod tests { #[gpui::test] fn test_serialization(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = - cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); + cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_0 = conversation.read(cx).message_anchors[0].id; let message_1 = conversation.update(cx, |conversation, cx| { @@ -3793,7 +3489,7 @@ mod tests { ] ); - let deserialized_conversation = cx.add_model(|cx| { + let deserialized_conversation = cx.new_model(|cx| { Conversation::deserialize( conversation.read(cx).serialize(cx), Default::default(), @@ -3814,7 +3510,7 @@ mod tests { } fn messages( - conversation: &ModelHandle, + conversation: &Model, cx: &AppContext, ) -> Vec<(MessageId, Role, Range)> { conversation @@ -3826,28 +3522,21 @@ mod tests { } fn report_assistant_event( - workspace: WeakViewHandle, + workspace: WeakView, conversation_id: Option, assistant_kind: AssistantKind, cx: &AppContext, ) { - let Some(workspace) = workspace.upgrade(cx) else { + let Some(workspace) = workspace.upgrade() else { return; }; let client = workspace.read(cx).project().read(cx).client(); let telemetry = client.telemetry(); - let model = settings::get::(cx) + let model = AssistantSettings::get_global(cx) .default_open_ai_model .clone(); - let telemetry_settings = *settings::get::(cx); - - telemetry.report_assistant_event( - telemetry_settings, - conversation_id, - assistant_kind, - model.full_name(), - ) + telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name()) } diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 65dd588b3c..b2a9231a57 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -1,7 +1,8 @@ use anyhow; +use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::Setting; +use settings::Settings; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub enum OpenAIModel { @@ -51,21 +52,37 @@ pub enum AssistantDockPosition { pub struct AssistantSettings { pub button: bool, pub dock: AssistantDockPosition, - pub default_width: f32, - pub default_height: f32, + pub default_width: Pixels, + pub default_height: Pixels, pub default_open_ai_model: OpenAIModel, } +/// Assistant panel settings #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct AssistantSettingsContent { + /// Whether to show the assistant panel button in the status bar. + /// + /// Default: true pub button: Option, + /// Where to dock the assistant. + /// + /// Default: right pub dock: Option, + /// Default width in pixels when the assistant is docked to the left or right. + /// + /// Default: 640 pub default_width: Option, + /// Default height in pixels when the assistant is docked to the bottom. + /// + /// Default: 320 pub default_height: Option, + /// The default OpenAI model to use when starting new conversations. + /// + /// Default: gpt-4-1106-preview pub default_open_ai_model: Option, } -impl Setting for AssistantSettings { +impl Settings for AssistantSettings { const KEY: Option<&'static str> = Some("assistant"); type FileContent = AssistantSettingsContent; @@ -73,7 +90,7 @@ impl Setting for AssistantSettings { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &gpui::AppContext, + _: &mut gpui::AppContext, ) -> anyhow::Result { Self::load_via_json_merge(default_value, user_values) } diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index 25c9deef7f..8af3010f40 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -3,7 +3,7 @@ use ai::completion::{CompletionProvider, CompletionRequest}; use anyhow::Result; use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; -use gpui::{Entity, ModelContext, ModelHandle, Task}; +use gpui::{EventEmitter, Model, ModelContext, Task}; use language::{Rope, TransactionId}; use multi_buffer; use std::{cmp, future, ops::Range, sync::Arc}; @@ -21,7 +21,7 @@ pub enum CodegenKind { pub struct Codegen { provider: Arc, - buffer: ModelHandle, + buffer: Model, snapshot: MultiBufferSnapshot, kind: CodegenKind, last_equal_ranges: Vec>, @@ -32,13 +32,11 @@ pub struct Codegen { _subscription: gpui::Subscription, } -impl Entity for Codegen { - type Event = Event; -} +impl EventEmitter for Codegen {} impl Codegen { pub fn new( - buffer: ModelHandle, + buffer: Model, kind: CodegenKind, provider: Arc, cx: &mut ModelContext, @@ -60,7 +58,7 @@ impl Codegen { fn handle_buffer_event( &mut self, - _buffer: ModelHandle, + _buffer: Model, event: &multi_buffer::Event, cx: &mut ModelContext, ) { @@ -111,13 +109,13 @@ impl Codegen { .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| { + self.generation = cx.spawn(|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 diff = cx.background_executor().spawn(async move { let chunks = strip_invalid_spans_from_codeblock(response.await?); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -183,12 +181,6 @@ impl Codegen { }); 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(); @@ -245,7 +237,7 @@ impl Codegen { } cx.notify(); - }); + })?; } diff.await?; @@ -253,17 +245,16 @@ impl Codegen { }; 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(); - }); - } + 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(); + }) + .ok(); } }); self.error.take(); @@ -372,7 +363,7 @@ mod tests { use super::*; use ai::test::FakeCompletionProvider; use futures::stream::{self}; - use gpui::{executor::Deterministic, TestAppContext}; + use gpui::{Context, TestAppContext}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use rand::prelude::*; @@ -391,12 +382,8 @@ mod tests { } #[gpui::test(iterations = 10)] - async fn test_transform_autoindent( - cx: &mut TestAppContext, - mut rng: StdRng, - deterministic: Arc, - ) { - cx.set_global(cx.read(SettingsStore::test)); + async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -408,14 +395,14 @@ mod tests { } "}; 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)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new_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, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Transform { range }, @@ -442,10 +429,10 @@ mod tests { println!("CHUNK: {:?}", &chunk); provider.send_completion(chunk); new_text = suffix; - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); } provider.finish_completion(); - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); assert_eq!( buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), @@ -464,9 +451,8 @@ mod tests { async fn test_autoindent_when_generating_past_indentation( cx: &mut TestAppContext, mut rng: StdRng, - deterministic: Arc, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -475,14 +461,14 @@ mod tests { } "}; 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)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new_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(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, @@ -508,10 +494,10 @@ mod tests { let (chunk, suffix) = new_text.split_at(len); provider.send_completion(chunk); new_text = suffix; - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); } provider.finish_completion(); - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); assert_eq!( buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), @@ -530,9 +516,8 @@ mod tests { async fn test_autoindent_when_generating_before_indentation( cx: &mut TestAppContext, mut rng: StdRng, - deterministic: Arc, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = concat!( @@ -541,14 +526,14 @@ mod tests { "}\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)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new_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(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, @@ -575,10 +560,10 @@ mod tests { println!("{:?}", &chunk); provider.send_completion(chunk); new_text = suffix; - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); } provider.finish_completion(); - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); assert_eq!( buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index b678c6fe3b..c88e257295 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -176,7 +176,7 @@ pub(crate) mod tests { use super::*; use std::sync::Arc; - use gpui::AppContext; + use gpui::{AppContext, Context}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use settings::SettingsStore; @@ -227,7 +227,8 @@ pub(crate) mod tests { #[gpui::test] fn test_outline_for_prompt(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); language_settings::init(cx); let text = indoc! {" struct X { @@ -253,7 +254,7 @@ pub(crate) mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); let snapshot = buffer.read(cx).snapshot(); assert_eq!( diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml deleted file mode 100644 index 2fbc8850be..0000000000 --- a/crates/assistant2/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "assistant2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/assistant.rs" -doctest = false - -[dependencies] -ai = { package = "ai2", path = "../ai2" } -client = { package = "client2", path = "../client2" } -collections = { path = "../collections"} -editor = { package = "editor2", path = "../editor2" } -fs = { package = "fs2", path = "../fs2" } -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -menu = { package = "menu2", path = "../menu2" } -multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } -project = { package = "project2", path = "../project2" } -search = { package = "search2", path = "../search2" } -semantic_index = { package = "semantic_index2", path = "../semantic_index2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -ui = { package = "ui2", path = "../ui2" } -util = { path = "../util" } -workspace = { package = "workspace2", path = "../workspace2" } - -uuid.workspace = true -log.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.workspace = true - -[dev-dependencies] -ai = { package = "ai2", path = "../ai2", features = ["test-support"]} -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -project = { package = "project2", path = "../project2", features = ["test-support"] } - -ctor.workspace = true -env_logger.workspace = true -log.workspace = true -rand.workspace = true diff --git a/crates/assistant2/README.zmd b/crates/assistant2/README.zmd deleted file mode 100644 index af8702b609..0000000000 --- a/crates/assistant2/README.zmd +++ /dev/null @@ -1,63 +0,0 @@ -> Hello, I'm Nathan Sobo. -I'm currently your sole creator and the leader of Zed Industries. -Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation. - -< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development. - -As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world. - -> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish. - -We've decided that our next milestone will be open-sourcing Zed on the Zed platform. - -< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further. - -Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation. - -Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future! - -@nathan> #zed, can you describe the conversation UI as you currently understand it. - -< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary. - -Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered. - -@nathan> What details around the conversation view are worth exploring? - -< @nathan, there are several aspects of the conversation view that are worth exploring and refining: - -1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context. -2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history. -3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials. -4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants. -5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view. - -Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed. - -@nathan> @nate, which of these would you like to explore? - -@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5. - -#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots). - -@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try... - -#zed describe a hypothetical conversation UI that meets the description. - -< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements: - -1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space. - -2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions. - -3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view. - -4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly. - -5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow. - -6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view. - -7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context. - -This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs. diff --git a/crates/assistant2/features.zmd b/crates/assistant2/features.zmd deleted file mode 100644 index f3183d68bc..0000000000 --- a/crates/assistant2/features.zmd +++ /dev/null @@ -1,3 +0,0 @@ -Push content to a deeper layer. -A context can have multiple sublayers. -You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document. diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs deleted file mode 100644 index 743c8b22e6..0000000000 --- a/crates/assistant2/src/assistant.rs +++ /dev/null @@ -1,129 +0,0 @@ -pub mod assistant_panel; -mod assistant_settings; -mod codegen; -mod prompts; -mod streaming_diff; - -use ai::providers::open_ai::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::{actions, AppContext, SharedString}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; -use util::paths::CONVERSATIONS_DIR; - -actions!( - assistant, - [ - NewConversation, - Assist, - Split, - CycleMessageRole, - QuoteSelection, - ToggleFocus, - ResetKey, - InlineAssist, - ToggleIncludeConversation, - ToggleRetrieveContext, - ] -); - -#[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(SharedString), -} - -#[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/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs deleted file mode 100644 index 79ebb6602d..0000000000 --- a/crates/assistant2/src/assistant_panel.rs +++ /dev/null @@ -1,3480 +0,0 @@ -use crate::{ - assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - codegen::{self, Codegen, CodegenKind}, - prompts::generate_content_prompt, - Assist, CycleMessageRole, InlineAssist, MessageId, MessageMetadata, MessageStatus, - NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, - SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext, -}; - -use ai::{ - auth::ProviderCredential, - completion::{CompletionProvider, CompletionRequest}, - providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage}, -}; - -use ai::prompts::repository_context::PromptCodeSnippet; -use anyhow::{anyhow, Result}; -use chrono::{DateTime, Local}; -use client::{telemetry::AssistantKind, TelemetrySettings}; -use collections::{hash_map, HashMap, HashSet, VecDeque}; -use editor::{ - display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, - }, - scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, - ToOffset, ToPoint, -}; -use fs::Fs; -use futures::StreamExt; -use gpui::{ - div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, - ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView, FontStyle, - FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, - ParentElement, Pixels, PromptLevel, Render, SharedString, StatefulInteractiveElement, Styled, - Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, - WeakModel, WeakView, WhiteSpace, WindowContext, -}; -use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; -use project::Project; -use search::BufferSearchBar; -use semantic_index::{SemanticIndex, SemanticIndexStatus}; -use settings::{Settings, SettingsStore}; -use std::{ - cell::Cell, - cmp, - fmt::Write, - iter, - ops::Range, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, - time::{Duration, Instant}, -}; -use theme::ThemeSettings; -use ui::{ - h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, Tooltip, -}; -use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; -use uuid::Uuid; -use workspace::{ - dock::{DockPosition, Panel, PanelEvent}, - searchable::Direction, - Save, Toast, ToggleZoom, Toolbar, Workspace, -}; - -pub fn init(cx: &mut AppContext) { - AssistantSettings::register(cx); - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { - workspace - .register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }) - .register_action(AssistantPanel::inline_assist) - .register_action(AssistantPanel::cancel_last_inline_assist) - .register_action(ConversationEditor::quote_selection); - }, - ) - .detach(); -} - -pub struct AssistantPanel { - workspace: WeakView, - width: Option, - height: Option, - active_editor_index: Option, - prev_active_editor_index: Option, - editors: Vec>, - saved_conversations: Vec, - saved_conversations_scroll_handle: UniformListScrollHandle, - zoomed: bool, - focus_handle: FocusHandle, - toolbar: View, - completion_provider: Arc, - api_key_editor: Option>, - languages: Arc, - fs: Arc, - subscriptions: Vec, - next_inline_assist_id: usize, - pending_inline_assists: HashMap, - pending_inline_assist_ids_by_editor: HashMap, Vec>, - include_conversation_in_next_inline_assist: bool, - inline_prompt_history: VecDeque, - _watch_saved_conversations: Task>, - semantic_index: Option>, - retrieve_context_in_next_inline_assist: bool, -} - -impl AssistantPanel { - const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; - - pub fn load( - workspace: WeakView, - cx: AsyncWindowContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; - let saved_conversations = SavedConversationMetadata::list(fs.clone()) - .await - .log_err() - .unwrap_or_default(); - - // TODO: deserialize state. - let workspace_handle = workspace.clone(); - workspace.update(&mut cx, |workspace, cx| { - cx.build_view::(|cx| { - const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); - let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { - let mut events = fs - .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION) - .await; - while events.next().await.is_some() { - let saved_conversations = SavedConversationMetadata::list(fs.clone()) - .await - .log_err() - .unwrap_or_default(); - this.update(&mut cx, |this, cx| { - this.saved_conversations = saved_conversations; - cx.notify(); - }) - .ok(); - } - - anyhow::Ok(()) - }); - - let toolbar = cx.build_view(|cx| { - let mut toolbar = Toolbar::new(); - toolbar.set_can_navigate(false, cx); - toolbar.add_item(cx.build_view(|cx| BufferSearchBar::new(cx)), cx); - toolbar - }); - - let semantic_index = SemanticIndex::global(cx); - // Defaulting currently to GPT4, allow for this to be set via config. - let completion_provider = Arc::new(OpenAICompletionProvider::new( - "gpt-4", - cx.background_executor().clone(), - )); - - let focus_handle = cx.focus_handle(); - cx.on_focus_in(&focus_handle, Self::focus_in).detach(); - cx.on_focus_out(&focus_handle, Self::focus_out).detach(); - - let mut this = Self { - workspace: workspace_handle, - active_editor_index: Default::default(), - prev_active_editor_index: Default::default(), - editors: Default::default(), - saved_conversations, - saved_conversations_scroll_handle: Default::default(), - zoomed: false, - focus_handle, - toolbar, - completion_provider, - api_key_editor: None, - languages: workspace.app_state().languages.clone(), - fs: workspace.app_state().fs.clone(), - width: None, - height: None, - subscriptions: Default::default(), - next_inline_assist_id: 0, - pending_inline_assists: Default::default(), - pending_inline_assist_ids_by_editor: Default::default(), - include_conversation_in_next_inline_assist: false, - inline_prompt_history: Default::default(), - _watch_saved_conversations, - semantic_index, - retrieve_context_in_next_inline_assist: false, - }; - - let mut old_dock_position = this.position(cx); - this.subscriptions = - vec![cx.observe_global::(move |this, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - cx.notify(); - })]; - - this - }) - }) - }) - } - - fn focus_in(&mut self, cx: &mut ViewContext) { - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); - cx.notify(); - if self.focus_handle.is_focused(cx) { - if let Some(editor) = self.active_editor() { - cx.focus_view(editor); - } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { - cx.focus_view(api_key_editor); - } - } - } - - fn focus_out(&mut self, cx: &mut ViewContext) { - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); - cx.notify(); - } - - pub fn inline_assist( - workspace: &mut Workspace, - _: &InlineAssist, - cx: &mut ViewContext, - ) { - let this = if let Some(this) = workspace.panel::(cx) { - if this.update(cx, |assistant, cx| { - if !assistant.has_credentials() { - assistant.load_credentials(cx); - }; - - assistant.has_credentials() - }) { - this - } else { - workspace.focus_panel::(cx); - return; - } - } else { - return; - }; - - let active_editor = if let Some(active_editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - { - active_editor - } else { - return; - }; - - let project = workspace.project(); - - this.update(cx, |assistant, cx| { - assistant.new_inline_assist(&active_editor, cx, project) - }); - } - - fn new_inline_assist( - &mut self, - editor: &View, - cx: &mut ViewContext, - project: &Model, - ) { - let selection = editor.read(cx).selections.newest_anchor().clone(); - if selection.start.excerpt_id != selection.end.excerpt_id { - return; - } - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - - // Extend the selection to the start and the end of the line. - let mut point_selection = selection.map(|selection| selection.to_point(&snapshot)); - if point_selection.end > point_selection.start { - point_selection.start.column = 0; - // If the selection ends at the start of the line, we don't want to include it. - if point_selection.end.column == 0 { - point_selection.end.row -= 1; - } - point_selection.end.column = snapshot.line_len(point_selection.end.row); - } - - let codegen_kind = if point_selection.start == point_selection.end { - CodegenKind::Generate { - position: snapshot.anchor_after(point_selection.start), - } - } else { - CodegenKind::Transform { - range: snapshot.anchor_before(point_selection.start) - ..snapshot.anchor_after(point_selection.end), - } - }; - - let inline_assist_id = post_inc(&mut self.next_inline_assist_id); - let provider = self.completion_provider.clone(); - - // Retrieve Credentials Authenticates the Provider - provider.retrieve_credentials(cx); - - let codegen = cx.build_model(|cx| { - Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) - }); - - if let Some(semantic_index) = self.semantic_index.clone() { - let project = project.clone(); - cx.spawn(|_, mut cx| async move { - let previously_indexed = semantic_index - .update(&mut cx, |index, cx| { - index.project_previously_indexed(&project, cx) - })? - .await - .unwrap_or(false); - if previously_indexed { - let _ = semantic_index - .update(&mut cx, |index, cx| { - index.index_project(project.clone(), cx) - })? - .await; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - let measurements = Rc::new(Cell::new(BlockMeasurements::default())); - let inline_assistant = cx.build_view(|cx| { - InlineAssistant::new( - inline_assist_id, - measurements.clone(), - self.include_conversation_in_next_inline_assist, - self.inline_prompt_history.clone(), - codegen.clone(), - self.workspace.clone(), - cx, - self.retrieve_context_in_next_inline_assist, - self.semantic_index.clone(), - project.clone(), - ) - }); - let block_id = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { - selections.select_anchor_ranges([selection.head()..selection.head()]) - }); - editor.insert_blocks( - [BlockProperties { - style: BlockStyle::Flex, - position: snapshot.anchor_before(point_selection.head()), - height: 2, - render: Arc::new({ - let inline_assistant = inline_assistant.clone(); - move |cx: &mut BlockContext| { - measurements.set(BlockMeasurements { - anchor_x: cx.anchor_x, - gutter_width: cx.gutter_width, - }); - inline_assistant.clone().into_any_element() - } - }), - disposition: if selection.reversed { - BlockDisposition::Above - } else { - BlockDisposition::Below - }, - }], - Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), - cx, - )[0] - }); - - self.pending_inline_assists.insert( - inline_assist_id, - PendingInlineAssist { - editor: editor.downgrade(), - inline_assistant: Some((block_id, inline_assistant.clone())), - codegen: codegen.clone(), - project: project.downgrade(), - _subscriptions: vec![ - cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), - cx.subscribe(editor, { - let inline_assistant = inline_assistant.downgrade(); - move |_, editor, event, cx| { - if let Some(inline_assistant) = inline_assistant.upgrade() { - if let EditorEvent::SelectionsChanged { local } = event { - if *local - && inline_assistant.focus_handle(cx).contains_focused(cx) - { - cx.focus_view(&editor); - } - } - } - } - }), - cx.observe(&codegen, { - let editor = editor.downgrade(); - move |this, _, cx| { - if let Some(editor) = editor.upgrade() { - 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() { - 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); - } - } - }), - ], - }, - ); - self.pending_inline_assist_ids_by_editor - .entry(editor.downgrade()) - .or_default() - .push(inline_assist_id); - self.update_highlights_for_editor(&editor, cx); - } - - fn handle_inline_assistant_event( - &mut self, - inline_assistant: View, - event: &InlineAssistantEvent, - cx: &mut ViewContext, - ) { - let assist_id = inline_assistant.read(cx).id; - match event { - InlineAssistantEvent::Confirmed { - prompt, - include_conversation, - retrieve_context, - } => { - self.confirm_inline_assist( - assist_id, - prompt, - *include_conversation, - cx, - *retrieve_context, - ); - } - InlineAssistantEvent::Canceled => { - self.finish_inline_assist(assist_id, true, cx); - } - InlineAssistantEvent::Dismissed => { - self.hide_inline_assist(assist_id, cx); - } - InlineAssistantEvent::IncludeConversationToggled { - include_conversation, - } => { - self.include_conversation_in_next_inline_assist = *include_conversation; - } - InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => { - self.retrieve_context_in_next_inline_assist = *retrieve_context - } - } - } - - fn cancel_last_inline_assist( - workspace: &mut Workspace, - _: &editor::Cancel, - cx: &mut ViewContext, - ) { - if let Some(panel) = workspace.panel::(cx) { - if let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let handled = panel.update(cx, |panel, cx| { - if let Some(assist_id) = panel - .pending_inline_assist_ids_by_editor - .get(&editor.downgrade()) - .and_then(|assist_ids| assist_ids.last().copied()) - { - panel.finish_inline_assist(assist_id, true, cx); - true - } else { - false - } - }); - if handled { - return; - } - } - } - - cx.propagate(); - } - - 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) { - if let hash_map::Entry::Occupied(mut entry) = self - .pending_inline_assist_ids_by_editor - .entry(pending_assist.editor.clone()) - { - entry.get_mut().retain(|id| *id != assist_id); - if entry.get().is_empty() { - entry.remove(); - } - } - - if let Some(editor) = pending_assist.editor.upgrade() { - self.update_highlights_for_editor(&editor, cx); - - if undo { - pending_assist - .codegen - .update(cx, |codegen, cx| codegen.undo(cx)); - } - } - } - } - - fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { - if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { - if let Some(editor) = pending_assist.editor.upgrade() { - if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() { - editor.update(cx, |editor, cx| { - editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); - if inline_assistant.focus_handle(cx).contains_focused(cx) { - editor.focus(cx); - } - }); - } - } - } - } - - fn confirm_inline_assist( - &mut self, - inline_assist_id: usize, - user_prompt: &str, - include_conversation: bool, - cx: &mut ViewContext, - retrieve_context: bool, - ) { - let conversation = if include_conversation { - self.active_editor() - .map(|editor| editor.read(cx).conversation.clone()) - } else { - None - }; - - let pending_assist = - if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { - pending_assist - } else { - return; - }; - - let editor = if let Some(editor) = pending_assist.editor.upgrade() { - editor - } else { - return; - }; - - let project = pending_assist.project.clone(); - - let project_name = if let Some(project) = project.upgrade() { - Some( - project - .read(cx) - .worktree_root_names(cx) - .collect::>() - .join("/"), - ) - } else { - None - }; - - self.inline_prompt_history - .retain(|prompt| prompt != user_prompt); - self.inline_prompt_history.push_back(user_prompt.into()); - if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { - self.inline_prompt_history.pop_front(); - } - - let codegen = pending_assist.codegen.clone(); - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - 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 { - self.finish_inline_assist(inline_assist_id, false, cx); - return; - } - } else { - self.finish_inline_assist(inline_assist_id, false, cx); - return; - }; - - 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 - } else { - Some(language.name()) - } - } else { - None - }; - - // Higher Temperature increases the randomness of model outputs. - // If Markdown or No Language is Known, increase the randomness for more creative output - // If Code, decrease temperature to get more deterministic outputs - let temperature = if let Some(language) = language_name.clone() { - if language.to_string() != "Markdown".to_string() { - 0.5 - } else { - 1.0 - } - } else { - 1.0 - }; - - let user_prompt = user_prompt.to_string(); - - let snippets = if retrieve_context { - let Some(project) = project.upgrade() else { - return; - }; - - let search_results = if let Some(semantic_index) = self.semantic_index.clone() { - let search_results = semantic_index.update(cx, |this, cx| { - this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) - }); - - cx.background_executor() - .spawn(async move { search_results.await.unwrap_or_default() }) - } else { - Task::ready(Vec::new()) - }; - - let snippets = cx.spawn(|_, mut cx| async move { - let mut snippets = Vec::new(); - for result in search_results.await { - snippets.push(PromptCodeSnippet::new( - result.buffer, - result.range, - &mut cx, - )?); - } - anyhow::Ok(snippets) - }); - snippets - } else { - Task::ready(Ok(Vec::new())) - }; - - let mut model = AssistantSettings::get_global(cx) - .default_open_ai_model - .clone(); - let model_name = model.full_name(); - - let prompt = cx.background_executor().spawn(async move { - let snippets = snippets.await?; - - let language_name = language_name.as_deref(); - generate_content_prompt( - user_prompt, - language_name, - buffer, - range, - snippets, - model_name, - project_name, - ) - }); - - let mut messages = Vec::new(); - if let Some(conversation) = conversation { - let conversation = conversation.read(cx); - let buffer = conversation.buffer.read(cx); - messages.extend( - conversation - .messages(cx) - .map(|message| message.to_open_ai_message(buffer)), - ); - model = conversation.model.clone(); - } - - cx.spawn(|_, mut cx| async move { - // I Don't know if we want to return a ? here. - let prompt = prompt.await?; - - messages.push(RequestMessage { - role: Role::User, - content: prompt, - }); - - let request = Box::new(OpenAIRequest { - model: model.full_name().into(), - messages, - stream: true, - stop: vec!["|END|>".to_string()], - temperature, - }); - - codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?; - anyhow::Ok(()) - }) - .detach(); - } - - fn update_highlights_for_editor(&self, editor: &View, cx: &mut ViewContext) { - let mut background_ranges = Vec::new(); - let mut foreground_ranges = Vec::new(); - let empty_inline_assist_ids = Vec::new(); - let inline_assist_ids = self - .pending_inline_assist_ids_by_editor - .get(&editor.downgrade()) - .unwrap_or(&empty_inline_assist_ids); - - for inline_assist_id in inline_assist_ids { - if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { - let codegen = pending_assist.codegen.read(cx); - background_ranges.push(codegen.range()); - foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned()); - } - } - - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - merge_ranges(&mut background_ranges, &snapshot); - merge_ranges(&mut foreground_ranges, &snapshot); - editor.update(cx, |editor, cx| { - if background_ranges.is_empty() { - editor.clear_background_highlights::(cx); - } else { - editor.highlight_background::( - background_ranges, - |theme| theme.editor_active_line_background, // todo!("use the appropriate color") - cx, - ); - } - - if foreground_ranges.is_empty() { - editor.clear_highlights::(cx); - } else { - editor.highlight_text::( - foreground_ranges, - HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); - } - }); - } - - fn new_conversation(&mut self, cx: &mut ViewContext) -> View { - let editor = cx.build_view(|cx| { - ConversationEditor::new( - self.completion_provider.clone(), - self.languages.clone(), - self.fs.clone(), - self.workspace.clone(), - cx, - ) - }); - self.add_conversation(editor.clone(), cx); - editor - } - - fn add_conversation(&mut self, editor: View, cx: &mut ViewContext) { - self.subscriptions - .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); - - let conversation = editor.read(cx).conversation.clone(); - self.subscriptions - .push(cx.observe(&conversation, |_, _, cx| cx.notify())); - - let index = self.editors.len(); - self.editors.push(editor); - self.set_active_editor_index(Some(index), cx); - } - - fn set_active_editor_index(&mut self, index: Option, cx: &mut ViewContext) { - self.prev_active_editor_index = self.active_editor_index; - self.active_editor_index = index; - if let Some(editor) = self.active_editor() { - let editor = editor.read(cx).editor.clone(); - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_item(Some(&editor), cx); - }); - if self.focus_handle.contains_focused(cx) { - cx.focus_view(&editor); - } - } else { - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_item(None, cx); - }); - } - - cx.notify(); - } - - fn handle_conversation_editor_event( - &mut self, - _: View, - event: &ConversationEditorEvent, - cx: &mut ViewContext, - ) { - match event { - ConversationEditorEvent::TabContentChanged => cx.notify(), - } - } - - fn save_credentials(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - if let Some(api_key) = self - .api_key_editor - .as_ref() - .map(|editor| editor.read(cx).text(cx)) - { - if !api_key.is_empty() { - let credential = ProviderCredential::Credentials { - api_key: api_key.clone(), - }; - - self.completion_provider.save_credentials(cx, credential); - - self.api_key_editor.take(); - self.focus_handle.focus(cx); - cx.notify(); - } - } else { - cx.propagate(); - } - } - - fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { - self.completion_provider.delete_credentials(cx); - self.api_key_editor = Some(build_api_key_editor(cx)); - self.focus_handle.focus(cx); - cx.notify(); - } - - fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { - if self.zoomed { - cx.emit(PanelEvent::ZoomOut) - } else { - cx.emit(PanelEvent::ZoomIn) - } - } - - fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { - let mut propagate = true; - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if search_bar.show(cx) { - search_bar.search_suggested(cx); - if action.focus { - let focus_handle = search_bar.focus_handle(cx); - search_bar.select_query(cx); - cx.focus(&focus_handle); - } - propagate = false - } - }); - } - if propagate { - cx.propagate(); - } - } - - fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - if !search_bar.read(cx).is_dismissed() { - search_bar.update(cx, |search_bar, cx| { - search_bar.dismiss(&Default::default(), cx) - }); - return; - } - } - cx.propagate(); - } - - fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx)); - } - } - - fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx)); - } - } - - fn active_editor(&self) -> Option<&View> { - self.editors.get(self.active_editor_index?) - } - - fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("hamburger_button", Icon::Menu) - .on_click(cx.listener(|this, _event, cx| { - if this.active_editor().is_some() { - this.set_active_editor_index(None, cx); - } else { - this.set_active_editor_index(this.prev_active_editor_index, cx); - } - })) - .tooltip(|cx| Tooltip::text("History", cx)) - } - - fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { - if self.active_editor().is_some() { - vec![ - Self::render_split_button(cx).into_any_element(), - Self::render_quote_button(cx).into_any_element(), - Self::render_assist_button(cx).into_any_element(), - ] - } else { - Default::default() - } - } - - fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("split_button", Icon::SplitMessage) - .on_click(cx.listener(|this, _event, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); - } - })) - .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx)) - } - - fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("assist_button", Icon::MagicWand) - .on_click(cx.listener(|this, _event, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); - } - })) - .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx)) - } - - fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("quote_button", Icon::Quote) - .on_click(cx.listener(|this, _event, cx| { - if let Some(workspace) = this.workspace.upgrade() { - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - ConversationEditor::quote_selection(workspace, &Default::default(), cx) - }); - }); - } - })) - .tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) - } - - fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("plus_button", Icon::Plus) - .on_click(cx.listener(|this, _event, cx| { - this.new_conversation(cx); - })) - .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx)) - } - - fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { - let zoomed = self.zoomed; - IconButton::new("zoom_button", Icon::MagnifyingGlass) - .on_click(cx.listener(|this, _event, cx| { - this.toggle_zoom(&ToggleZoom, cx); - })) - .tooltip(move |cx| { - Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) - }) - } - - fn render_saved_conversation( - &mut self, - index: usize, - cx: &mut ViewContext, - ) -> impl IntoElement { - let conversation = &self.saved_conversations[index]; - let path = conversation.path.clone(); - - ButtonLike::new(index) - .on_click(cx.listener(move |this, _, cx| { - this.open_conversation(path.clone(), cx) - .detach_and_log_err(cx) - })) - .child(Label::new( - conversation.mtime.format("%F %I:%M%p").to_string(), - )) - .child(Label::new(conversation.title.clone())) - } - - fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { - cx.focus(&self.focus_handle); - - if let Some(ix) = self.editor_index_for_path(&path, cx) { - self.set_active_editor_index(Some(ix), cx); - return Task::ready(Ok(())); - } - - let fs = self.fs.clone(); - let workspace = self.workspace.clone(); - let languages = self.languages.clone(); - cx.spawn(|this, mut cx| async move { - let saved_conversation = fs.load(&path).await?; - let saved_conversation = serde_json::from_str(&saved_conversation)?; - let conversation = cx.build_model(|cx| { - Conversation::deserialize(saved_conversation, path.clone(), languages, cx) - })?; - this.update(&mut cx, |this, cx| { - // If, by the time we've loaded the conversation, the user has already opened - // the same conversation, we don't want to open it again. - if let Some(ix) = this.editor_index_for_path(&path, cx) { - this.set_active_editor_index(Some(ix), cx); - } else { - let editor = cx.build_view(|cx| { - ConversationEditor::for_conversation(conversation, fs, workspace, cx) - }); - this.add_conversation(editor, cx); - } - })?; - Ok(()) - }) - } - - fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option { - self.editors - .iter() - .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) - } - - fn has_credentials(&mut self) -> bool { - self.completion_provider.has_credentials() - } - - fn load_credentials(&mut self, cx: &mut ViewContext) { - self.completion_provider.retrieve_credentials(cx); - } -} - -fn build_api_key_editor(cx: &mut ViewContext) -> View { - cx.build_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); - editor - }) -} - -impl Render for AssistantPanel { - type Element = Focusable
; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - if let Some(api_key_editor) = self.api_key_editor.clone() { - v_stack() - .on_action(cx.listener(AssistantPanel::save_credentials)) - .track_focus(&self.focus_handle) - .child(Label::new( - "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", - )) - .child(Label::new( - " - Having a subscription for another service like GitHub Copilot won't work." - )) - .child(Label::new( - " - You can create a api key at: platform.openai.com/api-keys" - )) - .child(Label::new( - " " - )) - .child(Label::new( - "Paste your OpenAI API key and press Enter to use the assistant" - )) - .child(api_key_editor) - .child(Label::new( - "Click on the Z button in the status bar to close this panel." - )) - .border() - .border_color(gpui::red()) - } else { - let title = self - .active_editor() - .map(|editor| Label::new(editor.read(cx).title(cx))); - - let mut header = h_stack() - .child(Self::render_hamburger_button(cx)) - .children(title); - - if self.focus_handle.contains_focused(cx) { - header = header - .children(self.render_editor_tools(cx)) - .child(Self::render_plus_button(cx)) - .child(self.render_zoom_button(cx)); - } - - v_stack() - .size_full() - .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { - this.new_conversation(cx); - })) - .on_action(cx.listener(AssistantPanel::reset_credentials)) - .on_action(cx.listener(AssistantPanel::toggle_zoom)) - .on_action(cx.listener(AssistantPanel::deploy)) - .on_action(cx.listener(AssistantPanel::select_next_match)) - .on_action(cx.listener(AssistantPanel::select_prev_match)) - .on_action(cx.listener(AssistantPanel::handle_editor_cancel)) - .track_focus(&self.focus_handle) - .child(header) - .children(if self.toolbar.read(cx).hidden() { - None - } else { - Some(self.toolbar.clone()) - }) - .child( - div() - .flex_1() - .child(if let Some(editor) = self.active_editor() { - editor.clone().into_any_element() - } else { - uniform_list( - cx.view().clone(), - "saved_conversations", - self.saved_conversations.len(), - |this, range, cx| { - range - .map(|ix| this.render_saved_conversation(ix, cx)) - .collect() - }, - ) - .track_scroll(self.saved_conversations_scroll_handle.clone()) - .into_any_element() - }), - ) - .border() - .border_color(gpui::red()) - } - } -} - -impl Panel for AssistantPanel { - fn persistent_name() -> &'static str { - "AssistantPanel" - } - - fn position(&self, cx: &WindowContext) -> DockPosition { - match AssistantSettings::get_global(cx).dock { - AssistantDockPosition::Left => DockPosition::Left, - AssistantDockPosition::Bottom => DockPosition::Bottom, - AssistantDockPosition::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, _: DockPosition) -> bool { - true - } - - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - settings::update_settings_file::(self.fs.clone(), cx, move |settings| { - let dock = match position { - DockPosition::Left => AssistantDockPosition::Left, - DockPosition::Bottom => AssistantDockPosition::Bottom, - DockPosition::Right => AssistantDockPosition::Right, - }; - settings.dock = Some(dock); - }); - } - - fn size(&self, cx: &WindowContext) -> f32 { - let settings = AssistantSettings::get_global(cx); - match self.position(cx) { - DockPosition::Left | DockPosition::Right => { - self.width.unwrap_or_else(|| settings.default_width) - } - DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), - } - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - match self.position(cx) { - DockPosition::Left | DockPosition::Right => self.width = size, - DockPosition::Bottom => self.height = size, - } - cx.notify(); - } - - fn is_zoomed(&self, _: &WindowContext) -> bool { - self.zoomed - } - - fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.zoomed = zoomed; - cx.notify(); - } - - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - if active { - self.load_credentials(cx); - - if self.editors.is_empty() { - self.new_conversation(cx); - } - } - } - - fn icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::Ai) - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } -} - -impl EventEmitter for AssistantPanel {} - -impl FocusableView for AssistantPanel { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -enum ConversationEvent { - MessagesEdited, - SummaryChanged, - StreamedCompletion, -} - -#[derive(Default)] -struct Summary { - text: String, - done: bool, -} - -struct Conversation { - id: Option, - buffer: Model, - message_anchors: Vec, - messages_metadata: HashMap, - next_message_id: MessageId, - summary: Option, - pending_summary: Task>, - completion_count: usize, - pending_completions: Vec, - model: OpenAIModel, - token_count: Option, - max_token_count: usize, - pending_token_count: Task>, - pending_save: Task>, - path: Option, - _subscriptions: Vec, - completion_provider: Arc, -} - -impl EventEmitter for Conversation {} - -impl Conversation { - fn new( - language_registry: Arc, - cx: &mut ModelContext, - completion_provider: Arc, - ) -> Self { - let markdown = language_registry.language_for_name("Markdown"); - let buffer = cx.build_model(|cx| { - let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), ""); - buffer.set_language_registry(language_registry); - cx.spawn(|buffer, mut cx| async move { - let markdown = markdown.await?; - buffer.update(&mut cx, |buffer: &mut Buffer, cx| { - buffer.set_language(Some(markdown), cx) - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - buffer - }); - - let settings = AssistantSettings::get_global(cx); - 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(), - summary: None, - pending_summary: Task::ready(None), - completion_count: Default::default(), - pending_completions: Default::default(), - token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), - pending_token_count: Task::ready(None), - model: model.clone(), - _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], - pending_save: Task::ready(Ok(())), - path: None, - buffer, - completion_provider, - }; - let message = MessageAnchor { - id: MessageId(post_inc(&mut this.next_message_id.0)), - start: language::Anchor::MIN, - }; - this.message_anchors.push(message.clone()); - this.messages_metadata.insert( - message.id, - MessageMetadata { - role: Role::User, - sent_at: Local::now(), - status: MessageStatus::Done, - }, - ); - - this.count_remaining_tokens(cx); - this - } - - fn serialize(&self, cx: &AppContext) -> SavedConversation { - SavedConversation { - id: self.id.clone(), - zed: "conversation".into(), - version: SavedConversation::VERSION.into(), - text: self.buffer.read(cx).text(), - message_metadata: self.messages_metadata.clone(), - messages: self - .messages(cx) - .map(|message| SavedMessage { - id: message.id, - start: message.offset_range.start, - }) - .collect(), - summary: self - .summary - .as_ref() - .map(|summary| summary.text.clone()) - .unwrap_or_default(), - model: self.model.clone(), - } - } - - fn deserialize( - saved_conversation: SavedConversation, - path: PathBuf, - 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 completion_provider: Arc = Arc::new( - OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()), - ); - completion_provider.retrieve_credentials(cx); - let markdown = language_registry.language_for_name("Markdown"); - let mut message_anchors = Vec::new(); - let mut next_message_id = MessageId(0); - let buffer = cx.build_model(|cx| { - let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text); - for message in saved_conversation.messages { - message_anchors.push(MessageAnchor { - id: message.id, - start: buffer.anchor_before(message.start), - }); - next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); - } - buffer.set_language_registry(language_registry); - cx.spawn(|buffer, mut cx| async move { - let markdown = markdown.await?; - buffer.update(&mut cx, |buffer: &mut Buffer, cx| { - buffer.set_language(Some(markdown), cx) - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - buffer - }); - - let mut this = Self { - id, - message_anchors, - messages_metadata: saved_conversation.message_metadata, - next_message_id, - summary: Some(Summary { - text: saved_conversation.summary, - done: true, - }), - pending_summary: Task::ready(None), - completion_count: Default::default(), - pending_completions: Default::default(), - token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), - pending_token_count: Task::ready(None), - model, - _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], - pending_save: Task::ready(Ok(())), - path: Some(path), - buffer, - completion_provider, - }; - this.count_remaining_tokens(cx); - this - } - - fn handle_buffer_event( - &mut self, - _: Model, - event: &language::Event, - cx: &mut ModelContext, - ) { - match event { - language::Event::Edited => { - self.count_remaining_tokens(cx); - cx.emit(ConversationEvent::MessagesEdited); - } - _ => {} - } - } - - fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { - let messages = self - .messages(cx) - .into_iter() - .filter_map(|message| { - Some(tiktoken_rs::ChatCompletionRequestMessage { - role: match message.role { - Role::User => "user".into(), - Role::Assistant => "assistant".into(), - Role::System => "system".into(), - }, - content: Some( - self.buffer - .read(cx) - .text_for_range(message.offset_range) - .collect(), - ), - name: None, - function_call: None, - }) - }) - .collect::>(); - let model = self.model.clone(); - self.pending_token_count = cx.spawn(|this, mut cx| { - async move { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - let token_count = cx - .background_executor() - .spawn(async move { - tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) - }) - .await?; - - this.update(&mut cx, |this, cx| { - this.max_token_count = - tiktoken_rs::model::get_context_size(&this.model.full_name()); - this.token_count = Some(token_count); - cx.notify() - })?; - anyhow::Ok(()) - } - .log_err() - }); - } - - fn remaining_tokens(&self) -> Option { - Some(self.max_token_count as isize - self.token_count? as isize) - } - - fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext) { - self.model = model; - self.count_remaining_tokens(cx); - cx.notify(); - } - - fn assist( - &mut self, - selected_messages: HashSet, - cx: &mut ModelContext, - ) -> Vec { - let mut user_messages = Vec::new(); - - let last_message_id = if let Some(last_message_id) = - self.message_anchors.iter().rev().find_map(|message| { - message - .start - .is_valid(self.buffer.read(cx)) - .then_some(message.id) - }) { - last_message_id - } else { - return Default::default(); - }; - - let mut should_assist = false; - for selected_message_id in selected_messages { - let selected_message_role = - if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { - metadata.role - } else { - continue; - }; - - if selected_message_role == Role::Assistant { - if let Some(user_message) = self.insert_message_after( - selected_message_id, - Role::User, - MessageStatus::Done, - cx, - ) { - user_messages.push(user_message); - } - } else { - should_assist = true; - } - } - - if should_assist { - if !self.completion_provider.has_credentials() { - return Default::default(); - } - - let request: Box = Box::new(OpenAIRequest { - model: self.model.full_name().to_string(), - messages: self - .messages(cx) - .filter(|message| matches!(message.status, MessageStatus::Done)) - .map(|message| message.to_open_ai_message(self.buffer.read(cx))) - .collect(), - stream: true, - stop: vec![], - temperature: 1.0, - }); - - let stream = self.completion_provider.complete(request); - let assistant_message = self - .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) - .unwrap(); - - // Queue up the user's next reply. - let user_message = self - .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) - .unwrap(); - user_messages.push(user_message); - - let task = cx.spawn({ - |this, mut cx| async move { - let assistant_message_id = assistant_message.id; - let stream_completion = async { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let text = message?; - - this.update(&mut cx, |this, cx| { - let message_ix = this - .message_anchors - .iter() - .position(|message| message.id == assistant_message_id)?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message.start.to_offset(buffer).saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) - })?; - smol::future::yield_now().await; - } - - this.update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - })?; - - anyhow::Ok(()) - }; - - let result = stream_completion.await; - - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = MessageStatus::Error(SharedString::from( - error.to_string().trim().to_string(), - )); - } - } - cx.notify(); - } - }) - .ok(); - } - }); - - self.pending_completions.push(PendingCompletion { - id: post_inc(&mut self.completion_count), - _task: task, - }); - } - - user_messages - } - - fn cancel_last_assist(&mut self) -> bool { - self.pending_completions.pop().is_some() - } - - fn cycle_message_roles(&mut self, ids: HashSet, cx: &mut ModelContext) { - for id in ids { - if let Some(metadata) = self.messages_metadata.get_mut(&id) { - metadata.role.cycle(); - cx.emit(ConversationEvent::MessagesEdited); - cx.notify(); - } - } - } - - fn insert_message_after( - &mut self, - message_id: MessageId, - role: Role, - status: MessageStatus, - cx: &mut ModelContext, - ) -> Option { - if let Some(prev_message_ix) = self - .message_anchors - .iter() - .position(|message| message.id == message_id) - { - // Find the next valid message after the one we were given. - let mut next_message_ix = prev_message_ix + 1; - while let Some(next_message) = self.message_anchors.get(next_message_ix) { - if next_message.start.is_valid(self.buffer.read(cx)) { - break; - } - next_message_ix += 1; - } - - let start = self.buffer.update(cx, |buffer, cx| { - let offset = self - .message_anchors - .get(next_message_ix) - .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1); - buffer.edit([(offset..offset, "\n")], None, cx); - buffer.anchor_before(offset + 1) - }); - let message = MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start, - }; - self.message_anchors - .insert(next_message_ix, message.clone()); - self.messages_metadata.insert( - message.id, - MessageMetadata { - role, - sent_at: Local::now(), - status, - }, - ); - cx.emit(ConversationEvent::MessagesEdited); - Some(message) - } else { - None - } - } - - fn split_message( - &mut self, - range: Range, - cx: &mut ModelContext, - ) -> (Option, Option) { - let start_message = self.message_for_offset(range.start, cx); - let end_message = self.message_for_offset(range.end, cx); - if let Some((start_message, end_message)) = start_message.zip(end_message) { - // Prevent splitting when range spans multiple messages. - if start_message.id != end_message.id { - return (None, None); - } - - let message = start_message; - let role = message.role; - let mut edited_buffer = false; - - let mut suffix_start = None; - if range.start > message.offset_range.start && range.end < message.offset_range.end - 1 - { - if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') { - suffix_start = Some(range.end + 1); - } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') { - suffix_start = Some(range.end); - } - } - - let suffix = if let Some(suffix_start) = suffix_start { - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(suffix_start), - } - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit([(range.end..range.end, "\n")], None, cx); - }); - edited_buffer = true; - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(range.end + 1), - } - }; - - self.message_anchors - .insert(message.index_range.end + 1, suffix.clone()); - self.messages_metadata.insert( - suffix.id, - MessageMetadata { - role, - sent_at: Local::now(), - status: MessageStatus::Done, - }, - ); - - let new_messages = - if range.start == range.end || range.start == message.offset_range.start { - (None, Some(suffix)) - } else { - let mut prefix_end = None; - if range.start > message.offset_range.start - && range.end < message.offset_range.end - 1 - { - if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') { - prefix_end = Some(range.start + 1); - } else if self.buffer.read(cx).reversed_chars_at(range.start).next() - == Some('\n') - { - prefix_end = Some(range.start); - } - } - - let selection = if let Some(prefix_end) = prefix_end { - cx.emit(ConversationEvent::MessagesEdited); - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(prefix_end), - } - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit([(range.start..range.start, "\n")], None, cx) - }); - edited_buffer = true; - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(range.end + 1), - } - }; - - self.message_anchors - .insert(message.index_range.end + 1, selection.clone()); - self.messages_metadata.insert( - selection.id, - MessageMetadata { - role, - sent_at: Local::now(), - status: MessageStatus::Done, - }, - ); - (Some(selection), Some(suffix)) - }; - - if !edited_buffer { - cx.emit(ConversationEvent::MessagesEdited); - } - new_messages - } else { - (None, None) - } - } - - fn summarize(&mut self, cx: &mut ModelContext) { - if self.message_anchors.len() >= 2 && self.summary.is_none() { - if !self.completion_provider.has_credentials() { - return; - } - - let messages = self - .messages(cx) - .take(2) - .map(|message| message.to_open_ai_message(self.buffer.read(cx))) - .chain(Some(RequestMessage { - role: Role::User, - content: "Summarize the conversation into a short title without punctuation" - .into(), - })); - let request: Box = Box::new(OpenAIRequest { - model: self.model.full_name().to_string(), - messages: messages.collect(), - stream: true, - stop: vec![], - temperature: 1.0, - }); - - let stream = self.completion_provider.complete(request); - self.pending_summary = cx.spawn(|this, mut cx| { - async move { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let text = message?; - this.update(&mut cx, |this, cx| { - this.summary - .get_or_insert(Default::default()) - .text - .push_str(&text); - cx.emit(ConversationEvent::SummaryChanged); - })?; - } - - this.update(&mut cx, |this, cx| { - if let Some(summary) = this.summary.as_mut() { - summary.done = true; - cx.emit(ConversationEvent::SummaryChanged); - } - })?; - - anyhow::Ok(()) - } - .log_err() - }); - } - } - - fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option { - self.messages_for_offsets([offset], cx).pop() - } - - fn messages_for_offsets( - &self, - offsets: impl IntoIterator, - cx: &AppContext, - ) -> Vec { - let mut result = Vec::new(); - - let mut messages = self.messages(cx).peekable(); - let mut offsets = offsets.into_iter().peekable(); - let mut current_message = messages.next(); - while let Some(offset) = offsets.next() { - // Locate the message that contains the offset. - while current_message.as_ref().map_or(false, |message| { - !message.offset_range.contains(&offset) && messages.peek().is_some() - }) { - current_message = messages.next(); - } - let Some(message) = current_message.as_ref() else { - break; - }; - - // Skip offsets that are in the same message. - while offsets.peek().map_or(false, |offset| { - message.offset_range.contains(offset) || messages.peek().is_none() - }) { - offsets.next(); - } - - result.push(message.clone()); - } - result - } - - fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator { - let buffer = self.buffer.read(cx); - let mut message_anchors = self.message_anchors.iter().enumerate().peekable(); - iter::from_fn(move || { - while let Some((start_ix, message_anchor)) = message_anchors.next() { - let metadata = self.messages_metadata.get(&message_anchor.id)?; - let message_start = message_anchor.start.to_offset(buffer); - let mut message_end = None; - let mut end_ix = start_ix; - while let Some((_, next_message)) = message_anchors.peek() { - if next_message.start.is_valid(buffer) { - message_end = Some(next_message.start); - break; - } else { - end_ix += 1; - message_anchors.next(); - } - } - let message_end = message_end - .unwrap_or(language::Anchor::MAX) - .to_offset(buffer); - return Some(Message { - index_range: start_ix..end_ix, - offset_range: message_start..message_end, - id: message_anchor.id, - anchor: message_anchor.start, - role: metadata.role, - sent_at: metadata.sent_at, - status: metadata.status.clone(), - }); - } - None - }) - } - - fn save( - &mut self, - debounce: Option, - fs: Arc, - cx: &mut ModelContext, - ) { - self.pending_save = cx.spawn(|this, mut cx| async move { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - - let (old_path, summary) = this.read_with(&cx, |this, _| { - let path = this.path.clone(); - let summary = if let Some(summary) = this.summary.as_ref() { - if summary.done { - Some(summary.text.clone()) - } else { - None - } - } else { - None - }; - (path, summary) - })?; - - if let Some(summary) = summary { - let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?; - let path = if let Some(old_path) = old_path { - old_path - } else { - let mut discriminant = 1; - let mut new_path; - loop { - new_path = CONVERSATIONS_DIR.join(&format!( - "{} - {}.zed.json", - summary.trim(), - discriminant - )); - if fs.is_file(&new_path).await { - discriminant += 1; - } else { - break; - } - } - new_path - }; - - fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; - fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) - .await?; - this.update(&mut cx, |this, _| this.path = Some(path))?; - } - - Ok(()) - }); - } -} - -struct PendingCompletion { - id: usize, - _task: Task<()>, -} - -enum ConversationEditorEvent { - TabContentChanged, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -struct ScrollPosition { - offset_before_cursor: gpui::Point, - cursor: Anchor, -} - -struct ConversationEditor { - conversation: Model, - fs: Arc, - workspace: WeakView, - editor: View, - blocks: HashSet, - scroll_position: Option, - _subscriptions: Vec, -} - -impl ConversationEditor { - fn new( - completion_provider: Arc, - language_registry: Arc, - fs: Arc, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let conversation = - cx.build_model(|cx| Conversation::new(language_registry, cx, completion_provider)); - Self::for_conversation(conversation, fs, workspace, cx) - } - - fn for_conversation( - conversation: Model, - fs: Arc, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let editor = cx.build_view(|cx| { - let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_show_gutter(false, cx); - editor.set_show_wrap_guides(false, cx); - editor - }); - - let _subscriptions = vec![ - cx.observe(&conversation, |_, _, cx| cx.notify()), - cx.subscribe(&conversation, Self::handle_conversation_event), - cx.subscribe(&editor, Self::handle_editor_event), - ]; - - let mut this = Self { - conversation, - editor, - blocks: Default::default(), - scroll_position: None, - fs, - workspace, - _subscriptions, - }; - this.update_message_headers(cx); - this - } - - 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| { - let selected_messages = conversation - .messages_for_offsets(cursors, cx) - .into_iter() - .map(|message| message.id) - .collect(); - conversation.assist(selected_messages, cx) - }); - let new_selections = user_messages - .iter() - .map(|message| { - let cursor = message - .start - .to_offset(self.conversation.read(cx).buffer.read(cx)); - cursor..cursor - }) - .collect::>(); - if !new_selections.is_empty() { - self.editor.update(cx, |editor, cx| { - editor.change_selections( - Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), - cx, - |selections| selections.select_ranges(new_selections), - ); - }); - // Avoid scrolling to the new cursor position so the assistant's output is stable. - cx.defer(|this, _| this.scroll_position = None); - } - } - - fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - if !self - .conversation - .update(cx, |conversation, _| conversation.cancel_last_assist()) - { - cx.propagate(); - } - } - - fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext) { - let cursors = self.cursors(cx); - self.conversation.update(cx, |conversation, cx| { - let messages = conversation - .messages_for_offsets(cursors, cx) - .into_iter() - .map(|message| message.id) - .collect(); - conversation.cycle_message_roles(messages, cx) - }); - } - - fn cursors(&self, cx: &AppContext) -> Vec { - let selections = self.editor.read(cx).selections.all::(cx); - selections - .into_iter() - .map(|selection| selection.head()) - .collect() - } - - fn handle_conversation_event( - &mut self, - _: Model, - event: &ConversationEvent, - cx: &mut ViewContext, - ) { - match event { - ConversationEvent::MessagesEdited => { - self.update_message_headers(cx); - self.conversation.update(cx, |conversation, cx| { - conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); - }); - } - ConversationEvent::SummaryChanged => { - cx.emit(ConversationEditorEvent::TabContentChanged); - self.conversation.update(cx, |conversation, cx| { - conversation.save(None, self.fs.clone(), cx); - }); - } - ConversationEvent::StreamedCompletion => { - self.editor.update(cx, |editor, cx| { - if let Some(scroll_position) = self.scroll_position { - let snapshot = editor.snapshot(cx); - let cursor_point = scroll_position.cursor.to_display_point(&snapshot); - let scroll_top = - cursor_point.row() as f32 - scroll_position.offset_before_cursor.y; - editor.set_scroll_position( - point(scroll_position.offset_before_cursor.x, scroll_top), - cx, - ); - } - }); - } - } - } - - fn handle_editor_event( - &mut self, - _: View, - event: &EditorEvent, - cx: &mut ViewContext, - ) { - match event { - EditorEvent::ScrollPositionChanged { autoscroll, .. } => { - let cursor_scroll_position = self.cursor_scroll_position(cx); - if *autoscroll { - self.scroll_position = cursor_scroll_position; - } else if self.scroll_position != cursor_scroll_position { - self.scroll_position = None; - } - } - EditorEvent::SelectionsChanged { .. } => { - self.scroll_position = self.cursor_scroll_position(cx); - } - _ => {} - } - } - - fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { - self.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let cursor = editor.selections.newest_anchor().head(); - let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32; - let scroll_position = editor - .scroll_manager - .anchor() - .scroll_position(&snapshot.display_snapshot); - - let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); - if (scroll_position.y..scroll_bottom).contains(&cursor_row) { - Some(ScrollPosition { - cursor, - offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), - }) - } else { - None - } - }) - } - - fn update_message_headers(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let excerpt_id = *buffer.as_singleton().unwrap().0; - let old_blocks = std::mem::take(&mut self.blocks); - let new_blocks = self - .conversation - .read(cx) - .messages(cx) - .map(|message| BlockProperties { - position: buffer.anchor_in_excerpt(excerpt_id, message.anchor), - height: 2, - style: BlockStyle::Sticky, - render: Arc::new({ - let conversation = self.conversation.clone(); - move |_cx| { - let message_id = message.id; - let sender = ButtonLike::new("role") - .child(match message.role { - Role::User => Label::new("You").color(Color::Default), - Role::Assistant => { - Label::new("Assistant").color(Color::Modified) - } - Role::System => Label::new("System").color(Color::Warning), - }) - .on_click({ - let conversation = conversation.clone(); - move |_, cx| { - conversation.update(cx, |conversation, cx| { - conversation.cycle_message_roles( - HashSet::from_iter(Some(message_id)), - cx, - ) - }) - } - }); - - h_stack() - .id(("message_header", message_id.0)) - .border() - .border_color(gpui::red()) - .child(sender) - .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) - .children( - if let MessageStatus::Error(error) = message.status.clone() { - Some( - div() - .id("error") - .tooltip(move |cx| Tooltip::text(error.clone(), cx)) - .child(IconElement::new(Icon::XCircle)), - ) - } else { - None - }, - ) - .into_any_element() - } - }), - disposition: BlockDisposition::Above, - }) - .collect::>(); - - editor.remove_blocks(old_blocks, None, cx); - let ids = editor.insert_blocks(new_blocks, None, cx); - self.blocks = HashSet::from_iter(ids); - }); - } - - fn quote_selection( - workspace: &mut Workspace, - _: &QuoteSelection, - cx: &mut ViewContext, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - - let editor = editor.read(cx); - let range = editor.selections.newest::(cx).range(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); - - let selected_text = buffer.text_for_range(range).collect::(); - let text = if selected_text.is_empty() { - None - } else { - Some(if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - format!("```{language_name}\n{selected_text}\n```") - }) - }; - - // Activate the panel - if !panel.focus_handle(cx).contains_focused(cx) { - workspace.toggle_panel_focus::(cx); - } - - if let Some(text) = text { - panel.update(cx, |panel, cx| { - let conversation = panel - .active_editor() - .cloned() - .unwrap_or_else(|| panel.new_conversation(cx)); - conversation.update(cx, |conversation, cx| { - conversation - .editor - .update(cx, |editor, cx| editor.insert(&text, cx)) - }); - }); - } - } - - fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { - let editor = self.editor.read(cx); - let conversation = self.conversation.read(cx); - if editor.selections.count() == 1 { - let selection = editor.selections.newest::(cx); - let mut copied_text = String::new(); - let mut spanned_messages = 0; - for message in conversation.messages(cx) { - if message.offset_range.start >= selection.range().end { - break; - } else if message.offset_range.end >= selection.range().start { - let range = cmp::max(message.offset_range.start, selection.range().start) - ..cmp::min(message.offset_range.end, selection.range().end); - if !range.is_empty() { - spanned_messages += 1; - write!(&mut copied_text, "## {}\n\n", message.role).unwrap(); - for chunk in conversation.buffer.read(cx).text_for_range(range) { - copied_text.push_str(&chunk); - } - copied_text.push('\n'); - } - } - } - - if spanned_messages > 1 { - cx.write_to_clipboard(ClipboardItem::new(copied_text)); - return; - } - } - - cx.propagate(); - } - - fn split(&mut self, _: &Split, cx: &mut ViewContext) { - self.conversation.update(cx, |conversation, cx| { - let selections = self.editor.read(cx).selections.disjoint_anchors(); - for selection in selections.into_iter() { - let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); - let range = selection - .map(|endpoint| endpoint.to_offset(&buffer)) - .range(); - conversation.split_message(range, cx); - } - }); - } - - fn save(&mut self, _: &Save, cx: &mut ViewContext) { - self.conversation.update(cx, |conversation, cx| { - conversation.save(None, self.fs.clone(), cx) - }); - } - - fn cycle_model(&mut self, cx: &mut ViewContext) { - self.conversation.update(cx, |conversation, cx| { - let new_model = conversation.model.cycle(); - conversation.set_model(new_model, cx); - }); - } - - fn title(&self, cx: &AppContext) -> String { - self.conversation - .read(cx) - .summary - .as_ref() - .map(|summary| summary.text.clone()) - .unwrap_or_else(|| "New Conversation".into()) - } - - fn render_current_model(&self, cx: &mut ViewContext) -> impl IntoElement { - Button::new( - "current_model", - self.conversation.read(cx).model.short_name(), - ) - .tooltip(move |cx| Tooltip::text("Change Model", cx)) - .on_click(cx.listener(|this, _, cx| this.cycle_model(cx))) - } - - fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { - let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; - let remaining_tokens_color = if remaining_tokens <= 0 { - Color::Error - } else if remaining_tokens <= 500 { - Color::Warning - } else { - Color::Default - }; - Some( - div() - .border() - .border_color(gpui::red()) - .child(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)), - ) - } -} - -impl EventEmitter for ConversationEditor {} - -impl Render for ConversationEditor { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() - .key_context("ConversationEditor") - .size_full() - .relative() - .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) - .capture_action(cx.listener(ConversationEditor::save)) - .capture_action(cx.listener(ConversationEditor::copy)) - .capture_action(cx.listener(ConversationEditor::cycle_message_role)) - .on_action(cx.listener(ConversationEditor::assist)) - .on_action(cx.listener(ConversationEditor::split)) - .child(self.editor.clone()) - .child( - h_stack() - .absolute() - .gap_1() - .top_3() - .right_5() - .child(self.render_current_model(cx)) - .children(self.render_remaining_tokens(cx)), - ) - } -} - -impl FocusableView for ConversationEditor { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.editor.focus_handle(cx) - } -} - -#[derive(Clone, Debug)] -struct MessageAnchor { - id: MessageId, - start: language::Anchor, -} - -#[derive(Clone, Debug)] -pub struct Message { - offset_range: Range, - index_range: Range, - id: MessageId, - anchor: language::Anchor, - role: Role, - sent_at: DateTime, - status: MessageStatus, -} - -impl Message { - fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { - let content = buffer - .text_for_range(self.offset_range.clone()) - .collect::(); - RequestMessage { - role: self.role, - content: content.trim_end().into(), - } - } -} - -enum InlineAssistantEvent { - Confirmed { - prompt: String, - include_conversation: bool, - retrieve_context: bool, - }, - Canceled, - Dismissed, - IncludeConversationToggled { - include_conversation: bool, - }, - RetrieveContextToggled { - retrieve_context: bool, - }, -} - -struct InlineAssistant { - id: usize, - prompt_editor: View, - workspace: WeakView, - confirmed: bool, - include_conversation: bool, - measurements: Rc>, - prompt_history: VecDeque, - prompt_history_ix: Option, - pending_prompt: String, - codegen: Model, - _subscriptions: Vec, - retrieve_context: bool, - semantic_index: Option>, - semantic_permissioned: Option, - project: WeakModel, - maintain_rate_limit: Option>, -} - -impl EventEmitter for InlineAssistant {} - -impl Render for InlineAssistant { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let measurements = self.measurements.get(); - h_stack() - .w_full() - .py_2() - .border_y_1() - .border_color(cx.theme().colors().border) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::toggle_include_conversation)) - .on_action(cx.listener(Self::toggle_retrieve_context)) - .on_action(cx.listener(Self::move_up)) - .on_action(cx.listener(Self::move_down)) - .child( - h_stack() - .justify_center() - .w(measurements.gutter_width) - .child( - IconButton::new("include_conversation", Icon::Ai) - .on_click(cx.listener(|this, _, cx| { - this.toggle_include_conversation(&ToggleIncludeConversation, cx) - })) - .selected(self.include_conversation) - .tooltip(|cx| { - Tooltip::for_action( - "Include Conversation", - &ToggleIncludeConversation, - cx, - ) - }), - ) - .children(if SemanticIndex::enabled(cx) { - Some( - IconButton::new("retrieve_context", Icon::MagnifyingGlass) - .on_click(cx.listener(|this, _, cx| { - this.toggle_retrieve_context(&ToggleRetrieveContext, cx) - })) - .selected(self.retrieve_context) - .tooltip(|cx| { - Tooltip::for_action( - "Retrieve Context", - &ToggleRetrieveContext, - cx, - ) - }), - ) - } else { - None - }) - .children(if let Some(error) = self.codegen.read(cx).error() { - let error_message = SharedString::from(error.to_string()); - Some( - div() - .id("error") - .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) - .child(IconElement::new(Icon::XCircle).color(Color::Error)), - ) - } else { - None - }), - ) - .child( - h_stack() - .w_full() - .ml(measurements.anchor_x - measurements.gutter_width) - .child(self.render_prompt_editor(cx)), - ) - .children(if self.retrieve_context { - self.retrieve_context_status(cx) - } else { - None - }) - } -} - -impl FocusableView for InlineAssistant { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.prompt_editor.focus_handle(cx) - } -} - -impl InlineAssistant { - fn new( - id: usize, - measurements: Rc>, - include_conversation: bool, - prompt_history: VecDeque, - codegen: Model, - workspace: WeakView, - cx: &mut ViewContext, - retrieve_context: bool, - semantic_index: Option>, - project: Model, - ) -> Self { - let prompt_editor = cx.build_view(|cx| { - let mut editor = Editor::single_line(cx); - let placeholder = match codegen.read(cx).kind() { - CodegenKind::Transform { .. } => "Enter transformation prompt…", - CodegenKind::Generate { .. } => "Enter generation prompt…", - }; - editor.set_placeholder_text(placeholder, cx); - editor - }); - cx.focus_view(&prompt_editor); - - let mut subscriptions = vec![ - cx.observe(&codegen, Self::handle_codegen_changed), - cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), - ]; - - if let Some(semantic_index) = semantic_index.clone() { - subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed)); - } - - let assistant = Self { - id, - prompt_editor, - workspace, - confirmed: false, - include_conversation, - measurements, - prompt_history, - prompt_history_ix: None, - pending_prompt: String::new(), - codegen, - _subscriptions: subscriptions, - retrieve_context, - semantic_permissioned: None, - semantic_index, - project: project.downgrade(), - maintain_rate_limit: None, - }; - - assistant.index_project(cx).log_err(); - - assistant - } - - fn semantic_permissioned(&self, cx: &mut ViewContext) -> Task> { - if let Some(value) = self.semantic_permissioned { - return Task::ready(Ok(value)); - } - - let Some(project) = self.project.upgrade() else { - return Task::ready(Err(anyhow!("project was dropped"))); - }; - - self.semantic_index - .as_ref() - .map(|semantic| { - semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) - }) - .unwrap_or(Task::ready(Ok(false))) - } - - fn handle_prompt_editor_events( - &mut self, - _: View, - event: &EditorEvent, - cx: &mut ViewContext, - ) { - if let EditorEvent::Edited = event { - self.pending_prompt = self.prompt_editor.read(cx).text(cx); - cx.notify(); - } - } - - fn semantic_index_changed( - &mut self, - semantic_index: Model, - cx: &mut ViewContext, - ) { - let Some(project) = self.project.upgrade() else { - return; - }; - - let status = semantic_index.read(cx).status(&project); - match status { - SemanticIndexStatus::Indexing { - rate_limit_expiry: Some(_), - .. - } => { - if self.maintain_rate_limit.is_none() { - self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { - loop { - cx.background_executor().timer(Duration::from_secs(1)).await; - this.update(&mut cx, |_, cx| cx.notify()).log_err(); - } - })); - } - return; - } - _ => { - self.maintain_rate_limit = None; - } - } - } - - fn handle_codegen_changed(&mut self, _: Model, 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); - } else { - self.confirmed = false; - editor.set_read_only(false); - } - } - }); - cx.notify(); - } - - fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - cx.emit(InlineAssistantEvent::Canceled); - } - - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - 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)); - cx.emit(InlineAssistantEvent::Confirmed { - prompt, - include_conversation: self.include_conversation, - retrieve_context: self.retrieve_context, - }); - self.confirmed = true; - cx.notify(); - } - } - - fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { - let semantic_permissioned = self.semantic_permissioned(cx); - - let Some(project) = self.project.upgrade() else { - return; - }; - - let project_name = project - .read(cx) - .worktree_root_names(cx) - .collect::>() - .join("/"); - let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0; - let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name, - if is_plural { - "s" - } else {""}); - - cx.spawn(|this, mut cx| async move { - // If Necessary prompt user - if !semantic_permissioned.await.unwrap_or(false) { - let answer = this.update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Info, - prompt_text.as_str(), - &["Continue", "Cancel"], - ) - })?; - - if answer.await? == 0 { - this.update(&mut cx, |this, _| { - this.semantic_permissioned = Some(true); - })?; - } else { - return anyhow::Ok(()); - } - } - - // If permissioned, update context appropriately - this.update(&mut cx, |this, cx| { - this.retrieve_context = !this.retrieve_context; - - cx.emit(InlineAssistantEvent::RetrieveContextToggled { - retrieve_context: this.retrieve_context, - }); - - if this.retrieve_context { - this.index_project(cx).log_err(); - } - - cx.notify(); - })?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { - let Some(project) = self.project.upgrade() else { - return Err(anyhow!("project was dropped!")); - }; - - let semantic_permissioned = self.semantic_permissioned(cx); - if let Some(semantic_index) = SemanticIndex::global(cx) { - cx.spawn(|_, mut cx| async move { - // This has to be updated to accomodate for semantic_permissions - if semantic_permissioned.await.unwrap_or(false) { - semantic_index - .update(&mut cx, |index, cx| index.index_project(project, cx))? - .await - } else { - Err(anyhow!("project is not permissioned for semantic indexing")) - } - }) - .detach_and_log_err(cx); - } - - anyhow::Ok(()) - } - - fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { - let Some(project) = self.project.upgrade() else { - return None; - }; - - let semantic_index = SemanticIndex::global(cx)?; - let status = semantic_index.update(cx, |index, _| index.status(&project)); - match status { - SemanticIndexStatus::NotAuthenticated {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), - - SemanticIndexStatus::NotIndexed {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Indexed", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), - - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { - let mut status_text = if remaining_files == 0 { - "Indexing...".to_string() - } else { - format!("Remaining files to index: {remaining_files}") - }; - - if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { - write!( - status_text, - " (rate limit expires in {}s)", - remaining_seconds.as_secs() - ) - .unwrap(); - } - } - - let status_text = SharedString::from(status_text); - Some( - div() - .id("update") - .tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) - .child(IconElement::new(Icon::Update).color(Color::Info)) - .into_any_element() - ) - } - - SemanticIndexStatus::Indexed {} => Some( - div() - .id("check") - .tooltip(|cx| Tooltip::text("Index up to date", cx)) - .child(IconElement::new(Icon::Check).color(Color::Success)) - .into_any_element() - ), - } - } - - fn toggle_include_conversation( - &mut self, - _: &ToggleIncludeConversation, - cx: &mut ViewContext, - ) { - self.include_conversation = !self.include_conversation; - cx.emit(InlineAssistantEvent::IncludeConversationToggled { - include_conversation: self.include_conversation, - }); - cx.notify(); - } - - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - if let Some(ix) = self.prompt_history_ix { - if ix > 0 { - self.prompt_history_ix = Some(ix - 1); - let prompt = self.prompt_history[ix - 1].clone(); - self.set_prompt(&prompt, cx); - } - } else if !self.prompt_history.is_empty() { - self.prompt_history_ix = Some(self.prompt_history.len() - 1); - let prompt = self.prompt_history[self.prompt_history.len() - 1].clone(); - self.set_prompt(&prompt, cx); - } - } - - fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { - if let Some(ix) = self.prompt_history_ix { - if ix < self.prompt_history.len() - 1 { - self.prompt_history_ix = Some(ix + 1); - let prompt = self.prompt_history[ix + 1].clone(); - self.set_prompt(&prompt, cx); - } else { - self.prompt_history_ix = None; - let pending_prompt = self.pending_prompt.clone(); - self.set_prompt(&pending_prompt, cx); - } - } - } - - fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext) { - self.prompt_editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - let len = buffer.len(cx); - buffer.edit([(0..len, prompt)], None, cx); - }); - }); - } - - fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: if self.prompt_editor.read(cx).read_only() { - cx.theme().colors().text_disabled - } else { - cx.theme().colors().text - }, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, - font_size: rems(0.875).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(1.).into(), - background_color: None, - underline: None, - white_space: WhiteSpace::Normal, - }; - EditorElement::new( - &self.prompt_editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - } -} - -// This wouldn't need to exist if we could pass parameters when rendering child views. -#[derive(Copy, Clone, Default)] -struct BlockMeasurements { - anchor_x: Pixels, - gutter_width: Pixels, -} - -struct PendingInlineAssist { - editor: WeakView, - inline_assistant: Option<(BlockId, View)>, - codegen: Model, - _subscriptions: Vec, - project: WeakModel, -} - -fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { - ranges.sort_unstable_by(|a, b| { - a.start - .cmp(&b.start, buffer) - .then_with(|| b.end.cmp(&a.end, buffer)) - }); - - let mut ix = 0; - while ix + 1 < ranges.len() { - let b = ranges[ix + 1].clone(); - let a = &mut ranges[ix]; - if a.end.cmp(&b.start, buffer).is_gt() { - if a.end.cmp(&b.end, buffer).is_lt() { - a.end = b.end; - } - ranges.remove(ix + 1); - } else { - ix += 1; - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::MessageId; - use ai::test::FakeCompletionProvider; - use gpui::AppContext; - - #[gpui::test] - fn test_inserting_and_removing_messages(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - - let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = - cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - - let message_1 = conversation.read(cx).message_anchors[0].clone(); - assert_eq!( - messages(&conversation, cx), - vec![(message_1.id, Role::User, 0..0)] - ); - - let message_2 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..1), - (message_2.id, Role::Assistant, 1..1) - ] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "1"), (1..1, "2")], None, cx) - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..3) - ] - ); - - let message_3 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_3.id, Role::User, 4..4) - ] - ); - - let message_4 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_4.id, Role::User, 4..5), - (message_3.id, Role::User, 5..5), - ] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "C"), (5..5, "D")], None, cx) - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_4.id, Role::User, 4..6), - (message_3.id, Role::User, 6..7), - ] - ); - - // Deleting across message boundaries merges the messages. - buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx)); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..3), - (message_3.id, Role::User, 3..4), - ] - ); - - // Undoing the deletion should also undo the merge. - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_4.id, Role::User, 4..6), - (message_3.id, Role::User, 6..7), - ] - ); - - // Redoing the deletion should also redo the merge. - buffer.update(cx, |buffer, cx| buffer.redo(cx)); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..3), - (message_3.id, Role::User, 3..4), - ] - ); - - // Ensure we can still insert after a merged message. - let message_5 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..3), - (message_5.id, Role::System, 3..4), - (message_3.id, Role::User, 4..5) - ] - ); - } - - #[gpui::test] - fn test_message_splitting(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Arc::new(FakeCompletionProvider::new()); - - let conversation = - cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - - let message_1 = conversation.read(cx).message_anchors[0].clone(); - assert_eq!( - messages(&conversation, cx), - vec![(message_1.id, Role::User, 0..0)] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx) - }); - - let (_, message_2) = - conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); - let message_2 = message_2.unwrap(); - - // We recycle newlines in the middle of a split message - assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_2.id, Role::User, 4..16), - ] - ); - - let (_, message_3) = - conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); - let message_3 = message_3.unwrap(); - - // We don't recycle newlines at the end of a split message - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..17), - ] - ); - - let (_, message_4) = - conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); - let message_4 = message_4.unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..9), - (message_4.id, Role::User, 9..17), - ] - ); - - let (_, message_5) = - conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); - let message_5 = message_5.unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..9), - (message_4.id, Role::User, 9..10), - (message_5.id, Role::User, 10..18), - ] - ); - - let (message_6, message_7) = conversation.update(cx, |conversation, cx| { - conversation.split_message(14..16, cx) - }); - let message_6 = message_6.unwrap(); - let message_7 = message_7.unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..9), - (message_4.id, Role::User, 9..10), - (message_5.id, Role::User, 10..14), - (message_6.id, Role::User, 14..17), - (message_7.id, Role::User, 17..19), - ] - ); - } - - #[gpui::test] - fn test_messages_for_offsets(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = - cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - - let message_1 = conversation.read(cx).message_anchors[0].clone(); - assert_eq!( - messages(&conversation, cx), - vec![(message_1.id, Role::User, 0..0)] - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx)); - let message_2 = conversation - .update(cx, |conversation, cx| { - conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx) - }) - .unwrap(); - buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx)); - - let message_3 = conversation - .update(cx, |conversation, cx| { - conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) - }) - .unwrap(); - buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx)); - - assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_2.id, Role::User, 4..8), - (message_3.id, Role::User, 8..11) - ] - ); - - assert_eq!( - message_ids_for_offsets(&conversation, &[0, 4, 9], cx), - [message_1.id, message_2.id, message_3.id] - ); - assert_eq!( - message_ids_for_offsets(&conversation, &[0, 1, 11], cx), - [message_1.id, message_3.id] - ); - - let message_4 = conversation - .update(cx, |conversation, cx| { - conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx) - }) - .unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_2.id, Role::User, 4..8), - (message_3.id, Role::User, 8..12), - (message_4.id, Role::User, 12..12) - ] - ); - assert_eq!( - message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx), - [message_1.id, message_2.id, message_3.id, message_4.id] - ); - - fn message_ids_for_offsets( - conversation: &Model, - offsets: &[usize], - cx: &AppContext, - ) -> Vec { - conversation - .read(cx) - .messages_for_offsets(offsets.iter().copied(), cx) - .into_iter() - .map(|message| message.id) - .collect() - } - } - - #[gpui::test] - fn test_serialization(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = - cx.build_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - let message_0 = conversation.read(cx).message_anchors[0].id; - let message_1 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx) - .unwrap() - }); - let message_2 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx); - buffer.finalize_last_transaction(); - }); - let _message_3 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - assert_eq!(buffer.read(cx).text(), "a\nb\nc\n"); - assert_eq!( - messages(&conversation, cx), - [ - (message_0, Role::User, 0..2), - (message_1.id, Role::Assistant, 2..6), - (message_2.id, Role::System, 6..6), - ] - ); - - let deserialized_conversation = cx.build_model(|cx| { - Conversation::deserialize( - conversation.read(cx).serialize(cx), - Default::default(), - registry.clone(), - cx, - ) - }); - let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone(); - assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n"); - assert_eq!( - messages(&deserialized_conversation, cx), - [ - (message_0, Role::User, 0..2), - (message_1.id, Role::Assistant, 2..6), - (message_2.id, Role::System, 6..6), - ] - ); - } - - fn messages( - conversation: &Model, - cx: &AppContext, - ) -> Vec<(MessageId, Role, Range)> { - conversation - .read(cx) - .messages(cx) - .map(|message| (message.id, message.role, message.offset_range)) - .collect() - } -} - -fn report_assistant_event( - workspace: WeakView, - conversation_id: Option, - assistant_kind: AssistantKind, - cx: &AppContext, -) { - let Some(workspace) = workspace.upgrade() else { - return; - }; - - let client = workspace.read(cx).project().read(cx).client(); - let telemetry = client.telemetry(); - - let model = AssistantSettings::get_global(cx) - .default_open_ai_model - .clone(); - - let telemetry_settings = TelemetrySettings::get_global(cx).clone(); - - telemetry.report_assistant_event( - telemetry_settings, - conversation_id, - assistant_kind, - model.full_name(), - ) -} diff --git a/crates/assistant2/src/assistant_settings.rs b/crates/assistant2/src/assistant_settings.rs deleted file mode 100644 index 5a727df621..0000000000 --- a/crates/assistant2/src/assistant_settings.rs +++ /dev/null @@ -1,80 +0,0 @@ -use anyhow; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -pub enum OpenAIModel { - #[serde(rename = "gpt-3.5-turbo-0613")] - ThreePointFiveTurbo, - #[serde(rename = "gpt-4-0613")] - Four, - #[serde(rename = "gpt-4-1106-preview")] - FourTurbo, -} - -impl OpenAIModel { - pub fn full_name(&self) -> &'static str { - match self { - OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", - OpenAIModel::Four => "gpt-4-0613", - OpenAIModel::FourTurbo => "gpt-4-1106-preview", - } - } - - pub fn short_name(&self) -> &'static str { - match self { - OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", - OpenAIModel::Four => "gpt-4", - OpenAIModel::FourTurbo => "gpt-4-turbo", - } - } - - pub fn cycle(&self) -> Self { - match self { - OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, - OpenAIModel::Four => OpenAIModel::FourTurbo, - OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AssistantDockPosition { - Left, - Right, - Bottom, -} - -#[derive(Deserialize, Debug)] -pub struct AssistantSettings { - pub button: bool, - pub dock: AssistantDockPosition, - pub default_width: f32, - pub default_height: f32, - pub default_open_ai_model: OpenAIModel, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -pub struct AssistantSettingsContent { - pub button: Option, - pub dock: Option, - pub default_width: Option, - pub default_height: Option, - pub default_open_ai_model: Option, -} - -impl Settings for AssistantSettings { - const KEY: Option<&'static str> = Some("assistant"); - - type FileContent = AssistantSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &mut gpui::AppContext, - ) -> anyhow::Result { - Self::load_via_json_merge(default_value, user_values) - } -} diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs deleted file mode 100644 index 2f7b2f0378..0000000000 --- a/crates/assistant2/src/codegen.rs +++ /dev/null @@ -1,688 +0,0 @@ -use crate::streaming_diff::{Hunk, StreamingDiff}; -use ai::completion::{CompletionProvider, CompletionRequest}; -use anyhow::Result; -use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; -use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; -use gpui::{EventEmitter, Model, ModelContext, Task}; -use language::{Rope, TransactionId}; -use multi_buffer; -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: Model, - snapshot: MultiBufferSnapshot, - kind: CodegenKind, - last_equal_ranges: Vec>, - transaction_id: Option, - error: Option, - generation: Task<()>, - idle: bool, - _subscription: gpui::Subscription, -} - -impl EventEmitter for Codegen {} - -impl Codegen { - pub fn new( - buffer: Model, - kind: CodegenKind, - provider: Arc, - cx: &mut ModelContext, - ) -> Self { - let snapshot = buffer.read(cx).snapshot(cx); - 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: Model, - 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: Box, 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(|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_executor().spawn(async move { - let chunks = strip_invalid_spans_from_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 { - 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; - 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(); - }) - .ok(); - } - }); - 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_invalid_spans_from_codeblock( - stream: impl Stream>, -) -> impl Stream> { - let mut first_line = true; - let mut buffer = String::new(); - let mut starts_with_markdown_codeblock = false; - let mut includes_start_or_end_span = 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 buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") { - includes_start_or_end_span = true; - - buffer = buffer - .strip_prefix("<|S|>") - .or_else(|| buffer.strip_prefix("<|S|")) - .unwrap_or(&buffer) - .to_string(); - } else if buffer.ends_with("|E|>") { - includes_start_or_end_span = true; - } else if buffer.starts_with("<|") - || buffer.starts_with("<|S") - || buffer.starts_with("<|S|") - || buffer.ends_with("|") - || buffer.ends_with("|E") - || buffer.ends_with("|E|") - { - return future::ready(None); - } - - if first_line { - if buffer == "" || buffer == "`" || buffer == "``" { - return future::ready(None); - } else if buffer.starts_with("```") { - starts_with_markdown_codeblock = true; - if let Some(newline_ix) = buffer.find('\n') { - buffer.replace_range(..newline_ix + 1, ""); - first_line = false; - } else { - return future::ready(None); - } - } - } - - let mut text = buffer.to_string(); - if starts_with_markdown_codeblock { - text = text - .strip_suffix("\n```\n") - .or_else(|| text.strip_suffix("\n```")) - .or_else(|| text.strip_suffix("\n``")) - .or_else(|| text.strip_suffix("\n`")) - .or_else(|| text.strip_suffix('\n')) - .unwrap_or(&text) - .to_string(); - } - - if includes_start_or_end_span { - text = text - .strip_suffix("|E|>") - .or_else(|| text.strip_suffix("E|>")) - .or_else(|| text.strip_prefix("|>")) - .or_else(|| text.strip_prefix(">")) - .unwrap_or(&text) - .to_string(); - }; - - 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 std::sync::Arc; - - use super::*; - use ai::test::FakeCompletionProvider; - use futures::stream::{self}; - use gpui::{Context, TestAppContext}; - use indoc::indoc; - use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; - use rand::prelude::*; - use serde::Serialize; - use settings::SettingsStore; - - #[derive(Serialize)] - pub struct DummyCompletionRequest { - pub name: String, - } - - impl CompletionRequest for DummyCompletionRequest { - fn data(&self) -> serde_json::Result { - serde_json::to_string(self) - } - } - - #[gpui::test(iterations = 10)] - async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { - cx.set_global(cx.update(SettingsStore::test)); - cx.update(language_settings::init); - - let text = indoc! {" - fn main() { - let x = 0; - for _ in 0..10 { - x += 1; - } - } - "}; - let buffer = - cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.build_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, 0))..snapshot.anchor_after(Point::new(4, 5)) - }); - let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.build_model(|cx| { - Codegen::new( - buffer.clone(), - CodegenKind::Transform { range }, - provider.clone(), - cx, - ) - }); - - let request = Box::new(DummyCompletionRequest { - name: "test".to_string(), - }); - codegen.update(cx, |codegen, cx| codegen.start(request, 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); - println!("CHUNK: {:?}", &chunk); - provider.send_completion(chunk); - new_text = suffix; - cx.background_executor.run_until_parked(); - } - provider.finish_completion(); - cx.background_executor.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, - ) { - cx.set_global(cx.update(SettingsStore::test)); - cx.update(language_settings::init); - - let text = indoc! {" - fn main() { - le - } - "}; - let buffer = - cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.build_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(FakeCompletionProvider::new()); - let codegen = cx.build_model(|cx| { - Codegen::new( - buffer.clone(), - CodegenKind::Generate { position }, - provider.clone(), - cx, - ) - }); - - let request = Box::new(DummyCompletionRequest { - name: "test".to_string(), - }); - codegen.update(cx, |codegen, cx| codegen.start(request, 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; - cx.background_executor.run_until_parked(); - } - provider.finish_completion(); - cx.background_executor.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, - ) { - cx.set_global(cx.update(SettingsStore::test)); - cx.update(language_settings::init); - - let text = concat!( - "fn main() {\n", - " \n", - "}\n" // - ); - let buffer = - cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.build_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(FakeCompletionProvider::new()); - let codegen = cx.build_model(|cx| { - Codegen::new( - buffer.clone(), - CodegenKind::Generate { position }, - provider.clone(), - cx, - ) - }); - - let request = Box::new(DummyCompletionRequest { - name: "test".to_string(), - }); - codegen.update(cx, |codegen, cx| codegen.start(request, 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); - println!("{:?}", &chunk); - provider.send_completion(chunk); - new_text = suffix; - cx.background_executor.run_until_parked(); - } - provider.finish_completion(); - cx.background_executor.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_invalid_spans_from_codeblock() { - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_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_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "``\nLorem ipsum dolor\n```" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - fn chunks(text: &str, size: usize) -> impl Stream> { - stream::iter( - text.chars() - .collect::>() - .chunks(size) - .map(|chunk| Ok(chunk.iter().collect::())) - .collect::>(), - ) - } - } - - 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/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs deleted file mode 100644 index 06881ad9c2..0000000000 --- a/crates/assistant2/src/prompts.rs +++ /dev/null @@ -1,389 +0,0 @@ -use ai::models::LanguageModel; -use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate}; -use ai::prompts::file_context::FileContext; -use ai::prompts::generate::GenerateInlineContent; -use ai::prompts::preamble::EngineerPreamble; -use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext}; -use ai::providers::open_ai::OpenAILanguageModel; -use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; -use std::cmp::{self, Reverse}; -use std::ops::Range; -use std::sync::Arc; - -#[allow(dead_code)] -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("<|S|"); - if selected_range.end == selected_range.start { - summary.push_str(">"); - } else { - summary.extend(buffer.text_for_range(selected_range.clone())); - summary.push_str("|E|>"); - } - 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("<|S|"); - if selected_range.end == selected_range.start { - summary.push_str(">"); - } else { - summary.extend(buffer.text_for_range(selected_range.clone())); - summary.push_str("|E|>"); - } - 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, - search_results: Vec, - model: &str, - project_name: Option, -) -> anyhow::Result { - // Using new Prompt Templates - let openai_model: Arc = Arc::new(OpenAILanguageModel::load(model)); - let lang_name = if let Some(language_name) = language_name { - Some(language_name.to_string()) - } else { - None - }; - - let args = PromptArguments { - model: openai_model, - language_name: lang_name.clone(), - project_name, - snippets: search_results.clone(), - reserved_tokens: 1000, - buffer: Some(buffer), - selected_range: Some(range), - user_prompt: Some(user_prompt.clone()), - }; - - let templates: Vec<(PromptPriority, Box)> = vec![ - (PromptPriority::Mandatory, Box::new(EngineerPreamble {})), - ( - PromptPriority::Ordered { order: 1 }, - Box::new(RepositoryContext {}), - ), - ( - PromptPriority::Ordered { order: 0 }, - Box::new(FileContext {}), - ), - ( - PromptPriority::Mandatory, - Box::new(GenerateInlineContent {}), - ), - ]; - let chain = PromptChain::new(args, templates); - let (prompt, _) = chain.generate(true)?; - - anyhow::Ok(prompt) -} - -#[cfg(test)] -pub(crate) mod tests { - - use super::*; - use std::sync::Arc; - - use gpui::{AppContext, Context}; - 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) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - 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.build_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 { - <|S|>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 <|S|a |E|>= 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 { - <|S|> - 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 {} - } - <|S|>"} - ); - - // 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! {" - <|S|>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/assistant2/src/streaming_diff.rs b/crates/assistant2/src/streaming_diff.rs deleted file mode 100644 index 7399a7b4fa..0000000000 --- a/crates/assistant2/src/streaming_diff.rs +++ /dev/null @@ -1,293 +0,0 @@ -use collections::HashMap; -use ordered_float::OrderedFloat; -use std::{ - cmp, - fmt::{self, Debug}, - ops::Range, -}; - -struct Matrix { - cells: Vec, - rows: usize, - cols: usize, -} - -impl Matrix { - fn new() -> Self { - Self { - cells: Vec::new(), - rows: 0, - cols: 0, - } - } - - fn resize(&mut self, rows: usize, cols: usize) { - self.cells.resize(rows * cols, 0.); - self.rows = rows; - self.cols = cols; - } - - fn get(&self, row: usize, col: usize) -> f64 { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("col out of bounds") - } - self.cells[col * self.rows + row] - } - - fn set(&mut self, row: usize, col: usize, value: f64) { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("col out of bounds") - } - - self.cells[col * self.rows + row] = value; - } -} - -impl Debug for Matrix { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f)?; - for i in 0..self.rows { - for j in 0..self.cols { - write!(f, "{:5}", self.get(i, j))?; - } - writeln!(f)?; - } - Ok(()) - } -} - -#[derive(Debug)] -pub enum Hunk { - Insert { text: String }, - Remove { len: usize }, - Keep { len: usize }, -} - -pub struct StreamingDiff { - old: Vec, - new: Vec, - scores: Matrix, - old_text_ix: usize, - new_text_ix: usize, - equal_runs: HashMap<(usize, usize), u32>, -} - -impl StreamingDiff { - const INSERTION_SCORE: f64 = -1.; - const DELETION_SCORE: f64 = -20.; - const EQUALITY_BASE: f64 = 1.8; - const MAX_EQUALITY_EXPONENT: i32 = 16; - - pub fn new(old: String) -> Self { - let old = old.chars().collect::>(); - let mut scores = Matrix::new(); - scores.resize(old.len() + 1, 1); - for i in 0..=old.len() { - scores.set(i, 0, i as f64 * Self::DELETION_SCORE); - } - Self { - old, - new: Vec::new(), - scores, - old_text_ix: 0, - new_text_ix: 0, - equal_runs: Default::default(), - } - } - - pub fn push_new(&mut self, text: &str) -> Vec { - self.new.extend(text.chars()); - self.scores.resize(self.old.len() + 1, self.new.len() + 1); - - for j in self.new_text_ix + 1..=self.new.len() { - self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE); - for i in 1..=self.old.len() { - let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE; - let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE; - let equality_score = if self.old[i - 1] == self.new[j - 1] { - let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0); - equal_run += 1; - self.equal_runs.insert((i, j), equal_run); - - let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT); - self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) - } else { - f64::NEG_INFINITY - }; - - let score = insertion_score.max(deletion_score).max(equality_score); - self.scores.set(i, j, score); - } - } - - let mut max_score = f64::NEG_INFINITY; - let mut next_old_text_ix = self.old_text_ix; - let next_new_text_ix = self.new.len(); - for i in self.old_text_ix..=self.old.len() { - let score = self.scores.get(i, next_new_text_ix); - if score > max_score { - max_score = score; - next_old_text_ix = i; - } - } - - let hunks = self.backtrack(next_old_text_ix, next_new_text_ix); - self.old_text_ix = next_old_text_ix; - self.new_text_ix = next_new_text_ix; - hunks - } - - fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { - let mut pending_insert: Option> = None; - let mut hunks = Vec::new(); - let mut i = old_text_ix; - let mut j = new_text_ix; - while (i, j) != (self.old_text_ix, self.new_text_ix) { - let insertion_score = if j > self.new_text_ix { - Some((i, j - 1)) - } else { - None - }; - let deletion_score = if i > self.old_text_ix { - Some((i - 1, j)) - } else { - None - }; - let equality_score = if i > self.old_text_ix && j > self.new_text_ix { - if self.old[i - 1] == self.new[j - 1] { - Some((i - 1, j - 1)) - } else { - None - } - } else { - None - }; - - let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] - .iter() - .max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j)))) - .unwrap() - .unwrap(); - - if prev_i == i && prev_j == j - 1 { - if let Some(pending_insert) = pending_insert.as_mut() { - pending_insert.start = prev_j; - } else { - pending_insert = Some(prev_j..j); - } - } else { - if let Some(range) = pending_insert.take() { - hunks.push(Hunk::Insert { - text: self.new[range].iter().collect(), - }); - } - - let char_len = self.old[i - 1].len_utf8(); - if prev_i == i - 1 && prev_j == j { - if let Some(Hunk::Remove { len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(Hunk::Remove { len: char_len }) - } - } else { - if let Some(Hunk::Keep { len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(Hunk::Keep { len: char_len }) - } - } - } - - i = prev_i; - j = prev_j; - } - - if let Some(range) = pending_insert.take() { - hunks.push(Hunk::Insert { - text: self.new[range].iter().collect(), - }); - } - - hunks.reverse(); - hunks - } - - pub fn finish(self) -> Vec { - self.backtrack(self.old.len(), self.new.len()) - } -} - -#[cfg(test)] -mod tests { - use std::env; - - use super::*; - use rand::prelude::*; - - #[gpui::test(iterations = 100)] - fn test_random_diffs(mut rng: StdRng) { - let old_text_len = env::var("OLD_TEXT_LEN") - .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) - .unwrap_or(10); - let new_text_len = env::var("NEW_TEXT_LEN") - .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable")) - .unwrap_or(10); - - let old = util::RandomCharIter::new(&mut rng) - .take(old_text_len) - .collect::(); - log::info!("old text: {:?}", old); - - let mut diff = StreamingDiff::new(old.clone()); - let mut hunks = Vec::new(); - let mut new_len = 0; - let mut new = String::new(); - while new_len < new_text_len { - let new_chunk_len = rng.gen_range(1..=new_text_len - new_len); - let new_chunk = util::RandomCharIter::new(&mut rng) - .take(new_len) - .collect::(); - log::info!("new chunk: {:?}", new_chunk); - new_len += new_chunk_len; - new.push_str(&new_chunk); - let new_hunks = diff.push_new(&new_chunk); - log::info!("hunks: {:?}", new_hunks); - hunks.extend(new_hunks); - } - let final_hunks = diff.finish(); - log::info!("final hunks: {:?}", final_hunks); - hunks.extend(final_hunks); - - log::info!("new text: {:?}", new); - let mut old_ix = 0; - let mut new_ix = 0; - let mut patched = String::new(); - for hunk in hunks { - match hunk { - Hunk::Keep { len } => { - assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]); - patched.push_str(&old[old_ix..old_ix + len]); - old_ix += len; - new_ix += len; - } - Hunk::Remove { len } => { - old_ix += len; - } - Hunk::Insert { text } => { - assert_eq!(text, &new[new_ix..new_ix + text.len()]); - patched.push_str(&text); - new_ix += text.len(); - } - } - } - assert_eq!(patched, new); - } -} diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 36135a1e76..8b38cdd103 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -13,11 +13,10 @@ gpui = { path = "../gpui" } collections = { path = "../collections" } util = { path = "../util" } + rodio ={version = "0.17.1", default-features=false, features = ["wav"]} log.workspace = true - +futures.workspace = true anyhow.workspace = true parking_lot.workspace = true - -[dev-dependencies] diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index d80fb6738f..9264ed25d6 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -60,7 +60,7 @@ impl Audio { return; } - cx.update_global::(|this, cx| { + cx.update_global::(|this, cx| { let output_handle = this.ensure_output_exists()?; let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; output_handle.play_raw(source).log_err()?; @@ -73,7 +73,7 @@ impl Audio { return; } - cx.update_global::(|this, _| { + cx.update_global::(|this, _| { this._output_stream.take(); this.output_handle.take(); }); diff --git a/crates/audio2/Cargo.toml b/crates/audio2/Cargo.toml deleted file mode 100644 index 3688f108f4..0000000000 --- a/crates/audio2/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "audio2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/audio2.rs" -doctest = false - -[dependencies] -gpui = { package = "gpui2", path = "../gpui2" } -collections = { path = "../collections" } -util = { path = "../util" } - - -rodio ={version = "0.17.1", default-features=false, features = ["wav"]} - -log.workspace = true -futures.workspace = true -anyhow.workspace = true -parking_lot.workspace = true - -[dev-dependencies] diff --git a/crates/audio2/audio/Cargo.toml b/crates/audio2/audio/Cargo.toml deleted file mode 100644 index 36135a1e76..0000000000 --- a/crates/audio2/audio/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "audio" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/audio.rs" -doctest = false - -[dependencies] -gpui = { path = "../gpui" } -collections = { path = "../collections" } -util = { path = "../util" } - -rodio ={version = "0.17.1", default-features=false, features = ["wav"]} - -log.workspace = true - -anyhow.workspace = true -parking_lot.workspace = true - -[dev-dependencies] diff --git a/crates/audio2/audio/src/assets.rs b/crates/audio2/audio/src/assets.rs deleted file mode 100644 index b58e1f6aee..0000000000 --- a/crates/audio2/audio/src/assets.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::{io::Cursor, sync::Arc}; - -use anyhow::Result; -use collections::HashMap; -use gpui::{AppContext, AssetSource}; -use rodio::{ - source::{Buffered, SamplesConverter}, - Decoder, Source, -}; - -type Sound = Buffered>>, f32>>; - -pub struct SoundRegistry { - cache: Arc>>, - assets: Box, -} - -impl SoundRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - cache: Default::default(), - assets: Box::new(source), - }) - } - - pub fn global(cx: &AppContext) -> Arc { - cx.global::>().clone() - } - - pub fn get(&self, name: &str) -> Result> { - if let Some(wav) = self.cache.lock().get(name) { - return Ok(wav.clone()); - } - - let path = format!("sounds/{}.wav", name); - let bytes = self.assets.load(&path)?.into_owned(); - let cursor = Cursor::new(bytes); - let source = Decoder::new(cursor)?.convert_samples::().buffered(); - - self.cache.lock().insert(name.to_string(), source.clone()); - - Ok(source) - } -} diff --git a/crates/audio2/audio/src/audio.rs b/crates/audio2/audio/src/audio.rs deleted file mode 100644 index d80fb6738f..0000000000 --- a/crates/audio2/audio/src/audio.rs +++ /dev/null @@ -1,81 +0,0 @@ -use assets::SoundRegistry; -use gpui::{AppContext, AssetSource}; -use rodio::{OutputStream, OutputStreamHandle}; -use util::ResultExt; - -mod assets; - -pub fn init(source: impl AssetSource, cx: &mut AppContext) { - cx.set_global(SoundRegistry::new(source)); - cx.set_global(Audio::new()); -} - -pub enum Sound { - Joined, - Leave, - Mute, - Unmute, - StartScreenshare, - StopScreenshare, -} - -impl Sound { - fn file(&self) -> &'static str { - match self { - Self::Joined => "joined_call", - Self::Leave => "leave_call", - Self::Mute => "mute", - Self::Unmute => "unmute", - Self::StartScreenshare => "start_screenshare", - Self::StopScreenshare => "stop_screenshare", - } - } -} - -pub struct Audio { - _output_stream: Option, - output_handle: Option, -} - -impl Audio { - pub fn new() -> Self { - Self { - _output_stream: None, - output_handle: None, - } - } - - fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> { - if self.output_handle.is_none() { - let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); - self.output_handle = output_handle; - self._output_stream = _output_stream; - } - - self.output_handle.as_ref() - } - - pub fn play_sound(sound: Sound, cx: &mut AppContext) { - if !cx.has_global::() { - return; - } - - cx.update_global::(|this, cx| { - let output_handle = this.ensure_output_exists()?; - let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; - output_handle.play_raw(source).log_err()?; - Some(()) - }); - } - - pub fn end_call(cx: &mut AppContext) { - if !cx.has_global::() { - return; - } - - cx.update_global::(|this, _| { - this._output_stream.take(); - this.output_handle.take(); - }); - } -} diff --git a/crates/audio2/src/assets.rs b/crates/audio2/src/assets.rs deleted file mode 100644 index b58e1f6aee..0000000000 --- a/crates/audio2/src/assets.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::{io::Cursor, sync::Arc}; - -use anyhow::Result; -use collections::HashMap; -use gpui::{AppContext, AssetSource}; -use rodio::{ - source::{Buffered, SamplesConverter}, - Decoder, Source, -}; - -type Sound = Buffered>>, f32>>; - -pub struct SoundRegistry { - cache: Arc>>, - assets: Box, -} - -impl SoundRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - cache: Default::default(), - assets: Box::new(source), - }) - } - - pub fn global(cx: &AppContext) -> Arc { - cx.global::>().clone() - } - - pub fn get(&self, name: &str) -> Result> { - if let Some(wav) = self.cache.lock().get(name) { - return Ok(wav.clone()); - } - - let path = format!("sounds/{}.wav", name); - let bytes = self.assets.load(&path)?.into_owned(); - let cursor = Cursor::new(bytes); - let source = Decoder::new(cursor)?.convert_samples::().buffered(); - - self.cache.lock().insert(name.to_string(), source.clone()); - - Ok(source) - } -} diff --git a/crates/audio2/src/audio2.rs b/crates/audio2/src/audio2.rs deleted file mode 100644 index 9264ed25d6..0000000000 --- a/crates/audio2/src/audio2.rs +++ /dev/null @@ -1,81 +0,0 @@ -use assets::SoundRegistry; -use gpui::{AppContext, AssetSource}; -use rodio::{OutputStream, OutputStreamHandle}; -use util::ResultExt; - -mod assets; - -pub fn init(source: impl AssetSource, cx: &mut AppContext) { - cx.set_global(SoundRegistry::new(source)); - cx.set_global(Audio::new()); -} - -pub enum Sound { - Joined, - Leave, - Mute, - Unmute, - StartScreenshare, - StopScreenshare, -} - -impl Sound { - fn file(&self) -> &'static str { - match self { - Self::Joined => "joined_call", - Self::Leave => "leave_call", - Self::Mute => "mute", - Self::Unmute => "unmute", - Self::StartScreenshare => "start_screenshare", - Self::StopScreenshare => "stop_screenshare", - } - } -} - -pub struct Audio { - _output_stream: Option, - output_handle: Option, -} - -impl Audio { - pub fn new() -> Self { - Self { - _output_stream: None, - output_handle: None, - } - } - - fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> { - if self.output_handle.is_none() { - let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); - self.output_handle = output_handle; - self._output_stream = _output_stream; - } - - self.output_handle.as_ref() - } - - pub fn play_sound(sound: Sound, cx: &mut AppContext) { - if !cx.has_global::() { - return; - } - - cx.update_global::(|this, cx| { - let output_handle = this.ensure_output_exists()?; - let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; - output_handle.play_raw(source).log_err()?; - Some(()) - }); - } - - pub fn end_call(cx: &mut AppContext) { - if !cx.has_global::() { - return; - } - - cx.update_global::(|this, _| { - this._output_stream.take(); - this.output_handle.take(); - }); - } -} diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 884ed2b7a0..5f0224aa7b 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -22,6 +22,7 @@ anyhow.workspace = true isahc.workspace = true lazy_static.workspace = true log.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index cf285ac7cf..06e445e3de 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -3,18 +3,24 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; +use db::RELEASE_CHANNEL; use gpui::{ - actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, - Task, WeakViewHandle, + actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task, + ViewContext, VisualContext, WindowContext, }; use isahc::AsyncBody; + +use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; -use settings::{Setting, SettingsStore}; -use smol::{fs::File, io::AsyncReadExt, process::Command}; +use smol::io::AsyncReadExt; + +use settings::{Settings, SettingsStore}; +use smol::{fs::File, process::Command}; + use std::{ffi::OsString, sync::Arc, time::Duration}; use update_notification::UpdateNotification; -use util::channel::ReleaseChannel; +use util::channel::{AppCommitSha, ReleaseChannel}; use util::http::HttpClient; use workspace::Workspace; @@ -42,9 +48,9 @@ pub enum AutoUpdateStatus { pub struct AutoUpdater { status: AutoUpdateStatus, - current_version: AppVersion, + current_version: SemanticVersion, http_client: Arc, - pending_poll: Option>, + pending_poll: Option>>, server_url: String, } @@ -54,41 +60,59 @@ struct JsonRelease { url: String, } -impl Entity for AutoUpdater { - type Event = (); -} - struct AutoUpdateSetting(bool); -impl Setting for AutoUpdateSetting { +/// Whether or not to automatically check for updates. +/// +/// Default: true +#[derive(Clone, Default, JsonSchema, Deserialize, Serialize)] +#[serde(transparent)] +struct AutoUpdateSettingOverride(Option); + +impl Settings for AutoUpdateSetting { const KEY: Option<&'static str> = Some("auto_update"); - type FileContent = Option; + type FileContent = AutoUpdateSettingOverride; fn load( - default_value: &Option, - user_values: &[&Option], - _: &AppContext, + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut AppContext, ) -> Result { Ok(Self( - Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?, + Self::json_merge(default_value, user_values)? + .0 + .ok_or_else(Self::missing_default)?, )) } } pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { - settings::register::(cx); + AutoUpdateSetting::register(cx); - if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) { - let auto_updater = cx.add_model(|cx| { + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(|_, action: &Check, cx| check(action, cx)); + + workspace.register_action(|_, action, cx| view_release_notes(action, cx)); + + // @nate - code to trigger update notification on launch + // todo!("remove this when Nate is done") + // workspace.show_notification(0, _cx, |cx| { + // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) + // }); + }) + .detach(); + + if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) { + let auto_updater = cx.new_model(|cx| { let updater = AutoUpdater::new(version, http_client, server_url); - let mut update_subscription = settings::get::(cx) + let mut update_subscription = AutoUpdateSetting::get_global(cx) .0 .then(|| updater.start_polling(cx)); - cx.observe_global::(move |updater, cx| { - if settings::get::(cx).0 { + cx.observe_global::(move |updater, cx| { + if AutoUpdateSetting::get_global(cx).0 { if update_subscription.is_none() { update_subscription = Some(updater.start_polling(cx)) } @@ -101,19 +125,22 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo updater }); cx.set_global(Some(auto_updater)); - cx.add_global_action(check); - cx.add_global_action(view_release_notes); - cx.add_action(UpdateNotification::dismiss); } } -pub fn check(_: &Check, cx: &mut AppContext) { +pub fn check(_: &Check, cx: &mut WindowContext) { if let Some(updater) = AutoUpdater::get(cx) { updater.update(cx, |updater, cx| updater.poll(cx)); + } else { + drop(cx.prompt( + gpui::PromptLevel::Info, + "Auto-updates disabled for non-bundled app.", + &["Ok"], + )); } } -fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { let auto_updater = auto_updater.read(cx); let server_url = &auto_updater.server_url; @@ -122,31 +149,28 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { match cx.global::() { ReleaseChannel::Dev => {} ReleaseChannel::Nightly => {} - ReleaseChannel::Preview => cx - .platform() - .open_url(&format!("{server_url}/releases/preview/{current_version}")), - ReleaseChannel::Stable => cx - .platform() - .open_url(&format!("{server_url}/releases/stable/{current_version}")), + ReleaseChannel::Preview => { + cx.open_url(&format!("{server_url}/releases/preview/{current_version}")) + } + ReleaseChannel::Stable => { + cx.open_url(&format!("{server_url}/releases/stable/{current_version}")) + } } } } } -pub fn notify_of_any_new_update( - workspace: WeakViewHandle, - cx: &mut AppContext, -) -> Option<()> { +pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { let updater = AutoUpdater::get(cx)?; let version = updater.read(cx).current_version; let should_show_notification = updater.read(cx).should_show_update_notification(cx); - cx.spawn(|mut cx| async move { + cx.spawn(|workspace, mut cx| async move { let should_show_notification = should_show_notification.await?; if should_show_notification { workspace.update(&mut cx, |workspace, cx| { workspace.show_notification(0, cx, |cx| { - cx.add_view(|_| UpdateNotification::new(version)) + cx.new_view(|_| UpdateNotification::new(version)) }); updater .read(cx) @@ -162,12 +186,12 @@ pub fn notify_of_any_new_update( } impl AutoUpdater { - pub fn get(cx: &mut AppContext) -> Option> { - cx.default_global::>>().clone() + pub fn get(cx: &mut AppContext) -> Option> { + cx.default_global::>>().clone() } fn new( - current_version: AppVersion, + current_version: SemanticVersion, http_client: Arc, server_url: String, ) -> Self { @@ -180,11 +204,11 @@ impl AutoUpdater { } } - pub fn start_polling(&self, cx: &mut ModelContext) -> Task<()> { + pub fn start_polling(&self, cx: &mut ModelContext) -> Task> { cx.spawn(|this, mut cx| async move { loop { - this.update(&mut cx, |this, cx| this.poll(cx)); - cx.background().timer(POLL_INTERVAL).await; + this.update(&mut cx, |this, cx| this.poll(cx))?; + cx.background_executor().timer(POLL_INTERVAL).await; } }) } @@ -198,7 +222,7 @@ impl AutoUpdater { cx.notify(); self.pending_poll = Some(cx.spawn(|this, mut cx| async move { - let result = Self::update(this.clone(), cx.clone()).await; + let result = Self::update(this.upgrade()?, cx.clone()).await; this.update(&mut cx, |this, cx| { this.pending_poll = None; if let Err(error) = result { @@ -206,7 +230,8 @@ impl AutoUpdater { this.status = AutoUpdateStatus::Errored; cx.notify(); } - }); + }) + .ok() })); } @@ -219,26 +244,26 @@ impl AutoUpdater { cx.notify(); } - async fn update(this: ModelHandle, mut cx: AsyncAppContext) -> Result<()> { + async fn update(this: Model, mut cx: AsyncAppContext) -> Result<()> { let (client, server_url, current_version) = this.read_with(&cx, |this, _| { ( this.http_client.clone(), this.server_url.clone(), this.current_version, ) - }); + })?; let mut url_string = format!( "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg" ); - cx.read(|cx| { + cx.update(|cx| { if cx.has_global::() { if let Some(param) = cx.global::().release_query_param() { url_string += "&"; url_string += param; } } - }); + })?; let mut response = client.get(&url_string, Default::default(), true).await?; @@ -251,26 +276,32 @@ impl AutoUpdater { let release: JsonRelease = serde_json::from_slice(body.as_slice()).context("error deserializing release")?; - let latest_version = release.version.parse::()?; - if latest_version <= current_version { + let should_download = match *RELEASE_CHANNEL { + ReleaseChannel::Nightly => cx + .try_read_global::(|sha, _| release.version != sha.0) + .unwrap_or(true), + _ => release.version.parse::()? > current_version, + }; + + if !should_download { this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Idle; cx.notify(); - }); + })?; return Ok(()); } this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Downloading; cx.notify(); - }); + })?; let temp_dir = tempdir::TempDir::new("zed-auto-update")?; let dmg_path = temp_dir.path().join("Zed.dmg"); let mount_path = temp_dir.path().join("Zed"); let running_app_path = ZED_APP_PATH .clone() - .map_or_else(|| cx.platform().app_path(), Ok)?; + .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?; let running_app_filename = running_app_path .file_name() .ok_or_else(|| anyhow!("invalid running app path"))?; @@ -279,15 +310,15 @@ impl AutoUpdater { let mut dmg_file = File::create(&dmg_path).await?; - let (installation_id, release_channel, telemetry) = cx.read(|cx| { + let (installation_id, release_channel, telemetry) = cx.update(|cx| { let installation_id = cx.global::>().telemetry().installation_id(); let release_channel = cx .has_global::() .then(|| cx.global::().display_name()); - let telemetry = settings::get::(cx).metrics; + let telemetry = TelemetrySettings::get_global(cx).metrics; (installation_id, release_channel, telemetry) - }); + })?; let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody { installation_id, @@ -302,7 +333,7 @@ impl AutoUpdater { this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Installing; cx.notify(); - }); + })?; let output = Command::new("hdiutil") .args(&["attach", "-nobrowse"]) @@ -348,7 +379,7 @@ impl AutoUpdater { .detach_and_log_err(cx); this.status = AutoUpdateStatus::Updated; cx.notify(); - }); + })?; Ok(()) } @@ -357,7 +388,7 @@ impl AutoUpdater { should_show: bool, cx: &AppContext, ) -> Task> { - cx.background().spawn(async move { + cx.background_executor().spawn(async move { if should_show { KEY_VALUE_STORE .write_kvp( @@ -375,7 +406,7 @@ impl AutoUpdater { } fn should_show_update_notification(&self, cx: &AppContext) -> Task> { - cx.background().spawn(async move { + cx.background_executor().spawn(async move { Ok(KEY_VALUE_STORE .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? .is_some()) diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index e4a5c23534..65f786bca4 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -1,106 +1,56 @@ -use crate::ViewReleaseNotes; use gpui::{ - elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, - platform::{AppVersion, CursorStyle, MouseButton}, - Element, Entity, View, ViewContext, + div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render, + SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, }; use menu::Cancel; use util::channel::ReleaseChannel; -use workspace::notifications::Notification; +use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt}; pub struct UpdateNotification { - version: AppVersion, + version: SemanticVersion, } -pub enum Event { - Dismiss, -} - -impl Entity for UpdateNotification { - type Event = Event; -} - -impl View for UpdateNotification { - fn ui_name() -> &'static str { - "UpdateNotification" - } - - fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - let theme = theme::current(cx).clone(); - let theme = &theme.update_notification; +impl EventEmitter for UpdateNotification {} +impl Render for UpdateNotification { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let app_name = cx.global::().display_name(); - MouseEventHandler::new::(0, cx, |state, cx| { - Flex::column() - .with_child( - Flex::row() - .with_child( - Text::new( - format!("Updated to {app_name} {}", self.version), - theme.message.text.clone(), - ) - .contained() - .with_style(theme.message.container) - .aligned() - .top() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_padding(Padding::uniform(5.)) - .on_click(MouseButton::Left, move |_, this, cx| { - this.dismiss(&Default::default(), cx) - }) - .aligned() - .constrained() - .with_height(cx.font_cache().line_height(theme.message.text.font_size)) - .aligned() - .top() - .flex_float(), - ), - ) - .with_child({ - let style = theme.action_message.style_for(state); - Text::new("View the release notes", style.text.clone()) - .contained() - .with_style(style.container) - }) - .contained() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - crate::view_release_notes(&Default::default(), cx) - }) - .into_any_named("update notification") - } -} - -impl Notification for UpdateNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - matches!(event, Event::Dismiss) + v_stack() + .on_action(cx.listener(UpdateNotification::dismiss)) + .elevation_3(cx) + .p_4() + .child( + h_stack() + .justify_between() + .child(Label::new(format!( + "Updated to {app_name} {}", + self.version + ))) + .child( + div() + .id("cancel") + .child(Icon::new(IconName::Close)) + .cursor_pointer() + .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))), + ), + ) + .child( + div() + .id("notes") + .child(Label::new("View the release notes")) + .cursor_pointer() + .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), + ) } } impl UpdateNotification { - pub fn new(version: AppVersion) -> Self { + pub fn new(version: SemanticVersion) -> Self { Self { version } } pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(Event::Dismiss); + cx.emit(DismissEvent); } } diff --git a/crates/auto_update2/Cargo.toml b/crates/auto_update2/Cargo.toml deleted file mode 100644 index 20eb129746..0000000000 --- a/crates/auto_update2/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "auto_update2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/auto_update.rs" -doctest = false - -[dependencies] -db = { package = "db2", path = "../db2" } -client = { package = "client2", path = "../client2" } -gpui = { package = "gpui2", path = "../gpui2" } -menu = { package = "menu2", path = "../menu2" } -project = { package = "project2", path = "../project2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2" } -util = { path = "../util" } -anyhow.workspace = true -isahc.workspace = true -lazy_static.workspace = true -log.workspace = true -serde.workspace = true -serde_derive.workspace = true -serde_json.workspace = true -smol.workspace = true -tempdir.workspace = true diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs deleted file mode 100644 index 31e474242a..0000000000 --- a/crates/auto_update2/src/auto_update.rs +++ /dev/null @@ -1,412 +0,0 @@ -mod update_notification; - -use anyhow::{anyhow, Context, Result}; -use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; -use db::kvp::KEY_VALUE_STORE; -use db::RELEASE_CHANNEL; -use gpui::{ - actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task, - ViewContext, VisualContext, -}; -use isahc::AsyncBody; -use serde::Deserialize; -use serde_derive::Serialize; -use smol::io::AsyncReadExt; - -use settings::{Settings, SettingsStore}; -use smol::{fs::File, process::Command}; -use std::{ffi::OsString, sync::Arc, time::Duration}; -use update_notification::UpdateNotification; -use util::channel::{AppCommitSha, ReleaseChannel}; -use util::http::HttpClient; -use workspace::Workspace; - -const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; -const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); - -//todo!(remove CheckThatAutoUpdaterWorks) -actions!( - auto_update, - [ - Check, - DismissErrorMessage, - ViewReleaseNotes, - CheckThatAutoUpdaterWorks - ] -); - -#[derive(Serialize)] -struct UpdateRequestBody { - installation_id: Option>, - release_channel: Option<&'static str>, - telemetry: bool, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum AutoUpdateStatus { - Idle, - Checking, - Downloading, - Installing, - Updated, - Errored, -} - -pub struct AutoUpdater { - status: AutoUpdateStatus, - current_version: SemanticVersion, - http_client: Arc, - pending_poll: Option>>, - server_url: String, -} - -#[derive(Deserialize)] -struct JsonRelease { - version: String, - url: String, -} - -struct AutoUpdateSetting(bool); - -impl Settings for AutoUpdateSetting { - const KEY: Option<&'static str> = Some("auto_update"); - - type FileContent = Option; - - fn load( - default_value: &Option, - user_values: &[&Option], - _: &mut AppContext, - ) -> Result { - Ok(Self( - Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?, - )) - } -} - -pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { - AutoUpdateSetting::register(cx); - - cx.observe_new_views(|workspace: &mut Workspace, _cx| { - workspace.register_action(|_, action: &Check, cx| check(action, cx)); - - // @nate - code to trigger update notification on launch - // workspace.show_notification(0, _cx, |cx| { - // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) - // }); - }) - .detach(); - - if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) { - let auto_updater = cx.build_model(|cx| { - let updater = AutoUpdater::new(version, http_client, server_url); - - let mut update_subscription = AutoUpdateSetting::get_global(cx) - .0 - .then(|| updater.start_polling(cx)); - - cx.observe_global::(move |updater, cx| { - if AutoUpdateSetting::get_global(cx).0 { - if update_subscription.is_none() { - update_subscription = Some(updater.start_polling(cx)) - } - } else { - update_subscription.take(); - } - }) - .detach(); - - updater - }); - cx.set_global(Some(auto_updater)); - //todo!(action) - // cx.add_global_action(view_release_notes); - // cx.add_action(UpdateNotification::dismiss); - } -} - -pub fn check(_: &Check, cx: &mut ViewContext) { - if let Some(updater) = AutoUpdater::get(cx) { - updater.update(cx, |updater, cx| updater.poll(cx)); - } else { - drop(cx.prompt( - gpui::PromptLevel::Info, - "Auto-updates disabled for non-bundled app.", - &["Ok"], - )); - } -} - -pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { - if let Some(auto_updater) = AutoUpdater::get(cx) { - let auto_updater = auto_updater.read(cx); - let server_url = &auto_updater.server_url; - let current_version = auto_updater.current_version; - if cx.has_global::() { - match cx.global::() { - ReleaseChannel::Dev => {} - ReleaseChannel::Nightly => {} - ReleaseChannel::Preview => { - cx.open_url(&format!("{server_url}/releases/preview/{current_version}")) - } - ReleaseChannel::Stable => { - cx.open_url(&format!("{server_url}/releases/stable/{current_version}")) - } - } - } - } -} - -pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { - let updater = AutoUpdater::get(cx)?; - let version = updater.read(cx).current_version; - let should_show_notification = updater.read(cx).should_show_update_notification(cx); - - cx.spawn(|workspace, mut cx| async move { - let should_show_notification = should_show_notification.await?; - if should_show_notification { - workspace.update(&mut cx, |workspace, cx| { - workspace.show_notification(0, cx, |cx| { - cx.build_view(|_| UpdateNotification::new(version)) - }); - updater - .read(cx) - .set_should_show_update_notification(false, cx) - .detach_and_log_err(cx); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - None -} - -impl AutoUpdater { - pub fn get(cx: &mut AppContext) -> Option> { - cx.default_global::>>().clone() - } - - fn new( - current_version: SemanticVersion, - http_client: Arc, - server_url: String, - ) -> Self { - Self { - status: AutoUpdateStatus::Idle, - current_version, - http_client, - server_url, - pending_poll: None, - } - } - - pub fn start_polling(&self, cx: &mut ModelContext) -> Task> { - cx.spawn(|this, mut cx| async move { - loop { - this.update(&mut cx, |this, cx| this.poll(cx))?; - cx.background_executor().timer(POLL_INTERVAL).await; - } - }) - } - - pub fn poll(&mut self, cx: &mut ModelContext) { - if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated { - return; - } - - self.status = AutoUpdateStatus::Checking; - cx.notify(); - - self.pending_poll = Some(cx.spawn(|this, mut cx| async move { - let result = Self::update(this.upgrade()?, cx.clone()).await; - this.update(&mut cx, |this, cx| { - this.pending_poll = None; - if let Err(error) = result { - log::error!("auto-update failed: error:{:?}", error); - this.status = AutoUpdateStatus::Errored; - cx.notify(); - } - }) - .ok() - })); - } - - pub fn status(&self) -> AutoUpdateStatus { - self.status - } - - pub fn dismiss_error(&mut self, cx: &mut ModelContext) { - self.status = AutoUpdateStatus::Idle; - cx.notify(); - } - - async fn update(this: Model, mut cx: AsyncAppContext) -> Result<()> { - let (client, server_url, current_version) = this.read_with(&cx, |this, _| { - ( - this.http_client.clone(), - this.server_url.clone(), - this.current_version, - ) - })?; - - let mut url_string = format!( - "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg" - ); - cx.update(|cx| { - if cx.has_global::() { - if let Some(param) = cx.global::().release_query_param() { - url_string += "&"; - url_string += param; - } - } - })?; - - let mut response = client.get(&url_string, Default::default(), true).await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading release")?; - let release: JsonRelease = - serde_json::from_slice(body.as_slice()).context("error deserializing release")?; - - let should_download = match *RELEASE_CHANNEL { - ReleaseChannel::Nightly => cx - .try_read_global::(|sha, _| release.version != sha.0) - .unwrap_or(true), - _ => release.version.parse::()? <= current_version, - }; - - if !should_download { - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Idle; - cx.notify(); - })?; - return Ok(()); - } - - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Downloading; - cx.notify(); - })?; - - let temp_dir = tempdir::TempDir::new("zed-auto-update")?; - let dmg_path = temp_dir.path().join("Zed.dmg"); - let mount_path = temp_dir.path().join("Zed"); - let running_app_path = ZED_APP_PATH - .clone() - .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?; - let running_app_filename = running_app_path - .file_name() - .ok_or_else(|| anyhow!("invalid running app path"))?; - let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into(); - mounted_app_path.push("/"); - - let mut dmg_file = File::create(&dmg_path).await?; - - let (installation_id, release_channel, telemetry) = cx.update(|cx| { - let installation_id = cx.global::>().telemetry().installation_id(); - let release_channel = cx - .has_global::() - .then(|| cx.global::().display_name()); - let telemetry = TelemetrySettings::get_global(cx).metrics; - - (installation_id, release_channel, telemetry) - })?; - - let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody { - installation_id, - release_channel, - telemetry, - })?); - - let mut response = client.get(&release.url, request_body, true).await?; - smol::io::copy(response.body_mut(), &mut dmg_file).await?; - log::info!("downloaded update. path:{:?}", dmg_path); - - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Installing; - cx.notify(); - })?; - - let output = Command::new("hdiutil") - .args(&["attach", "-nobrowse"]) - .arg(&dmg_path) - .arg("-mountroot") - .arg(&temp_dir.path()) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to mount: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - let output = Command::new("rsync") - .args(&["-av", "--delete"]) - .arg(&mounted_app_path) - .arg(&running_app_path) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to copy app: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - let output = Command::new("hdiutil") - .args(&["detach"]) - .arg(&mount_path) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to unmount: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - this.update(&mut cx, |this, cx| { - this.set_should_show_update_notification(true, cx) - .detach_and_log_err(cx); - this.status = AutoUpdateStatus::Updated; - cx.notify(); - })?; - Ok(()) - } - - fn set_should_show_update_notification( - &self, - should_show: bool, - cx: &AppContext, - ) -> Task> { - cx.background_executor().spawn(async move { - if should_show { - KEY_VALUE_STORE - .write_kvp( - SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(), - "".to_string(), - ) - .await?; - } else { - KEY_VALUE_STORE - .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string()) - .await?; - } - Ok(()) - }) - } - - fn should_show_update_notification(&self, cx: &AppContext) -> Task> { - cx.background_executor().spawn(async move { - Ok(KEY_VALUE_STORE - .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? - .is_some()) - }) - } -} diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs deleted file mode 100644 index 4a2efcf807..0000000000 --- a/crates/auto_update2/src/update_notification.rs +++ /dev/null @@ -1,56 +0,0 @@ -use gpui::{ - div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render, - SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, -}; -use util::channel::ReleaseChannel; -use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; - -pub struct UpdateNotification { - version: SemanticVersion, -} - -impl EventEmitter for UpdateNotification {} - -impl Render for UpdateNotification { - type Element = Div; - - fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - let app_name = cx.global::().display_name(); - - v_stack() - .elevation_3(cx) - .p_4() - .child( - h_stack() - .justify_between() - .child(Label::new(format!( - "Updated to {app_name} {}", - self.version - ))) - .child( - div() - .id("cancel") - .child(IconElement::new(Icon::Close)) - .cursor_pointer() - .on_click(cx.listener(|this, _, cx| this.dismiss(cx))), - ), - ) - .child( - div() - .id("notes") - .child(Label::new("View the release notes")) - .cursor_pointer() - .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), - ) - } -} - -impl UpdateNotification { - pub fn new(version: SemanticVersion) -> Self { - Self { version } - } - - pub fn dismiss(&mut self, cx: &mut ViewContext) { - cx.emit(DismissEvent); - } -} diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 412a79a317..e8663f9bc7 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -12,6 +12,7 @@ doctest = false collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } +ui = { path = "../ui" } language = { path = "../language" } project = { path = "../project" } search = { path = "../search" } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 41985edb75..e41c0c06b1 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -1,108 +1,77 @@ +use editor::Editor; use gpui::{ - elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext, - ViewHandle, WeakViewHandle, + Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, + ViewContext, }; use itertools::Itertools; -use search::ProjectSearchView; +use theme::ActiveTheme; +use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip}; use workspace::{ item::{ItemEvent, ItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, }; -pub enum Event { - UpdateLocation, -} - pub struct Breadcrumbs { pane_focused: bool, active_item: Option>, - project_search: Option>, subscription: Option, - workspace: WeakViewHandle, } impl Breadcrumbs { - pub fn new(workspace: &Workspace) -> Self { + pub fn new() -> Self { Self { pane_focused: false, active_item: Default::default(), subscription: Default::default(), - project_search: Default::default(), - workspace: workspace.weak_handle(), } } } -impl Entity for Breadcrumbs { - type Event = Event; -} +impl EventEmitter for Breadcrumbs {} -impl View for Breadcrumbs { - fn ui_name() -> &'static str { - "Breadcrumbs" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let active_item = match &self.active_item { - Some(active_item) => active_item, - None => return Empty::new().into_any(), +impl Render for Breadcrumbs { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let element = h_stack().text_ui(); + let Some(active_item) = self.active_item.as_ref() else { + return element; + }; + let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { + return element; }; - let not_editor = active_item.downcast::().is_none(); - let theme = theme::current(cx).clone(); - let style = &theme.workspace.toolbar.breadcrumbs; + let highlighted_segments = segments.into_iter().map(|segment| { + let mut text_style = cx.text_style(); + text_style.color = Color::Muted.color(cx); - let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { - Some(breadcrumbs) => breadcrumbs, - None => return Empty::new().into_any(), - } - .into_iter() - .map(|breadcrumb| { - Text::new( - breadcrumb.text, - theme.workspace.toolbar.breadcrumbs.default.text.clone(), - ) - .with_highlights(breadcrumb.highlights.unwrap_or_default()) - .into_any() + StyledText::new(segment.text) + .with_highlights(&text_style, segment.highlights.unwrap_or_default()) + .into_any() + }); + let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { + Label::new("›").color(Color::Muted).into_any_element() }); - let crumbs = Flex::row() - .with_children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" › ", style.default.text.clone()).into_any() - })) - .constrained() - .with_height(theme.workspace.toolbar.breadcrumb_height) - .contained(); - - if not_editor || !self.pane_focused { - return crumbs - .with_style(style.default.container) - .aligned() - .left() - .into_any(); + let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs); + match active_item + .downcast::() + .map(|editor| editor.downgrade()) + { + Some(editor) => element.child( + ButtonLike::new("toggle outline view") + .child(breadcrumbs_stack) + .style(ButtonStyle::Subtle) + .on_click(move |_, cx| { + if let Some(editor) = editor.upgrade() { + outline::toggle(editor, &outline::Toggle, cx) + } + }) + .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)), + ), + None => element + // Match the height of the `ButtonLike` in the other arm. + .h(rems(22. / 16.)) + .child(breadcrumbs_stack), } - - MouseEventHandler::new::(0, cx, |state, _| { - let style = style.style_for(state); - crumbs.with_style(style.container) - }) - .on_click(MouseButton::Left, |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - outline::toggle(workspace, &Default::default(), cx) - }) - } - }) - .with_tooltip::( - 0, - "Show symbol outline".to_owned(), - Some(Box::new(outline::Toggle)), - theme.tooltip.clone(), - cx, - ) - .aligned() - .left() - .into_any() } } @@ -114,19 +83,21 @@ impl ToolbarItemView for Breadcrumbs { ) -> ToolbarItemLocation { cx.notify(); self.active_item = None; - self.project_search = None; if let Some(item) = active_pane_item { - let this = cx.weak_handle(); + let this = cx.view().downgrade(); self.subscription = Some(item.subscribe_to_item_events( cx, Box::new(move |event, cx| { - if let Some(this) = this.upgrade(cx) { - if let ItemEvent::UpdateBreadcrumbs = event { - this.update(cx, |_, cx| { - cx.emit(Event::UpdateLocation); - cx.notify(); - }); - } + if let ItemEvent::UpdateBreadcrumbs = event { + this.update(cx, |this, cx| { + cx.notify(); + if let Some(active_item) = this.active_item.as_ref() { + cx.emit(ToolbarItemEvent::ChangeLocation( + active_item.breadcrumb_location(cx), + )) + } + }) + .ok(); } }), )); @@ -137,19 +108,6 @@ impl ToolbarItemView for Breadcrumbs { } } - fn location_for_event( - &self, - _: &Event, - current_location: ToolbarItemLocation, - cx: &AppContext, - ) -> ToolbarItemLocation { - if let Some(active_item) = self.active_item.as_ref() { - active_item.breadcrumb_location(cx) - } else { - current_location - } - } - fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext) { self.pane_focused = pane_focused; } diff --git a/crates/breadcrumbs2/Cargo.toml b/crates/breadcrumbs2/Cargo.toml deleted file mode 100644 index de3a54d40e..0000000000 --- a/crates/breadcrumbs2/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "breadcrumbs2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/breadcrumbs.rs" -doctest = false - -[dependencies] -collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } -gpui = { package = "gpui2", path = "../gpui2" } -ui = { package = "ui2", path = "../ui2" } -language = { package = "language2", path = "../language2" } -project = { package = "project2", path = "../project2" } -search = { package = "search2", path = "../search2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2" } -outline = { package = "outline2", path = "../outline2" } -itertools = "0.10" - -[dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/breadcrumbs2/src/breadcrumbs.rs b/crates/breadcrumbs2/src/breadcrumbs.rs deleted file mode 100644 index 3b8ce1c6c5..0000000000 --- a/crates/breadcrumbs2/src/breadcrumbs.rs +++ /dev/null @@ -1,127 +0,0 @@ -use editor::Editor; -use gpui::{ - Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, - ViewContext, -}; -use itertools::Itertools; -use theme::ActiveTheme; -use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip}; -use workspace::{ - item::{ItemEvent, ItemHandle}, - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, -}; - -pub enum Event { - UpdateLocation, -} - -pub struct Breadcrumbs { - pane_focused: bool, - active_item: Option>, - subscription: Option, -} - -impl Breadcrumbs { - pub fn new() -> Self { - Self { - pane_focused: false, - active_item: Default::default(), - subscription: Default::default(), - } - } -} - -impl EventEmitter for Breadcrumbs {} -impl EventEmitter for Breadcrumbs {} - -impl Render for Breadcrumbs { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let element = h_stack().text_ui(); - - let Some(active_item) = &self - .active_item - .as_ref() - .filter(|item| item.downcast::().is_some()) - else { - return element; - }; - - let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { - return element; - }; - - let highlighted_segments = segments.into_iter().map(|segment| { - StyledText::new(segment.text) - .with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default()) - .into_any() - }); - let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { - Label::new("›").into_any_element() - }); - - let editor = active_item - .downcast::() - .map(|editor| editor.downgrade()); - - element.child( - ButtonLike::new("toggle outline view") - .style(ButtonStyle::Subtle) - .child(h_stack().gap_1().children(breadcrumbs)) - .on_click(move |_, cx| { - if let Some(editor) = editor.as_ref().and_then(|editor| editor.upgrade()) { - outline::toggle(editor, &outline::Toggle, cx) - } - }) - .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)), - ) - } -} - -impl ToolbarItemView for Breadcrumbs { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> ToolbarItemLocation { - cx.notify(); - self.active_item = None; - if let Some(item) = active_pane_item { - let this = cx.view().downgrade(); - self.subscription = Some(item.subscribe_to_item_events( - cx, - Box::new(move |event, cx| { - if let ItemEvent::UpdateBreadcrumbs = event { - this.update(cx, |_, cx| { - cx.emit(Event::UpdateLocation); - cx.notify(); - }) - .ok(); - } - }), - )); - self.active_item = Some(item.boxed_clone()); - item.breadcrumb_location(cx) - } else { - ToolbarItemLocation::Hidden - } - } - - // fn location_for_event( - // &self, - // _: &Event, - // current_location: ToolbarItemLocation, - // cx: &AppContext, - // ) -> ToolbarItemLocation { - // if let Some(active_item) = self.active_item.as_ref() { - // active_item.breadcrumb_location(cx) - // } else { - // current_location - // } - // } - - fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext) { - self.pane_focused = pane_focused; - } -} diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index eb448d8d8d..7d200a0d21 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -35,11 +35,13 @@ util = { path = "../util" } anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true +image = "0.23" postage.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_derive.workspace = true +smallvec.workspace = true [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 7959a8c7d1..3561cc3385 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -5,35 +5,29 @@ pub mod room; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; +use client::{proto, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, - WeakModelHandle, + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, + WeakModel, }; use postage::watch; use project::Project; +use room::Event; +use settings::Settings; use std::sync::Arc; pub use participant::ParticipantLocation; pub use room::Room; -pub fn init(client: Arc, user_store: ModelHandle, cx: &mut AppContext) { - settings::register::(cx); +pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { + CallSettings::register(cx); - let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx)); + let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx)); cx.set_global(active_call); } -#[derive(Clone)] -pub struct IncomingCall { - pub room_id: u64, - pub calling_user: Arc, - pub participants: Vec>, - pub initial_project: Option, -} - pub struct OneAtATime { cancel: Option>, } @@ -65,43 +59,44 @@ impl OneAtATime { } } +#[derive(Clone)] +pub struct IncomingCall { + pub room_id: u64, + pub calling_user: Arc, + pub participants: Vec>, + pub initial_project: Option, +} + /// Singleton global maintaining the user's participation in a room across workspaces. pub struct ActiveCall { - room: Option<(ModelHandle, Vec)>, - pending_room_creation: Option, Arc>>>>, + room: Option<(Model, Vec)>, + pending_room_creation: Option, Arc>>>>, + location: Option>, _join_debouncer: OneAtATime, - location: Option>, pending_invites: HashSet, incoming_call: ( watch::Sender>, watch::Receiver>, ), client: Arc, - user_store: ModelHandle, + user_store: Model, _subscriptions: Vec, } -impl Entity for ActiveCall { - type Event = room::Event; -} +impl EventEmitter for ActiveCall {} impl ActiveCall { - fn new( - client: Arc, - user_store: ModelHandle, - cx: &mut ModelContext, - ) -> Self { + fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { Self { room: None, pending_room_creation: None, location: None, pending_invites: Default::default(), incoming_call: watch::channel(), - _join_debouncer: OneAtATime { cancel: None }, _subscriptions: vec![ - client.add_request_handler(cx.handle(), Self::handle_incoming_call), - client.add_message_handler(cx.handle(), Self::handle_call_canceled), + client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), + client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), ], client, user_store, @@ -113,35 +108,35 @@ impl ActiveCall { } async fn handle_incoming_call( - this: ModelHandle, + this: Model, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result { - let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; let call = IncomingCall { room_id: envelope.payload.room_id, participants: user_store .update(&mut cx, |user_store, cx| { user_store.get_users(envelope.payload.participant_user_ids, cx) - }) + })? .await?, calling_user: user_store .update(&mut cx, |user_store, cx| { user_store.get_user(envelope.payload.calling_user_id, cx) - }) + })? .await?, initial_project: envelope.payload.initial_project, }; this.update(&mut cx, |this, _| { *this.incoming_call.0.borrow_mut() = Some(call); - }); + })?; Ok(proto::Ack {}) } async fn handle_call_canceled( - this: ModelHandle, + this: Model, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -154,18 +149,18 @@ impl ActiveCall { { incoming_call.take(); } - }); + })?; Ok(()) } - pub fn global(cx: &AppContext) -> ModelHandle { - cx.global::>().clone() + pub fn global(cx: &AppContext) -> Model { + cx.global::>().clone() } pub fn invite( &mut self, called_user_id: u64, - initial_project: Option>, + initial_project: Option>, cx: &mut ModelContext, ) -> Task> { if !self.pending_invites.insert(called_user_id) { @@ -184,21 +179,21 @@ impl ActiveCall { }; let invite = if let Some(room) = room { - cx.spawn_weak(|_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let room = room.await.map_err(|err| anyhow!("{:?}", err))?; let initial_project_id = if let Some(initial_project) = initial_project { Some( - room.update(&mut cx, |room, cx| room.share_project(initial_project, cx)) + room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))? .await?, ) } else { None }; - room.update(&mut cx, |room, cx| { + room.update(&mut cx, move |room, cx| { room.call(called_user_id, initial_project_id, cx) - }) + })? .await?; anyhow::Ok(()) @@ -207,7 +202,7 @@ impl ActiveCall { let client = self.client.clone(); let user_store = self.user_store.clone(); let room = cx - .spawn(|this, mut cx| async move { + .spawn(move |this, mut cx| async move { let create_room = async { let room = cx .update(|cx| { @@ -218,31 +213,31 @@ impl ActiveCall { user_store, cx, ) - }) + })? .await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))? .await?; anyhow::Ok(room) }; let room = create_room.await; - this.update(&mut cx, |this, _| this.pending_room_creation = None); + this.update(&mut cx, |this, _| this.pending_room_creation = None)?; room.map_err(Arc::new) }) .shared(); self.pending_room_creation = Some(room.clone()); - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { room.await.map_err(|err| anyhow!("{:?}", err))?; anyhow::Ok(()) }) }; - cx.spawn(|this, mut cx| async move { + cx.spawn(move |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)); + this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?; } else { // TODO: Resport collaboration error } @@ -250,7 +245,7 @@ impl ActiveCall { this.update(&mut cx, |this, cx| { this.pending_invites.remove(&called_user_id); cx.notify(); - }); + })?; result }) } @@ -267,7 +262,7 @@ impl ActiveCall { }; let client = self.client.clone(); - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { client .request(proto::CancelCall { room_id, @@ -306,23 +301,23 @@ impl ActiveCall { cx.spawn(|this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? .await?; this.update(&mut cx, |this, cx| { this.report_call_event("accept incoming", cx) - }); + })?; Ok(()) }) } - pub fn decline_incoming(&mut self, cx: &mut ModelContext) -> Result<()> { + pub fn decline_incoming(&mut self, _: &mut ModelContext) -> Result<()> { let call = self .incoming_call .0 .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); + report_call_event_for_room("decline incoming", call.room_id, None, &self.client); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -333,7 +328,7 @@ 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(Some(room))); @@ -352,13 +347,13 @@ impl ActiveCall { Room::join_channel(channel_id, client, user_store, cx).await }); - cx.spawn(move |this, mut cx| async move { + cx.spawn(|this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? .await?; this.update(&mut cx, |this, cx| { this.report_call_event("join channel", cx) - }); + })?; Ok(room) }) } @@ -366,6 +361,7 @@ impl ActiveCall { pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { cx.notify(); self.report_call_event("hang up", cx); + Audio::end_call(cx); if let Some((room, _)) = self.room.take() { room.update(cx, |room, cx| room.leave(cx)) @@ -376,7 +372,7 @@ impl ActiveCall { pub fn share_project( &mut self, - project: ModelHandle, + project: Model, cx: &mut ModelContext, ) -> Task> { if let Some((room, _)) = self.room.as_ref() { @@ -389,7 +385,7 @@ impl ActiveCall { pub fn unshare_project( &mut self, - project: ModelHandle, + project: Model, cx: &mut ModelContext, ) -> Result<()> { if let Some((room, _)) = self.room.as_ref() { @@ -400,13 +396,13 @@ impl ActiveCall { } } - pub fn location(&self) -> Option<&WeakModelHandle> { + pub fn location(&self) -> Option<&WeakModel> { self.location.as_ref() } pub fn set_location( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ModelContext, ) -> Task> { if project.is_some() || !*ZED_ALWAYS_ACTIVE { @@ -420,7 +416,7 @@ impl ActiveCall { fn set_room( &mut self, - room: Option>, + room: Option>, cx: &mut ModelContext, ) -> Task> { if room.as_ref() != self.room.as_ref().map(|room| &room.0) { @@ -441,7 +437,10 @@ impl ActiveCall { cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), ]; self.room = Some((room.clone(), subscriptions)); - let location = self.location.and_then(|location| location.upgrade(cx)); + let location = self + .location + .as_ref() + .and_then(|location| location.upgrade()); room.update(cx, |room, cx| room.set_location(location.as_ref(), cx)) } } else { @@ -453,7 +452,7 @@ impl ActiveCall { } } - pub fn room(&self) -> Option<&ModelHandle> { + pub fn room(&self) -> Option<&Model> { self.room.as_ref().map(|(room, _)| room) } @@ -465,10 +464,10 @@ impl ActiveCall { &self.pending_invites } - pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + pub fn report_call_event(&self, operation: &'static str, cx: &mut 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); + report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client); } } } @@ -478,12 +477,10 @@ pub fn report_call_event_for_room( room_id: u64, channel_id: Option, client: &Arc, - cx: &AppContext, ) { let telemetry = client.telemetry(); - let telemetry_settings = *settings::get::(cx); - telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id) + telemetry.report_call_event(operation, Some(room_id), channel_id) } pub fn report_call_event_for_channel( @@ -495,14 +492,8 @@ pub fn report_call_event_for_channel( let room = ActiveCall::global(cx).read(cx).room(); let telemetry = client.telemetry(); - let telemetry_settings = *settings::get::(cx); - telemetry.report_call_event( - telemetry_settings, - operation, - room.map(|r| r.read(cx).id()), - Some(channel_id), - ) + telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id)) } #[cfg(test)] diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 2808a99617..441323ad5f 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -1,18 +1,24 @@ +use anyhow::Result; +use gpui::AppContext; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::Setting; +use settings::Settings; #[derive(Deserialize, Debug)] pub struct CallSettings { pub mute_on_join: bool, } +/// Configuration of voice calls in Zed. #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct CallSettingsContent { + /// Whether the microphone should be muted when joining a channel or a call. + /// + /// Default: false pub mute_on_join: Option, } -impl Setting for CallSettings { +impl Settings for CallSettings { const KEY: Option<&'static str> = Some("calls"); type FileContent = CallSettingsContent; @@ -20,8 +26,11 @@ impl Setting for CallSettings { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &gpui::AppContext, - ) -> anyhow::Result { + _cx: &mut AppContext, + ) -> Result + where + Self: Sized, + { Self::load_via_json_merge(default_value, user_values) } } diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index ab796e56b0..9faefc63c3 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -2,11 +2,11 @@ use anyhow::{anyhow, Result}; use client::ParticipantIndex; use client::{proto, User}; use collections::HashMap; -use gpui::WeakModelHandle; +use gpui::WeakModel; pub use live_kit_client::Frame; -use live_kit_client::RemoteAudioTrack; +pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; -use std::{fmt, sync::Arc}; +use std::sync::Arc; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ParticipantLocation { @@ -35,13 +35,15 @@ impl ParticipantLocation { #[derive(Clone, Default)] pub struct LocalParticipant { pub projects: Vec, - pub active_project: Option>, + pub active_project: Option>, + pub role: proto::ChannelRole, } #[derive(Clone, Debug)] pub struct RemoteParticipant { pub user: Arc, pub peer_id: proto::PeerId, + pub role: proto::ChannelRole, pub projects: Vec, pub location: ParticipantLocation, pub participant_index: ParticipantIndex, @@ -50,20 +52,3 @@ pub struct RemoteParticipant { pub video_tracks: HashMap>, pub audio_tracks: HashMap>, } - -#[derive(Clone)] -pub struct RemoteVideoTrack { - pub(crate) live_kit_track: Arc, -} - -impl fmt::Debug for RemoteVideoTrack { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RemoteVideoTrack").finish() - } -} - -impl RemoteVideoTrack { - pub fn frames(&self) -> async_broadcast::Receiver { - self.live_kit_track.frames() - } -} diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 8d37194f3a..e2c9bf5886 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,6 +1,6 @@ use crate::{ call_settings::CallSettings, - participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack}, + participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, }; use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; @@ -11,7 +11,9 @@ use client::{ use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; use futures::{FutureExt, StreamExt}; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, +}; use language::LanguageRegistry; use live_kit_client::{ LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate, @@ -19,7 +21,8 @@ use live_kit_client::{ }; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; -use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; +use settings::Settings as _; +use std::{future::Future, mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -54,11 +57,11 @@ pub enum Event { pub struct Room { id: u64, - pub channel_id: Option, + channel_id: Option, live_kit: Option, status: RoomStatus, - shared_projects: HashSet>, - joined_projects: HashSet>, + shared_projects: HashSet>, + joined_projects: HashSet>, local_participant: LocalParticipant, remote_participants: BTreeMap, pending_participants: Vec>, @@ -66,39 +69,17 @@ pub struct Room { pending_call_count: usize, leave_when_empty: bool, client: Arc, - user_store: ModelHandle, + user_store: Model, follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, - subscriptions: Vec, + client_subscriptions: Vec, + _subscriptions: Vec, room_update_completed_tx: watch::Sender>, room_update_completed_rx: watch::Receiver>, pending_room_update: Option>, maintain_connection: Option>>, } -impl Entity for Room { - type Event = Event; - - fn release(&mut self, cx: &mut AppContext) { - if self.status.is_online() { - self.leave_internal(cx).detach_and_log_err(cx); - } - } - - fn app_will_quit(&mut self, cx: &mut AppContext) -> Option>>> { - if self.status.is_online() { - let leave = self.leave_internal(cx); - Some( - cx.background() - .spawn(async move { - leave.await.log_err(); - }) - .boxed(), - ) - } else { - None - } - } -} +impl EventEmitter for Room {} impl Room { pub fn channel_id(&self) -> Option { @@ -121,16 +102,12 @@ impl Room { } } - pub fn can_publish(&self) -> bool { - self.live_kit.as_ref().is_some_and(|room| room.can_publish) - } - fn new( id: u64, channel_id: Option, live_kit_connection_info: Option, client: Arc, - user_store: ModelHandle, + user_store: Model, cx: &mut ModelContext, ) -> Self { let live_kit_room = if let Some(connection_info) = live_kit_connection_info { @@ -138,69 +115,75 @@ impl Room { let mut status = room.status(); // Consume the initial status of the room. let _ = status.try_recv(); - let _maintain_room = cx.spawn_weak(|this, mut cx| async move { + let _maintain_room = cx.spawn(|this, mut cx| async move { while let Some(status) = status.next().await { - let this = if let Some(this) = this.upgrade(&cx) { + let this = if let Some(this) = this.upgrade() { this } else { break; }; if status == live_kit_client::ConnectionState::Disconnected { - this.update(&mut cx, |this, cx| this.leave(cx).log_err()); + this.update(&mut cx, |this, cx| this.leave(cx).log_err()) + .ok(); break; } } }); - let mut track_video_changes = room.remote_video_track_updates(); - let _maintain_video_tracks = cx.spawn_weak(|this, mut cx| async move { - while let Some(track_change) = track_video_changes.next().await { - let this = if let Some(this) = this.upgrade(&cx) { - this - } else { - break; - }; + let _maintain_video_tracks = cx.spawn({ + let room = room.clone(); + move |this, mut cx| async move { + let mut track_video_changes = room.remote_video_track_updates(); + while let Some(track_change) = track_video_changes.next().await { + let this = if let Some(this) = this.upgrade() { + this + } else { + break; + }; - this.update(&mut cx, |this, cx| { - this.remote_video_track_updated(track_change, cx).log_err() - }); + this.update(&mut cx, |this, cx| { + this.remote_video_track_updated(track_change, cx).log_err() + }) + .ok(); + } } }); - let mut track_audio_changes = room.remote_audio_track_updates(); - let _maintain_audio_tracks = cx.spawn_weak(|this, mut cx| async move { - while let Some(track_change) = track_audio_changes.next().await { - let this = if let Some(this) = this.upgrade(&cx) { - this - } else { - break; - }; + let _maintain_audio_tracks = cx.spawn({ + let room = room.clone(); + |this, mut cx| async move { + let mut track_audio_changes = room.remote_audio_track_updates(); + while let Some(track_change) = track_audio_changes.next().await { + let this = if let Some(this) = this.upgrade() { + this + } else { + break; + }; - this.update(&mut cx, |this, cx| { - this.remote_audio_track_updated(track_change, cx).log_err() - }); + this.update(&mut cx, |this, cx| { + this.remote_audio_track_updated(track_change, cx).log_err() + }) + .ok(); + } } }); let connect = room.connect(&connection_info.server_url, &connection_info.token); - if connection_info.can_publish { - cx.spawn(|this, mut cx| async move { - connect.await?; + cx.spawn(|this, mut cx| async move { + connect.await?; - if !cx.read(Self::mute_on_join) { - this.update(&mut cx, |this, cx| this.share_microphone(cx)) - .await?; - } + if !cx.update(|cx| Self::mute_on_join(cx))? { + this.update(&mut cx, |this, cx| this.share_microphone(cx))? + .await?; + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); Some(LiveKitRoom { room, - can_publish: connection_info.can_publish, screen_track: LocalTrack::None, microphone_track: LocalTrack::None, next_publish_id: 0, @@ -214,8 +197,10 @@ impl Room { None }; - let maintain_connection = - cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()); + let maintain_connection = cx.spawn({ + let client = client.clone(); + move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err() + }); Audio::play_sound(Sound::Joined, cx); @@ -233,7 +218,13 @@ impl Room { remote_participants: Default::default(), pending_participants: Default::default(), pending_call_count: 0, - subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], + client_subscriptions: vec![ + client.add_message_handler(cx.weak_model(), Self::handle_room_updated) + ], + _subscriptions: vec![ + cx.on_release(Self::released), + cx.on_app_quit(Self::app_will_quit), + ], leave_when_empty: false, pending_room_update: None, client, @@ -247,30 +238,34 @@ impl Room { pub(crate) fn create( called_user_id: u64, - initial_project: Option>, + initial_project: Option>, client: Arc, - user_store: ModelHandle, + user_store: Model, cx: &mut AppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { + ) -> Task>> { + cx.spawn(move |mut cx| async move { let response = client.request(proto::CreateRoom {}).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.add_model(|cx| { - Self::new( + let room = cx.new_model(|cx| { + let mut room = Self::new( room_proto.id, None, response.live_kit_connection_info, client, user_store, cx, - ) - }); + ); + if let Some(participant) = room_proto.participants.first() { + room.local_participant.role = participant.role() + } + room + })?; let initial_project_id = if let Some(initial_project) = initial_project { let initial_project_id = room .update(&mut cx, |room, cx| { room.share_project(initial_project.clone(), cx) - }) + })? .await?; Some(initial_project_id) } else { @@ -281,7 +276,7 @@ impl Room { .update(&mut cx, |room, cx| { room.leave_when_empty = true; room.call(called_user_id, initial_project_id, cx) - }) + })? .await { Ok(()) => Ok(room), @@ -293,9 +288,9 @@ impl Room { pub(crate) async fn join_channel( channel_id: u64, client: Arc, - user_store: ModelHandle, + user_store: Model, cx: AsyncAppContext, - ) -> Result> { + ) -> Result> { Self::from_join_response( client.request(proto::JoinChannel { channel_id }).await?, client, @@ -307,9 +302,9 @@ impl Room { pub(crate) async fn join( room_id: u64, client: Arc, - user_store: ModelHandle, + user_store: Model, cx: AsyncAppContext, - ) -> Result> { + ) -> Result> { Self::from_join_response( client.request(proto::JoinRoom { id: room_id }).await?, client, @@ -318,18 +313,41 @@ impl Room { ) } + fn released(&mut self, cx: &mut AppContext) { + if self.status.is_online() { + self.leave_internal(cx).detach_and_log_err(cx); + } + } + + fn app_will_quit(&mut self, cx: &mut ModelContext) -> impl Future { + let task = if self.status.is_online() { + let leave = self.leave_internal(cx); + Some(cx.background_executor().spawn(async move { + leave.await.log_err(); + })) + } else { + None + }; + + async move { + if let Some(task) = task { + task.await; + } + } + } + pub fn mute_on_join(cx: &AppContext) -> bool { - settings::get::(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( response: proto::JoinRoomResponse, client: Arc, - user_store: ModelHandle, + user_store: Model, mut cx: AsyncAppContext, - ) -> Result> { + ) -> Result> { let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.add_model(|cx| { + let room = cx.new_model(|cx| { Self::new( room_proto.id, response.channel_id, @@ -338,12 +356,12 @@ impl Room { user_store, cx, ) - }); + })?; room.update(&mut cx, |room, cx| { room.leave_when_empty = room.channel_id.is_none(); room.apply_room_update(room_proto, cx)?; anyhow::Ok(()) - })?; + })??; Ok(room) } @@ -372,7 +390,7 @@ impl Room { self.clear_state(cx); let leave_room = self.client.request(proto::LeaveRoom {}); - cx.background().spawn(async move { + cx.background_executor().spawn(async move { leave_room.await?; anyhow::Ok(()) }) @@ -380,14 +398,14 @@ impl Room { pub(crate) fn clear_state(&mut self, cx: &mut AppContext) { for project in self.shared_projects.drain() { - if let Some(project) = project.upgrade(cx) { + if let Some(project) = project.upgrade() { project.update(cx, |project, cx| { project.unshare(cx).log_err(); }); } } for project in self.joined_projects.drain() { - if let Some(project) = project.upgrade(cx) { + if let Some(project) = project.upgrade() { project.update(cx, |project, cx| { project.disconnected_from_host(cx); project.close(cx); @@ -399,14 +417,14 @@ impl Room { self.remote_participants.clear(); self.pending_participants.clear(); self.participant_user_ids.clear(); - self.subscriptions.clear(); + self.client_subscriptions.clear(); self.live_kit.take(); self.pending_room_update.take(); self.maintain_connection.take(); } async fn maintain_connection( - this: WeakModelHandle, + this: WeakModel, client: Arc, mut cx: AsyncAppContext, ) -> Result<()> { @@ -418,32 +436,33 @@ impl Room { if !is_connected || client_status.next().await.is_some() { log::info!("detected client disconnection"); - this.upgrade(&cx) + this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? .update(&mut cx, |this, cx| { this.status = RoomStatus::Rejoining; cx.notify(); - }); + })?; // Wait for client to re-establish a connection to the server. { - let mut reconnection_timeout = cx.background().timer(RECONNECT_TIMEOUT).fuse(); + let mut reconnection_timeout = + cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); let client_reconnection = async { let mut remaining_attempts = 3; while remaining_attempts > 0 { if client_status.borrow().is_connected() { log::info!("client reconnected, attempting to rejoin room"); - let Some(this) = this.upgrade(&cx) else { break }; - if this - .update(&mut cx, |this, cx| this.rejoin(cx)) - .await - .log_err() - .is_some() - { - return true; - } else { - remaining_attempts -= 1; + let Some(this) = this.upgrade() else { break }; + match this.update(&mut cx, |this, cx| this.rejoin(cx)) { + Ok(task) => { + if task.await.log_err().is_some() { + return true; + } else { + remaining_attempts -= 1; + } + } + Err(_app_dropped) => return false, } } else if client_status.borrow().is_signed_out() { return false; @@ -482,9 +501,9 @@ impl Room { // The client failed to re-establish a connection to the server // or an error occurred while trying to re-join the room. Either way // we leave the room and return an error. - if let Some(this) = this.upgrade(&cx) { + if let Some(this) = this.upgrade() { log::info!("reconnection failed, leaving room"); - let _ = this.update(&mut cx, |this, cx| this.leave(cx)); + let _ = this.update(&mut cx, |this, cx| this.leave(cx))?; } Err(anyhow!( "can't reconnect to room: client failed to re-establish connection" @@ -496,7 +515,7 @@ impl Room { let mut reshared_projects = Vec::new(); let mut rejoined_projects = Vec::new(); self.shared_projects.retain(|project| { - if let Some(handle) = project.upgrade(cx) { + if let Some(handle) = project.upgrade() { let project = handle.read(cx); if let Some(project_id) = project.remote_id() { projects.insert(project_id, handle.clone()); @@ -510,14 +529,14 @@ impl Room { false }); self.joined_projects.retain(|project| { - if let Some(handle) = project.upgrade(cx) { + if let Some(handle) = project.upgrade() { let project = handle.read(cx); if let Some(project_id) = project.remote_id() { projects.insert(project_id, handle.clone()); rejoined_projects.push(proto::RejoinProject { id: project_id, worktrees: project - .worktrees(cx) + .worktrees() .map(|worktree| { let worktree = worktree.read(cx); proto::RejoinWorktree { @@ -565,7 +584,7 @@ impl Room { } anyhow::Ok(()) - }) + })? }) } @@ -591,6 +610,16 @@ impl Room { .find(|p| p.peer_id == peer_id) } + pub fn role_for_user(&self, user_id: u64) -> Option { + self.remote_participants + .get(&user_id) + .map(|participant| participant.role) + } + + pub fn local_participant_is_admin(&self) -> bool { + self.local_participant.role == proto::ChannelRole::Admin + } + pub fn pending_participants(&self) -> &[Arc] { &self.pending_participants } @@ -643,7 +672,7 @@ impl Room { } async fn handle_room_updated( - this: ModelHandle, + this: Model, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -652,7 +681,7 @@ impl Room { .payload .room .ok_or_else(|| anyhow!("invalid room"))?; - this.update(&mut cx, |this, cx| this.apply_room_update(room, cx)) + this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))? } fn apply_room_update( @@ -695,7 +724,20 @@ impl Room { this.participant_user_ids.clear(); if let Some(participant) = local_participant { + let role = participant.role(); this.local_participant.projects = participant.projects; + if this.local_participant.role != role { + this.local_participant.role = role; + + this.joined_projects.retain(|project| { + if let Some(project) = project.upgrade() { + project.update(cx, |project, _| project.set_role(role)); + true + } else { + false + } + }); + } } else { this.local_participant.projects.clear(); } @@ -733,7 +775,7 @@ impl Room { for unshared_project_id in old_projects.difference(&new_projects) { this.joined_projects.retain(|project| { - if let Some(project) = project.upgrade(cx) { + if let Some(project) = project.upgrade() { project.update(cx, |project, cx| { if project.remote_id() == Some(*unshared_project_id) { project.disconnected_from_host(cx); @@ -751,6 +793,7 @@ impl Room { }); } + let role = participant.role(); let location = ParticipantLocation::from_proto(participant.location) .unwrap_or(ParticipantLocation::External); if let Some(remote_participant) = @@ -759,8 +802,11 @@ impl Room { remote_participant.peer_id = peer_id; remote_participant.projects = participant.projects; remote_participant.participant_index = participant_index; - if location != remote_participant.location { + if location != remote_participant.location + || role != remote_participant.role + { remote_participant.location = location; + remote_participant.role = role; cx.emit(Event::ParticipantLocationChanged { participant_id: peer_id, }); @@ -774,6 +820,7 @@ impl Room { peer_id, projects: participant.projects, location, + role, muted: true, speaking: false, video_tracks: Default::default(), @@ -876,7 +923,8 @@ impl Room { this.check_invariants(); this.room_update_completed_tx.try_send(Some(())).ok(); cx.notify(); - }); + }) + .ok(); })); cx.notify(); @@ -907,12 +955,7 @@ impl Room { .remote_participants .get_mut(&user_id) .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - participant.video_tracks.insert( - track_id.clone(), - Arc::new(RemoteVideoTrack { - live_kit_track: track, - }), - ); + participant.video_tracks.insert(track_id.clone(), track); cx.emit(Event::RemoteVideoTracksChanged { participant_id: participant.peer_id, }); @@ -991,7 +1034,6 @@ impl Room { .remote_participants .get_mut(&user_id) .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - participant.audio_tracks.insert(track_id.clone(), track); participant.muted = publication.is_muted(); @@ -1053,7 +1095,7 @@ impl Room { let client = self.client.clone(); let room_id = self.id; self.pending_call_count += 1; - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let result = client .request(proto::Call { room_id, @@ -1066,7 +1108,7 @@ impl Room { if this.should_leave() { this.leave(cx).detach_and_log_err(cx); } - }); + })?; result?; Ok(()) }) @@ -1078,31 +1120,40 @@ impl Room { language_registry: Arc, fs: Arc, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); + let role = self.local_participant.role; 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?; + cx.spawn(move |this, mut cx| async move { + let project = Project::remote( + id, + client, + user_store, + language_registry, + fs, + role, + cx.clone(), + ) + .await?; this.update(&mut cx, |this, cx| { this.joined_projects.retain(|project| { - if let Some(project) = project.upgrade(cx) { - !project.read(cx).is_read_only() + if let Some(project) = project.upgrade() { + !project.read(cx).is_disconnected() } else { false } }); this.joined_projects.insert(project.downgrade()); - }); + })?; Ok(project) }) } pub(crate) fn share_project( &mut self, - project: ModelHandle, + project: Model, cx: &mut ModelContext, ) -> Task> { if let Some(project_id) = project.read(cx).remote_id() { @@ -1118,7 +1169,7 @@ impl Room { project.update(&mut cx, |project, cx| { project.shared(response.project_id, cx) - })?; + })??; // If the user's location is in this project, it changes from UnsharedProject to SharedProject. this.update(&mut cx, |this, cx| { @@ -1129,7 +1180,7 @@ impl Room { } else { Task::ready(Ok(())) } - }) + })? .await?; Ok(response.project_id) @@ -1138,7 +1189,7 @@ impl Room { pub(crate) fn unshare_project( &mut self, - project: ModelHandle, + project: Model, cx: &mut ModelContext, ) -> Result<()> { let project_id = match project.read(cx).remote_id() { @@ -1152,7 +1203,7 @@ impl Room { pub(crate) fn set_location( &mut self, - project: Option<&ModelHandle>, + project: Option<&Model>, cx: &mut ModelContext, ) -> Task> { if self.status.is_offline() { @@ -1178,7 +1229,7 @@ impl Room { }; cx.notify(); - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { client .request(proto::UpdateParticipantLocation { room_id, @@ -1214,6 +1265,11 @@ impl Room { .unwrap_or(false) } + pub fn read_only(&self) -> bool { + !(self.local_participant().role == proto::ChannelRole::Member + || self.local_participant().role == proto::ChannelRole::Admin) + } + pub fn is_speaking(&self) -> bool { self.live_kit .as_ref() @@ -1244,22 +1300,21 @@ impl Room { return Task::ready(Err(anyhow!("live-kit was not initialized"))); }; - cx.spawn_weak(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let publish_track = async { let track = LocalAudioTrack::create(); - this.upgrade(&cx) + this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? - .read_with(&cx, |this, _| { + .update(&mut cx, |this, _| { this.live_kit .as_ref() .map(|live_kit| live_kit.room.publish_audio_track(track)) - }) + })? .ok_or_else(|| anyhow!("live-kit was not initialized"))? .await }; - let publication = publish_track.await; - this.upgrade(&cx) + this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? .update(&mut cx, |this, cx| { let live_kit = this @@ -1283,7 +1338,9 @@ impl Room { live_kit.room.unpublish_track(publication); } else { if muted { - cx.background().spawn(publication.set_mute(muted)).detach(); + cx.background_executor() + .spawn(publication.set_mute(muted)) + .detach(); } live_kit.microphone_track = LocalTrack::Published { track_publication: publication, @@ -1303,7 +1360,7 @@ impl Room { } } } - }) + })? }) } @@ -1326,26 +1383,26 @@ impl Room { return Task::ready(Err(anyhow!("live-kit was not initialized"))); }; - cx.spawn_weak(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let publish_track = async { let displays = displays.await?; let display = displays .first() .ok_or_else(|| anyhow!("no display found"))?; let track = LocalVideoTrack::screen_share_for_display(&display); - this.upgrade(&cx) + this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? - .read_with(&cx, |this, _| { + .update(&mut cx, |this, _| { this.live_kit .as_ref() .map(|live_kit| live_kit.room.publish_video_track(track)) - }) + })? .ok_or_else(|| anyhow!("live-kit was not initialized"))? .await }; let publication = publish_track.await; - this.upgrade(&cx) + this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? .update(&mut cx, |this, cx| { let live_kit = this @@ -1369,7 +1426,9 @@ impl Room { live_kit.room.unpublish_track(publication); } else { if muted { - cx.background().spawn(publication.set_mute(muted)).detach(); + cx.background_executor() + .spawn(publication.set_mute(muted)) + .detach(); } live_kit.screen_track = LocalTrack::Published { track_publication: publication, @@ -1392,7 +1451,7 @@ impl Room { } } } - }) + })? }) } @@ -1435,11 +1494,12 @@ impl Room { .room .remote_audio_track_publications(&participant.user.id.to_string()) { - tasks.push(cx.foreground().spawn(track.set_enabled(!live_kit.deafened))); + let deafened = live_kit.deafened; + tasks.push(cx.foreground_executor().spawn(track.set_enabled(!deafened))); } } - Ok(cx.foreground().spawn(async move { + Ok(cx.foreground_executor().spawn(async move { if let Some(mute_task) = mute_task { mute_task.await?; } @@ -1499,7 +1559,6 @@ struct LiveKitRoom { deafened: bool, speaking: bool, next_publish_id: usize, - can_publish: bool, _maintain_room: Task<()>, _maintain_tracks: [Task<()>; 2], } @@ -1531,7 +1590,8 @@ impl LiveKitRoom { *muted = should_mute; cx.notify(); Ok(( - cx.background().spawn(track_publication.set_mute(*muted)), + cx.background_executor() + .spawn(track_publication.set_mute(*muted)), old_muted, )) } diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml deleted file mode 100644 index c2d95c8b52..0000000000 --- a/crates/call2/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "call2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/call2.rs" -doctest = false - -[features] -test-support = [ - "client/test-support", - "collections/test-support", - "gpui/test-support", - "live_kit_client/test-support", - "project/test-support", - "util/test-support" -] - -[dependencies] -audio = { package = "audio2", path = "../audio2" } -client = { package = "client2", path = "../client2" } -collections = { path = "../collections" } -gpui = { package = "gpui2", path = "../gpui2" } -log.workspace = true -live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2" } -fs = { package = "fs2", path = "../fs2" } -language = { package = "language2", path = "../language2" } -media = { path = "../media" } -project = { package = "project2", path = "../project2" } -settings = { package = "settings2", path = "../settings2" } -util = { path = "../util" } - -anyhow.workspace = true -async-broadcast = "0.4" -futures.workspace = true -image = "0.23" -postage.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_derive.workspace = true -smallvec.workspace = true - -[dev-dependencies] -client = { package = "client2", path = "../client2", features = ["test-support"] } -fs = { package = "fs2", path = "../fs2", features = ["test-support"] } -language = { package = "language2", path = "../language2", features = ["test-support"] } -collections = { path = "../collections", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2", features = ["test-support"] } -project = { package = "project2", path = "../project2", features = ["test-support"] } -util = { path = "../util", features = ["test-support"] } diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs deleted file mode 100644 index 14cb28c32d..0000000000 --- a/crates/call2/src/call2.rs +++ /dev/null @@ -1,543 +0,0 @@ -pub mod call_settings; -pub mod participant; -pub mod room; - -use anyhow::{anyhow, Result}; -use audio::Audio; -use call_settings::CallSettings; -use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; -use collections::HashSet; -use futures::{channel::oneshot, future::Shared, Future, FutureExt}; -use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, - WeakModel, -}; -use postage::watch; -use project::Project; -use room::Event; -use settings::Settings; -use std::sync::Arc; - -pub use participant::ParticipantLocation; -pub use room::Room; - -pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { - CallSettings::register(cx); - - let active_call = cx.build_model(|cx| ActiveCall::new(client, user_store, cx)); - cx.set_global(active_call); -} - -pub struct OneAtATime { - cancel: Option>, -} - -impl OneAtATime { - /// spawn a task in the given context. - /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) - /// otherwise you'll see the result of the task. - fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> - where - F: 'static + FnOnce(AsyncAppContext) -> Fut, - Fut: Future>, - R: 'static, - { - let (tx, rx) = oneshot::channel(); - self.cancel.replace(tx); - cx.spawn(|cx| async move { - futures::select_biased! { - _ = rx.fuse() => Ok(None), - result = f(cx).fuse() => result.map(Some), - } - }) - } - - fn running(&self) -> bool { - self.cancel - .as_ref() - .is_some_and(|cancel| !cancel.is_canceled()) - } -} - -#[derive(Clone)] -pub struct IncomingCall { - pub room_id: u64, - pub calling_user: Arc, - pub participants: Vec>, - pub initial_project: Option, -} - -/// Singleton global maintaining the user's participation in a room across workspaces. -pub struct ActiveCall { - room: Option<(Model, Vec)>, - pending_room_creation: Option, Arc>>>>, - location: Option>, - _join_debouncer: OneAtATime, - pending_invites: HashSet, - incoming_call: ( - watch::Sender>, - watch::Receiver>, - ), - client: Arc, - user_store: Model, - _subscriptions: Vec, -} - -impl EventEmitter for ActiveCall {} - -impl ActiveCall { - fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { - Self { - room: None, - pending_room_creation: None, - location: None, - pending_invites: Default::default(), - incoming_call: watch::channel(), - _join_debouncer: OneAtATime { cancel: None }, - _subscriptions: vec![ - client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), - client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), - ], - client, - user_store, - } - } - - pub fn channel_id(&self, cx: &AppContext) -> Option { - self.room()?.read(cx).channel_id() - } - - async fn handle_incoming_call( - this: Model, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result { - let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; - let call = IncomingCall { - room_id: envelope.payload.room_id, - participants: user_store - .update(&mut cx, |user_store, cx| { - user_store.get_users(envelope.payload.participant_user_ids, cx) - })? - .await?, - calling_user: user_store - .update(&mut cx, |user_store, cx| { - user_store.get_user(envelope.payload.calling_user_id, cx) - })? - .await?, - initial_project: envelope.payload.initial_project, - }; - this.update(&mut cx, |this, _| { - *this.incoming_call.0.borrow_mut() = Some(call); - })?; - - Ok(proto::Ack {}) - } - - async fn handle_call_canceled( - this: Model, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - let mut incoming_call = this.incoming_call.0.borrow_mut(); - if incoming_call - .as_ref() - .map_or(false, |call| call.room_id == envelope.payload.room_id) - { - incoming_call.take(); - } - })?; - Ok(()) - } - - pub fn global(cx: &AppContext) -> Model { - cx.global::>().clone() - } - - pub fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut ModelContext, - ) -> Task> { - if !self.pending_invites.insert(called_user_id) { - return Task::ready(Err(anyhow!("user was already invited"))); - } - cx.notify(); - - if self._join_debouncer.running() { - return Task::ready(Ok(())); - } - - let room = if let Some(room) = self.room().cloned() { - Some(Task::ready(Ok(room)).shared()) - } else { - self.pending_room_creation.clone() - }; - - let invite = if let Some(room) = room { - cx.spawn(move |_, mut cx| async move { - let room = room.await.map_err(|err| anyhow!("{:?}", err))?; - - let initial_project_id = if let Some(initial_project) = initial_project { - Some( - room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))? - .await?, - ) - } else { - None - }; - - room.update(&mut cx, move |room, cx| { - room.call(called_user_id, initial_project_id, cx) - })? - .await?; - - anyhow::Ok(()) - }) - } else { - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let room = cx - .spawn(move |this, mut cx| async move { - let create_room = async { - let room = cx - .update(|cx| { - Room::create( - called_user_id, - initial_project, - client, - user_store, - cx, - ) - })? - .await?; - - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))? - .await?; - - anyhow::Ok(room) - }; - - let room = create_room.await; - this.update(&mut cx, |this, _| this.pending_room_creation = None)?; - room.map_err(Arc::new) - }) - .shared(); - self.pending_room_creation = Some(room.clone()); - cx.background_executor().spawn(async move { - room.await.map_err(|err| anyhow!("{:?}", err))?; - anyhow::Ok(()) - }) - }; - - cx.spawn(move |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); - cx.notify(); - })?; - result - }) - } - - pub fn cancel_invite( - &mut self, - called_user_id: u64, - cx: &mut ModelContext, - ) -> Task> { - let room_id = if let Some(room) = self.room() { - room.read(cx).id() - } else { - return Task::ready(Err(anyhow!("no active call"))); - }; - - let client = self.client.clone(); - cx.background_executor().spawn(async move { - client - .request(proto::CancelCall { - room_id, - called_user_id, - }) - .await?; - anyhow::Ok(()) - }) - } - - pub fn incoming(&self) -> watch::Receiver> { - self.incoming_call.1.clone() - } - - pub fn accept_incoming(&mut self, cx: &mut ModelContext) -> Task> { - if self.room.is_some() { - return Task::ready(Err(anyhow!("cannot join while on another call"))); - } - - let call = if let Some(call) = self.incoming_call.1.borrow().clone() { - call - } else { - return Task::ready(Err(anyhow!("no incoming call"))); - }; - - if self.pending_room_creation.is_some() { - return Task::ready(Ok(())); - } - - let room_id = call.room_id.clone(); - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let join = self - ._join_debouncer - .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); - - cx.spawn(|this, mut cx| async move { - let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? - .await?; - this.update(&mut cx, |this, cx| { - this.report_call_event("accept incoming", cx) - })?; - Ok(()) - }) - } - - pub fn decline_incoming(&mut self, cx: &mut ModelContext) -> Result<()> { - let call = self - .incoming_call - .0 - .borrow_mut() - .take() - .ok_or_else(|| anyhow!("no incoming call"))?; - report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); - self.client.send(proto::DeclineCall { - room_id: call.room_id, - })?; - Ok(()) - } - - pub fn join_channel( - &mut self, - channel_id: u64, - cx: &mut ModelContext, - ) -> Task>>> { - if let Some(room) = self.room().cloned() { - if room.read(cx).channel_id() == Some(channel_id) { - return Task::ready(Ok(Some(room))); - } else { - room.update(cx, |room, cx| room.clear_state(cx)); - } - } - - if self.pending_room_creation.is_some() { - return Task::ready(Ok(None)); - } - - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let join = self._join_debouncer.spawn(cx, move |cx| async move { - Room::join_channel(channel_id, client, user_store, cx).await - }); - - cx.spawn(|this, mut cx| async move { - let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? - .await?; - this.update(&mut cx, |this, cx| { - this.report_call_event("join channel", cx) - })?; - Ok(room) - }) - } - - pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { - cx.notify(); - self.report_call_event("hang up", cx); - - Audio::end_call(cx); - if let Some((room, _)) = self.room.take() { - room.update(cx, |room, cx| room.leave(cx)) - } else { - Task::ready(Ok(())) - } - } - - pub fn share_project( - &mut self, - project: Model, - cx: &mut ModelContext, - ) -> Task> { - if let Some((room, _)) = self.room.as_ref() { - self.report_call_event("share project", cx); - room.update(cx, |room, cx| room.share_project(project, cx)) - } else { - Task::ready(Err(anyhow!("no active call"))) - } - } - - pub fn unshare_project( - &mut self, - project: Model, - cx: &mut ModelContext, - ) -> Result<()> { - if let Some((room, _)) = self.room.as_ref() { - self.report_call_event("unshare project", cx); - room.update(cx, |room, cx| room.unshare_project(project, cx)) - } else { - Err(anyhow!("no active call")) - } - } - - pub fn location(&self) -> Option<&WeakModel> { - self.location.as_ref() - } - - pub fn set_location( - &mut self, - project: Option<&Model>, - cx: &mut ModelContext, - ) -> Task> { - 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( - &mut self, - room: Option>, - cx: &mut ModelContext, - ) -> Task> { - if room.as_ref() != self.room.as_ref().map(|room| &room.0) { - cx.notify(); - if let Some(room) = room { - if room.read(cx).status().is_offline() { - self.room = None; - Task::ready(Ok(())) - } else { - let subscriptions = vec![ - cx.observe(&room, |this, room, cx| { - if room.read(cx).status().is_offline() { - this.set_room(None, cx).detach_and_log_err(cx); - } - - cx.notify(); - }), - cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), - ]; - self.room = Some((room.clone(), subscriptions)); - let location = self - .location - .as_ref() - .and_then(|location| location.upgrade()); - room.update(cx, |room, cx| room.set_location(location.as_ref(), cx)) - } - } else { - self.room = None; - Task::ready(Ok(())) - } - } else { - Task::ready(Ok(())) - } - } - - pub fn room(&self) -> Option<&Model> { - self.room.as_ref().map(|(room, _)| room) - } - - pub fn client(&self) -> Arc { - self.client.clone() - } - - pub fn pending_invites(&self) -> &HashSet { - &self.pending_invites - } - - pub fn report_call_event(&self, operation: &'static str, cx: &mut 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: u64, - channel_id: Option, - client: &Arc, - cx: &mut AppContext, -) { - let telemetry = client.telemetry(); - let telemetry_settings = *TelemetrySettings::get_global(cx); - - telemetry.report_call_event(telemetry_settings, operation, Some(room_id), channel_id) -} - -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 = *TelemetrySettings::get_global(cx); - - telemetry.report_call_event( - telemetry_settings, - operation, - room.map(|r| r.read(cx).id()), - Some(channel_id), - ) -} - -#[cfg(test)] -mod test { - use gpui::TestAppContext; - - use crate::OneAtATime; - - #[gpui::test] - async fn test_one_at_a_time(cx: &mut TestAppContext) { - let mut one_at_a_time = OneAtATime { cancel: None }; - - assert_eq!( - cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) })) - .await - .unwrap(), - Some(1) - ); - - let (a, b) = cx.update(|cx| { - ( - one_at_a_time.spawn(cx, |_| async { - assert!(false); - Ok(2) - }), - one_at_a_time.spawn(cx, |_| async { Ok(3) }), - ) - }); - - assert_eq!(a.await.unwrap(), None); - assert_eq!(b.await.unwrap(), Some(3)); - - let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) })); - drop(one_at_a_time); - - assert_eq!(promise.await.unwrap(), None); - } -} diff --git a/crates/call2/src/call_settings.rs b/crates/call2/src/call_settings.rs deleted file mode 100644 index 9375feedf0..0000000000 --- a/crates/call2/src/call_settings.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::Result; -use gpui::AppContext; -use schemars::JsonSchema; -use serde_derive::{Deserialize, Serialize}; -use settings::Settings; - -#[derive(Deserialize, Debug)] -pub struct CallSettings { - pub mute_on_join: bool, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -pub struct CallSettingsContent { - pub mute_on_join: Option, -} - -impl Settings for CallSettings { - const KEY: Option<&'static str> = Some("calls"); - - type FileContent = CallSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _cx: &mut AppContext, - ) -> Result - where - Self: Sized, - { - Self::load_via_json_merge(default_value, user_values) - } -} diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs deleted file mode 100644 index 11a58b4b09..0000000000 --- a/crates/call2/src/participant.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::{anyhow, Result}; -use client::ParticipantIndex; -use client::{proto, User}; -use collections::HashMap; -use gpui::WeakModel; -pub use live_kit_client::Frame; -pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; -use project::Project; -use std::sync::Arc; - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum ParticipantLocation { - SharedProject { project_id: u64 }, - UnsharedProject, - External, -} - -impl ParticipantLocation { - pub fn from_proto(location: Option) -> Result { - match location.and_then(|l| l.variant) { - Some(proto::participant_location::Variant::SharedProject(project)) => { - Ok(Self::SharedProject { - project_id: project.id, - }) - } - Some(proto::participant_location::Variant::UnsharedProject(_)) => { - Ok(Self::UnsharedProject) - } - Some(proto::participant_location::Variant::External(_)) => Ok(Self::External), - None => Err(anyhow!("participant location was not provided")), - } - } -} - -#[derive(Clone, Default)] -pub struct LocalParticipant { - pub projects: Vec, - pub active_project: Option>, -} - -#[derive(Clone, Debug)] -pub struct RemoteParticipant { - pub user: Arc, - 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>, - pub audio_tracks: HashMap>, -} diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs deleted file mode 100644 index 694966abe9..0000000000 --- a/crates/call2/src/room.rs +++ /dev/null @@ -1,1599 +0,0 @@ -use crate::{ - call_settings::CallSettings, - participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, -}; -use anyhow::{anyhow, Result}; -use audio::{Audio, Sound}; -use client::{ - proto::{self, PeerId}, - Client, ParticipantIndex, TypedEnvelope, User, UserStore, -}; -use collections::{BTreeMap, HashMap, HashSet}; -use fs::Fs; -use futures::{FutureExt, StreamExt}; -use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, -}; -use language::LanguageRegistry; -use live_kit_client::{ - LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate, - RemoteVideoTrackUpdate, -}; -use postage::{sink::Sink, stream::Stream, watch}; -use project::Project; -use settings::Settings as _; -use std::{future::Future, mem, sync::Arc, time::Duration}; -use util::{post_inc, ResultExt, TryFutureExt}; - -pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - ParticipantLocationChanged { - participant_id: proto::PeerId, - }, - RemoteVideoTracksChanged { - participant_id: proto::PeerId, - }, - RemoteAudioTracksChanged { - participant_id: proto::PeerId, - }, - RemoteProjectShared { - owner: Arc, - project_id: u64, - worktree_root_names: Vec, - }, - RemoteProjectUnshared { - project_id: u64, - }, - RemoteProjectJoined { - project_id: u64, - }, - RemoteProjectInvitationDiscarded { - project_id: u64, - }, - Left, -} - -pub struct Room { - id: u64, - channel_id: Option, - live_kit: Option, - status: RoomStatus, - shared_projects: HashSet>, - joined_projects: HashSet>, - local_participant: LocalParticipant, - remote_participants: BTreeMap, - pending_participants: Vec>, - participant_user_ids: HashSet, - pending_call_count: usize, - leave_when_empty: bool, - client: Arc, - user_store: Model, - follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, - client_subscriptions: Vec, - _subscriptions: Vec, - room_update_completed_tx: watch::Sender>, - room_update_completed_rx: watch::Receiver>, - pending_room_update: Option>, - maintain_connection: Option>>, -} - -impl EventEmitter for Room {} - -impl Room { - pub fn channel_id(&self) -> Option { - 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() { - matches!( - *live_kit.room.status().borrow(), - live_kit_client::ConnectionState::Connected { .. } - ) - } else { - false - } - } - - fn new( - id: u64, - channel_id: Option, - live_kit_connection_info: Option, - client: Arc, - user_store: Model, - cx: &mut ModelContext, - ) -> Self { - let live_kit_room = if let Some(connection_info) = live_kit_connection_info { - let room = live_kit_client::Room::new(); - let mut status = room.status(); - // Consume the initial status of the room. - let _ = status.try_recv(); - let _maintain_room = cx.spawn(|this, mut cx| async move { - while let Some(status) = status.next().await { - let this = if let Some(this) = this.upgrade() { - this - } else { - break; - }; - - if status == live_kit_client::ConnectionState::Disconnected { - this.update(&mut cx, |this, cx| this.leave(cx).log_err()) - .ok(); - break; - } - } - }); - - let _maintain_video_tracks = cx.spawn({ - let room = room.clone(); - move |this, mut cx| async move { - let mut track_video_changes = room.remote_video_track_updates(); - while let Some(track_change) = track_video_changes.next().await { - let this = if let Some(this) = this.upgrade() { - this - } else { - break; - }; - - this.update(&mut cx, |this, cx| { - this.remote_video_track_updated(track_change, cx).log_err() - }) - .ok(); - } - } - }); - - let _maintain_audio_tracks = cx.spawn({ - let room = room.clone(); - |this, mut cx| async move { - let mut track_audio_changes = room.remote_audio_track_updates(); - while let Some(track_change) = track_audio_changes.next().await { - let this = if let Some(this) = this.upgrade() { - this - } else { - break; - }; - - this.update(&mut cx, |this, cx| { - this.remote_audio_track_updated(track_change, cx).log_err() - }) - .ok(); - } - } - }); - - let connect = room.connect(&connection_info.server_url, &connection_info.token); - cx.spawn(|this, mut cx| async move { - connect.await?; - - if !cx.update(|cx| Self::mute_on_join(cx))? { - this.update(&mut cx, |this, cx| this.share_microphone(cx))? - .await?; - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - Some(LiveKitRoom { - room, - screen_track: LocalTrack::None, - microphone_track: LocalTrack::None, - next_publish_id: 0, - muted_by_user: false, - deafened: false, - speaking: false, - _maintain_room, - _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks], - }) - } else { - None - }; - - let maintain_connection = cx.spawn({ - let client = client.clone(); - move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err() - }); - - Audio::play_sound(Sound::Joined, cx); - - let (room_update_completed_tx, room_update_completed_rx) = watch::channel(); - - Self { - id, - channel_id, - live_kit: live_kit_room, - status: RoomStatus::Online, - shared_projects: Default::default(), - joined_projects: Default::default(), - participant_user_ids: Default::default(), - local_participant: Default::default(), - remote_participants: Default::default(), - pending_participants: Default::default(), - pending_call_count: 0, - client_subscriptions: vec![ - client.add_message_handler(cx.weak_model(), Self::handle_room_updated) - ], - _subscriptions: vec![ - cx.on_release(Self::released), - cx.on_app_quit(Self::app_will_quit), - ], - leave_when_empty: false, - pending_room_update: None, - client, - user_store, - follows_by_leader_id_project_id: Default::default(), - maintain_connection: Some(maintain_connection), - room_update_completed_tx, - room_update_completed_rx, - } - } - - pub(crate) fn create( - called_user_id: u64, - initial_project: Option>, - client: Arc, - user_store: Model, - cx: &mut AppContext, - ) -> Task>> { - cx.spawn(move |mut cx| async move { - let response = client.request(proto::CreateRoom {}).await?; - let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.build_model(|cx| { - Self::new( - room_proto.id, - None, - response.live_kit_connection_info, - client, - user_store, - cx, - ) - })?; - - let initial_project_id = if let Some(initial_project) = initial_project { - let initial_project_id = room - .update(&mut cx, |room, cx| { - room.share_project(initial_project.clone(), cx) - })? - .await?; - Some(initial_project_id) - } else { - None - }; - - match room - .update(&mut cx, |room, cx| { - room.leave_when_empty = true; - room.call(called_user_id, initial_project_id, cx) - })? - .await - { - Ok(()) => Ok(room), - Err(error) => Err(anyhow!("room creation failed: {:?}", error)), - } - }) - } - - pub(crate) async fn join_channel( - channel_id: u64, - client: Arc, - user_store: Model, - cx: AsyncAppContext, - ) -> Result> { - Self::from_join_response( - client.request(proto::JoinChannel { channel_id }).await?, - client, - user_store, - cx, - ) - } - - pub(crate) async fn join( - room_id: u64, - client: Arc, - user_store: Model, - cx: AsyncAppContext, - ) -> Result> { - Self::from_join_response( - client.request(proto::JoinRoom { id: room_id }).await?, - client, - user_store, - cx, - ) - } - - fn released(&mut self, cx: &mut AppContext) { - if self.status.is_online() { - self.leave_internal(cx).detach_and_log_err(cx); - } - } - - fn app_will_quit(&mut self, cx: &mut ModelContext) -> impl Future { - let task = if self.status.is_online() { - let leave = self.leave_internal(cx); - Some(cx.background_executor().spawn(async move { - leave.await.log_err(); - })) - } else { - None - }; - - async move { - if let Some(task) = task { - task.await; - } - } - } - - pub fn mute_on_join(cx: &AppContext) -> bool { - CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() - } - - fn from_join_response( - response: proto::JoinRoomResponse, - client: Arc, - user_store: Model, - mut cx: AsyncAppContext, - ) -> Result> { - let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.build_model(|cx| { - Self::new( - room_proto.id, - response.channel_id, - response.live_kit_connection_info, - client, - user_store, - cx, - ) - })?; - room.update(&mut cx, |room, cx| { - room.leave_when_empty = room.channel_id.is_none(); - room.apply_room_update(room_proto, cx)?; - anyhow::Ok(()) - })??; - Ok(room) - } - - fn should_leave(&self) -> bool { - self.leave_when_empty - && self.pending_room_update.is_none() - && self.pending_participants.is_empty() - && self.remote_participants.is_empty() - && self.pending_call_count == 0 - } - - pub(crate) fn leave(&mut self, cx: &mut ModelContext) -> Task> { - cx.notify(); - cx.emit(Event::Left); - self.leave_internal(cx) - } - - fn leave_internal(&mut self, cx: &mut AppContext) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - log::info!("leaving room"); - Audio::play_sound(Sound::Leave, cx); - - self.clear_state(cx); - - let leave_room = self.client.request(proto::LeaveRoom {}); - cx.background_executor().spawn(async move { - leave_room.await?; - anyhow::Ok(()) - }) - } - - pub(crate) fn clear_state(&mut self, cx: &mut AppContext) { - for project in self.shared_projects.drain() { - if let Some(project) = project.upgrade() { - project.update(cx, |project, cx| { - project.unshare(cx).log_err(); - }); - } - } - for project in self.joined_projects.drain() { - if let Some(project) = project.upgrade() { - project.update(cx, |project, cx| { - project.disconnected_from_host(cx); - project.close(cx); - }); - } - } - - self.status = RoomStatus::Offline; - self.remote_participants.clear(); - self.pending_participants.clear(); - self.participant_user_ids.clear(); - self.client_subscriptions.clear(); - self.live_kit.take(); - self.pending_room_update.take(); - self.maintain_connection.take(); - } - - async fn maintain_connection( - this: WeakModel, - client: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let mut client_status = client.status(); - loop { - let _ = client_status.try_recv(); - let is_connected = client_status.borrow().is_connected(); - // Even if we're initially connected, any future change of the status means we momentarily disconnected. - if !is_connected || client_status.next().await.is_some() { - log::info!("detected client disconnection"); - - this.upgrade() - .ok_or_else(|| anyhow!("room was dropped"))? - .update(&mut cx, |this, cx| { - this.status = RoomStatus::Rejoining; - cx.notify(); - })?; - - // Wait for client to re-establish a connection to the server. - { - let mut reconnection_timeout = - cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); - let client_reconnection = async { - let mut remaining_attempts = 3; - while remaining_attempts > 0 { - if client_status.borrow().is_connected() { - log::info!("client reconnected, attempting to rejoin room"); - - let Some(this) = this.upgrade() else { break }; - match this.update(&mut cx, |this, cx| this.rejoin(cx)) { - Ok(task) => { - if task.await.log_err().is_some() { - return true; - } else { - remaining_attempts -= 1; - } - } - Err(_app_dropped) => return false, - } - } else if client_status.borrow().is_signed_out() { - return false; - } - - log::info!( - "waiting for client status change, remaining attempts {}", - remaining_attempts - ); - client_status.next().await; - } - false - } - .fuse(); - futures::pin_mut!(client_reconnection); - - futures::select_biased! { - reconnected = client_reconnection => { - if reconnected { - log::info!("successfully reconnected to room"); - // If we successfully joined the room, go back around the loop - // waiting for future connection status changes. - continue; - } - } - _ = reconnection_timeout => { - log::info!("room reconnection timeout expired"); - } - } - } - - break; - } - } - - // The client failed to re-establish a connection to the server - // or an error occurred while trying to re-join the room. Either way - // we leave the room and return an error. - if let Some(this) = this.upgrade() { - log::info!("reconnection failed, leaving room"); - let _ = this.update(&mut cx, |this, cx| this.leave(cx))?; - } - Err(anyhow!( - "can't reconnect to room: client failed to re-establish connection" - )) - } - - fn rejoin(&mut self, cx: &mut ModelContext) -> Task> { - let mut projects = HashMap::default(); - let mut reshared_projects = Vec::new(); - let mut rejoined_projects = Vec::new(); - self.shared_projects.retain(|project| { - if let Some(handle) = project.upgrade() { - let project = handle.read(cx); - if let Some(project_id) = project.remote_id() { - projects.insert(project_id, handle.clone()); - reshared_projects.push(proto::UpdateProject { - project_id, - worktrees: project.worktree_metadata_protos(cx), - }); - return true; - } - } - false - }); - self.joined_projects.retain(|project| { - if let Some(handle) = project.upgrade() { - let project = handle.read(cx); - if let Some(project_id) = project.remote_id() { - projects.insert(project_id, handle.clone()); - rejoined_projects.push(proto::RejoinProject { - id: project_id, - worktrees: project - .worktrees() - .map(|worktree| { - let worktree = worktree.read(cx); - proto::RejoinWorktree { - id: worktree.id().to_proto(), - scan_id: worktree.completed_scan_id() as u64, - } - }) - .collect(), - }); - } - return true; - } - false - }); - - let response = self.client.request_envelope(proto::RejoinRoom { - id: self.id, - reshared_projects, - rejoined_projects, - }); - - cx.spawn(|this, mut cx| async move { - let response = response.await?; - let message_id = response.message_id; - let response = response.payload; - let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - this.update(&mut cx, |this, cx| { - this.status = RoomStatus::Online; - this.apply_room_update(room_proto, cx)?; - - for reshared_project in response.reshared_projects { - if let Some(project) = projects.get(&reshared_project.id) { - project.update(cx, |project, cx| { - project.reshared(reshared_project, cx).log_err(); - }); - } - } - - for rejoined_project in response.rejoined_projects { - if let Some(project) = projects.get(&rejoined_project.id) { - project.update(cx, |project, cx| { - project.rejoined(rejoined_project, message_id, cx).log_err(); - }); - } - } - - anyhow::Ok(()) - })? - }) - } - - pub fn id(&self) -> u64 { - self.id - } - - pub fn status(&self) -> RoomStatus { - self.status - } - - pub fn local_participant(&self) -> &LocalParticipant { - &self.local_participant - } - - pub fn remote_participants(&self) -> &BTreeMap { - &self.remote_participants - } - - pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> { - self.remote_participants - .values() - .find(|p| p.peer_id == peer_id) - } - - pub fn pending_participants(&self) -> &[Arc] { - &self.pending_participants - } - - pub fn contains_participant(&self, user_id: u64) -> bool { - self.participant_user_ids.contains(&user_id) - } - - pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] { - self.follows_by_leader_id_project_id - .get(&(leader_id, project_id)) - .map_or(&[], |v| v.as_slice()) - } - - /// Returns the most 'active' projects, defined as most people in the project - pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> { - let mut project_hosts_and_guest_counts = HashMap::, u32)>::default(); - for participant in self.remote_participants.values() { - match participant.location { - ParticipantLocation::SharedProject { project_id } => { - project_hosts_and_guest_counts - .entry(project_id) - .or_default() - .1 += 1; - } - ParticipantLocation::External | ParticipantLocation::UnsharedProject => {} - } - for project in &participant.projects { - project_hosts_and_guest_counts - .entry(project.id) - .or_default() - .0 = Some(participant.user.id); - } - } - - if let Some(user) = self.user_store.read(cx).current_user() { - for project in &self.local_participant.projects { - project_hosts_and_guest_counts - .entry(project.id) - .or_default() - .0 = Some(user.id); - } - } - - project_hosts_and_guest_counts - .into_iter() - .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count))) - .max_by_key(|(_, _, guest_count)| *guest_count) - .map(|(id, host, _)| (id, host)) - } - - async fn handle_room_updated( - this: Model, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let room = envelope - .payload - .room - .ok_or_else(|| anyhow!("invalid room"))?; - this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))? - } - - fn apply_room_update( - &mut self, - mut room: proto::Room, - cx: &mut ModelContext, - ) -> Result<()> { - // Filter ourselves out from the room's participants. - let local_participant_ix = room - .participants - .iter() - .position(|participant| Some(participant.user_id) == self.client.user_id()); - let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix)); - - let pending_participant_user_ids = room - .pending_participants - .iter() - .map(|p| p.user_id) - .collect::>(); - - let remote_participant_user_ids = room - .participants - .iter() - .map(|p| p.user_id) - .collect::>(); - - let (remote_participants, pending_participants) = - self.user_store.update(cx, move |user_store, cx| { - ( - user_store.get_users(remote_participant_user_ids, cx), - user_store.get_users(pending_participant_user_ids, cx), - ) - }); - - self.pending_room_update = Some(cx.spawn(|this, mut cx| async move { - let (remote_participants, pending_participants) = - futures::join!(remote_participants, pending_participants); - - this.update(&mut cx, |this, cx| { - this.participant_user_ids.clear(); - - if let Some(participant) = local_participant { - this.local_participant.projects = participant.projects; - } else { - this.local_participant.projects.clear(); - } - - if let Some(participants) = remote_participants.log_err() { - for (participant, user) in room.participants.into_iter().zip(participants) { - let Some(peer_id) = participant.peer_id else { - continue; - }; - let participant_index = ParticipantIndex(participant.participant_index); - this.participant_user_ids.insert(participant.user_id); - - let old_projects = this - .remote_participants - .get(&participant.user_id) - .into_iter() - .flat_map(|existing| &existing.projects) - .map(|project| project.id) - .collect::>(); - let new_projects = participant - .projects - .iter() - .map(|project| project.id) - .collect::>(); - - for project in &participant.projects { - if !old_projects.contains(&project.id) { - cx.emit(Event::RemoteProjectShared { - owner: user.clone(), - project_id: project.id, - worktree_root_names: project.worktree_root_names.clone(), - }); - } - } - - for unshared_project_id in old_projects.difference(&new_projects) { - this.joined_projects.retain(|project| { - if let Some(project) = project.upgrade() { - project.update(cx, |project, cx| { - if project.remote_id() == Some(*unshared_project_id) { - project.disconnected_from_host(cx); - false - } else { - true - } - }) - } else { - false - } - }); - cx.emit(Event::RemoteProjectUnshared { - project_id: *unshared_project_id, - }); - } - - let location = ParticipantLocation::from_proto(participant.location) - .unwrap_or(ParticipantLocation::External); - if let Some(remote_participant) = - this.remote_participants.get_mut(&participant.user_id) - { - remote_participant.peer_id = peer_id; - remote_participant.projects = participant.projects; - remote_participant.participant_index = participant_index; - if location != remote_participant.location { - remote_participant.location = location; - cx.emit(Event::ParticipantLocationChanged { - participant_id: peer_id, - }); - } - } else { - this.remote_participants.insert( - participant.user_id, - RemoteParticipant { - user: user.clone(), - participant_index, - peer_id, - projects: participant.projects, - location, - muted: true, - speaking: false, - video_tracks: Default::default(), - audio_tracks: Default::default(), - }, - ); - - Audio::play_sound(Sound::Joined, cx); - - if let Some(live_kit) = this.live_kit.as_ref() { - let video_tracks = - live_kit.room.remote_video_tracks(&user.id.to_string()); - let audio_tracks = - live_kit.room.remote_audio_tracks(&user.id.to_string()); - let publications = live_kit - .room - .remote_audio_track_publications(&user.id.to_string()); - - for track in video_tracks { - this.remote_video_track_updated( - RemoteVideoTrackUpdate::Subscribed(track), - cx, - ) - .log_err(); - } - - for (track, publication) in - audio_tracks.iter().zip(publications.iter()) - { - this.remote_audio_track_updated( - RemoteAudioTrackUpdate::Subscribed( - track.clone(), - publication.clone(), - ), - cx, - ) - .log_err(); - } - } - } - } - - this.remote_participants.retain(|user_id, participant| { - if this.participant_user_ids.contains(user_id) { - true - } else { - for project in &participant.projects { - cx.emit(Event::RemoteProjectUnshared { - project_id: project.id, - }); - } - false - } - }); - } - - if let Some(pending_participants) = pending_participants.log_err() { - this.pending_participants = pending_participants; - for participant in &this.pending_participants { - this.participant_user_ids.insert(participant.id); - } - } - - this.follows_by_leader_id_project_id.clear(); - for follower in room.followers { - let project_id = follower.project_id; - let (leader, follower) = match (follower.leader_id, follower.follower_id) { - (Some(leader), Some(follower)) => (leader, follower), - - _ => { - log::error!("Follower message {follower:?} missing some state"); - continue; - } - }; - - let list = this - .follows_by_leader_id_project_id - .entry((leader, project_id)) - .or_insert(Vec::new()); - if !list.contains(&follower) { - list.push(follower); - } - } - - this.pending_room_update.take(); - if this.should_leave() { - log::info!("room is empty, leaving"); - 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(); - this.room_update_completed_tx.try_send(Some(())).ok(); - cx.notify(); - }) - .ok(); - })); - - cx.notify(); - Ok(()) - } - - pub fn room_update_completed(&mut self) -> impl Future { - let mut done_rx = self.room_update_completed_rx.clone(); - async move { - while let Some(result) = done_rx.next().await { - if result.is_some() { - break; - } - } - } - } - - fn remote_video_track_updated( - &mut self, - change: RemoteVideoTrackUpdate, - cx: &mut ModelContext, - ) -> Result<()> { - match change { - RemoteVideoTrackUpdate::Subscribed(track) => { - let user_id = track.publisher_id().parse()?; - let track_id = track.sid().to_string(); - let participant = self - .remote_participants - .get_mut(&user_id) - .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - participant.video_tracks.insert(track_id.clone(), track); - cx.emit(Event::RemoteVideoTracksChanged { - participant_id: participant.peer_id, - }); - } - RemoteVideoTrackUpdate::Unsubscribed { - publisher_id, - track_id, - } => { - let user_id = publisher_id.parse()?; - let participant = self - .remote_participants - .get_mut(&user_id) - .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; - participant.video_tracks.remove(&track_id); - cx.emit(Event::RemoteVideoTracksChanged { - participant_id: participant.peer_id, - }); - } - } - - cx.notify(); - Ok(()) - } - - fn remote_audio_track_updated( - &mut self, - change: RemoteAudioTrackUpdate, - cx: &mut ModelContext, - ) -> Result<()> { - match change { - RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => { - let mut speaker_ids = speakers - .into_iter() - .filter_map(|speaker_sid| speaker_sid.parse().ok()) - .collect::>(); - speaker_ids.sort_unstable(); - for (sid, participant) in &mut self.remote_participants { - if let Ok(_) = speaker_ids.binary_search(sid) { - participant.speaking = true; - } else { - participant.speaking = false; - } - } - if let Some(id) = self.client.user_id() { - if let Some(room) = &mut self.live_kit { - if let Ok(_) = speaker_ids.binary_search(&id) { - room.speaking = true; - } else { - room.speaking = false; - } - } - } - cx.notify(); - } - RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => { - let mut found = false; - for participant in &mut self.remote_participants.values_mut() { - for track in participant.audio_tracks.values() { - if track.sid() == track_id { - found = true; - break; - } - } - if found { - participant.muted = muted; - break; - } - } - - cx.notify(); - } - RemoteAudioTrackUpdate::Subscribed(track, publication) => { - let user_id = track.publisher_id().parse()?; - let track_id = track.sid().to_string(); - let participant = self - .remote_participants - .get_mut(&user_id) - .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - participant.audio_tracks.insert(track_id.clone(), track); - participant.muted = publication.is_muted(); - - cx.emit(Event::RemoteAudioTracksChanged { - participant_id: participant.peer_id, - }); - } - RemoteAudioTrackUpdate::Unsubscribed { - publisher_id, - track_id, - } => { - let user_id = publisher_id.parse()?; - let participant = self - .remote_participants - .get_mut(&user_id) - .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; - participant.audio_tracks.remove(&track_id); - cx.emit(Event::RemoteAudioTracksChanged { - participant_id: participant.peer_id, - }); - } - } - - cx.notify(); - Ok(()) - } - - fn check_invariants(&self) { - #[cfg(any(test, feature = "test-support"))] - { - for participant in self.remote_participants.values() { - assert!(self.participant_user_ids.contains(&participant.user.id)); - assert_ne!(participant.user.id, self.client.user_id().unwrap()); - } - - for participant in &self.pending_participants { - assert!(self.participant_user_ids.contains(&participant.id)); - assert_ne!(participant.id, self.client.user_id().unwrap()); - } - - assert_eq!( - self.participant_user_ids.len(), - self.remote_participants.len() + self.pending_participants.len() - ); - } - } - - pub(crate) fn call( - &mut self, - called_user_id: u64, - initial_project_id: Option, - cx: &mut ModelContext, - ) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - cx.notify(); - let client = self.client.clone(); - let room_id = self.id; - self.pending_call_count += 1; - cx.spawn(move |this, mut cx| async move { - let result = client - .request(proto::Call { - room_id, - called_user_id, - initial_project_id, - }) - .await; - this.update(&mut cx, |this, cx| { - this.pending_call_count -= 1; - if this.should_leave() { - this.leave(cx).detach_and_log_err(cx); - } - })?; - result?; - Ok(()) - }) - } - - pub fn join_project( - &mut self, - id: u64, - language_registry: Arc, - fs: Arc, - cx: &mut ModelContext, - ) -> Task>> { - let client = self.client.clone(); - let user_store = self.user_store.clone(); - cx.emit(Event::RemoteProjectJoined { project_id: id }); - cx.spawn(move |this, mut cx| async move { - let project = - Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; - - this.update(&mut cx, |this, cx| { - this.joined_projects.retain(|project| { - if let Some(project) = project.upgrade() { - !project.read(cx).is_read_only() - } else { - false - } - }); - this.joined_projects.insert(project.downgrade()); - })?; - Ok(project) - }) - } - - pub(crate) fn share_project( - &mut self, - project: Model, - cx: &mut ModelContext, - ) -> Task> { - if let Some(project_id) = project.read(cx).remote_id() { - return Task::ready(Ok(project_id)); - } - - let request = self.client.request(proto::ShareProject { - room_id: self.id(), - worktrees: project.read(cx).worktree_metadata_protos(cx), - }); - cx.spawn(|this, mut cx| async move { - let response = request.await?; - - project.update(&mut cx, |project, cx| { - project.shared(response.project_id, cx) - })??; - - // If the user's location is in this project, it changes from UnsharedProject to SharedProject. - this.update(&mut cx, |this, cx| { - this.shared_projects.insert(project.downgrade()); - let active_project = this.local_participant.active_project.as_ref(); - if active_project.map_or(false, |location| *location == project) { - this.set_location(Some(&project), cx) - } else { - Task::ready(Ok(())) - } - })? - .await?; - - Ok(response.project_id) - }) - } - - pub(crate) fn unshare_project( - &mut self, - project: Model, - cx: &mut ModelContext, - ) -> Result<()> { - let project_id = match project.read(cx).remote_id() { - Some(project_id) => project_id, - None => return Ok(()), - }; - - self.client.send(proto::UnshareProject { project_id })?; - project.update(cx, |this, cx| this.unshare(cx)) - } - - pub(crate) fn set_location( - &mut self, - project: Option<&Model>, - cx: &mut ModelContext, - ) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - let client = self.client.clone(); - let room_id = self.id; - let location = if let Some(project) = project { - self.local_participant.active_project = Some(project.downgrade()); - if let Some(project_id) = project.read(cx).remote_id() { - proto::participant_location::Variant::SharedProject( - proto::participant_location::SharedProject { id: project_id }, - ) - } else { - proto::participant_location::Variant::UnsharedProject( - proto::participant_location::UnsharedProject {}, - ) - } - } else { - self.local_participant.active_project = None; - proto::participant_location::Variant::External(proto::participant_location::External {}) - }; - - cx.notify(); - cx.background_executor().spawn(async move { - client - .request(proto::UpdateParticipantLocation { - room_id, - location: Some(proto::ParticipantLocation { - variant: Some(location), - }), - }) - .await?; - Ok(()) - }) - } - - pub fn is_screen_sharing(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { - !matches!(live_kit.screen_track, LocalTrack::None) - }) - } - - pub fn is_sharing_mic(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { - !matches!(live_kit.microphone_track, LocalTrack::None) - }) - } - - pub fn is_muted(&self, cx: &AppContext) -> bool { - self.live_kit - .as_ref() - .and_then(|live_kit| match &live_kit.microphone_track { - LocalTrack::None => Some(Self::mute_on_join(cx)), - LocalTrack::Pending { muted, .. } => Some(*muted), - LocalTrack::Published { muted, .. } => Some(*muted), - }) - .unwrap_or(false) - } - - pub fn is_speaking(&self) -> bool { - self.live_kit - .as_ref() - .map_or(false, |live_kit| live_kit.speaking) - } - - pub fn is_deafened(&self) -> Option { - self.live_kit.as_ref().map(|live_kit| live_kit.deafened) - } - - #[track_caller] - pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } else if self.is_sharing_mic() { - return Task::ready(Err(anyhow!("microphone was already shared"))); - } - - let publish_id = if let Some(live_kit) = self.live_kit.as_mut() { - let publish_id = post_inc(&mut live_kit.next_publish_id); - live_kit.microphone_track = LocalTrack::Pending { - publish_id, - muted: false, - }; - cx.notify(); - publish_id - } else { - return Task::ready(Err(anyhow!("live-kit was not initialized"))); - }; - - cx.spawn(move |this, mut cx| async move { - let publish_track = async { - let track = LocalAudioTrack::create(); - this.upgrade() - .ok_or_else(|| anyhow!("room was dropped"))? - .update(&mut cx, |this, _| { - this.live_kit - .as_ref() - .map(|live_kit| live_kit.room.publish_audio_track(track)) - })? - .ok_or_else(|| anyhow!("live-kit was not initialized"))? - .await - }; - let publication = publish_track.await; - this.upgrade() - .ok_or_else(|| anyhow!("room was dropped"))? - .update(&mut cx, |this, cx| { - let live_kit = this - .live_kit - .as_mut() - .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - - let (canceled, muted) = if let LocalTrack::Pending { - publish_id: cur_publish_id, - muted, - } = &live_kit.microphone_track - { - (*cur_publish_id != publish_id, *muted) - } else { - (true, false) - }; - - match publication { - Ok(publication) => { - if canceled { - live_kit.room.unpublish_track(publication); - } else { - if muted { - cx.background_executor() - .spawn(publication.set_mute(muted)) - .detach(); - } - live_kit.microphone_track = LocalTrack::Published { - track_publication: publication, - muted, - }; - cx.notify(); - } - Ok(()) - } - Err(error) => { - if canceled { - Ok(()) - } else { - live_kit.microphone_track = LocalTrack::None; - cx.notify(); - Err(error) - } - } - } - })? - }) - } - - pub fn share_screen(&mut self, cx: &mut ModelContext) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } else if self.is_screen_sharing() { - return Task::ready(Err(anyhow!("screen was already shared"))); - } - - let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { - let publish_id = post_inc(&mut live_kit.next_publish_id); - live_kit.screen_track = LocalTrack::Pending { - publish_id, - muted: false, - }; - cx.notify(); - (live_kit.room.display_sources(), publish_id) - } else { - return Task::ready(Err(anyhow!("live-kit was not initialized"))); - }; - - cx.spawn(move |this, mut cx| async move { - let publish_track = async { - let displays = displays.await?; - let display = displays - .first() - .ok_or_else(|| anyhow!("no display found"))?; - let track = LocalVideoTrack::screen_share_for_display(&display); - this.upgrade() - .ok_or_else(|| anyhow!("room was dropped"))? - .update(&mut cx, |this, _| { - this.live_kit - .as_ref() - .map(|live_kit| live_kit.room.publish_video_track(track)) - })? - .ok_or_else(|| anyhow!("live-kit was not initialized"))? - .await - }; - - let publication = publish_track.await; - this.upgrade() - .ok_or_else(|| anyhow!("room was dropped"))? - .update(&mut cx, |this, cx| { - let live_kit = this - .live_kit - .as_mut() - .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - - let (canceled, muted) = if let LocalTrack::Pending { - publish_id: cur_publish_id, - muted, - } = &live_kit.screen_track - { - (*cur_publish_id != publish_id, *muted) - } else { - (true, false) - }; - - match publication { - Ok(publication) => { - if canceled { - live_kit.room.unpublish_track(publication); - } else { - if muted { - cx.background_executor() - .spawn(publication.set_mute(muted)) - .detach(); - } - live_kit.screen_track = LocalTrack::Published { - track_publication: publication, - muted, - }; - cx.notify(); - } - - Audio::play_sound(Sound::StartScreenshare, cx); - - Ok(()) - } - Err(error) => { - if canceled { - Ok(()) - } else { - live_kit.screen_track = LocalTrack::None; - cx.notify(); - Err(error) - } - } - } - })? - }) - } - - pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> { - let should_mute = !self.is_muted(cx); - if let Some(live_kit) = self.live_kit.as_mut() { - if matches!(live_kit.microphone_track, LocalTrack::None) { - return Ok(self.share_microphone(cx)); - } - - let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?; - live_kit.muted_by_user = should_mute; - - if old_muted == true && live_kit.deafened == true { - if let Some(task) = self.toggle_deafen(cx).ok() { - task.detach(); - } - } - - Ok(ret_task) - } else { - Err(anyhow!("LiveKit not started")) - } - } - - pub fn toggle_deafen(&mut self, cx: &mut ModelContext) -> Result>> { - if let Some(live_kit) = self.live_kit.as_mut() { - (*live_kit).deafened = !live_kit.deafened; - - let mut tasks = Vec::with_capacity(self.remote_participants.len()); - // Context notification is sent within set_mute itself. - let mut mute_task = None; - // When deafening, mute user's mic as well. - // When undeafening, unmute user's mic unless it was manually muted prior to deafening. - if live_kit.deafened || !live_kit.muted_by_user { - mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0); - }; - for participant in self.remote_participants.values() { - for track in live_kit - .room - .remote_audio_track_publications(&participant.user.id.to_string()) - { - let deafened = live_kit.deafened; - tasks.push(cx.foreground_executor().spawn(track.set_enabled(!deafened))); - } - } - - Ok(cx.foreground_executor().spawn(async move { - if let Some(mute_task) = mute_task { - mute_task.await?; - } - for task in tasks { - task.await?; - } - Ok(()) - })) - } else { - Err(anyhow!("LiveKit not started")) - } - } - - pub fn unshare_screen(&mut self, cx: &mut ModelContext) -> Result<()> { - if self.status.is_offline() { - return Err(anyhow!("room is offline")); - } - - let live_kit = self - .live_kit - .as_mut() - .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - match mem::take(&mut live_kit.screen_track) { - LocalTrack::None => Err(anyhow!("screen was not shared")), - LocalTrack::Pending { .. } => { - cx.notify(); - Ok(()) - } - LocalTrack::Published { - track_publication, .. - } => { - live_kit.room.unpublish_track(track_publication); - cx.notify(); - - Audio::play_sound(Sound::StopScreenshare, cx); - Ok(()) - } - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn set_display_sources(&self, sources: Vec) { - self.live_kit - .as_ref() - .unwrap() - .room - .set_display_sources(sources); - } -} - -struct LiveKitRoom { - room: Arc, - screen_track: LocalTrack, - microphone_track: LocalTrack, - /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user. - muted_by_user: bool, - deafened: bool, - speaking: bool, - next_publish_id: usize, - _maintain_room: Task<()>, - _maintain_tracks: [Task<()>; 2], -} - -impl LiveKitRoom { - fn set_mute( - self: &mut LiveKitRoom, - should_mute: bool, - cx: &mut ModelContext, - ) -> Result<(Task>, bool)> { - if !should_mute { - // clear user muting state. - self.muted_by_user = false; - } - - let (result, old_muted) = match &mut self.microphone_track { - LocalTrack::None => Err(anyhow!("microphone was not shared")), - LocalTrack::Pending { muted, .. } => { - let old_muted = *muted; - *muted = should_mute; - cx.notify(); - Ok((Task::Ready(Some(Ok(()))), old_muted)) - } - LocalTrack::Published { - track_publication, - muted, - } => { - let old_muted = *muted; - *muted = should_mute; - cx.notify(); - Ok(( - cx.background_executor() - .spawn(track_publication.set_mute(*muted)), - old_muted, - )) - } - }?; - - if old_muted != should_mute { - if should_mute { - Audio::play_sound(Sound::Mute, cx); - } else { - Audio::play_sound(Sound::Unmute, cx); - } - } - - Ok((result, old_muted)) - } -} - -enum LocalTrack { - None, - Pending { - publish_id: usize, - muted: bool, - }, - Published { - track_publication: LocalTrackPublication, - muted: bool, - }, -} - -impl Default for LocalTrack { - fn default() -> Self { - Self::None - } -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum RoomStatus { - Online, - Rejoining, - Offline, -} - -impl RoomStatus { - pub fn is_offline(&self) -> bool { - matches!(self, RoomStatus::Offline) - } - - pub fn is_online(&self) -> bool { - matches!(self, RoomStatus::Online) - } -} diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index d0a32e16ff..f38ae4078a 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -3,7 +3,7 @@ mod channel_chat; mod channel_store; use client::{Client, UserStore}; -use gpui::{AppContext, ModelHandle}; +use gpui::{AppContext, Model}; use std::sync::Arc; pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; @@ -16,7 +16,7 @@ pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, Cha #[cfg(test)] mod channel_store_tests; -pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { +pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { channel_store::init(client, user_store, cx); 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 9089973d32..1aca05ec86 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -2,7 +2,7 @@ use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::{Client, Collaborator, UserStore}; use collections::HashMap; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; use language::proto::serialize_version; use rpc::{ proto::{self, PeerId}, @@ -22,9 +22,9 @@ pub struct ChannelBuffer { pub channel_id: ChannelId, connected: bool, collaborators: HashMap, - user_store: ModelHandle, - channel_store: ModelHandle, - buffer: ModelHandle, + user_store: Model, + channel_store: Model, + buffer: Model, buffer_epoch: u64, client: Arc, subscription: Option, @@ -38,31 +38,16 @@ pub enum ChannelBufferEvent { ChannelChanged, } -impl Entity for ChannelBuffer { - 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, - }) - .log_err(); - } - } -} +impl EventEmitter for ChannelBuffer {} impl ChannelBuffer { pub(crate) async fn new( channel: Arc, client: Arc, - user_store: ModelHandle, - channel_store: ModelHandle, + user_store: Model, + channel_store: Model, mut cx: AsyncAppContext, - ) -> Result> { + ) -> Result> { let response = client .request(proto::JoinChannelBuffer { channel_id: channel.id, @@ -76,16 +61,21 @@ impl ChannelBuffer { .map(language::proto::deserialize_operation) .collect::, _>>()?; - let buffer = cx.add_model(|_| { - language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) - }); - buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + let buffer = cx.new_model(|_| { + language::Buffer::remote( + response.buffer_id, + response.replica_id as u16, + channel.channel_buffer_capability(), + base_text, + ) + })?; + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??; let subscription = client.subscribe_to_entity(channel.id)?; - anyhow::Ok(cx.add_model(|cx| { + anyhow::Ok(cx.new_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); - + cx.on_release(Self::release).detach(); let mut this = Self { buffer, buffer_epoch: response.epoch, @@ -100,14 +90,27 @@ impl ChannelBuffer { }; this.replace_collaborators(response.collaborators, cx); this - })) + })?) + } + + 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, + }) + .log_err(); + } } pub fn remote_id(&self, cx: &AppContext) -> u64 { self.buffer.read(cx).remote_id() } - pub fn user_store(&self) -> &ModelHandle { + pub fn user_store(&self) -> &Model { &self.user_store } @@ -136,7 +139,7 @@ impl ChannelBuffer { } async fn handle_update_channel_buffer( - this: ModelHandle, + this: Model, update_channel_buffer: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -152,13 +155,13 @@ impl ChannelBuffer { cx.notify(); this.buffer .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) - })?; + })??; Ok(()) } async fn handle_update_channel_buffer_collaborators( - this: ModelHandle, + this: Model, message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -167,14 +170,12 @@ impl ChannelBuffer { this.replace_collaborators(message.payload.collaborators, cx); cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); - }); - - Ok(()) + }) } fn on_buffer_update( &mut self, - _: ModelHandle, + _: Model, event: &language::Event, cx: &mut ModelContext, ) { @@ -202,8 +203,10 @@ impl ChannelBuffer { 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; + self.acknowledge_task = Some(cx.spawn(move |_, cx| async move { + cx.background_executor() + .timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL) + .await; client .send(proto::AckBufferOperation { buffer_id, @@ -219,7 +222,7 @@ impl ChannelBuffer { self.buffer_epoch } - pub fn buffer(&self) -> ModelHandle { + pub fn buffer(&self) -> Model { self.buffer.clone() } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index ef11d96424..d2250972f3 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -6,7 +6,7 @@ use client::{ Client, Subscription, TypedEnvelope, UserId, }; use futures::lock::Mutex; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; use rand::prelude::*; use std::{ collections::HashSet, @@ -22,11 +22,11 @@ pub struct ChannelChat { pub channel_id: ChannelId, messages: SumTree, acknowledged_message_ids: HashSet, - channel_store: ModelHandle, + channel_store: Model, loaded_all_messages: bool, last_acknowledged_id: Option, next_pending_message_id: usize, - user_store: ModelHandle, + user_store: Model, rpc: Arc, outgoing_messages_lock: Arc>, rng: StdRng, @@ -76,31 +76,20 @@ pub enum ChannelChatEvent { }, } +impl EventEmitter for ChannelChat {} 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, + channel_store: Model, + user_store: Model, client: Arc, mut cx: AsyncAppContext, - ) -> Result> { + ) -> Result> { let channel_id = channel.id; let subscription = client.subscribe_to_entity(channel_id).unwrap(); @@ -110,7 +99,8 @@ impl ChannelChat { let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; let loaded_all_messages = response.done; - Ok(cx.add_model(|cx| { + Ok(cx.new_model(|cx| { + cx.on_release(Self::release).detach(); let mut this = Self { channel_id: channel.id, user_store, @@ -127,7 +117,15 @@ impl ChannelChat { }; this.insert_messages(messages, cx); this - })) + })?) + } + + fn release(&mut self, _: &mut AppContext) { + self.rpc + .send(proto::LeaveChannelChat { + channel_id: self.channel_id, + }) + .log_err(); } pub fn channel(&self, cx: &AppContext) -> Option> { @@ -176,7 +174,7 @@ impl ChannelChat { 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 { + Ok(cx.spawn(move |this, mut cx| async move { let outgoing_message_guard = outgoing_messages_lock.lock().await; let request = rpc.request(proto::SendChannelMessage { channel_id, @@ -191,8 +189,8 @@ impl ChannelChat { let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?; this.update(&mut cx, |this, cx| { this.insert_messages(SumTree::from_item(message, &()), cx); - Ok(id) - }) + })?; + Ok(id) })) } @@ -201,13 +199,12 @@ impl ChannelChat { channel_id: self.channel_id, message_id: id, }); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { response.await?; - this.update(&mut cx, |this, cx| { this.message_removed(id, cx); - Ok(()) - }) + })?; + Ok(()) }) } @@ -220,7 +217,7 @@ impl ChannelChat { let user_store = self.user_store.clone(); let channel_id = self.channel_id; let before_message_id = self.first_loaded_message_id()?; - Some(cx.spawn(|this, mut cx| { + Some(cx.spawn(move |this, mut cx| { async move { let response = rpc .request(proto::GetChannelMessages { @@ -233,7 +230,7 @@ impl ChannelChat { this.update(&mut cx, |this, cx| { this.loaded_all_messages = loaded_all_messages; this.insert_messages(messages, cx); - }); + })?; anyhow::Ok(()) } .log_err() @@ -251,31 +248,33 @@ impl ChannelChat { /// /// For now, we always maintain a suffix of the channel's messages. pub async fn load_history_since_message( - chat: ModelHandle, + chat: Model, message_id: u64, mut cx: AsyncAppContext, ) -> Option { loop { - let step = chat.update(&mut cx, |chat, cx| { - if let Some(first_id) = chat.first_loaded_message_id() { - if first_id <= message_id { - let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(); - let message_id = ChannelMessageId::Saved(message_id); - cursor.seek(&message_id, Bias::Left, &()); - return ControlFlow::Break( - if cursor - .item() - .map_or(false, |message| message.id == message_id) - { - Some(cursor.start().1 .0) - } else { - None - }, - ); + let step = chat + .update(&mut cx, |chat, cx| { + if let Some(first_id) = chat.first_loaded_message_id() { + if first_id <= message_id { + let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(); + let message_id = ChannelMessageId::Saved(message_id); + cursor.seek(&message_id, Bias::Left, &()); + return ControlFlow::Break( + if cursor + .item() + .map_or(false, |message| message.id == message_id) + { + Some(cursor.start().1 .0) + } else { + None + }, + ); + } } - } - ControlFlow::Continue(chat.load_more_messages(cx)) - }); + ControlFlow::Continue(chat.load_more_messages(cx)) + }) + .log_err()?; match step { ControlFlow::Break(ix) => return ix, ControlFlow::Continue(task) => task?.await?, @@ -307,7 +306,7 @@ impl ChannelChat { let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); let channel_id = self.channel_id; - cx.spawn(|this, mut cx| { + cx.spawn(move |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?; @@ -333,7 +332,7 @@ impl ChannelChat { } this.pending_messages().cloned().collect::>() - }); + })?; for pending_message in pending_messages { let request = rpc.request(proto::SendChannelMessage { @@ -351,7 +350,7 @@ impl ChannelChat { .await?; this.update(&mut cx, |this, cx| { this.insert_messages(SumTree::from_item(message, &()), cx); - }); + })?; } anyhow::Ok(()) @@ -399,12 +398,12 @@ impl ChannelChat { } async fn handle_message_sent( - this: ModelHandle, + this: Model, message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; let message = message .payload .message @@ -418,20 +417,20 @@ impl ChannelChat { channel_id: this.channel_id, message_id, }) - }); + })?; Ok(()) } async fn handle_message_removed( - this: ModelHandle, + this: Model, message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, cx| { this.message_removed(message.payload.message_id, cx) - }); + })?; Ok(()) } @@ -515,7 +514,7 @@ impl ChannelChat { async fn messages_from_proto( proto_messages: Vec, - user_store: &ModelHandle, + user_store: &Model, cx: &mut AsyncAppContext, ) -> Result> { let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?; @@ -527,13 +526,13 @@ async fn messages_from_proto( impl ChannelMessage { pub async fn from_proto( message: proto::ChannelMessage, - user_store: &ModelHandle, + user_store: &Model, 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), @@ -561,7 +560,7 @@ impl ChannelMessage { pub async fn from_proto_vec( proto_messages: Vec, - user_store: &ModelHandle, + user_store: &Model, cx: &mut AsyncAppContext, ) -> Result> { let unique_user_ids = proto_messages @@ -573,7 +572,7 @@ impl ChannelMessage { 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()); diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index efa05d51a9..59b69405a5 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -7,17 +7,21 @@ use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task, + WeakModel, +}; +use language::Capability; use rpc::{ proto::{self, ChannelVisibility}, TypedEnvelope, }; use std::{mem, sync::Arc, time::Duration}; -use util::ResultExt; +use util::{async_maybe, ResultExt}; -pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { +pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); cx.set_global(channel_store); } @@ -34,7 +38,7 @@ pub struct ChannelStore { opened_buffers: HashMap>, opened_chats: HashMap>, client: Arc, - user_store: ModelHandle, + user_store: Model, _rpc_subscription: Subscription, _watch_connection_status: Task>, disconnect_channel_buffers_task: Option>, @@ -44,7 +48,7 @@ pub struct ChannelStore { #[derive(Clone, Debug, PartialEq)] pub struct Channel { pub id: ChannelId, - pub name: String, + pub name: SharedString, pub visibility: proto::ChannelVisibility, pub role: proto::ChannelRole, pub unseen_note_version: Option<(u64, clock::Global)>, @@ -71,8 +75,12 @@ impl Channel { slug.trim_matches(|c| c == '-').to_string() } - pub fn can_edit_notes(&self) -> bool { - self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin + pub fn channel_buffer_capability(&self) -> Capability { + if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin { + Capability::ReadWrite + } else { + Capability::ReadOnly + } } } @@ -112,44 +120,45 @@ pub enum ChannelEvent { ChannelRenamed(ChannelId), } -impl Entity for ChannelStore { - type Event = ChannelEvent; -} +impl EventEmitter for ChannelStore {} -enum OpenedModelHandle { - Open(WeakModelHandle), - Loading(Shared, Arc>>>), +enum OpenedModelHandle { + Open(WeakModel), + Loading(Shared, Arc>>>), } impl ChannelStore { - pub fn global(cx: &AppContext) -> ModelHandle { - cx.global::>().clone() + pub fn global(cx: &AppContext) -> Model { + cx.global::>().clone() } pub fn new( client: Arc, - user_store: ModelHandle, + user_store: Model, cx: &mut ModelContext, ) -> Self { let rpc_subscription = - client.add_message_handler(cx.handle(), Self::handle_update_channels); + client.add_message_handler(cx.weak_model(), Self::handle_update_channels); let mut connection_status = client.status(); let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded(); - let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { + let watch_connection_status = cx.spawn(|this, mut cx| async move { while let Some(status) = connection_status.next().await { - let this = this.upgrade(&cx)?; + let this = this.upgrade()?; match status { client::Status::Connected { .. } => { this.update(&mut cx, |this, cx| this.handle_connect(cx)) + .ok()? .await .log_err()?; } client::Status::SignedOut | client::Status::UpgradeRequired => { - this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx)); + this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx)) + .ok(); } _ => { - this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx)); + this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx)) + .ok(); } } } @@ -169,17 +178,22 @@ impl ChannelStore { _rpc_subscription: rpc_subscription, _watch_connection_status: watch_connection_status, disconnect_channel_buffers_task: None, - _update_channels: cx.spawn_weak(|this, mut cx| async move { - while let Some(update_channels) = update_channels_rx.next().await { - if let Some(this) = this.upgrade(&cx) { - let update_task = this.update(&mut cx, |this, cx| { - this.update_channels(update_channels, cx) - }); - if let Some(update_task) = update_task { - update_task.await.log_err(); + _update_channels: cx.spawn(|this, mut cx| async move { + async_maybe!({ + while let Some(update_channels) = update_channels_rx.next().await { + if let Some(this) = this.upgrade() { + let update_task = this.update(&mut cx, |this, cx| { + this.update_channels(update_channels, cx) + })?; + if let Some(update_task) = update_task { + update_task.await.log_err(); + } } } - } + anyhow::Ok(()) + }) + .await + .log_err(); }), } } @@ -240,10 +254,10 @@ impl ChannelStore { self.channel_index.by_id().get(&channel_id) } - pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool { + 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 OpenedModelHandle::Open(buffer) = buffer { - return buffer.upgrade(cx).is_some(); + return buffer.upgrade().is_some(); } } false @@ -253,7 +267,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); let channel_store = cx.handle(); @@ -278,13 +292,13 @@ impl ChannelStore { .request(proto::GetChannelMessagesById { message_ids }), ) }; - cx.spawn_weak(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { if let Some(request) = request { let response = request.await?; let this = this - .upgrade(&cx) + .upgrade() .ok_or_else(|| anyhow!("channel store dropped"))?; - let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await } else { Ok(Vec::new()) @@ -354,7 +368,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); let this = cx.handle(); @@ -371,22 +385,23 @@ impl ChannelStore { /// 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( + fn open_channel_resource( &mut self, channel_id: ChannelId, get_map: fn(&mut Self) -> &mut HashMap>, load: F, cx: &mut ModelContext, - ) -> Task>> + ) -> Task>> where F: 'static + FnOnce(Arc, AsyncAppContext) -> Fut, - Fut: Future>>, + Fut: Future>>, + T: 'static, { let task = loop { match get_map(self).entry(channel_id) { hash_map::Entry::Occupied(e) => match e.get() { OpenedModelHandle::Open(model) => { - if let Some(model) = model.upgrade(cx) { + if let Some(model) = model.upgrade() { break Task::ready(Ok(model)).shared(); } else { get_map(self).remove(&channel_id); @@ -399,12 +414,12 @@ impl ChannelStore { }, hash_map::Entry::Vacant(e) => { let task = cx - .spawn(|this, cx| async move { - let channel = this.read_with(&cx, |this, _| { + .spawn(move |this, mut cx| async move { + let channel = this.update(&mut cx, |this, _| { this.channel_for_id(channel_id).cloned().ok_or_else(|| { Arc::new(anyhow!("no channel for id: {}", channel_id)) }) - })?; + })??; load(channel, cx).await.map_err(Arc::new) }) @@ -413,7 +428,7 @@ impl ChannelStore { e.insert(OpenedModelHandle::Loading(task.clone())); cx.spawn({ let task = task.clone(); - |this, mut cx| async move { + move |this, mut cx| async move { let result = task.await; this.update(&mut cx, |this, _| match result { Ok(model) => { @@ -425,7 +440,8 @@ impl ChannelStore { Err(_) => { get_map(this).remove(&channel_id); } - }); + }) + .ok(); } }) .detach(); @@ -433,7 +449,7 @@ impl ChannelStore { } } }; - cx.foreground() + cx.background_executor() .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) } @@ -458,7 +474,7 @@ impl ChannelStore { ) -> Task> { let client = self.client.clone(); let name = name.trim_start_matches("#").to_owned(); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let response = client .request(proto::CreateChannel { name, parent_id }) .await?; @@ -468,15 +484,6 @@ impl ChannelStore { .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 { @@ -492,7 +499,7 @@ impl ChannelStore { // will resolve before this flush_effects finishes. Synchronously emitting this event // ensures that the collab panel will observe this creation before the frame completes cx.emit(ChannelEvent::ChannelCreated(channel_id)); - }); + })?; Ok(channel_id) }) @@ -505,7 +512,7 @@ impl ChannelStore { cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); - cx.spawn(|_, _| async move { + cx.spawn(move |_, _| async move { let _ = client .request(proto::MoveChannel { channel_id, to }) .await?; @@ -521,7 +528,7 @@ impl ChannelStore { cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); - cx.spawn(|_, _| async move { + cx.spawn(move |_, _| async move { let _ = client .request(proto::SetChannelVisibility { channel_id, @@ -546,7 +553,7 @@ impl ChannelStore { cx.notify(); let client = self.client.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let result = client .request(proto::InviteChannelMember { channel_id, @@ -558,7 +565,7 @@ impl ChannelStore { this.update(&mut cx, |this, cx| { this.outgoing_invites.remove(&(channel_id, user_id)); cx.notify(); - }); + })?; result?; @@ -578,7 +585,7 @@ impl ChannelStore { cx.notify(); let client = self.client.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let result = client .request(proto::RemoveChannelMember { channel_id, @@ -589,7 +596,7 @@ impl ChannelStore { this.update(&mut cx, |this, cx| { this.outgoing_invites.remove(&(channel_id, user_id)); cx.notify(); - }); + })?; result?; Ok(()) }) @@ -608,7 +615,7 @@ impl ChannelStore { cx.notify(); let client = self.client.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let result = client .request(proto::SetChannelMemberRole { channel_id, @@ -620,7 +627,7 @@ impl ChannelStore { this.update(&mut cx, |this, cx| { this.outgoing_invites.remove(&(channel_id, user_id)); cx.notify(); - }); + })?; result?; Ok(()) @@ -635,7 +642,7 @@ impl ChannelStore { ) -> Task> { let client = self.client.clone(); let name = new_name.to_string(); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let channel = client .request(proto::RenameChannel { channel_id, name }) .await? @@ -656,7 +663,7 @@ impl ChannelStore { // will resolve before this flush_effects finishes. Synchronously emitting this event // ensures that the collab panel will observe this creation before the frame complete cx.emit(ChannelEvent::ChannelRenamed(channel_id)) - }); + })?; Ok(()) }) } @@ -668,7 +675,7 @@ impl ChannelStore { cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); - cx.background().spawn(async move { + cx.background_executor().spawn(async move { client .request(proto::RespondToChannelInvite { channel_id, accept }) .await?; @@ -683,17 +690,17 @@ impl ChannelStore { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.downgrade(); - cx.spawn(|_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let response = client .request(proto::GetChannelMembers { channel_id }) .await?; let user_ids = response.members.iter().map(|m| m.user_id).collect(); let user_store = user_store - .upgrade(&cx) + .upgrade() .ok_or_else(|| anyhow!("user store dropped"))?; let users = user_store - .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx)) + .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))? .await?; Ok(users @@ -727,7 +734,7 @@ impl ChannelStore { } async fn handle_update_channels( - this: ModelHandle, + this: Model, message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -736,7 +743,7 @@ impl ChannelStore { this.update_channels_tx .unbounded_send(message.payload) .unwrap(); - }); + })?; Ok(()) } @@ -750,7 +757,7 @@ impl ChannelStore { for chat in self.opened_chats.values() { if let OpenedModelHandle::Open(chat) = chat { - if let Some(chat) = chat.upgrade(cx) { + if let Some(chat) = chat.upgrade() { chat.update(cx, |chat, cx| { chat.rejoin(cx); }); @@ -761,7 +768,7 @@ impl ChannelStore { let mut buffer_versions = Vec::new(); for buffer in self.opened_buffers.values() { if let OpenedModelHandle::Open(buffer) = buffer { - if let Some(buffer) = buffer.upgrade(cx) { + if let Some(buffer) = buffer.upgrade() { let channel_buffer = buffer.read(cx); let buffer = channel_buffer.buffer().read(cx); buffer_versions.push(proto::ChannelBufferVersion { @@ -787,7 +794,7 @@ impl ChannelStore { this.update(&mut cx, |this, cx| { this.opened_buffers.retain(|_, buffer| match buffer { OpenedModelHandle::Open(channel_buffer) => { - let Some(channel_buffer) = channel_buffer.upgrade(cx) else { + let Some(channel_buffer) = channel_buffer.upgrade() else { return false; }; @@ -824,7 +831,7 @@ impl ChannelStore { if let Some(operations) = operations { let client = this.client.clone(); - cx.background() + cx.background_executor() .spawn(async move { let operations = operations.await; for chunk in @@ -849,7 +856,8 @@ impl ChannelStore { } OpenedModelHandle::Loading(_) => true, }); - }); + }) + .ok(); anyhow::Ok(()) }) } @@ -858,21 +866,22 @@ impl ChannelStore { cx.notify(); self.disconnect_channel_buffers_task.get_or_insert_with(|| { - cx.spawn_weak(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { if wait_for_reconnect { - cx.background().timer(RECONNECT_TIMEOUT).await; + cx.background_executor().timer(RECONNECT_TIMEOUT).await; } - if let Some(this) = this.upgrade(&cx) { + if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { for (_, buffer) in this.opened_buffers.drain() { if let OpenedModelHandle::Open(buffer) = buffer { - if let Some(buffer) = buffer.upgrade(cx) { + if let Some(buffer) = buffer.upgrade() { buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); } } } - }); + }) + .ok(); } }) }); @@ -892,14 +901,16 @@ impl ChannelStore { .channel_invitations .binary_search_by_key(&channel.id, |c| c.id) { - Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name, + Ok(ix) => { + Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into() + } Err(ix) => self.channel_invitations.insert( ix, Arc::new(Channel { id: channel.id, visibility: channel.visibility(), role: channel.role(), - name: channel.name, + name: channel.name.into(), unseen_note_version: None, unseen_message_id: None, parent_path: channel.parent_path, @@ -931,7 +942,7 @@ impl ChannelStore { if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.remove(&channel_id) { - if let Some(buffer) = buffer.upgrade(cx) { + if let Some(buffer) = buffer.upgrade() { buffer.update(cx, ChannelBuffer::disconnect); } } @@ -945,7 +956,7 @@ impl ChannelStore { if channel_changed { if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.get(&id) { - if let Some(buffer) = buffer.upgrade(cx) { + if let Some(buffer) = buffer.upgrade() { buffer.update(cx, ChannelBuffer::channel_changed); } } @@ -1010,8 +1021,7 @@ impl ChannelStore { } cx.notify(); - }); - anyhow::Ok(()) + }) })) } } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 97b2ab6318..2682cf6ae2 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { existing_channel.visibility = channel_proto.visibility(); existing_channel.role = channel_proto.role(); - existing_channel.name = channel_proto.name; + existing_channel.name = channel_proto.name.into(); } else { self.channels_by_id.insert( channel_proto.id, @@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { id: channel_proto.id, visibility: channel_proto.visibility(), role: channel_proto.role(), - name: channel_proto.name, + name: channel_proto.name.into(), unseen_note_version: None, unseen_message_id: None, parent_path: channel_proto.parent_path, @@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>( let (parent_path, name) = channels_by_id .get(&id) .map_or((&[] as &[_], None), |channel| { - (channel.parent_path.as_slice(), Some(channel.name.as_str())) + (channel.parent_path.as_slice(), Some(channel.name.as_ref())) }); parent_path .iter() - .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str())) + .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref())) .chain(name) } diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index ff8761ee91..0b07918acf 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -2,7 +2,7 @@ use crate::channel_chat::ChannelChatEvent; use super::*; use client::{test::FakeServer, Client, UserStore}; -use gpui::{AppContext, ModelHandle, TestAppContext}; +use gpui::{AppContext, Context, Model, TestAppContext}; use rpc::proto::{self}; use settings::SettingsStore; use util::http::FakeHttpClient; @@ -147,7 +147,7 @@ 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 client = channel_store.update(cx, |s, _| s.client()); let server = FakeServer::for_client(user_id, &client, cx).await; // Get the available channels. @@ -161,8 +161,8 @@ async fn test_channel_messages(cx: &mut TestAppContext) { }], ..Default::default() }); - cx.foreground().run_until_parked(); - cx.read(|cx| { + cx.executor().run_until_parked(); + cx.update(|cx| { assert_channels( &channel_store, &[(0, "the-channel".to_string(), proto::ChannelRole::Member)], @@ -214,7 +214,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { }, ); - cx.foreground().start_waiting(); + cx.executor().start_waiting(); // Client requests all users for the received messages let mut get_users = server.receive::().await.unwrap(); @@ -232,7 +232,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { ); let channel = channel.await.unwrap(); - channel.read_with(cx, |channel, _| { + channel.update(cx, |channel, _| { assert_eq!( channel .messages_in_range(0..2) @@ -273,13 +273,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) { ); assert_eq!( - channel.next_event(cx).await, + channel.next_event(cx), ChannelChatEvent::MessagesUpdated { old_range: 2..2, new_count: 1, } ); - channel.read_with(cx, |channel, _| { + channel.update(cx, |channel, _| { assert_eq!( channel .messages_in_range(2..3) @@ -322,13 +322,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) { ); assert_eq!( - channel.next_event(cx).await, + channel.next_event(cx), ChannelChatEvent::MessagesUpdated { old_range: 0..0, new_count: 2, } ); - channel.read_with(cx, |channel, _| { + channel.update(cx, |channel, _| { assert_eq!( channel .messages_in_range(0..2) @@ -342,13 +342,14 @@ async fn test_channel_messages(cx: &mut TestAppContext) { }); } -fn init_test(cx: &mut AppContext) -> ModelHandle { +fn init_test(cx: &mut AppContext) -> Model { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + 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 user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); - cx.foreground().forbid_parking(); - cx.set_global(SettingsStore::test(cx)); client::init(&client, cx); crate::init(&client, user_store, cx); @@ -356,7 +357,7 @@ fn init_test(cx: &mut AppContext) -> ModelHandle { } fn update_channels( - channel_store: &ModelHandle, + channel_store: &Model, message: proto::UpdateChannels, cx: &mut AppContext, ) { @@ -366,11 +367,11 @@ fn update_channels( #[track_caller] fn assert_channels( - channel_store: &ModelHandle, + channel_store: &Model, expected_channels: &[(usize, String, proto::ChannelRole)], - cx: &AppContext, + cx: &mut AppContext, ) { - let actual = channel_store.read_with(cx, |store, _| { + let actual = channel_store.update(cx, |store, _| { store .ordered_channels() .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role)) diff --git a/crates/channel2/Cargo.toml b/crates/channel2/Cargo.toml deleted file mode 100644 index 7af5aa1224..0000000000 --- a/crates/channel2/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "channel2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/channel2.rs" -doctest = false - -[features] -test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] - -[dependencies] -client = { package = "client2", path = "../client2" } -collections = { path = "../collections" } -db = { package = "db2", path = "../db2" } -gpui = { package = "gpui2", path = "../gpui2" } -util = { path = "../util" } -rpc = { package = "rpc2", path = "../rpc2" } -text = { package = "text2", path = "../text2" } -language = { package = "language2", path = "../language2" } -settings = { package = "settings2", path = "../settings2" } -feature_flags = { package = "feature_flags2", path = "../feature_flags2" } -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 -rand.workspace = true -schemars.workspace = true -smol.workspace = true -thiserror.workspace = true -time.workspace = true -tiny_http = "0.8" -uuid.workspace = true -url = "2.2" -serde.workspace = true -serde_derive.workspace = true -tempfile = "3" - -[dev-dependencies] -collections = { path = "../collections", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } -client = { package = "client2", path = "../client2", features = ["test-support"] } -settings = { package = "settings2", path = "../settings2", features = ["test-support"] } -util = { path = "../util", features = ["test-support"] } diff --git a/crates/channel2/src/channel2.rs b/crates/channel2/src/channel2.rs deleted file mode 100644 index f38ae4078a..0000000000 --- a/crates/channel2/src/channel2.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod channel_buffer; -mod channel_chat; -mod channel_store; - -use client::{Client, UserStore}; -use gpui::{AppContext, Model}; -use std::sync::Arc; - -pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; -pub use channel_chat::{ - mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, - MessageParams, -}; -pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore}; - -#[cfg(test)] -mod channel_store_tests; - -pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { - channel_store::init(client, user_store, cx); - channel_buffer::init(client); - channel_chat::init(client); -} diff --git a/crates/channel2/src/channel_buffer.rs b/crates/channel2/src/channel_buffer.rs deleted file mode 100644 index 764f5f7661..0000000000 --- a/crates/channel2/src/channel_buffer.rs +++ /dev/null @@ -1,257 +0,0 @@ -use crate::{Channel, ChannelId, ChannelStore}; -use anyhow::Result; -use client::{Client, Collaborator, UserStore}; -use collections::HashMap; -use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, 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_update_channel_buffer_collaborators); -} - -pub struct ChannelBuffer { - pub channel_id: ChannelId, - connected: bool, - collaborators: HashMap, - user_store: Model, - channel_store: Model, - buffer: Model, - buffer_epoch: u64, - client: Arc, - subscription: Option, - acknowledge_task: Option>>, -} - -pub enum ChannelBufferEvent { - CollaboratorsChanged, - Disconnected, - BufferEdited, - ChannelChanged, -} - -impl EventEmitter for ChannelBuffer {} - -impl ChannelBuffer { - pub(crate) async fn new( - channel: Arc, - client: Arc, - user_store: Model, - channel_store: Model, - mut cx: AsyncAppContext, - ) -> Result> { - let response = client - .request(proto::JoinChannelBuffer { - channel_id: channel.id, - }) - .await?; - - let base_text = response.base_text; - let operations = response - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; - - let buffer = cx.build_model(|_| { - language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) - })?; - buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??; - - let subscription = client.subscribe_to_entity(channel.id)?; - - anyhow::Ok(cx.build_model(|cx| { - cx.subscribe(&buffer, Self::on_buffer_update).detach(); - cx.on_release(Self::release).detach(); - let mut this = Self { - buffer, - buffer_epoch: response.epoch, - client, - connected: true, - collaborators: Default::default(), - acknowledge_task: None, - channel_id: channel.id, - subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), - user_store, - channel_store, - }; - this.replace_collaborators(response.collaborators, cx); - this - })?) - } - - 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, - }) - .log_err(); - } - } - - pub fn remote_id(&self, cx: &AppContext) -> u64 { - self.buffer.read(cx).remote_id() - } - - pub fn user_store(&self) -> &Model { - &self.user_store - } - - pub(crate) fn replace_collaborators( - &mut self, - collaborators: Vec, - cx: &mut ModelContext, - ) { - 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 = new_collaborators; - cx.emit(ChannelBufferEvent::CollaboratorsChanged); - cx.notify(); - } - - async fn handle_update_channel_buffer( - this: Model, - update_channel_buffer: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let ops = update_channel_buffer - .payload - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; - - this.update(&mut cx, |this, cx| { - cx.notify(); - this.buffer - .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) - })??; - - Ok(()) - } - - async fn handle_update_channel_buffer_collaborators( - this: Model, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.replace_collaborators(message.payload.collaborators, cx); - cx.emit(ChannelBufferEvent::CollaboratorsChanged); - cx.notify(); - }) - } - - fn on_buffer_update( - &mut self, - _: Model, - event: &language::Event, - cx: &mut ModelContext, - ) { - 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(move |_, cx| async move { - cx.background_executor() - .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 - } - - pub fn buffer(&self) -> Model { - self.buffer.clone() - } - - pub fn collaborators(&self) -> &HashMap { - &self.collaborators - } - - pub fn channel(&self, cx: &AppContext) -> Option> { - self.channel_store - .read(cx) - .channel_for_id(self.channel_id) - .cloned() - } - - pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { - log::info!("channel buffer {} disconnected", self.channel_id); - if self.connected { - self.connected = false; - self.subscription.take(); - cx.emit(ChannelBufferEvent::Disconnected); - cx.notify() - } - } - - pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext) { - cx.emit(ChannelBufferEvent::ChannelChanged); - cx.notify() - } - - pub fn is_connected(&self) -> bool { - self.connected - } - - pub fn replica_id(&self, cx: &AppContext) -> u16 { - self.buffer.read(cx).replica_id() - } -} diff --git a/crates/channel2/src/channel_chat.rs b/crates/channel2/src/channel_chat.rs deleted file mode 100644 index eb92eb18e4..0000000000 --- a/crates/channel2/src/channel_chat.rs +++ /dev/null @@ -1,645 +0,0 @@ -use crate::{Channel, ChannelId, ChannelStore}; -use anyhow::{anyhow, Result}; -use client::{ - proto, - user::{User, UserStore}, - Client, Subscription, TypedEnvelope, UserId, -}; -use futures::lock::Mutex; -use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; -use rand::prelude::*; -use std::{ - collections::HashSet, - mem, - ops::{ControlFlow, Range}, - sync::Arc, -}; -use sum_tree::{Bias, SumTree}; -use time::OffsetDateTime; -use util::{post_inc, ResultExt as _, TryFutureExt}; - -pub struct ChannelChat { - pub channel_id: ChannelId, - messages: SumTree, - acknowledged_message_ids: HashSet, - channel_store: Model, - loaded_all_messages: bool, - last_acknowledged_id: Option, - next_pending_message_id: usize, - user_store: Model, - rpc: Arc, - outgoing_messages_lock: Arc>, - rng: StdRng, - _subscription: Subscription, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct MessageParams { - pub text: String, - pub mentions: Vec<(Range, UserId)>, -} - -#[derive(Clone, Debug)] -pub struct ChannelMessage { - pub id: ChannelMessageId, - pub body: String, - pub timestamp: OffsetDateTime, - pub sender: Arc, - pub nonce: u128, - pub mentions: Vec<(Range, UserId)>, -} - -#[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, - }, -} - -impl EventEmitter for ChannelChat {} -pub fn init(client: &Arc) { - client.add_model_message_handler(ChannelChat::handle_message_sent); - client.add_model_message_handler(ChannelChat::handle_message_removed); -} - -impl ChannelChat { - pub async fn new( - channel: Arc, - channel_store: Model, - user_store: Model, - 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.build_model(|cx| { - cx.on_release(Self::release).detach(); - let mut this = Self { - channel_id: channel.id, - user_store, - channel_store, - rpc: client, - outgoing_messages_lock: Default::default(), - messages: Default::default(), - acknowledged_message_ids: 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 - })?) - } - - fn release(&mut self, _: &mut AppContext) { - self.rpc - .send(proto::LeaveChannelChat { - channel_id: self.channel_id, - }) - .log_err(); - } - - pub fn channel(&self, cx: &AppContext) -> Option> { - self.channel_store - .read(cx) - .channel_for_id(self.channel_id) - .cloned() - } - - pub fn client(&self) -> &Arc { - &self.rpc - } - - pub fn send_message( - &mut self, - message: MessageParams, - cx: &mut ModelContext, - ) -> Result>> { - if message.text.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: message.text.clone(), - sender: current_user, - timestamp: OffsetDateTime::now_utc(), - mentions: message.mentions.clone(), - 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(move |this, mut cx| async move { - let outgoing_message_guard = outgoing_messages_lock.lock().await; - let request = rpc.request(proto::SendChannelMessage { - channel_id, - body: message.text, - nonce: Some(nonce.into()), - mentions: mentions_to_proto(&message.mentions), - }); - let response = request.await?; - drop(outgoing_message_guard); - let response = response.message.ok_or_else(|| anyhow!("invalid message"))?; - let id = response.id; - let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?; - this.update(&mut cx, |this, cx| { - this.insert_messages(SumTree::from_item(message, &()), cx); - })?; - Ok(id) - })) - } - - 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(move |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) -> Option>> { - if self.loaded_all_messages { - return None; - } - - let rpc = self.rpc.clone(); - let user_store = self.user_store.clone(); - let channel_id = self.channel_id; - let before_message_id = self.first_loaded_message_id()?; - Some(cx.spawn(move |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() - })) - } - - pub fn first_loaded_message_id(&mut self) -> Option { - self.messages.first().and_then(|message| match message.id { - ChannelMessageId::Saved(id) => Some(id), - ChannelMessageId::Pending(_) => None, - }) - } - - /// Load all of the chat messages since a certain message id. - /// - /// For now, we always maintain a suffix of the channel's messages. - pub async fn load_history_since_message( - chat: Model, - message_id: u64, - mut cx: AsyncAppContext, - ) -> Option { - loop { - let step = chat - .update(&mut cx, |chat, cx| { - if let Some(first_id) = chat.first_loaded_message_id() { - if first_id <= message_id { - let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(); - let message_id = ChannelMessageId::Saved(message_id); - cursor.seek(&message_id, Bias::Left, &()); - return ControlFlow::Break( - if cursor - .item() - .map_or(false, |message| message.id == message_id) - { - Some(cursor.start().1 .0) - } else { - None - }, - ); - } - } - ControlFlow::Continue(chat.load_more_messages(cx)) - }) - .log_err()?; - match step { - ControlFlow::Break(ix) => return ix, - ControlFlow::Continue(task) => task?.await?, - } - } - } - - 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(move |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, - mentions: mentions_to_proto(&pending_message.mentions), - 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 acknowledge_message(&mut self, id: u64) { - if self.acknowledged_message_ids.insert(id) { - self.rpc - .send(proto::AckChannelMessage { - channel_id: self.channel_id, - message_id: id, - }) - .ok(); - } - } - - 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: Model, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let user_store = this.update(&mut 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: Model, - 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: &Model, - cx: &mut AsyncAppContext, -) -> Result> { - let messages = ChannelMessage::from_proto_vec(proto_messages, 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: &Model, - 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, - mentions: message - .mentions - .into_iter() - .filter_map(|mention| { - let range = mention.range?; - Some((range.start as usize..range.end as usize, mention.user_id)) - }) - .collect(), - 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(_)) - } - - pub async fn from_proto_vec( - proto_messages: Vec, - user_store: &Model, - 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?); - } - Ok(messages) - } -} - -pub fn mentions_to_proto(mentions: &[(Range, UserId)]) -> Vec { - mentions - .iter() - .map(|(range, user_id)| proto::ChatMention { - range: Some(proto::Range { - start: range.start as u64, - end: range.end as u64, - }), - user_id: *user_id as u64, - }) - .collect() -} - -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; - } -} - -impl<'a> From<&'a str> for MessageParams { - fn from(value: &'a str) -> Self { - Self { - text: value.into(), - mentions: Vec::new(), - } - } -} diff --git a/crates/channel2/src/channel_store.rs b/crates/channel2/src/channel_store.rs deleted file mode 100644 index 1bd987274c..0000000000 --- a/crates/channel2/src/channel_store.rs +++ /dev/null @@ -1,1022 +0,0 @@ -mod channel_index; - -use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage}; -use anyhow::{anyhow, Result}; -use channel_index::ChannelIndex; -use client::{Client, Subscription, User, UserId, UserStore}; -use collections::{hash_map, HashMap, HashSet}; -use db::RELEASE_CHANNEL; -use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; -use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task, - WeakModel, -}; -use rpc::{ - proto::{self, ChannelVisibility}, - TypedEnvelope, -}; -use std::{mem, sync::Arc, time::Duration}; -use util::{async_maybe, ResultExt}; - -pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { - let channel_store = - cx.build_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); - cx.set_global(channel_store); -} - -pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); - -pub type ChannelId = u64; - -pub struct ChannelStore { - pub channel_index: ChannelIndex, - channel_invitations: Vec>, - channel_participants: HashMap>>, - outgoing_invites: HashSet<(ChannelId, UserId)>, - update_channels_tx: mpsc::UnboundedSender, - opened_buffers: HashMap>, - opened_chats: HashMap>, - client: Arc, - user_store: Model, - _rpc_subscription: Subscription, - _watch_connection_status: Task>, - disconnect_channel_buffers_task: Option>, - _update_channels: Task<()>, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Channel { - pub id: ChannelId, - pub name: SharedString, - pub visibility: proto::ChannelVisibility, - pub role: proto::ChannelRole, - pub unseen_note_version: Option<(u64, clock::Global)>, - pub unseen_message_id: Option, - pub parent_path: Vec, -} - -impl Channel { - pub fn link(&self) -> String { - RELEASE_CHANNEL.link_prefix().to_owned() - + "channel/" - + &self.slug() - + "-" - + &self.id.to_string() - } - - pub fn slug(&self) -> String { - let slug: String = self - .name - .chars() - .map(|c| if c.is_alphanumeric() { c } else { '-' }) - .collect(); - - slug.trim_matches(|c| c == '-').to_string() - } - - pub fn can_edit_notes(&self) -> bool { - self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin - } -} - -pub struct ChannelMembership { - pub user: Arc, - pub kind: proto::channel_member::Kind, - pub role: proto::ChannelRole, -} -impl ChannelMembership { - pub fn sort_key(&self) -> MembershipSortKey { - MembershipSortKey { - role_order: match self.role { - proto::ChannelRole::Admin => 0, - proto::ChannelRole::Member => 1, - proto::ChannelRole::Banned => 2, - proto::ChannelRole::Guest => 3, - }, - kind_order: match self.kind { - proto::channel_member::Kind::Member => 0, - proto::channel_member::Kind::AncestorMember => 1, - proto::channel_member::Kind::Invitee => 2, - }, - username_order: self.user.github_login.as_str(), - } - } -} - -#[derive(PartialOrd, Ord, PartialEq, Eq)] -pub struct MembershipSortKey<'a> { - role_order: u8, - kind_order: u8, - username_order: &'a str, -} - -pub enum ChannelEvent { - ChannelCreated(ChannelId), - ChannelRenamed(ChannelId), -} - -impl EventEmitter for ChannelStore {} - -enum OpenedModelHandle { - Open(WeakModel), - Loading(Shared, Arc>>>), -} - -impl ChannelStore { - pub fn global(cx: &AppContext) -> Model { - cx.global::>().clone() - } - - pub fn new( - client: Arc, - user_store: Model, - cx: &mut ModelContext, - ) -> Self { - let rpc_subscription = - client.add_message_handler(cx.weak_model(), Self::handle_update_channels); - - let mut connection_status = client.status(); - let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded(); - let watch_connection_status = cx.spawn(|this, mut cx| async move { - while let Some(status) = connection_status.next().await { - let this = this.upgrade()?; - match status { - client::Status::Connected { .. } => { - this.update(&mut cx, |this, cx| this.handle_connect(cx)) - .ok()? - .await - .log_err()?; - } - client::Status::SignedOut | client::Status::UpgradeRequired => { - this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx)) - .ok(); - } - _ => { - this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx)) - .ok(); - } - } - } - Some(()) - }); - - Self { - channel_invitations: Vec::default(), - channel_index: ChannelIndex::default(), - channel_participants: Default::default(), - outgoing_invites: Default::default(), - opened_buffers: Default::default(), - opened_chats: Default::default(), - update_channels_tx, - client, - user_store, - _rpc_subscription: rpc_subscription, - _watch_connection_status: watch_connection_status, - disconnect_channel_buffers_task: None, - _update_channels: cx.spawn(|this, mut cx| async move { - async_maybe!({ - while let Some(update_channels) = update_channels_rx.next().await { - if let Some(this) = this.upgrade() { - let update_task = this.update(&mut cx, |this, cx| { - this.update_channels(update_channels, cx) - })?; - if let Some(update_task) = update_task { - update_task.await.log_err(); - } - } - } - anyhow::Ok(()) - }) - .await - .log_err(); - }), - } - } - - pub fn client(&self) -> Arc { - self.client.clone() - } - - /// Returns the number of unique channels in the store - pub fn channel_count(&self) -> usize { - 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) - } - - /// 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 ordered_channels(&self) -> impl '_ + Iterator)> { - self.channel_index - .ordered_channels() - .iter() - .filter_map(move |id| { - let channel = self.channel_index.by_id().get(id)?; - Some((channel.parent_path.len(), channel)) - }) - } - - pub fn channel_at_index(&self, ix: usize) -> Option<&Arc> { - let channel_id = self.channel_index.ordered_channels().get(ix)?; - self.channel_index.by_id().get(channel_id) - } - - pub fn channel_at(&self, ix: usize) -> Option<&Arc> { - self.channel_index.by_id().values().nth(ix) - } - - pub fn has_channel_invitation(&self, channel_id: ChannelId) -> bool { - self.channel_invitations - .iter() - .any(|channel| channel.id == channel_id) - } - - pub fn channel_invitations(&self) -> &[Arc] { - &self.channel_invitations - } - - pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc> { - 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 OpenedModelHandle::Open(buffer) = buffer { - return buffer.upgrade().is_some(); - } - } - false - } - - pub fn open_channel_buffer( - &mut self, - channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>> { - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let channel_store = cx.handle(); - self.open_channel_resource( - channel_id, - |this| &mut this.opened_buffers, - |channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx), - cx, - ) - } - - pub fn fetch_channel_messages( - &self, - message_ids: Vec, - cx: &mut ModelContext, - ) -> Task>> { - let request = if message_ids.is_empty() { - None - } else { - Some( - self.client - .request(proto::GetChannelMessagesById { message_ids }), - ) - }; - cx.spawn(|this, mut cx| async move { - if let Some(request) = request { - let response = request.await?; - let this = this - .upgrade() - .ok_or_else(|| anyhow!("channel store dropped"))?; - let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; - ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await - } else { - Ok(Vec::new()) - } - }) - } - - 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>>, - T: 'static, - { - let task = loop { - match get_map(self).entry(channel_id) { - hash_map::Entry::Occupied(e) => match e.get() { - OpenedModelHandle::Open(model) => { - if let Some(model) = model.upgrade() { - break Task::ready(Ok(model)).shared(); - } else { - get_map(self).remove(&channel_id); - continue; - } - } - OpenedModelHandle::Loading(task) => { - break task.clone(); - } - }, - hash_map::Entry::Vacant(e) => { - let task = cx - .spawn(move |this, mut cx| async move { - let channel = this.update(&mut cx, |this, _| { - this.channel_for_id(channel_id).cloned().ok_or_else(|| { - Arc::new(anyhow!("no channel for id: {}", channel_id)) - }) - })??; - - load(channel, cx).await.map_err(Arc::new) - }) - .shared(); - - e.insert(OpenedModelHandle::Loading(task.clone())); - cx.spawn({ - let task = task.clone(); - move |this, mut cx| async move { - let result = task.await; - this.update(&mut cx, |this, _| match result { - Ok(model) => { - get_map(this).insert( - channel_id, - OpenedModelHandle::Open(model.downgrade()), - ); - } - Err(_) => { - get_map(this).remove(&channel_id); - } - }) - .ok(); - } - }) - .detach(); - break task; - } - } - }; - cx.background_executor() - .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) - } - - pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool { - let Some(channel) = self.channel_for_id(channel_id) else { - return false; - }; - channel.role == proto::ChannelRole::Admin - } - - pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { - self.channel_participants - .get(&channel_id) - .map_or(&[], |v| v.as_slice()) - } - - pub fn create_channel( - &self, - name: &str, - parent_id: Option, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - let name = name.trim_start_matches("#").to_owned(); - cx.spawn(move |this, mut cx| async move { - let response = client - .request(proto::CreateChannel { name, parent_id }) - .await?; - - let channel = response - .channel - .ok_or_else(|| anyhow!("missing channel in response"))?; - let channel_id = channel.id; - - this.update(&mut cx, |this, cx| { - let task = this.update_channels( - proto::UpdateChannels { - channels: vec![channel], - ..Default::default() - }, - cx, - ); - assert!(task.is_none()); - - // This event is emitted because the collab panel wants to clear the pending edit state - // before this frame is rendered. But we can't guarantee that the collab panel's future - // will resolve before this flush_effects finishes. Synchronously emitting this event - // ensures that the collab panel will observe this creation before the frame completes - cx.emit(ChannelEvent::ChannelCreated(channel_id)); - })?; - - Ok(channel_id) - }) - } - - pub fn move_channel( - &mut self, - channel_id: ChannelId, - to: Option, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.spawn(move |_, _| async move { - let _ = client - .request(proto::MoveChannel { channel_id, to }) - .await?; - - Ok(()) - }) - } - - pub fn set_channel_visibility( - &mut self, - channel_id: ChannelId, - visibility: ChannelVisibility, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.spawn(move |_, _| async move { - let _ = client - .request(proto::SetChannelVisibility { - channel_id, - visibility: visibility.into(), - }) - .await?; - - Ok(()) - }) - } - - pub fn invite_member( - &mut self, - channel_id: ChannelId, - user_id: UserId, - role: proto::ChannelRole, - cx: &mut ModelContext, - ) -> Task> { - if !self.outgoing_invites.insert((channel_id, user_id)) { - return Task::ready(Err(anyhow!("invite request already in progress"))); - } - - cx.notify(); - let client = self.client.clone(); - cx.spawn(move |this, mut cx| async move { - let result = client - .request(proto::InviteChannelMember { - channel_id, - user_id, - role: role.into(), - }) - .await; - - this.update(&mut cx, |this, cx| { - this.outgoing_invites.remove(&(channel_id, user_id)); - cx.notify(); - })?; - - result?; - - Ok(()) - }) - } - - pub fn remove_member( - &mut self, - channel_id: ChannelId, - user_id: u64, - cx: &mut ModelContext, - ) -> Task> { - if !self.outgoing_invites.insert((channel_id, user_id)) { - return Task::ready(Err(anyhow!("invite request already in progress"))); - } - - cx.notify(); - let client = self.client.clone(); - cx.spawn(move |this, mut cx| async move { - let result = client - .request(proto::RemoveChannelMember { - channel_id, - user_id, - }) - .await; - - this.update(&mut cx, |this, cx| { - this.outgoing_invites.remove(&(channel_id, user_id)); - cx.notify(); - })?; - result?; - Ok(()) - }) - } - - pub fn set_member_role( - &mut self, - channel_id: ChannelId, - user_id: UserId, - role: proto::ChannelRole, - cx: &mut ModelContext, - ) -> Task> { - if !self.outgoing_invites.insert((channel_id, user_id)) { - return Task::ready(Err(anyhow!("member request already in progress"))); - } - - cx.notify(); - let client = self.client.clone(); - cx.spawn(move |this, mut cx| async move { - let result = client - .request(proto::SetChannelMemberRole { - channel_id, - user_id, - role: role.into(), - }) - .await; - - this.update(&mut cx, |this, cx| { - this.outgoing_invites.remove(&(channel_id, user_id)); - cx.notify(); - })?; - - result?; - Ok(()) - }) - } - - pub fn rename( - &mut self, - channel_id: ChannelId, - new_name: &str, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - let name = new_name.to_string(); - cx.spawn(move |this, mut cx| async move { - let channel = client - .request(proto::RenameChannel { channel_id, name }) - .await? - .channel - .ok_or_else(|| anyhow!("missing channel in response"))?; - this.update(&mut cx, |this, cx| { - let task = this.update_channels( - proto::UpdateChannels { - channels: vec![channel], - ..Default::default() - }, - cx, - ); - assert!(task.is_none()); - - // This event is emitted because the collab panel wants to clear the pending edit state - // before this frame is rendered. But we can't guarantee that the collab panel's future - // will resolve before this flush_effects finishes. Synchronously emitting this event - // ensures that the collab panel will observe this creation before the frame complete - cx.emit(ChannelEvent::ChannelRenamed(channel_id)) - })?; - Ok(()) - }) - } - - pub fn respond_to_channel_invite( - &mut self, - channel_id: ChannelId, - accept: bool, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.background_executor().spawn(async move { - client - .request(proto::RespondToChannelInvite { channel_id, accept }) - .await?; - Ok(()) - }) - } - - pub fn get_channel_member_details( - &self, - channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>> { - let client = self.client.clone(); - let user_store = self.user_store.downgrade(); - cx.spawn(move |_, mut cx| async move { - let response = client - .request(proto::GetChannelMembers { channel_id }) - .await?; - - let user_ids = response.members.iter().map(|m| m.user_id).collect(); - let user_store = user_store - .upgrade() - .ok_or_else(|| anyhow!("user store dropped"))?; - let users = user_store - .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))? - .await?; - - Ok(users - .into_iter() - .zip(response.members) - .filter_map(|(user, member)| { - Some(ChannelMembership { - user, - role: member.role(), - kind: member.kind(), - }) - }) - .collect()) - }) - } - - pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { - let client = self.client.clone(); - async move { - client.request(proto::DeleteChannel { channel_id }).await?; - Ok(()) - } - } - - pub fn has_pending_channel_invite_response(&self, _: &Arc) -> bool { - false - } - - pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool { - self.outgoing_invites.contains(&(channel_id, user_id)) - } - - async fn handle_update_channels( - this: Model, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - this.update_channels_tx - .unbounded_send(message.payload) - .unwrap(); - })?; - Ok(()) - } - - fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { - self.channel_index.clear(); - self.channel_invitations.clear(); - self.channel_participants.clear(); - self.channel_index.clear(); - self.outgoing_invites.clear(); - 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() { - chat.update(cx, |chat, cx| { - chat.rejoin(cx); - }); - } - } - } - - let mut buffer_versions = Vec::new(); - for buffer in self.opened_buffers.values() { - if let OpenedModelHandle::Open(buffer) = buffer { - if let Some(buffer) = buffer.upgrade() { - let channel_buffer = buffer.read(cx); - let buffer = channel_buffer.buffer().read(cx); - buffer_versions.push(proto::ChannelBufferVersion { - channel_id: channel_buffer.channel_id, - epoch: channel_buffer.epoch(), - version: language::proto::serialize_version(&buffer.version()), - }); - } - } - } - - if buffer_versions.is_empty() { - return Task::ready(Ok(())); - } - - let response = self.client.request(proto::RejoinChannelBuffers { - buffers: buffer_versions, - }); - - cx.spawn(|this, mut cx| async move { - let mut response = response.await?; - - this.update(&mut cx, |this, cx| { - this.opened_buffers.retain(|_, buffer| match buffer { - OpenedModelHandle::Open(channel_buffer) => { - let Some(channel_buffer) = channel_buffer.upgrade() else { - return false; - }; - - channel_buffer.update(cx, |channel_buffer, cx| { - let channel_id = channel_buffer.channel_id; - if let Some(remote_buffer) = response - .buffers - .iter_mut() - .find(|buffer| buffer.channel_id == channel_id) - { - let channel_id = channel_buffer.channel_id; - let remote_version = - language::proto::deserialize_version(&remote_buffer.version); - - channel_buffer.replace_collaborators( - mem::take(&mut remote_buffer.collaborators), - cx, - ); - - let operations = channel_buffer - .buffer() - .update(cx, |buffer, cx| { - let outgoing_operations = - buffer.serialize_ops(Some(remote_version), cx); - let incoming_operations = - mem::take(&mut remote_buffer.operations) - .into_iter() - .map(language::proto::deserialize_operation) - .collect::>>()?; - buffer.apply_ops(incoming_operations, cx)?; - anyhow::Ok(outgoing_operations) - }) - .log_err(); - - if let Some(operations) = operations { - let client = this.client.clone(); - cx.background_executor() - .spawn(async move { - let operations = operations.await; - for chunk in - language::proto::split_operations(operations) - { - client - .send(proto::UpdateChannelBuffer { - channel_id, - operations: chunk, - }) - .ok(); - } - }) - .detach(); - return true; - } - } - - channel_buffer.disconnect(cx); - false - }) - } - OpenedModelHandle::Loading(_) => true, - }); - }) - .ok(); - anyhow::Ok(()) - }) - } - - fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext) { - cx.notify(); - - self.disconnect_channel_buffers_task.get_or_insert_with(|| { - cx.spawn(move |this, mut cx| async move { - if wait_for_reconnect { - cx.background_executor().timer(RECONNECT_TIMEOUT).await; - } - - if let Some(this) = this.upgrade() { - this.update(&mut cx, |this, cx| { - for (_, buffer) in this.opened_buffers.drain() { - if let OpenedModelHandle::Open(buffer) = buffer { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); - } - } - } - }) - .ok(); - } - }) - }); - } - - pub(crate) fn update_channels( - &mut self, - payload: proto::UpdateChannels, - cx: &mut ModelContext, - ) -> Option>> { - if !payload.remove_channel_invitations.is_empty() { - self.channel_invitations - .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); - } - for channel in payload.channel_invitations { - match self - .channel_invitations - .binary_search_by_key(&channel.id, |c| c.id) - { - Ok(ix) => { - Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into() - } - Err(ix) => self.channel_invitations.insert( - ix, - Arc::new(Channel { - id: channel.id, - visibility: channel.visibility(), - role: channel.role(), - name: channel.name.into(), - unseen_note_version: None, - unseen_message_id: None, - parent_path: channel.parent_path, - }), - ), - } - } - - let channels_changed = !payload.channels.is_empty() - || !payload.delete_channels.is_empty() - || !payload.unseen_channel_messages.is_empty() - || !payload.unseen_channel_buffer_changes.is_empty(); - - if channels_changed { - if !payload.delete_channels.is_empty() { - self.channel_index.delete_channels(&payload.delete_channels); - self.channel_participants - .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id)); - - for channel_id in &payload.delete_channels { - let channel_id = *channel_id; - if payload - .channels - .iter() - .any(|channel| channel.id == channel_id) - { - continue; - } - if let Some(OpenedModelHandle::Open(buffer)) = - self.opened_buffers.remove(&channel_id) - { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, ChannelBuffer::disconnect); - } - } - } - } - - let mut index = self.channel_index.bulk_insert(); - for channel in payload.channels { - let id = channel.id; - let channel_changed = index.insert(channel); - - if channel_changed { - if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.get(&id) { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, ChannelBuffer::channel_changed); - } - } - } - } - - 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, - ); - } - } - - cx.notify(); - if payload.channel_participants.is_empty() { - return None; - } - - let mut all_user_ids = Vec::new(); - let channel_participants = payload.channel_participants; - for entry in &channel_participants { - for user_id in entry.participant_user_ids.iter() { - if let Err(ix) = all_user_ids.binary_search(user_id) { - all_user_ids.insert(ix, *user_id); - } - } - } - - let users = self - .user_store - .update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx)); - Some(cx.spawn(|this, mut cx| async move { - let users = users.await?; - - this.update(&mut cx, |this, cx| { - for entry in &channel_participants { - let mut participants: Vec<_> = entry - .participant_user_ids - .iter() - .filter_map(|user_id| { - users - .binary_search_by_key(&user_id, |user| &user.id) - .ok() - .map(|ix| users[ix].clone()) - }) - .collect(); - - participants.sort_by_key(|u| u.id); - - this.channel_participants - .insert(entry.channel_id, participants); - } - - cx.notify(); - }) - })) - } -} diff --git a/crates/channel2/src/channel_store/channel_index.rs b/crates/channel2/src/channel_store/channel_index.rs deleted file mode 100644 index 2682cf6ae2..0000000000 --- a/crates/channel2/src/channel_store/channel_index.rs +++ /dev/null @@ -1,184 +0,0 @@ -use crate::{Channel, ChannelId}; -use collections::BTreeMap; -use rpc::proto; -use std::sync::Arc; - -#[derive(Default, Debug)] -pub struct ChannelIndex { - channels_ordered: Vec, - channels_by_id: BTreeMap>, -} - -impl ChannelIndex { - pub fn by_id(&self) -> &BTreeMap> { - &self.channels_by_id - } - - pub fn ordered_channels(&self) -> &[ChannelId] { - &self.channels_ordered - } - - pub fn clear(&mut self) { - self.channels_ordered.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.channels_ordered - .retain(|channel_id| !channels.contains(channel_id)); - } - - pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { - ChannelPathsInsertGuard { - channels_ordered: &mut self.channels_ordered, - 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) - } -} - -/// 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> { - channels_ordered: &'a mut Vec, - channels_by_id: &'a mut BTreeMap>, -} - -impl<'a> ChannelPathsInsertGuard<'a> { - 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) -> bool { - let mut ret = false; - if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { - let existing_channel = Arc::make_mut(existing_channel); - - ret = existing_channel.visibility != channel_proto.visibility() - || existing_channel.role != channel_proto.role() - || existing_channel.name != channel_proto.name; - - existing_channel.visibility = channel_proto.visibility(); - existing_channel.role = channel_proto.role(); - existing_channel.name = channel_proto.name.into(); - } else { - self.channels_by_id.insert( - channel_proto.id, - Arc::new(Channel { - id: channel_proto.id, - visibility: channel_proto.visibility(), - role: channel_proto.role(), - name: channel_proto.name.into(), - unseen_note_version: None, - unseen_message_id: None, - parent_path: channel_proto.parent_path, - }), - ); - self.insert_root(channel_proto.id); - } - ret - } - - fn insert_root(&mut self, channel_id: ChannelId) { - self.channels_ordered.push(channel_id); - } -} - -impl<'a> Drop for ChannelPathsInsertGuard<'a> { - fn drop(&mut self) { - self.channels_ordered.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.channels_ordered.dedup(); - } -} - -fn channel_path_sorting_key<'a>( - id: ChannelId, - channels_by_id: &'a BTreeMap>, -) -> impl Iterator { - let (parent_path, name) = channels_by_id - .get(&id) - .map_or((&[] as &[_], None), |channel| { - (channel.parent_path.as_slice(), Some(channel.name.as_ref())) - }); - parent_path - .iter() - .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref())) - .chain(name) -} - -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/channel2/src/channel_store_tests.rs b/crates/channel2/src/channel_store_tests.rs deleted file mode 100644 index 7f392032cd..0000000000 --- a/crates/channel2/src/channel_store_tests.rs +++ /dev/null @@ -1,380 +0,0 @@ -use crate::channel_chat::ChannelChatEvent; - -use super::*; -use client::{test::FakeServer, Client, UserStore}; -use gpui::{AppContext, Context, Model, TestAppContext}; -use rpc::proto::{self}; -use settings::SettingsStore; -use util::http::FakeHttpClient; - -#[gpui::test] -fn test_update_channels(cx: &mut AppContext) { - let channel_store = init_test(cx); - - update_channels( - &channel_store, - proto::UpdateChannels { - channels: vec![ - proto::Channel { - id: 1, - name: "b".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), - }, - proto::Channel { - id: 2, - name: "a".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Member.into(), - parent_path: Vec::new(), - }, - ], - ..Default::default() - }, - cx, - ); - assert_channels( - &channel_store, - &[ - // - (0, "a".to_string(), proto::ChannelRole::Member), - (0, "b".to_string(), proto::ChannelRole::Admin), - ], - cx, - ); - - update_channels( - &channel_store, - proto::UpdateChannels { - channels: vec![ - proto::Channel { - id: 3, - name: "x".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Admin.into(), - parent_path: vec![1], - }, - proto::Channel { - id: 4, - name: "y".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Member.into(), - parent_path: vec![2], - }, - ], - ..Default::default() - }, - cx, - ); - assert_channels( - &channel_store, - &[ - (0, "a".to_string(), proto::ChannelRole::Member), - (1, "y".to_string(), proto::ChannelRole::Member), - (0, "b".to_string(), proto::ChannelRole::Admin), - (1, "x".to_string(), proto::ChannelRole::Admin), - ], - cx, - ); -} - -#[gpui::test] -fn test_dangling_channel_paths(cx: &mut AppContext) { - let channel_store = init_test(cx); - - update_channels( - &channel_store, - proto::UpdateChannels { - channels: vec![ - proto::Channel { - id: 0, - name: "a".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Admin.into(), - parent_path: vec![], - }, - proto::Channel { - id: 1, - name: "b".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Admin.into(), - parent_path: vec![0], - }, - proto::Channel { - id: 2, - name: "c".to_string(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Admin.into(), - parent_path: vec![0, 1], - }, - ], - ..Default::default() - }, - cx, - ); - // Sanity check - assert_channels( - &channel_store, - &[ - // - (0, "a".to_string(), proto::ChannelRole::Admin), - (1, "b".to_string(), proto::ChannelRole::Admin), - (2, "c".to_string(), proto::ChannelRole::Admin), - ], - cx, - ); - - update_channels( - &channel_store, - proto::UpdateChannels { - delete_channels: vec![1, 2], - ..Default::default() - }, - cx, - ); - - // Make sure that the 1/2/3 path is gone - assert_channels( - &channel_store, - &[(0, "a".to_string(), proto::ChannelRole::Admin)], - 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.update(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(), - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Member.into(), - parent_path: vec![], - }], - ..Default::default() - }); - cx.executor().run_until_parked(); - cx.update(|cx| { - assert_channels( - &channel_store, - &[(0, "the-channel".to_string(), proto::ChannelRole::Member)], - 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.ordered_channels().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, - mentions: vec![], - nonce: Some(1.into()), - }, - proto::ChannelMessage { - id: 11, - body: "b".into(), - timestamp: 1001, - sender_id: 6, - mentions: vec![], - nonce: Some(2.into()), - }, - ], - done: false, - }, - ); - - cx.executor().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.update(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, - mentions: vec![], - 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), - ChannelChatEvent::MessagesUpdated { - old_range: 2..2, - new_count: 1, - } - ); - channel.update(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| { - channel.load_more_messages(cx).unwrap().detach(); - }); - 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()), - mentions: vec![], - }, - proto::ChannelMessage { - id: 9, - body: "z".into(), - timestamp: 999, - sender_id: 6, - nonce: Some(5.into()), - mentions: vec![], - }, - ], - }, - ); - - assert_eq!( - channel.next_event(cx), - ChannelChatEvent::MessagesUpdated { - old_range: 0..0, - new_count: 2, - } - ); - channel.update(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) -> Model { - let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); - - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - client::init(&client, cx); - crate::init(&client, user_store, cx); - - ChannelStore::global(cx) -} - -fn update_channels( - channel_store: &Model, - message: proto::UpdateChannels, - cx: &mut AppContext, -) { - let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx)); - assert!(task.is_none()); -} - -#[track_caller] -fn assert_channels( - channel_store: &Model, - expected_channels: &[(usize, String, proto::ChannelRole)], - cx: &mut AppContext, -) { - let actual = channel_store.update(cx, |store, _| { - store - .ordered_channels() - .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role)) - .collect::>() - }); - assert_eq!(actual, expected_channels); -} diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index c24cbca35b..03d6c06fe3 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -36,6 +36,7 @@ rand.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true +serde_json.workspace = true smol.workspace = true sysinfo.workspace = true tempfile = "3" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c1666e9c1d..0821a8e534 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -4,20 +4,19 @@ pub mod test; pub mod telemetry; pub mod user; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use async_recursion::async_recursion; use async_tungstenite::tungstenite::{ error::Error as WebsocketError, http::{Request, StatusCode}, }; use futures::{ - future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, - TryStreamExt, + channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, + TryFutureExt as _, TryStreamExt, }; use gpui::{ - actions, platform::AppVersion, serde_json, AnyModelHandle, AnyWeakModelHandle, - AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext, - WeakViewHandle, + actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task, + WeakModel, }; use lazy_static::lazy_static; use parking_lot::RwLock; @@ -26,6 +25,8 @@ use rand::prelude::*; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json; +use settings::Settings; use std::{ any::TypeId, collections::HashMap, @@ -45,7 +46,7 @@ use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; pub use rpc::*; -pub use telemetry::ClickhouseEvent; +pub use telemetry::Event; pub use user::*; lazy_static! { @@ -57,7 +58,7 @@ lazy_static! { pub static ref ADMIN_API_TOKEN: Option = std::env::var("ZED_ADMIN_API_TOKEN") .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }); - pub static ref ZED_APP_VERSION: Option = std::env::var("ZED_APP_VERSION") + pub static ref ZED_APP_VERSION: Option = std::env::var("ZED_APP_VERSION") .ok() .and_then(|v| v.parse().ok()); pub static ref ZED_APP_PATH: Option = @@ -73,14 +74,14 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); actions!(client, [SignIn, SignOut, Reconnect]); pub fn init_settings(cx: &mut AppContext) { - settings::register::(cx); + TelemetrySettings::register(cx); } pub fn init(client: &Arc, cx: &mut AppContext) { init_settings(cx); let client = Arc::downgrade(client); - cx.add_global_action({ + cx.on_action({ let client = client.clone(); move |_: &SignIn, cx| { if let Some(client) = client.upgrade() { @@ -91,7 +92,8 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } } }); - cx.add_global_action({ + + cx.on_action({ let client = client.clone(); move |_: &SignOut, cx| { if let Some(client) = client.upgrade() { @@ -102,7 +104,8 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } } }); - cx.add_global_action({ + + cx.on_action({ let client = client.clone(); move |_: &Reconnect, cx| { if let Some(client) = client.upgrade() { @@ -216,7 +219,7 @@ struct ClientState { _reconnect_task: Option>, reconnect_interval: Duration, entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>, - models_by_message_type: HashMap, + models_by_message_type: HashMap, entity_types_by_message_type: HashMap, #[allow(clippy::type_complexity)] message_handlers: HashMap< @@ -225,7 +228,7 @@ struct ClientState { dyn Send + Sync + Fn( - Subscriber, + AnyModel, Box, &Arc, AsyncAppContext, @@ -235,16 +238,10 @@ struct ClientState { } enum WeakSubscriber { - Model(AnyWeakModelHandle), - View(AnyWeakViewHandle), + Entity { handle: AnyWeakModel }, Pending(Vec>), } -enum Subscriber { - Model(AnyModelHandle), - View(AnyWeakViewHandle), -} - #[derive(Clone, Debug)] pub struct Credentials { pub user_id: u64, @@ -298,15 +295,15 @@ impl Drop for Subscription { } } -pub struct PendingEntitySubscription { +pub struct PendingEntitySubscription { client: Arc, remote_id: u64, _entity_type: PhantomData, consumed: bool, } -impl PendingEntitySubscription { - pub fn set_model(mut self, model: &ModelHandle, cx: &mut AsyncAppContext) -> Subscription { +impl PendingEntitySubscription { + pub fn set_model(mut self, model: &Model, cx: &mut AsyncAppContext) -> Subscription { self.consumed = true; let mut state = self.client.state.write(); let id = (TypeId::of::(), self.remote_id); @@ -316,9 +313,12 @@ impl PendingEntitySubscription { unreachable!() }; - state - .entities_by_type_and_remote_id - .insert(id, WeakSubscriber::Model(model.downgrade().into_any())); + state.entities_by_type_and_remote_id.insert( + id, + WeakSubscriber::Entity { + handle: model.downgrade().into(), + }, + ); drop(state); for message in messages { self.client.handle_message(message, cx); @@ -330,7 +330,7 @@ impl PendingEntitySubscription { } } -impl Drop for PendingEntitySubscription { +impl Drop for PendingEntitySubscription { fn drop(&mut self) { if !self.consumed { let mut state = self.client.state.write(); @@ -346,19 +346,26 @@ impl Drop for PendingEntitySubscription { } } -#[derive(Debug, Copy, Clone)] +#[derive(Copy, Clone)] pub struct TelemetrySettings { pub diagnostics: bool, pub metrics: bool, } +/// Control what info is collected by Zed. #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] pub struct TelemetrySettingsContent { + /// Send debug info like crash reports. + /// + /// Default: true pub diagnostics: Option, + /// Send anonymized usage data like what languages you're using Zed with. + /// + /// Default: true pub metrics: Option, } -impl settings::Setting for TelemetrySettings { +impl settings::Settings for TelemetrySettings { const KEY: Option<&'static str> = Some("telemetry"); type FileContent = TelemetrySettingsContent; @@ -366,7 +373,7 @@ impl settings::Setting for TelemetrySettings { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &AppContext, + _: &mut AppContext, ) -> Result { Ok(Self { diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or( @@ -383,7 +390,7 @@ impl settings::Setting for TelemetrySettings { } impl Client { - pub fn new(http: Arc, cx: &AppContext) -> Arc { + pub fn new(http: Arc, cx: &mut AppContext) -> Arc { Arc::new(Self { id: AtomicU64::new(0), peer: Peer::new(0), @@ -475,7 +482,7 @@ impl Client { Status::ConnectionLost => { let this = self.clone(); let reconnect_interval = state.reconnect_interval; - state._reconnect_task = Some(cx.spawn(|cx| async move { + state._reconnect_task = Some(cx.spawn(move |cx| async move { #[cfg(any(test, feature = "test-support"))] let mut rng = StdRng::seed_from_u64(0); #[cfg(not(any(test, feature = "test-support")))] @@ -491,7 +498,7 @@ impl Client { }, &cx, ); - cx.background().timer(delay).await; + cx.background_executor().timer(delay).await; delay = delay .mul_f32(rng.gen_range(1.0..=2.0)) .min(reconnect_interval); @@ -502,33 +509,20 @@ impl Client { })); } Status::SignedOut | Status::UpgradeRequired => { - cx.read(|cx| self.telemetry.set_authenticated_user_info(None, false, cx)); + self.telemetry.set_authenticated_user_info(None, false); state._reconnect_task.take(); } _ => {} } } - pub fn add_view_for_remote_entity( + pub fn subscribe_to_entity( self: &Arc, remote_id: u64, - cx: &mut ViewContext, - ) -> Subscription { - let id = (TypeId::of::(), remote_id); - self.state - .write() - .entities_by_type_and_remote_id - .insert(id, WeakSubscriber::View(cx.weak_handle().into_any())); - Subscription::Entity { - client: Arc::downgrade(self), - id, - } - } - - pub fn subscribe_to_entity( - self: &Arc, - remote_id: u64, - ) -> Result> { + ) -> Result> + where + T: 'static, + { let id = (TypeId::of::(), remote_id); let mut state = self.state.write(); @@ -550,36 +544,31 @@ impl Client { #[track_caller] pub fn add_message_handler( self: &Arc, - model: ModelHandle, + entity: WeakModel, handler: H, ) -> Subscription where M: EnvelopedMessage, - E: Entity, + E: 'static, H: 'static - + Send + Sync - + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + + Send + + Sync, F: 'static + Future>, { let message_type_id = TypeId::of::(); - let mut state = self.state.write(); state .models_by_message_type - .insert(message_type_id, model.downgrade().into_any()); + .insert(message_type_id, entity.into()); let prev_handler = state.message_handlers.insert( message_type_id, - Arc::new(move |handle, envelope, client, cx| { - let handle = if let Subscriber::Model(handle) = handle { - handle - } else { - unreachable!(); - }; - let model = handle.downcast::().unwrap(); + Arc::new(move |subscriber, envelope, client, cx| { + let subscriber = subscriber.downcast::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); - handler(model, *envelope, client.clone(), cx).boxed_local() + handler(subscriber, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -600,16 +589,17 @@ impl Client { pub fn add_request_handler( self: &Arc, - model: ModelHandle, + model: WeakModel, handler: H, ) -> Subscription where M: RequestMessage, - E: Entity, + E: 'static, H: 'static - + Send + Sync - + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + + Send + + Sync, F: 'static + Future>, { self.add_message_handler(model, move |handle, envelope, this, cx| { @@ -621,52 +611,23 @@ impl Client { }) } - pub fn add_view_message_handler(self: &Arc, handler: H) - where - M: EntityMessage, - E: View, - H: 'static - + Send - + Sync - + Fn(WeakViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, - F: 'static + Future>, - { - self.add_entity_message_handler::(move |handle, message, client, cx| { - if let Subscriber::View(handle) = handle { - handler(handle.downcast::().unwrap(), message, client, cx) - } else { - unreachable!(); - } - }) - } - pub fn add_model_message_handler(self: &Arc, handler: H) where M: EntityMessage, - E: Entity, - H: 'static - + Send - + Sync - + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + E: 'static, + H: 'static + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, F: 'static + Future>, { - self.add_entity_message_handler::(move |handle, message, client, cx| { - if let Subscriber::Model(handle) = handle { - handler(handle.downcast::().unwrap(), message, client, cx) - } else { - unreachable!(); - } + self.add_entity_message_handler::(move |subscriber, message, client, cx| { + handler(subscriber.downcast::().unwrap(), message, client, cx) }) } fn add_entity_message_handler(self: &Arc, handler: H) where M: EntityMessage, - E: Entity, - H: 'static - + Send - + Sync - + Fn(Subscriber, TypedEnvelope, Arc, AsyncAppContext) -> F, + E: 'static, + H: 'static + Fn(AnyModel, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, F: 'static + Future>, { let model_type_id = TypeId::of::(); @@ -704,11 +665,8 @@ impl Client { pub fn add_model_request_handler(self: &Arc, handler: H) where M: EntityMessage + RequestMessage, - E: Entity, - H: 'static - + Send - + Sync - + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + E: 'static, + H: 'static + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, F: 'static + Future>, { self.add_model_message_handler(move |entity, envelope, client, cx| { @@ -720,25 +678,6 @@ impl Client { }) } - pub fn add_view_request_handler(self: &Arc, handler: H) - where - M: EntityMessage + RequestMessage, - E: View, - H: 'static - + Send - + Sync - + Fn(WeakViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, - F: 'static + Future>, - { - self.add_view_message_handler(move |entity, envelope, client, cx| { - Self::respond_to_request::( - envelope.receipt(), - handler(entity, envelope, client.clone(), cx), - client, - ) - }) - } - async fn respond_to_request>>( receipt: Receipt, response: F, @@ -823,14 +762,15 @@ impl Client { self.set_status(Status::Reconnecting, cx); } - let mut timeout = cx.background().timer(CONNECTION_TIMEOUT).fuse(); + let mut timeout = + futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT)); futures::select_biased! { connection = self.establish_connection(&credentials, cx).fuse() => { match connection { Ok(conn) => { self.state.write().credentials = Some(credentials.clone()); if !read_from_keychain && IMPERSONATE_LOGIN.is_none() { - write_credentials_to_keychain(&credentials, cx).log_err(); + write_credentials_to_keychain(credentials, cx).log_err(); } futures::select_biased! { @@ -844,7 +784,7 @@ impl Client { Err(EstablishConnectionError::Unauthorized) => { self.state.write().credentials.take(); if read_from_keychain { - cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); + delete_credentials_from_keychain(cx).log_err(); self.set_status(Status::SignedOut, cx); self.authenticate_and_connect(false, cx).await } else { @@ -874,12 +814,13 @@ impl Client { conn: Connection, cx: &AsyncAppContext, ) -> Result<()> { - let executor = cx.background(); + let executor = cx.background_executor(); log::info!("add connection to peer"); - let (connection_id, handle_io, mut incoming) = self - .peer - .add_connection(conn, move |duration| executor.timer(duration)); - let handle_io = cx.background().spawn(handle_io); + let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn, { + let executor = executor.clone(); + move |duration| executor.timer(duration) + }); + let handle_io = executor.spawn(handle_io); let peer_id = async { log::info!("waiting for server hello"); @@ -925,10 +866,10 @@ impl Client { }, cx, ); - cx.foreground() - .spawn({ - let cx = cx.clone(); - let this = self.clone(); + + cx.spawn({ + let this = self.clone(); + |cx| { async move { while let Some(message) = incoming.next().await { this.handle_message(message, &cx); @@ -936,13 +877,13 @@ impl Client { smol::future::yield_now().await; } } - }) - .detach(); + } + }) + .detach(); - let this = self.clone(); - let cx = cx.clone(); - cx.foreground() - .spawn(async move { + cx.spawn({ + let this = self.clone(); + move |cx| async move { match handle_io.await { Ok(()) => { if this.status().borrow().clone() @@ -959,8 +900,9 @@ impl Client { this.set_status(Status::ConnectionLost, &cx); } } - }) - .detach(); + } + }) + .detach(); Ok(()) } @@ -1032,13 +974,7 @@ impl Client { credentials: &Credentials, cx: &AsyncAppContext, ) -> Task> { - let release_channel = cx.read(|cx| { - if cx.has_global::() { - Some(*cx.global::()) - } else { - None - } - }); + let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel); let request = Request::builder() .header( @@ -1048,7 +984,7 @@ impl Client { .header("x-zed-protocol-version", rpc::PROTOCOL_VERSION); let http = self.http.clone(); - cx.background().spawn(async move { + cx.background_executor().spawn(async move { let mut rpc_url = Self::get_rpc_url(http, release_channel).await?; let rpc_host = rpc_url .host_str() @@ -1089,96 +1025,118 @@ impl Client { self: &Arc, cx: &AsyncAppContext, ) -> Task> { - let platform = cx.platform(); - let executor = cx.background(); let http = self.http.clone(); + cx.spawn(|cx| async move { + let background = cx.background_executor().clone(); - executor.clone().spawn(async move { - // Generate a pair of asymmetric encryption keys. The public key will be used by the - // zed server to encrypt the user's access token, so that it can'be intercepted by - // any other app running on the user's device. - let (public_key, private_key) = - rpc::auth::keypair().expect("failed to generate keypair for auth"); - let public_key_string = - String::try_from(public_key).expect("failed to serialize public key for auth"); + let (open_url_tx, open_url_rx) = oneshot::channel::(); + cx.update(|cx| { + cx.spawn(move |cx| async move { + let url = open_url_rx.await?; + cx.update(|cx| cx.open_url(&url)) + }) + .detach_and_log_err(cx); + }) + .log_err(); - if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - return Self::authenticate_as_admin(http, login.clone(), token.clone()).await; - } - - // Start an HTTP server to receive the redirect from Zed's sign-in page. - let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); - let port = server.server_addr().port(); - - // Open the Zed sign-in page in the user's browser, with query parameters that indicate - // that the user is signing in from a Zed app running on the same device. - let mut url = format!( - "{}/native_app_signin?native_app_port={}&native_app_public_key={}", - *ZED_SERVER_URL, port, public_key_string - ); - - if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { - log::info!("impersonating user @{}", impersonate_login); - write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); - } - - platform.open_url(&url); - - // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted - // access token from the query params. - // - // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a - // custom URL scheme instead of this local HTTP server. - let (user_id, access_token) = executor + let credentials = background + .clone() .spawn(async move { - for _ in 0..100 { - if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { - let path = req.url(); - let mut user_id = None; - let mut access_token = None; - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); + // Generate a pair of asymmetric encryption keys. The public key will be used by the + // zed server to encrypt the user's access token, so that it can'be intercepted by + // any other app running on the user's device. + let (public_key, private_key) = + rpc::auth::keypair().expect("failed to generate keypair for auth"); + let public_key_string = String::try_from(public_key) + .expect("failed to serialize public key for auth"); + + if let Some((login, token)) = + IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) + { + return Self::authenticate_as_admin(http, login.clone(), token.clone()) + .await; + } + + // Start an HTTP server to receive the redirect from Zed's sign-in page. + let server = + tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); + let port = server.server_addr().port(); + + // Open the Zed sign-in page in the user's browser, with query parameters that indicate + // that the user is signing in from a Zed app running on the same device. + let mut url = format!( + "{}/native_app_signin?native_app_port={}&native_app_public_key={}", + *ZED_SERVER_URL, port, public_key_string + ); + + if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { + log::info!("impersonating user @{}", impersonate_login); + write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); + } + + open_url_tx.send(url).log_err(); + + // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted + // access token from the query params. + // + // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a + // custom URL scheme instead of this local HTTP server. + let (user_id, access_token) = background + .spawn(async move { + for _ in 0..100 { + if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { + let path = req.url(); + let mut user_id = None; + let mut access_token = None; + let url = Url::parse(&format!("http://example.com{}", path)) + .context("failed to parse login notification url")?; + for (key, value) in url.query_pairs() { + if key == "access_token" { + access_token = Some(value.to_string()); + } else if key == "user_id" { + user_id = Some(value.to_string()); + } + } + + let post_auth_url = + format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); + req.respond( + tiny_http::Response::empty(302).with_header( + tiny_http::Header::from_bytes( + &b"Location"[..], + post_auth_url.as_bytes(), + ) + .unwrap(), + ), + ) + .context("failed to respond to login http request")?; + return Ok(( + user_id + .ok_or_else(|| anyhow!("missing user_id parameter"))?, + access_token.ok_or_else(|| { + anyhow!("missing access_token parameter") + })?, + )); } } - let post_auth_url = - format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); - req.respond( - tiny_http::Response::empty(302).with_header( - tiny_http::Header::from_bytes( - &b"Location"[..], - post_auth_url.as_bytes(), - ) - .unwrap(), - ), - ) - .context("failed to respond to login http request")?; - return Ok(( - user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, - access_token - .ok_or_else(|| anyhow!("missing access_token parameter"))?, - )); - } - } + Err(anyhow!("didn't receive login redirect")) + }) + .await?; - Err(anyhow!("didn't receive login redirect")) + let access_token = private_key + .decrypt_string(&access_token) + .context("failed to decrypt access token")?; + + Ok(Credentials { + user_id: user_id.parse()?, + access_token, + }) }) .await?; - let access_token = private_key - .decrypt_string(&access_token) - .context("failed to decrypt access token")?; - platform.activate(true); - - Ok(Credentials { - user_id: user_id.parse()?, - access_token, - }) + cx.update(|cx| cx.activate(true))?; + Ok(credentials) }) } @@ -1307,12 +1265,12 @@ impl Client { let mut subscriber = None; - if let Some(message_model) = state + if let Some(handle) = state .models_by_message_type .get(&payload_type_id) - .and_then(|model| model.upgrade(cx)) + .and_then(|handle| handle.upgrade()) { - subscriber = Some(Subscriber::Model(message_model)); + subscriber = Some(handle); } else if let Some((extract_entity_id, entity_type_id)) = state.entity_id_extractors.get(&payload_type_id).zip( state @@ -1332,12 +1290,10 @@ impl Client { return; } Some(weak_subscriber @ _) => match weak_subscriber { - WeakSubscriber::Model(handle) => { - subscriber = handle.upgrade(cx).map(Subscriber::Model); - } - WeakSubscriber::View(handle) => { - subscriber = Some(Subscriber::View(handle.clone())); + WeakSubscriber::Entity { handle } => { + subscriber = handle.upgrade(); } + WeakSubscriber::Pending(_) => {} }, _ => {} @@ -1367,8 +1323,7 @@ impl Client { sender_id, type_name ); - cx.foreground() - .spawn(async move { + cx.spawn(move |_| async move { match future.await { Ok(()) => { log::debug!( @@ -1407,22 +1362,30 @@ fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { } let (user_id, access_token) = cx - .platform() - .read_credentials(&ZED_SERVER_URL) - .log_err() - .flatten()?; + .update(|cx| cx.read_credentials(&ZED_SERVER_URL).log_err().flatten()) + .ok()??; + Some(Credentials { user_id: user_id.parse().ok()?, access_token: String::from_utf8(access_token).ok()?, }) } -fn write_credentials_to_keychain(credentials: &Credentials, cx: &AsyncAppContext) -> Result<()> { - cx.platform().write_credentials( - &ZED_SERVER_URL, - &credentials.user_id.to_string(), - credentials.access_token.as_bytes(), - ) +async fn write_credentials_to_keychain( + credentials: Credentials, + cx: &AsyncAppContext, +) -> Result<()> { + cx.update(move |cx| { + cx.write_credentials( + &ZED_SERVER_URL, + &credentials.user_id.to_string(), + credentials.access_token.as_bytes(), + ) + })? +} + +async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> { + cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))? } const WORKTREE_URL_PREFIX: &str = "zed://worktrees/"; @@ -1446,15 +1409,16 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { mod tests { use super::*; use crate::test::FakeServer; - use gpui::{executor::Deterministic, TestAppContext}; + + use gpui::{BackgroundExecutor, Context, TestAppContext}; use parking_lot::Mutex; + use settings::SettingsStore; use std::future; use util::http::FakeHttpClient; #[gpui::test(iterations = 10)] async fn test_reconnection(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; @@ -1470,7 +1434,7 @@ mod tests { while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} server.allow_connections(); - cx.foreground().advance_clock(Duration::from_secs(10)); + cx.executor().advance_clock(Duration::from_secs(10)); while !matches!(status.next().await, Some(Status::Connected { .. })) {} assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting @@ -1481,22 +1445,22 @@ mod tests { // Clear cached credentials after authentication fails server.roll_access_token(); server.allow_connections(); - cx.foreground().advance_clock(Duration::from_secs(10)); + cx.executor().run_until_parked(); + cx.executor().advance_clock(Duration::from_secs(10)); while !matches!(status.next().await, Some(Status::Connected { .. })) {} assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } #[gpui::test(iterations = 10)] - async fn test_connection_timeout(deterministic: Arc, cx: &mut TestAppContext) { - deterministic.forbid_parking(); - + async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let mut status = client.status(); // Time out when client tries to connect. client.override_authenticate(move |cx| { - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { Ok(Credentials { user_id, access_token: "token".into(), @@ -1504,7 +1468,7 @@ mod tests { }) }); client.override_establish_connection(|_, cx| { - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { future::pending::<()>().await; unreachable!() }) @@ -1513,10 +1477,10 @@ mod tests { let client = client.clone(); |cx| async move { client.authenticate_and_connect(false, &cx).await } }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(matches!(status.next().await, Some(Status::Connecting))); - deterministic.advance_clock(CONNECTION_TIMEOUT); + executor.advance_clock(CONNECTION_TIMEOUT); assert!(matches!( status.next().await, Some(Status::ConnectionError { .. }) @@ -1538,18 +1502,18 @@ mod tests { // Time out when re-establishing the connection. server.allow_connections(); client.override_establish_connection(|_, cx| { - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { future::pending::<()>().await; unreachable!() }) }); - deterministic.advance_clock(2 * INITIAL_RECONNECTION_DELAY); + executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY); assert!(matches!( status.next().await, Some(Status::Reconnecting { .. }) )); - deterministic.advance_clock(CONNECTION_TIMEOUT); + executor.advance_clock(CONNECTION_TIMEOUT); assert!(matches!( status.next().await, Some(Status::ReconnectionError { .. }) @@ -1559,10 +1523,9 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_authenticating_more_than_once( cx: &mut TestAppContext, - deterministic: Arc, + executor: BackgroundExecutor, ) { - cx.foreground().forbid_parking(); - + init_test(cx); let auth_count = Arc::new(Mutex::new(0)); let dropped_auth_count = Arc::new(Mutex::new(0)); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); @@ -1572,7 +1535,7 @@ mod tests { move |cx| { let auth_count = auth_count.clone(); let dropped_auth_count = dropped_auth_count.clone(); - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { *auth_count.lock() += 1; let _drop = util::defer(move || *dropped_auth_count.lock() += 1); future::pending::<()>().await; @@ -1581,19 +1544,19 @@ mod tests { } }); - let _authenticate = cx.spawn(|cx| { + let _authenticate = cx.spawn({ let client = client.clone(); - async move { client.authenticate_and_connect(false, &cx).await } + move |cx| async move { client.authenticate_and_connect(false, &cx).await } }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(*auth_count.lock(), 1); assert_eq!(*dropped_auth_count.lock(), 0); - let _authenticate = cx.spawn(|cx| { + let _authenticate = cx.spawn({ let client = client.clone(); - async move { client.authenticate_and_connect(false, &cx).await } + |cx| async move { client.authenticate_and_connect(false, &cx).await } }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(*auth_count.lock(), 2); assert_eq!(*dropped_auth_count.lock(), 1); } @@ -1611,8 +1574,7 @@ mod tests { #[gpui::test] async fn test_subscribing_to_entity(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; @@ -1620,8 +1582,8 @@ mod tests { let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded(); client.add_model_message_handler( - move |model: ModelHandle, _: TypedEnvelope, _, cx| { - match model.read_with(&cx, |model, _| model.id) { + move |model: Model, _: TypedEnvelope, _, mut cx| { + match model.update(&mut cx, |model, _| model.id).unwrap() { 1 => done_tx1.try_send(()).unwrap(), 2 => done_tx2.try_send(()).unwrap(), _ => unreachable!(), @@ -1629,15 +1591,15 @@ mod tests { async { Ok(()) } }, ); - let model1 = cx.add_model(|_| Model { + let model1 = cx.new_model(|_| TestModel { id: 1, subscription: None, }); - let model2 = cx.add_model(|_| Model { + let model2 = cx.new_model(|_| TestModel { id: 2, subscription: None, }); - let model3 = cx.add_model(|_| Model { + let model3 = cx.new_model(|_| TestModel { id: 3, subscription: None, }); @@ -1666,17 +1628,16 @@ mod tests { #[gpui::test] async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; - let model = cx.add_model(|_| Model::default()); + let model = cx.new_model(|_| TestModel::default()); let (done_tx1, _done_rx1) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded(); let subscription1 = client.add_message_handler( - model.clone(), + model.downgrade(), move |_, _: TypedEnvelope, _, _| { done_tx1.try_send(()).unwrap(); async { Ok(()) } @@ -1684,7 +1645,7 @@ mod tests { ); drop(subscription1); let _subscription2 = client.add_message_handler( - model.clone(), + model.downgrade(), move |_, _: TypedEnvelope, _, _| { done_tx2.try_send(()).unwrap(); async { Ok(()) } @@ -1696,18 +1657,19 @@ mod tests { #[gpui::test] async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; - let model = cx.add_model(|_| Model::default()); + let model = cx.new_model(|_| TestModel::default()); let (done_tx, mut done_rx) = smol::channel::unbounded(); let subscription = client.add_message_handler( - model.clone(), - move |model, _: TypedEnvelope, _, mut cx| { - model.update(&mut cx, |model, _| model.subscription.take()); + model.clone().downgrade(), + move |model: Model, _: TypedEnvelope, _, mut cx| { + model + .update(&mut cx, |model, _| model.subscription.take()) + .unwrap(); done_tx.try_send(()).unwrap(); async { Ok(()) } }, @@ -1720,12 +1682,15 @@ mod tests { } #[derive(Default)] - struct Model { + struct TestModel { id: usize, subscription: Option, } - impl Entity for Model { - type Event = (); + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + }); } } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index a3e7449cf8..26b5748187 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,9 +1,12 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use chrono::{DateTime, Utc}; -use gpui::{executor::Background, serde_json, AppContext, Task}; +use futures::Future; +use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; +use serde_json; +use settings::{Settings, SettingsStore}; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{ CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, @@ -14,53 +17,50 @@ use util::{channel::ReleaseChannel, TryFutureExt}; pub struct Telemetry { http_client: Arc, - executor: Arc, - state: Mutex, + executor: BackgroundExecutor, + state: Arc>, } -#[derive(Default)] struct TelemetryState { + settings: TelemetrySettings, metrics_id: Option>, // Per logged-in user installation_id: Option>, // Per app installation (different for dev, nightly, preview, and stable) session_id: Option>, // Per app launch - app_version: Option>, release_channel: Option<&'static str>, - os_name: &'static str, - os_version: Option>, + app_metadata: AppMetadata, architecture: &'static str, - clickhouse_events_queue: Vec, - flush_clickhouse_events_task: Option>, + events_queue: Vec, + flush_events_task: Option>, log_file: Option, is_staff: Option, first_event_datetime: Option>, } -const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; +const EVENTS_URL_PATH: &'static str = "/api/events"; lazy_static! { - static ref CLICKHOUSE_EVENTS_URL: String = - format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH); + static ref EVENTS_URL: String = format!("{}{}", *ZED_SERVER_URL, EVENTS_URL_PATH); } #[derive(Serialize, Debug)] -struct ClickhouseEventRequestBody { +struct EventRequestBody { token: &'static str, installation_id: Option>, session_id: Option>, is_staff: Option, - app_version: Option>, + app_version: Option, os_name: &'static str, - os_version: Option>, + os_version: Option, architecture: &'static str, release_channel: Option<&'static str>, - events: Vec, + events: Vec, } #[derive(Serialize, Debug)] -struct ClickhouseEventWrapper { +struct EventWrapper { signed_in: bool, #[serde(flatten)] - event: ClickhouseEvent, + event: Event, } #[derive(Serialize, Debug)] @@ -72,7 +72,7 @@ pub enum AssistantKind { #[derive(Serialize, Debug)] #[serde(tag = "type")] -pub enum ClickhouseEvent { +pub enum Event { Editor { operation: &'static str, file_extension: Option, @@ -113,52 +113,91 @@ pub enum ClickhouseEvent { operation: &'static str, milliseconds_since_first_event: i64, }, + Setting { + setting: &'static str, + value: String, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] const MAX_QUEUE_LEN: usize = 1; #[cfg(not(debug_assertions))] -const MAX_QUEUE_LEN: usize = 10; +const MAX_QUEUE_LEN: usize = 50; #[cfg(debug_assertions)] const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); #[cfg(not(debug_assertions))] -const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); +const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5); impl Telemetry { - pub fn new(client: Arc, cx: &AppContext) -> Arc { - let platform = cx.platform(); + pub fn new(client: Arc, cx: &mut AppContext) -> Arc { let release_channel = if cx.has_global::() { Some(cx.global::().display_name()) } else { None }; + + TelemetrySettings::register(cx); + + let state = Arc::new(Mutex::new(TelemetryState { + settings: TelemetrySettings::get_global(cx).clone(), + app_metadata: cx.app_metadata(), + architecture: env::consts::ARCH, + release_channel, + installation_id: None, + metrics_id: None, + session_id: None, + events_queue: Default::default(), + flush_events_task: Default::default(), + log_file: None, + is_staff: None, + first_event_datetime: None, + })); + + cx.observe_global::({ + let state = state.clone(); + + move |cx| { + let mut state = state.lock(); + state.settings = TelemetrySettings::get_global(cx).clone(); + } + }) + .detach(); + // TODO: Replace all hardware stuff with nested SystemSpecs json let this = Arc::new(Self { http_client: client, - executor: cx.background().clone(), - state: Mutex::new(TelemetryState { - os_name: platform.os_name().into(), - os_version: platform.os_version().ok().map(|v| v.to_string().into()), - architecture: env::consts::ARCH, - app_version: platform.app_version().ok().map(|v| v.to_string().into()), - release_channel, - installation_id: None, - metrics_id: None, - session_id: None, - clickhouse_events_queue: Default::default(), - flush_clickhouse_events_task: Default::default(), - log_file: None, - is_staff: None, - first_event_datetime: None, - }), + executor: cx.background_executor().clone(), + state, }); + // We should only ever have one instance of Telemetry, leak the subscription to keep it alive + // rather than store in TelemetryState, complicating spawn as subscriptions are not Send + std::mem::forget(cx.on_app_quit({ + let this = this.clone(); + move |_| this.shutdown_telemetry() + })); + this } + #[cfg(any(test, feature = "test-support"))] + fn shutdown_telemetry(self: &Arc) -> impl Future { + Task::ready(()) + } + + // Skip calling this function in tests. + // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings + #[cfg(not(any(test, feature = "test-support")))] + fn shutdown_telemetry(self: &Arc) -> impl Future { + self.report_app_event("close"); + self.flush_events(); + Task::ready(()) + } + pub fn log_file_path(&self) -> Option { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } @@ -175,7 +214,7 @@ impl Telemetry { drop(state); let this = self.clone(); - cx.spawn(|mut cx| async move { + cx.spawn(|_| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it let refresh_kind = RefreshKind::new() .with_memory() // For memory usage @@ -187,10 +226,11 @@ impl Telemetry { // Avoiding calling `refresh_all()`, just update what we need system.refresh_specifics(refresh_kind); + // 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(4 * 60); + 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_specifics(refresh_kind); @@ -203,18 +243,8 @@ impl Telemetry { return; }; - let telemetry_settings = cx.update(|cx| *settings::get::(cx)); - - this.report_memory_event( - telemetry_settings, - process.memory(), - process.virtual_memory(), - ); - this.report_cpu_event( - telemetry_settings, - process.cpu_usage(), - system.cpus().len() as u32, - ); + this.report_memory_event(process.memory(), process.virtual_memory()); + this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32); } }) .detach(); @@ -224,13 +254,13 @@ impl Telemetry { self: &Arc, metrics_id: Option, is_staff: bool, - cx: &AppContext, ) { - if !settings::get::(cx).metrics { + let mut state = self.state.lock(); + + if !state.settings.metrics { return; } - let mut state = self.state.lock(); let metrics_id: Option> = metrics_id.map(|id| id.into()); state.metrics_id = metrics_id.clone(); state.is_staff = Some(is_staff); @@ -239,14 +269,13 @@ impl Telemetry { pub fn report_editor_event( self: &Arc, - telemetry_settings: TelemetrySettings, file_extension: Option, vim_mode: bool, operation: &'static str, copilot_enabled: bool, copilot_enabled_for_language: bool, ) { - let event = ClickhouseEvent::Editor { + let event = Event::Editor { file_extension, vim_mode, operation, @@ -255,102 +284,98 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_event(event) } pub fn report_copilot_event( self: &Arc, - telemetry_settings: TelemetrySettings, suggestion_id: Option, suggestion_accepted: bool, file_extension: Option, ) { - let event = ClickhouseEvent::Copilot { + let event = Event::Copilot { suggestion_id, suggestion_accepted, file_extension, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_event(event) } pub fn report_assistant_event( self: &Arc, - telemetry_settings: TelemetrySettings, conversation_id: Option, kind: AssistantKind, model: &'static str, ) { - let event = ClickhouseEvent::Assistant { + let event = Event::Assistant { conversation_id, kind, model, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_event(event) } pub fn report_call_event( self: &Arc, - telemetry_settings: TelemetrySettings, operation: &'static str, room_id: Option, channel_id: Option, ) { - let event = ClickhouseEvent::Call { + let event = Event::Call { operation, room_id, channel_id, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_event(event) } - pub fn report_cpu_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - usage_as_percentage: f32, - core_count: u32, - ) { - let event = ClickhouseEvent::Cpu { + pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { + let event = Event::Cpu { usage_as_percentage, core_count, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_event(event) } pub fn report_memory_event( self: &Arc, - telemetry_settings: TelemetrySettings, memory_in_bytes: u64, virtual_memory_in_bytes: u64, ) { - let event = ClickhouseEvent::Memory { + let event = Event::Memory { memory_in_bytes, virtual_memory_in_bytes, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, false) + self.report_event(event) } - // app_events are called at app open and app close, so flush is set to immediately send - pub fn report_app_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - operation: &'static str, - ) { - let event = ClickhouseEvent::App { + pub fn report_app_event(self: &Arc, operation: &'static str) { + let event = Event::App { operation, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings, true) + self.report_event(event) + } + + pub fn report_setting_event(self: &Arc, setting: &'static str, value: String) { + let event = Event::Setting { + setting, + value, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_event(event) } fn milliseconds_since_first_event(&self) -> i64 { @@ -367,32 +392,26 @@ impl Telemetry { } } - fn report_clickhouse_event( - self: &Arc, - event: ClickhouseEvent, - telemetry_settings: TelemetrySettings, - immediate_flush: bool, - ) { - if !telemetry_settings.metrics { + fn report_event(self: &Arc, event: Event) { + let mut state = self.state.lock(); + + if !state.settings.metrics { return; } - let mut state = self.state.lock(); let signed_in = state.metrics_id.is_some(); - state - .clickhouse_events_queue - .push(ClickhouseEventWrapper { signed_in, event }); + state.events_queue.push(EventWrapper { signed_in, event }); if state.installation_id.is_some() { - if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if state.events_queue.len() >= MAX_QUEUE_LEN { drop(state); - self.flush_clickhouse_events(); + self.flush_events(); } else { let this = self.clone(); let executor = self.executor.clone(); - state.flush_clickhouse_events_task = Some(self.executor.spawn(async move { + state.flush_events_task = Some(self.executor.spawn(async move { executor.timer(DEBOUNCE_INTERVAL).await; - this.flush_clickhouse_events(); + this.flush_events(); })); } } @@ -410,11 +429,11 @@ impl Telemetry { self.state.lock().is_staff } - fn flush_clickhouse_events(self: &Arc) { + pub fn flush_events(self: &Arc) { let mut state = self.state.lock(); state.first_event_datetime = None; - let mut events = mem::take(&mut state.clickhouse_events_queue); - state.flush_clickhouse_events_task.take(); + let mut events = mem::take(&mut state.events_queue); + state.flush_events_task.take(); drop(state); let this = self.clone(); @@ -435,14 +454,20 @@ impl Telemetry { { let state = this.state.lock(); - let request_body = ClickhouseEventRequestBody { + let request_body = EventRequestBody { 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, - os_version: state.os_version.clone(), + app_version: state + .app_metadata + .app_version + .map(|version| version.to_string()), + os_name: state.app_metadata.os_name, + os_version: state + .app_metadata + .os_version + .map(|version| version.to_string()), architecture: state.architecture, release_channel: state.release_channel, @@ -453,7 +478,7 @@ impl Telemetry { } this.http_client - .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into()) + .post_json(EVENTS_URL.as_str(), json_bytes.into()) .await?; anyhow::Ok(()) } diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 38cd12f21c..9338e8cb91 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -1,20 +1,19 @@ use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; use anyhow::{anyhow, Result}; use futures::{stream::BoxStream, StreamExt}; -use gpui::{executor, ModelHandle, TestAppContext}; +use gpui::{BackgroundExecutor, Context, Model, TestAppContext}; use parking_lot::Mutex; use rpc::{ proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, ConnectionId, Peer, Receipt, TypedEnvelope, }; -use std::{rc::Rc, sync::Arc}; -use util::http::FakeHttpClient; +use std::sync::Arc; pub struct FakeServer { peer: Arc, state: Arc>, user_id: u64, - executor: Rc, + executor: BackgroundExecutor, } #[derive(Default)] @@ -36,7 +35,7 @@ impl FakeServer { peer: Peer::new(0), state: Default::default(), user_id: client_user_id, - executor: cx.foreground(), + executor: cx.executor(), }; client @@ -78,10 +77,11 @@ impl FakeServer { Err(EstablishConnectionError::Unauthorized)? } - let (client_conn, server_conn, _) = Connection::in_memory(cx.background()); + let (client_conn, server_conn, _) = + Connection::in_memory(cx.background_executor().clone()); let (connection_id, io, incoming) = - peer.add_test_connection(server_conn, cx.background()); - cx.background().spawn(io).detach(); + peer.add_test_connection(server_conn, cx.background_executor().clone()); + cx.background_executor().spawn(io).detach(); { let mut state = state.lock(); state.connection_id = Some(connection_id); @@ -193,9 +193,8 @@ impl FakeServer { &self, client: Arc, cx: &mut TestAppContext, - ) -> ModelHandle { - let http_client = FakeHttpClient::with_404_response(); - let user_store = cx.add_model(|cx| UserStore::new(client, http_client, cx)); + ) -> Model { + let user_store = cx.new_model(|cx| UserStore::new(client, cx)); assert_eq!( self.receive::() .await diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 8299b7c6e4..4453bb40ea 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -2,13 +2,12 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use feature_flags::FeatureFlagAppExt; -use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; -use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; +use futures::{channel::mpsc, Future, StreamExt}; +use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, 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; @@ -20,7 +19,7 @@ pub struct ParticipantIndex(pub u32); pub struct User { pub id: UserId, pub github_login: String, - pub avatar: Option>, + pub avatar_uri: SharedUrl, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -76,9 +75,8 @@ pub struct UserStore { pending_contact_requests: HashMap, invite_info: Option, client: Weak, - http: Arc, _maintain_contacts: Task<()>, - _maintain_current_user: Task<()>, + _maintain_current_user: Task>, } #[derive(Clone)] @@ -103,9 +101,7 @@ pub enum ContactEventKind { Cancelled, } -impl Entity for UserStore { - type Event = Event; -} +impl EventEmitter for UserStore {} enum UpdateContacts { Update(proto::UpdateContacts), @@ -114,17 +110,13 @@ enum UpdateContacts { } impl UserStore { - pub fn new( - client: Arc, - http: Arc, - cx: &mut ModelContext, - ) -> Self { + pub fn new(client: Arc, cx: &mut ModelContext) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ - client.add_message_handler(cx.handle(), Self::handle_update_contacts), - client.add_message_handler(cx.handle(), Self::handle_update_invite_info), - client.add_message_handler(cx.handle(), Self::handle_show_contacts), + client.add_message_handler(cx.weak_model(), Self::handle_update_contacts), + client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info), + client.add_message_handler(cx.weak_model(), Self::handle_show_contacts), ]; Self { users: Default::default(), @@ -136,76 +128,70 @@ impl UserStore { invite_info: None, client: Arc::downgrade(&client), update_contacts_tx, - http, - _maintain_contacts: cx.spawn_weak(|this, mut cx| async move { + _maintain_contacts: cx.spawn(|this, mut cx| async move { let _subscriptions = rpc_subscriptions; while let Some(message) = update_contacts_rx.next().await { - if let Some(this) = this.upgrade(&cx) { + if let Ok(task) = this.update(&mut cx, |this, cx| this.update_contacts(message, cx)) - .log_err() - .await; + { + task.log_err().await; + } else { + break; } } }), - _maintain_current_user: cx.spawn_weak(|this, mut cx| async move { + _maintain_current_user: cx.spawn(|this, mut cx| async move { let mut status = client.status(); while let Some(status) = status.next().await { match status { Status::Connected { .. } => { - if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { - let fetch_user = this - .update(&mut cx, |this, cx| this.get_user(user_id, cx)) - .log_err(); + if let Some(user_id) = client.user_id() { + let fetch_user = if let Ok(fetch_user) = this + .update(&mut cx, |this, cx| { + this.get_user(user_id, cx).log_err() + }) { + fetch_user + } else { + break; + }; let fetch_metrics_id = client.request(proto::GetPrivateUserInfo {}).log_err(); let (user, info) = futures::join!(fetch_user, fetch_metrics_id); - if let Some(info) = info { - cx.update(|cx| { + cx.update(|cx| { + if let Some(info) = info { cx.update_flags(info.staff, info.flags); client.telemetry.set_authenticated_user_info( Some(info.metrics_id.clone()), info.staff, - cx, ) - }); - } else { - cx.read(|cx| { - client - .telemetry - .set_authenticated_user_info(None, false, cx) - }); - } + } + })?; current_user_tx.send(user).await.ok(); - this.update(&mut cx, |_, cx| { - cx.notify(); - }); + this.update(&mut cx, |_, cx| cx.notify())?; } } Status::SignedOut => { current_user_tx.send(None).await.ok(); - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - cx.notify(); - this.clear_contacts() - }) - .await; - } + this.update(&mut cx, |this, cx| { + cx.notify(); + this.clear_contacts() + })? + .await; } Status::ConnectionLost => { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - cx.notify(); - this.clear_contacts() - }) - .await; - } + this.update(&mut cx, |this, cx| { + cx.notify(); + this.clear_contacts() + })? + .await; } _ => {} } } + Ok(()) }), pending_contact_requests: Default::default(), } @@ -217,7 +203,7 @@ impl UserStore { } async fn handle_update_invite_info( - this: ModelHandle, + this: Model, message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -228,17 +214,17 @@ impl UserStore { count: message.payload.count, }); cx.notify(); - }); + })?; Ok(()) } async fn handle_show_contacts( - this: ModelHandle, + this: Model, _: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts)); + this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?; Ok(()) } @@ -247,7 +233,7 @@ impl UserStore { } async fn handle_update_contacts( - this: ModelHandle, + this: Model, message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, @@ -256,7 +242,7 @@ impl UserStore { this.update_contacts_tx .unbounded_send(UpdateContacts::Update(message.payload)) .unwrap(); - }); + })?; Ok(()) } @@ -292,6 +278,9 @@ impl UserStore { // Users are fetched in parallel above and cached in call to get_users // No need to paralellize here let mut updated_contacts = Vec::new(); + let this = this + .upgrade() + .ok_or_else(|| anyhow!("can't upgrade user store handle"))?; for contact in message.contacts { updated_contacts.push(Arc::new( Contact::from_proto(contact, &this, &mut cx).await?, @@ -300,18 +289,18 @@ impl UserStore { let mut incoming_requests = Vec::new(); for request in message.incoming_requests { - incoming_requests.push( + incoming_requests.push({ this.update(&mut cx, |this, cx| { this.get_user(request.requester_id, cx) - }) - .await?, - ); + })? + .await? + }); } let mut outgoing_requests = Vec::new(); for requested_user_id in message.outgoing_requests { outgoing_requests.push( - this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx)) + this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))? .await?, ); } @@ -378,7 +367,7 @@ impl UserStore { } cx.notify(); - }); + })?; Ok(()) }) @@ -400,12 +389,6 @@ impl UserStore { &self.incoming_contact_requests } - pub fn has_incoming_contact_request(&self, user_id: u64) -> bool { - self.incoming_contact_requests - .iter() - .any(|user| user.id == user_id) - } - pub fn outgoing_contact_requests(&self) -> &[Arc] { &self.outgoing_contact_requests } @@ -454,6 +437,12 @@ impl UserStore { self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx) } + pub fn has_incoming_contact_request(&self, user_id: u64) -> bool { + self.incoming_contact_requests + .iter() + .any(|user| user.id == user_id) + } + pub fn respond_to_contact_request( &mut self, requester_id: u64, @@ -480,7 +469,7 @@ impl UserStore { cx: &mut ModelContext, ) -> Task> { let client = self.client.upgrade(); - cx.spawn_weak(|_, _| async move { + cx.spawn(move |_, _| async move { client .ok_or_else(|| anyhow!("can't upgrade client reference"))? .request(proto::RespondToContactRequest { @@ -502,7 +491,7 @@ impl UserStore { *self.pending_contact_requests.entry(user_id).or_insert(0) += 1; cx.notify(); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let response = client .ok_or_else(|| anyhow!("can't upgrade client reference"))? .request(request) @@ -517,7 +506,7 @@ impl UserStore { } } cx.notify(); - }); + })?; response?; Ok(()) }) @@ -560,11 +549,11 @@ impl UserStore { }, cx, ) - }) + })? .await?; } - this.read_with(&cx, |this, _| { + this.update(&mut cx, |this, _| { user_ids .iter() .map(|user_id| { @@ -574,7 +563,7 @@ impl UserStore { .ok_or_else(|| anyhow!("user {} not found", user_id)) }) .collect() - }) + })? }) } @@ -596,18 +585,18 @@ impl UserStore { cx: &mut ModelContext, ) -> Task>> { if let Some(user) = self.users.get(&user_id).cloned() { - return cx.foreground().spawn(async move { Ok(user) }); + return Task::ready(Ok(user)); } let load_users = self.get_users(vec![user_id], cx); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { load_users.await?; this.update(&mut cx, |this, _| { this.users .get(&user_id) .cloned() .ok_or_else(|| anyhow!("server responded with no users")) - }) + })? }) } @@ -625,25 +614,22 @@ impl UserStore { cx: &mut ModelContext, ) -> Task>>> { let client = self.client.clone(); - let http = self.http.clone(); - cx.spawn_weak(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { if let Some(rpc) = client.upgrade() { let response = rpc.request(request).await.context("error loading users")?; - let users = future::join_all( - response - .users - .into_iter() - .map(|user| User::new(user, http.as_ref())), - ) - .await; + let users = response + .users + .into_iter() + .map(|user| User::new(user)) + .collect::>(); + + this.update(&mut cx, |this, _| { + for user in &users { + this.users.insert(user.id, user.clone()); + } + }) + .ok(); - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - for user in &users { - this.users.insert(user.id, user.clone()); - } - }); - } Ok(users) } else { Ok(Vec::new()) @@ -668,11 +654,11 @@ impl UserStore { } impl User { - async fn new(message: proto::User, http: &dyn HttpClient) -> Arc { + fn new(message: proto::User) -> Arc { Arc::new(User { id: message.id, github_login: message.github_login, - avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await, + avatar_uri: message.avatar_url.into(), }) } } @@ -680,13 +666,13 @@ impl User { impl Contact { async fn from_proto( contact: proto::Contact, - user_store: &ModelHandle, + user_store: &Model, cx: &mut AsyncAppContext, ) -> Result { let user = user_store .update(cx, |user_store, cx| { user_store.get_user(contact.user_id, cx) - }) + })? .await?; Ok(Self { user, @@ -705,24 +691,3 @@ impl Collaborator { }) } } - -async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { - let mut response = http - .get(url, Default::default(), true) - .await - .map_err(|e| anyhow!("failed to send user avatar request: {}", e))?; - - if !response.status().is_success() { - return Err(anyhow!("avatar request failed {:?}", response.status())); - } - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?; - let format = image::guess_format(&body)?; - let image = image::load_from_memory_with_format(&body, format)?.into_bgra8(); - Ok(ImageData::new(image)) -} diff --git a/crates/client2/Cargo.toml b/crates/client2/Cargo.toml deleted file mode 100644 index b1c993e3a4..0000000000 --- a/crates/client2/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "client2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/client2.rs" -doctest = false - -[features] -test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] - -[dependencies] -chrono = { version = "0.4", features = ["serde"] } -collections = { path = "../collections" } -db = { package = "db2", path = "../db2" } -gpui = { package = "gpui2", path = "../gpui2" } -util = { path = "../util" } -rpc = { package = "rpc2", path = "../rpc2" } -text = { package = "text2", path = "../text2" } -settings = { package = "settings2", path = "../settings2" } -feature_flags = { package = "feature_flags2", path = "../feature_flags2" } -sum_tree = { path = "../sum_tree" } - -anyhow.workspace = true -async-recursion = "0.3" -async-tungstenite = { version = "0.16", features = ["async-tls"] } -futures.workspace = true -image = "0.23" -lazy_static.workspace = true -log.workspace = true -parking_lot.workspace = true -postage.workspace = true -rand.workspace = true -schemars.workspace = true -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.workspace = true -url = "2.2" - -[dev-dependencies] -collections = { path = "../collections", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } -settings = { package = "settings2", path = "../settings2", features = ["test-support"] } -util = { path = "../util", features = ["test-support"] } diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs deleted file mode 100644 index bf8d5dda77..0000000000 --- a/crates/client2/src/client2.rs +++ /dev/null @@ -1,1650 +0,0 @@ -#[cfg(any(test, feature = "test-support"))] -pub mod test; - -pub mod telemetry; -pub mod user; - -use anyhow::{anyhow, Context as _, Result}; -use async_recursion::async_recursion; -use async_tungstenite::tungstenite::{ - error::Error as WebsocketError, - http::{Request, StatusCode}, -}; -use futures::{ - future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, - TryStreamExt, -}; -use gpui::{ - actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, - SemanticVersion, Task, WeakModel, -}; -use lazy_static::lazy_static; -use parking_lot::RwLock; -use postage::watch; -use rand::prelude::*; -use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{ - any::TypeId, - collections::HashMap, - convert::TryFrom, - fmt::Write as _, - future::Future, - marker::PhantomData, - path::PathBuf, - sync::{atomic::AtomicU64, Arc, Weak}, - time::{Duration, Instant}, -}; -use telemetry::Telemetry; -use thiserror::Error; -use url::Url; -use util::channel::ReleaseChannel; -use util::http::HttpClient; -use util::{ResultExt, TryFutureExt}; - -pub use rpc::*; -pub use telemetry::ClickhouseEvent; -pub use user::*; - -lazy_static! { - pub static ref ZED_SERVER_URL: String = - std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string()); - pub static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") - .ok() - .and_then(|s| if s.is_empty() { None } else { Some(s) }); - pub static ref ADMIN_API_TOKEN: Option = std::env::var("ZED_ADMIN_API_TOKEN") - .ok() - .and_then(|s| if s.is_empty() { None } else { Some(s) }); - pub static ref ZED_APP_VERSION: Option = std::env::var("ZED_APP_VERSION") - .ok() - .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"; -pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); -pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); - -actions!(client, [SignIn, SignOut, Reconnect]); - -pub fn init_settings(cx: &mut AppContext) { - TelemetrySettings::register(cx); -} - -pub fn init(client: &Arc, cx: &mut AppContext) { - init_settings(cx); - - let client = Arc::downgrade(client); - cx.on_action({ - let client = client.clone(); - move |_: &SignIn, cx| { - if let Some(client) = client.upgrade() { - cx.spawn( - |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await }, - ) - .detach(); - } - } - }); - - cx.on_action({ - let client = client.clone(); - move |_: &SignOut, cx| { - if let Some(client) = client.upgrade() { - cx.spawn(|cx| async move { - client.disconnect(&cx); - }) - .detach(); - } - } - }); - - cx.on_action({ - let client = client.clone(); - move |_: &Reconnect, cx| { - if let Some(client) = client.upgrade() { - cx.spawn(|cx| async move { - client.reconnect(&cx); - }) - .detach(); - } - } - }); -} - -pub struct Client { - id: AtomicU64, - peer: Arc, - http: Arc, - telemetry: Arc, - state: RwLock, - - #[allow(clippy::type_complexity)] - #[cfg(any(test, feature = "test-support"))] - authenticate: RwLock< - Option Task>>>, - >, - - #[allow(clippy::type_complexity)] - #[cfg(any(test, feature = "test-support"))] - establish_connection: RwLock< - Option< - Box< - dyn 'static - + Send - + Sync - + Fn( - &Credentials, - &AsyncAppContext, - ) -> Task>, - >, - >, - >, -} - -#[derive(Error, Debug)] -pub enum EstablishConnectionError { - #[error("upgrade required")] - UpgradeRequired, - #[error("unauthorized")] - Unauthorized, - #[error("{0}")] - Other(#[from] anyhow::Error), - #[error("{0}")] - Http(#[from] util::http::Error), - #[error("{0}")] - Io(#[from] std::io::Error), - #[error("{0}")] - Websocket(#[from] async_tungstenite::tungstenite::http::Error), -} - -impl From for EstablishConnectionError { - fn from(error: WebsocketError) -> Self { - if let WebsocketError::Http(response) = &error { - match response.status() { - StatusCode::UNAUTHORIZED => return EstablishConnectionError::Unauthorized, - StatusCode::UPGRADE_REQUIRED => return EstablishConnectionError::UpgradeRequired, - _ => {} - } - } - EstablishConnectionError::Other(error.into()) - } -} - -impl EstablishConnectionError { - pub fn other(error: impl Into + Send + Sync) -> Self { - Self::Other(error.into()) - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum Status { - SignedOut, - UpgradeRequired, - Authenticating, - Connecting, - ConnectionError, - Connected { - peer_id: PeerId, - connection_id: ConnectionId, - }, - ConnectionLost, - Reauthenticating, - Reconnecting, - ReconnectionError { - next_reconnection: Instant, - }, -} - -impl Status { - pub fn is_connected(&self) -> bool { - matches!(self, Self::Connected { .. }) - } - - pub fn is_signed_out(&self) -> bool { - matches!(self, Self::SignedOut | Self::UpgradeRequired) - } -} - -struct ClientState { - credentials: Option, - status: (watch::Sender, watch::Receiver), - entity_id_extractors: HashMap u64>, - _reconnect_task: Option>, - reconnect_interval: Duration, - entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>, - models_by_message_type: HashMap, - entity_types_by_message_type: HashMap, - #[allow(clippy::type_complexity)] - message_handlers: HashMap< - TypeId, - Arc< - dyn Send - + Sync - + Fn( - AnyModel, - Box, - &Arc, - AsyncAppContext, - ) -> LocalBoxFuture<'static, Result<()>>, - >, - >, -} - -enum WeakSubscriber { - Entity { handle: AnyWeakModel }, - Pending(Vec>), -} - -#[derive(Clone, Debug)] -pub struct Credentials { - pub user_id: u64, - pub access_token: String, -} - -impl Default for ClientState { - fn default() -> Self { - Self { - credentials: None, - status: watch::channel_with(Status::SignedOut), - entity_id_extractors: Default::default(), - _reconnect_task: None, - reconnect_interval: Duration::from_secs(5), - models_by_message_type: Default::default(), - entities_by_type_and_remote_id: Default::default(), - entity_types_by_message_type: Default::default(), - message_handlers: Default::default(), - } - } -} - -pub enum Subscription { - Entity { - client: Weak, - id: (TypeId, u64), - }, - Message { - client: Weak, - id: TypeId, - }, -} - -impl Drop for Subscription { - fn drop(&mut self) { - match self { - Subscription::Entity { client, id } => { - if let Some(client) = client.upgrade() { - let mut state = client.state.write(); - let _ = state.entities_by_type_and_remote_id.remove(id); - } - } - Subscription::Message { client, id } => { - if let Some(client) = client.upgrade() { - let mut state = client.state.write(); - let _ = state.entity_types_by_message_type.remove(id); - let _ = state.message_handlers.remove(id); - } - } - } - } -} - -pub struct PendingEntitySubscription { - client: Arc, - remote_id: u64, - _entity_type: PhantomData, - consumed: bool, -} - -impl PendingEntitySubscription { - pub fn set_model(mut self, model: &Model, cx: &mut AsyncAppContext) -> Subscription { - self.consumed = true; - let mut state = self.client.state.write(); - let id = (TypeId::of::(), self.remote_id); - let Some(WeakSubscriber::Pending(messages)) = - state.entities_by_type_and_remote_id.remove(&id) - else { - unreachable!() - }; - - state.entities_by_type_and_remote_id.insert( - id, - WeakSubscriber::Entity { - handle: model.downgrade().into(), - }, - ); - drop(state); - for message in messages { - self.client.handle_message(message, cx); - } - Subscription::Entity { - client: Arc::downgrade(&self.client), - id, - } - } -} - -impl Drop for PendingEntitySubscription { - fn drop(&mut self) { - if !self.consumed { - let mut state = self.client.state.write(); - if let Some(WeakSubscriber::Pending(messages)) = state - .entities_by_type_and_remote_id - .remove(&(TypeId::of::(), self.remote_id)) - { - for message in messages { - log::info!("unhandled message {}", message.payload_type_name()); - } - } - } - } -} - -#[derive(Copy, Clone)] -pub struct TelemetrySettings { - pub diagnostics: bool, - pub metrics: bool, -} - -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] -pub struct TelemetrySettingsContent { - pub diagnostics: Option, - pub metrics: Option, -} - -impl settings::Settings for TelemetrySettings { - const KEY: Option<&'static str> = Some("telemetry"); - - type FileContent = TelemetrySettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &mut AppContext, - ) -> Result { - Ok(Self { - diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or( - default_value - .diagnostics - .ok_or_else(Self::missing_default)?, - ), - metrics: user_values - .first() - .and_then(|v| v.metrics) - .unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?), - }) - } -} - -impl Client { - pub fn new(http: Arc, cx: &mut AppContext) -> Arc { - Arc::new(Self { - id: AtomicU64::new(0), - peer: Peer::new(0), - telemetry: Telemetry::new(http.clone(), cx), - http, - state: Default::default(), - - #[cfg(any(test, feature = "test-support"))] - authenticate: Default::default(), - #[cfg(any(test, feature = "test-support"))] - establish_connection: Default::default(), - }) - } - - pub fn id(&self) -> u64 { - self.id.load(std::sync::atomic::Ordering::SeqCst) - } - - pub fn http_client(&self) -> Arc { - self.http.clone() - } - - pub fn set_id(&self, id: u64) -> &Self { - self.id.store(id, std::sync::atomic::Ordering::SeqCst); - self - } - - #[cfg(any(test, feature = "test-support"))] - pub fn teardown(&self) { - let mut state = self.state.write(); - state._reconnect_task.take(); - state.message_handlers.clear(); - state.models_by_message_type.clear(); - state.entities_by_type_and_remote_id.clear(); - state.entity_id_extractors.clear(); - self.peer.teardown(); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn override_authenticate(&self, authenticate: F) -> &Self - where - F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task>, - { - *self.authenticate.write() = Some(Box::new(authenticate)); - self - } - - #[cfg(any(test, feature = "test-support"))] - pub fn override_establish_connection(&self, connect: F) -> &Self - where - F: 'static - + Send - + Sync - + Fn(&Credentials, &AsyncAppContext) -> Task>, - { - *self.establish_connection.write() = Some(Box::new(connect)); - self - } - - pub fn user_id(&self) -> Option { - self.state - .read() - .credentials - .as_ref() - .map(|credentials| credentials.user_id) - } - - pub fn peer_id(&self) -> Option { - if let Status::Connected { peer_id, .. } = &*self.status().borrow() { - Some(*peer_id) - } else { - None - } - } - - pub fn status(&self) -> watch::Receiver { - self.state.read().status.1.clone() - } - - fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { - log::info!("set status on client {}: {:?}", self.id(), status); - let mut state = self.state.write(); - *state.status.0.borrow_mut() = status; - - match status { - Status::Connected { .. } => { - state._reconnect_task = None; - } - Status::ConnectionLost => { - let this = self.clone(); - let reconnect_interval = state.reconnect_interval; - state._reconnect_task = Some(cx.spawn(move |cx| async move { - #[cfg(any(test, feature = "test-support"))] - let mut rng = StdRng::seed_from_u64(0); - #[cfg(not(any(test, feature = "test-support")))] - let mut rng = StdRng::from_entropy(); - - let mut delay = INITIAL_RECONNECTION_DELAY; - while let Err(error) = this.authenticate_and_connect(true, &cx).await { - log::error!("failed to connect {}", error); - if matches!(*this.status().borrow(), Status::ConnectionError) { - this.set_status( - Status::ReconnectionError { - next_reconnection: Instant::now() + delay, - }, - &cx, - ); - cx.background_executor().timer(delay).await; - delay = delay - .mul_f32(rng.gen_range(1.0..=2.0)) - .min(reconnect_interval); - } else { - break; - } - } - })); - } - Status::SignedOut | Status::UpgradeRequired => { - cx.update(|cx| self.telemetry.set_authenticated_user_info(None, false, cx)) - .log_err(); - state._reconnect_task.take(); - } - _ => {} - } - } - - pub fn subscribe_to_entity( - self: &Arc, - remote_id: u64, - ) -> Result> - where - T: 'static, - { - let id = (TypeId::of::(), remote_id); - - let mut state = self.state.write(); - if state.entities_by_type_and_remote_id.contains_key(&id) { - return Err(anyhow!("already subscribed to entity")); - } else { - state - .entities_by_type_and_remote_id - .insert(id, WeakSubscriber::Pending(Default::default())); - Ok(PendingEntitySubscription { - client: self.clone(), - remote_id, - consumed: false, - _entity_type: PhantomData, - }) - } - } - - #[track_caller] - pub fn add_message_handler( - self: &Arc, - entity: WeakModel, - handler: H, - ) -> Subscription - where - M: EnvelopedMessage, - E: 'static, - H: 'static - + Sync - + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F - + Send - + Sync, - F: 'static + Future>, - { - let message_type_id = TypeId::of::(); - let mut state = self.state.write(); - state - .models_by_message_type - .insert(message_type_id, entity.into()); - - let prev_handler = state.message_handlers.insert( - message_type_id, - Arc::new(move |subscriber, envelope, client, cx| { - let subscriber = subscriber.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - handler(subscriber, *envelope, client.clone(), cx).boxed_local() - }), - ); - if prev_handler.is_some() { - let location = std::panic::Location::caller(); - panic!( - "{}:{} registered handler for the same message {} twice", - location.file(), - location.line(), - std::any::type_name::() - ); - } - - Subscription::Message { - client: Arc::downgrade(self), - id: message_type_id, - } - } - - pub fn add_request_handler( - self: &Arc, - model: WeakModel, - handler: H, - ) -> Subscription - where - M: RequestMessage, - E: 'static, - H: 'static - + Sync - + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F - + Send - + Sync, - F: 'static + Future>, - { - self.add_message_handler(model, move |handle, envelope, this, cx| { - Self::respond_to_request( - envelope.receipt(), - handler(handle, envelope, this.clone(), cx), - this, - ) - }) - } - - pub fn add_model_message_handler(self: &Arc, handler: H) - where - M: EntityMessage, - E: 'static, - H: 'static + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, - F: 'static + Future>, - { - self.add_entity_message_handler::(move |subscriber, message, client, cx| { - handler(subscriber.downcast::().unwrap(), message, client, cx) - }) - } - - fn add_entity_message_handler(self: &Arc, handler: H) - where - M: EntityMessage, - E: 'static, - H: 'static + Fn(AnyModel, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, - F: 'static + Future>, - { - let model_type_id = TypeId::of::(); - let message_type_id = TypeId::of::(); - - let mut state = self.state.write(); - state - .entity_types_by_message_type - .insert(message_type_id, model_type_id); - state - .entity_id_extractors - .entry(message_type_id) - .or_insert_with(|| { - |envelope| { - envelope - .as_any() - .downcast_ref::>() - .unwrap() - .payload - .remote_entity_id() - } - }); - let prev_handler = state.message_handlers.insert( - message_type_id, - Arc::new(move |handle, envelope, client, cx| { - let envelope = envelope.into_any().downcast::>().unwrap(); - handler(handle, *envelope, client.clone(), cx).boxed_local() - }), - ); - if prev_handler.is_some() { - panic!("registered handler for the same message twice"); - } - } - - pub fn add_model_request_handler(self: &Arc, handler: H) - where - M: EntityMessage + RequestMessage, - E: 'static, - H: 'static + Fn(Model, TypedEnvelope, Arc, AsyncAppContext) -> F + Send + Sync, - F: 'static + Future>, - { - self.add_model_message_handler(move |entity, envelope, client, cx| { - Self::respond_to_request::( - envelope.receipt(), - handler(entity, envelope, client.clone(), cx), - client, - ) - }) - } - - async fn respond_to_request>>( - receipt: Receipt, - response: F, - client: Arc, - ) -> Result<()> { - match response.await { - Ok(response) => { - client.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - client.respond_with_error( - receipt, - proto::Error { - message: format!("{:?}", error), - }, - )?; - Err(error) - } - } - } - - pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { - read_credentials_from_keychain(cx).is_some() - } - - #[async_recursion(?Send)] - pub async fn authenticate_and_connect( - self: &Arc, - try_keychain: bool, - cx: &AsyncAppContext, - ) -> anyhow::Result<()> { - let was_disconnected = match *self.status().borrow() { - Status::SignedOut => true, - Status::ConnectionError - | Status::ConnectionLost - | Status::Authenticating { .. } - | Status::Reauthenticating { .. } - | Status::ReconnectionError { .. } => false, - Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => { - return Ok(()) - } - Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?, - }; - - if was_disconnected { - self.set_status(Status::Authenticating, cx); - } else { - self.set_status(Status::Reauthenticating, cx) - } - - let mut read_from_keychain = false; - let mut credentials = self.state.read().credentials.clone(); - if credentials.is_none() && try_keychain { - credentials = read_credentials_from_keychain(cx); - read_from_keychain = credentials.is_some(); - } - if credentials.is_none() { - let mut status_rx = self.status(); - let _ = status_rx.next().await; - futures::select_biased! { - authenticate = self.authenticate(cx).fuse() => { - match authenticate { - Ok(creds) => credentials = Some(creds), - Err(err) => { - self.set_status(Status::ConnectionError, cx); - return Err(err); - } - } - } - _ = status_rx.next().fuse() => { - return Err(anyhow!("authentication canceled")); - } - } - } - let credentials = credentials.unwrap(); - self.set_id(credentials.user_id); - - if was_disconnected { - self.set_status(Status::Connecting, cx); - } else { - self.set_status(Status::Reconnecting, cx); - } - - let mut timeout = - futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT)); - futures::select_biased! { - connection = self.establish_connection(&credentials, cx).fuse() => { - match connection { - Ok(conn) => { - self.state.write().credentials = Some(credentials.clone()); - if !read_from_keychain && IMPERSONATE_LOGIN.is_none() { - write_credentials_to_keychain(credentials, cx).log_err(); - } - - futures::select_biased! { - result = self.set_connection(conn, cx).fuse() => result, - _ = timeout => { - self.set_status(Status::ConnectionError, cx); - Err(anyhow!("timed out waiting on hello message from server")) - } - } - } - Err(EstablishConnectionError::Unauthorized) => { - self.state.write().credentials.take(); - if read_from_keychain { - delete_credentials_from_keychain(cx).log_err(); - self.set_status(Status::SignedOut, cx); - self.authenticate_and_connect(false, cx).await - } else { - self.set_status(Status::ConnectionError, cx); - Err(EstablishConnectionError::Unauthorized)? - } - } - Err(EstablishConnectionError::UpgradeRequired) => { - self.set_status(Status::UpgradeRequired, cx); - Err(EstablishConnectionError::UpgradeRequired)? - } - Err(error) => { - self.set_status(Status::ConnectionError, cx); - Err(error)? - } - } - } - _ = &mut timeout => { - self.set_status(Status::ConnectionError, cx); - Err(anyhow!("timed out trying to establish connection")) - } - } - } - - async fn set_connection( - self: &Arc, - conn: Connection, - cx: &AsyncAppContext, - ) -> Result<()> { - let executor = cx.background_executor(); - log::info!("add connection to peer"); - let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn, { - let executor = executor.clone(); - move |duration| executor.timer(duration) - }); - let handle_io = executor.spawn(handle_io); - - let peer_id = async { - log::info!("waiting for server hello"); - let message = incoming - .next() - .await - .ok_or_else(|| anyhow!("no hello message received"))?; - log::info!("got server hello"); - let hello_message_type_name = message.payload_type_name().to_string(); - let hello = message - .into_any() - .downcast::>() - .map_err(|_| { - anyhow!( - "invalid hello message received: {:?}", - hello_message_type_name - ) - })?; - let peer_id = hello - .payload - .peer_id - .ok_or_else(|| anyhow!("invalid peer id"))?; - Ok(peer_id) - }; - - let peer_id = match peer_id.await { - Ok(peer_id) => peer_id, - Err(error) => { - self.peer.disconnect(connection_id); - return Err(error); - } - }; - - log::info!( - "set status to connected (connection id: {:?}, peer id: {:?})", - connection_id, - peer_id - ); - self.set_status( - Status::Connected { - peer_id, - connection_id, - }, - cx, - ); - - cx.spawn({ - let this = self.clone(); - |cx| { - async move { - while let Some(message) = incoming.next().await { - this.handle_message(message, &cx); - // Don't starve the main thread when receiving lots of messages at once. - smol::future::yield_now().await; - } - } - } - }) - .detach(); - - cx.spawn({ - let this = self.clone(); - move |cx| async move { - match handle_io.await { - Ok(()) => { - if this.status().borrow().clone() - == (Status::Connected { - connection_id, - peer_id, - }) - { - this.set_status(Status::SignedOut, &cx); - } - } - Err(err) => { - log::error!("connection error: {:?}", err); - this.set_status(Status::ConnectionLost, &cx); - } - } - } - }) - .detach(); - - Ok(()) - } - - fn authenticate(self: &Arc, cx: &AsyncAppContext) -> Task> { - #[cfg(any(test, feature = "test-support"))] - if let Some(callback) = self.authenticate.read().as_ref() { - return callback(cx); - } - - self.authenticate_with_browser(cx) - } - - fn establish_connection( - self: &Arc, - credentials: &Credentials, - cx: &AsyncAppContext, - ) -> Task> { - #[cfg(any(test, feature = "test-support"))] - if let Some(callback) = self.establish_connection.read().as_ref() { - return callback(credentials, cx); - } - - self.establish_websocket_connection(credentials, cx) - } - - async fn get_rpc_url( - http: Arc, - release_channel: Option, - ) -> Result { - let mut url = format!("{}/rpc", *ZED_SERVER_URL); - if let Some(preview_param) = - release_channel.and_then(|channel| channel.release_query_param()) - { - url += "?"; - url += preview_param; - } - let response = http.get(&url, Default::default(), false).await?; - - // Normally, ZED_SERVER_URL is set to the URL of zed.dev website. - // The website's /rpc endpoint redirects to a collab server's /rpc endpoint, - // which requires authorization via an HTTP header. - // - // For testing purposes, ZED_SERVER_URL can also set to the direct URL of - // of a collab server. In that case, a request to the /rpc endpoint will - // return an 'unauthorized' response. - let collab_url = if response.status().is_redirection() { - response - .headers() - .get("Location") - .ok_or_else(|| anyhow!("missing location header in /rpc response"))? - .to_str() - .map_err(EstablishConnectionError::other)? - .to_string() - } else if response.status() == StatusCode::UNAUTHORIZED { - url - } else { - Err(anyhow!( - "unexpected /rpc response status {}", - response.status() - ))? - }; - - Url::parse(&collab_url).context("invalid rpc url") - } - - fn establish_websocket_connection( - self: &Arc, - credentials: &Credentials, - cx: &AsyncAppContext, - ) -> Task> { - let release_channel = cx.try_read_global(|channel: &ReleaseChannel, _| *channel); - - let request = Request::builder() - .header( - "Authorization", - format!("{} {}", credentials.user_id, credentials.access_token), - ) - .header("x-zed-protocol-version", rpc::PROTOCOL_VERSION); - - let http = self.http.clone(); - cx.background_executor().spawn(async move { - let mut rpc_url = Self::get_rpc_url(http, release_channel).await?; - let rpc_host = rpc_url - .host_str() - .zip(rpc_url.port_or_known_default()) - .ok_or_else(|| anyhow!("missing host in rpc url"))?; - let stream = smol::net::TcpStream::connect(rpc_host).await?; - - log::info!("connected to rpc endpoint {}", rpc_url); - - match rpc_url.scheme() { - "https" => { - rpc_url.set_scheme("wss").unwrap(); - let request = request.uri(rpc_url.as_str()).body(())?; - let (stream, _) = - async_tungstenite::async_tls::client_async_tls(request, stream).await?; - Ok(Connection::new( - stream - .map_err(|error| anyhow!(error)) - .sink_map_err(|error| anyhow!(error)), - )) - } - "http" => { - rpc_url.set_scheme("ws").unwrap(); - let request = request.uri(rpc_url.as_str()).body(())?; - let (stream, _) = async_tungstenite::client_async(request, stream).await?; - Ok(Connection::new( - stream - .map_err(|error| anyhow!(error)) - .sink_map_err(|error| anyhow!(error)), - )) - } - _ => Err(anyhow!("invalid rpc url: {}", rpc_url))?, - } - }) - } - - pub fn authenticate_with_browser( - self: &Arc, - cx: &AsyncAppContext, - ) -> Task> { - let http = self.http.clone(); - cx.spawn(|cx| async move { - // Generate a pair of asymmetric encryption keys. The public key will be used by the - // zed server to encrypt the user's access token, so that it can'be intercepted by - // any other app running on the user's device. - let (public_key, private_key) = - rpc::auth::keypair().expect("failed to generate keypair for auth"); - let public_key_string = - String::try_from(public_key).expect("failed to serialize public key for auth"); - - if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - return Self::authenticate_as_admin(http, login.clone(), token.clone()).await; - } - - // Start an HTTP server to receive the redirect from Zed's sign-in page. - let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); - let port = server.server_addr().port(); - - // Open the Zed sign-in page in the user's browser, with query parameters that indicate - // that the user is signing in from a Zed app running on the same device. - let mut url = format!( - "{}/native_app_signin?native_app_port={}&native_app_public_key={}", - *ZED_SERVER_URL, port, public_key_string - ); - - if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { - log::info!("impersonating user @{}", impersonate_login); - write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); - } - - cx.update(|cx| cx.open_url(&url))?; - - // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted - // access token from the query params. - // - // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a - // custom URL scheme instead of this local HTTP server. - let (user_id, access_token) = cx - .spawn(|_| async move { - for _ in 0..100 { - if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { - let path = req.url(); - let mut user_id = None; - let mut access_token = None; - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); - } - } - - let post_auth_url = - format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL); - req.respond( - tiny_http::Response::empty(302).with_header( - tiny_http::Header::from_bytes( - &b"Location"[..], - post_auth_url.as_bytes(), - ) - .unwrap(), - ), - ) - .context("failed to respond to login http request")?; - return Ok(( - user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?, - access_token - .ok_or_else(|| anyhow!("missing access_token parameter"))?, - )); - } - } - - Err(anyhow!("didn't receive login redirect")) - }) - .await?; - - let access_token = private_key - .decrypt_string(&access_token) - .context("failed to decrypt access token")?; - cx.update(|cx| cx.activate(true))?; - - Ok(Credentials { - user_id: user_id.parse()?, - access_token, - }) - }) - } - - async fn authenticate_as_admin( - http: Arc, - login: String, - mut api_token: String, - ) -> Result { - #[derive(Deserialize)] - struct AuthenticatedUserResponse { - user: User, - } - - #[derive(Deserialize)] - struct User { - id: u64, - } - - // Use the collab server's admin API to retrieve the id - // of the impersonated user. - let mut url = Self::get_rpc_url(http.clone(), None).await?; - url.set_path("/user"); - url.set_query(Some(&format!("github_login={login}"))); - let request = Request::get(url.as_str()) - .header("Authorization", format!("token {api_token}")) - .body("".into())?; - - let mut response = http.send(request).await?; - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - if !response.status().is_success() { - Err(anyhow!( - "admin user request failed {} - {}", - response.status().as_u16(), - body, - ))?; - } - let response: AuthenticatedUserResponse = serde_json::from_str(&body)?; - - // Use the admin API token to authenticate as the impersonated user. - api_token.insert_str(0, "ADMIN_TOKEN:"); - Ok(Credentials { - user_id: response.user.id, - access_token: api_token, - }) - } - - pub fn disconnect(self: &Arc, cx: &AsyncAppContext) { - self.peer.teardown(); - self.set_status(Status::SignedOut, cx); - } - - pub fn reconnect(self: &Arc, cx: &AsyncAppContext) { - self.peer.teardown(); - self.set_status(Status::ConnectionLost, cx); - } - - fn connection_id(&self) -> Result { - if let Status::Connected { connection_id, .. } = *self.status().borrow() { - Ok(connection_id) - } else { - Err(anyhow!("not connected")) - } - } - - pub fn send(&self, message: T) -> Result<()> { - log::debug!("rpc send. client_id:{}, name:{}", self.id(), T::NAME); - self.peer.send(self.connection_id()?, message) - } - - pub fn request( - &self, - request: T, - ) -> impl Future> { - self.request_envelope(request) - .map_ok(|envelope| envelope.payload) - } - - pub fn request_envelope( - &self, - request: T, - ) -> impl Future>> { - let client_id = self.id(); - log::debug!( - "rpc request start. client_id:{}. name:{}", - client_id, - T::NAME - ); - let response = self - .connection_id() - .map(|conn_id| self.peer.request_envelope(conn_id, request)); - async move { - let response = response?.await; - log::debug!( - "rpc request finish. client_id:{}. name:{}", - client_id, - T::NAME - ); - response - } - } - - fn respond(&self, receipt: Receipt, response: T::Response) -> Result<()> { - log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME); - self.peer.respond(receipt, response) - } - - fn respond_with_error( - &self, - receipt: Receipt, - error: proto::Error, - ) -> Result<()> { - log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME); - self.peer.respond_with_error(receipt, error) - } - - fn handle_message( - self: &Arc, - message: Box, - cx: &AsyncAppContext, - ) { - let mut state = self.state.write(); - let type_name = message.payload_type_name(); - let payload_type_id = message.payload_type_id(); - let sender_id = message.original_sender_id(); - - let mut subscriber = None; - - if let Some(handle) = state - .models_by_message_type - .get(&payload_type_id) - .and_then(|handle| handle.upgrade()) - { - subscriber = Some(handle); - } else if let Some((extract_entity_id, entity_type_id)) = - state.entity_id_extractors.get(&payload_type_id).zip( - state - .entity_types_by_message_type - .get(&payload_type_id) - .copied(), - ) - { - let entity_id = (extract_entity_id)(message.as_ref()); - - match state - .entities_by_type_and_remote_id - .get_mut(&(entity_type_id, entity_id)) - { - Some(WeakSubscriber::Pending(pending)) => { - pending.push(message); - return; - } - Some(weak_subscriber @ _) => match weak_subscriber { - WeakSubscriber::Entity { handle } => { - subscriber = handle.upgrade(); - } - - WeakSubscriber::Pending(_) => {} - }, - _ => {} - } - } - - let subscriber = if let Some(subscriber) = subscriber { - subscriber - } else { - log::info!("unhandled message {}", type_name); - self.peer.respond_with_unhandled_message(message).log_err(); - return; - }; - - let handler = state.message_handlers.get(&payload_type_id).cloned(); - // Dropping the state prevents deadlocks if the handler interacts with rpc::Client. - // It also ensures we don't hold the lock while yielding back to the executor, as - // that might cause the executor thread driving this future to block indefinitely. - drop(state); - - if let Some(handler) = handler { - let future = handler(subscriber, message, &self, cx.clone()); - let client_id = self.id(); - log::debug!( - "rpc message received. client_id:{}, sender_id:{:?}, type:{}", - client_id, - sender_id, - type_name - ); - cx.spawn(move |_| async move { - match future.await { - Ok(()) => { - log::debug!( - "rpc message handled. client_id:{}, sender_id:{:?}, type:{}", - client_id, - sender_id, - type_name - ); - } - Err(error) => { - log::error!( - "error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}", - client_id, - sender_id, - type_name, - error - ); - } - } - }) - .detach(); - } else { - log::info!("unhandled message {}", type_name); - self.peer.respond_with_unhandled_message(message).log_err(); - } - } - - pub fn telemetry(&self) -> &Arc { - &self.telemetry - } -} - -fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { - if IMPERSONATE_LOGIN.is_some() { - return None; - } - - let (user_id, access_token) = cx - .update(|cx| cx.read_credentials(&ZED_SERVER_URL).log_err().flatten()) - .ok()??; - - Some(Credentials { - user_id: user_id.parse().ok()?, - access_token: String::from_utf8(access_token).ok()?, - }) -} - -async fn write_credentials_to_keychain( - credentials: Credentials, - cx: &AsyncAppContext, -) -> Result<()> { - cx.update(move |cx| { - cx.write_credentials( - &ZED_SERVER_URL, - &credentials.user_id.to_string(), - credentials.access_token.as_bytes(), - ) - })? -} - -async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> { - cx.update(move |cx| cx.delete_credentials(&ZED_SERVER_URL))? -} - -const WORKTREE_URL_PREFIX: &str = "zed://worktrees/"; - -pub fn encode_worktree_url(id: u64, access_token: &str) -> String { - format!("{}{}/{}", WORKTREE_URL_PREFIX, id, access_token) -} - -pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { - let path = url.trim().strip_prefix(WORKTREE_URL_PREFIX)?; - let mut parts = path.split('/'); - let id = parts.next()?.parse::().ok()?; - let access_token = parts.next()?; - if access_token.is_empty() { - return None; - } - Some((id, access_token.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test::FakeServer; - - use gpui::{BackgroundExecutor, Context, TestAppContext}; - use parking_lot::Mutex; - use std::future; - use util::http::FakeHttpClient; - - #[gpui::test(iterations = 10)] - async fn test_reconnection(cx: &mut TestAppContext) { - let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - let server = FakeServer::for_client(user_id, &client, cx).await; - let mut status = client.status(); - assert!(matches!( - status.next().await, - Some(Status::Connected { .. }) - )); - assert_eq!(server.auth_count(), 1); - - server.forbid_connections(); - server.disconnect(); - while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} - - server.allow_connections(); - cx.executor().advance_clock(Duration::from_secs(10)); - while !matches!(status.next().await, Some(Status::Connected { .. })) {} - assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting - - server.forbid_connections(); - server.disconnect(); - while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} - - // Clear cached credentials after authentication fails - server.roll_access_token(); - server.allow_connections(); - cx.executor().run_until_parked(); - cx.executor().advance_clock(Duration::from_secs(10)); - while !matches!(status.next().await, Some(Status::Connected { .. })) {} - assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token - } - - #[gpui::test(iterations = 10)] - async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { - let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - let mut status = client.status(); - - // Time out when client tries to connect. - client.override_authenticate(move |cx| { - cx.background_executor().spawn(async move { - Ok(Credentials { - user_id, - access_token: "token".into(), - }) - }) - }); - client.override_establish_connection(|_, cx| { - cx.background_executor().spawn(async move { - future::pending::<()>().await; - unreachable!() - }) - }); - let auth_and_connect = cx.spawn({ - let client = client.clone(); - |cx| async move { client.authenticate_and_connect(false, &cx).await } - }); - executor.run_until_parked(); - assert!(matches!(status.next().await, Some(Status::Connecting))); - - executor.advance_clock(CONNECTION_TIMEOUT); - assert!(matches!( - status.next().await, - Some(Status::ConnectionError { .. }) - )); - auth_and_connect.await.unwrap_err(); - - // Allow the connection to be established. - let server = FakeServer::for_client(user_id, &client, cx).await; - assert!(matches!( - status.next().await, - Some(Status::Connected { .. }) - )); - - // Disconnect client. - server.forbid_connections(); - server.disconnect(); - while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} - - // Time out when re-establishing the connection. - server.allow_connections(); - client.override_establish_connection(|_, cx| { - cx.background_executor().spawn(async move { - future::pending::<()>().await; - unreachable!() - }) - }); - executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY); - assert!(matches!( - status.next().await, - Some(Status::Reconnecting { .. }) - )); - - executor.advance_clock(CONNECTION_TIMEOUT); - assert!(matches!( - status.next().await, - Some(Status::ReconnectionError { .. }) - )); - } - - #[gpui::test(iterations = 10)] - async fn test_authenticating_more_than_once( - cx: &mut TestAppContext, - executor: BackgroundExecutor, - ) { - let auth_count = Arc::new(Mutex::new(0)); - let dropped_auth_count = Arc::new(Mutex::new(0)); - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - client.override_authenticate({ - let auth_count = auth_count.clone(); - let dropped_auth_count = dropped_auth_count.clone(); - move |cx| { - let auth_count = auth_count.clone(); - let dropped_auth_count = dropped_auth_count.clone(); - cx.background_executor().spawn(async move { - *auth_count.lock() += 1; - let _drop = util::defer(move || *dropped_auth_count.lock() += 1); - future::pending::<()>().await; - unreachable!() - }) - } - }); - - let _authenticate = cx.spawn({ - let client = client.clone(); - move |cx| async move { client.authenticate_and_connect(false, &cx).await } - }); - executor.run_until_parked(); - assert_eq!(*auth_count.lock(), 1); - assert_eq!(*dropped_auth_count.lock(), 0); - - let _authenticate = cx.spawn({ - let client = client.clone(); - |cx| async move { client.authenticate_and_connect(false, &cx).await } - }); - executor.run_until_parked(); - assert_eq!(*auth_count.lock(), 2); - assert_eq!(*dropped_auth_count.lock(), 1); - } - - #[test] - fn test_encode_and_decode_worktree_url() { - let url = encode_worktree_url(5, "deadbeef"); - assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string()))); - assert_eq!( - decode_worktree_url(&format!("\n {}\t", url)), - Some((5, "deadbeef".to_string())) - ); - assert_eq!(decode_worktree_url("not://the-right-format"), None); - } - - #[gpui::test] - async fn test_subscribing_to_entity(cx: &mut TestAppContext) { - let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - let server = FakeServer::for_client(user_id, &client, cx).await; - - let (done_tx1, mut done_rx1) = smol::channel::unbounded(); - let (done_tx2, mut done_rx2) = smol::channel::unbounded(); - client.add_model_message_handler( - move |model: Model, _: TypedEnvelope, _, mut cx| { - match model.update(&mut cx, |model, _| model.id).unwrap() { - 1 => done_tx1.try_send(()).unwrap(), - 2 => done_tx2.try_send(()).unwrap(), - _ => unreachable!(), - } - async { Ok(()) } - }, - ); - let model1 = cx.build_model(|_| TestModel { - id: 1, - subscription: None, - }); - let model2 = cx.build_model(|_| TestModel { - id: 2, - subscription: None, - }); - let model3 = cx.build_model(|_| TestModel { - id: 3, - subscription: None, - }); - - let _subscription1 = client - .subscribe_to_entity(1) - .unwrap() - .set_model(&model1, &mut cx.to_async()); - let _subscription2 = client - .subscribe_to_entity(2) - .unwrap() - .set_model(&model2, &mut cx.to_async()); - // Ensure dropping a subscription for the same entity type still allows receiving of - // messages for other entity IDs of the same type. - let subscription3 = client - .subscribe_to_entity(3) - .unwrap() - .set_model(&model3, &mut cx.to_async()); - drop(subscription3); - - server.send(proto::JoinProject { project_id: 1 }); - server.send(proto::JoinProject { project_id: 2 }); - done_rx1.next().await.unwrap(); - done_rx2.next().await.unwrap(); - } - - #[gpui::test] - async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) { - let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - let server = FakeServer::for_client(user_id, &client, cx).await; - - let model = cx.build_model(|_| TestModel::default()); - let (done_tx1, _done_rx1) = smol::channel::unbounded(); - let (done_tx2, mut done_rx2) = smol::channel::unbounded(); - let subscription1 = client.add_message_handler( - model.downgrade(), - move |_, _: TypedEnvelope, _, _| { - done_tx1.try_send(()).unwrap(); - async { Ok(()) } - }, - ); - drop(subscription1); - let _subscription2 = client.add_message_handler( - model.downgrade(), - move |_, _: TypedEnvelope, _, _| { - done_tx2.try_send(()).unwrap(); - async { Ok(()) } - }, - ); - server.send(proto::Ping {}); - done_rx2.next().await.unwrap(); - } - - #[gpui::test] - async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) { - let user_id = 5; - let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - let server = FakeServer::for_client(user_id, &client, cx).await; - - let model = cx.build_model(|_| TestModel::default()); - let (done_tx, mut done_rx) = smol::channel::unbounded(); - let subscription = client.add_message_handler( - model.clone().downgrade(), - move |model: Model, _: TypedEnvelope, _, mut cx| { - model - .update(&mut cx, |model, _| model.subscription.take()) - .unwrap(); - done_tx.try_send(()).unwrap(); - async { Ok(()) } - }, - ); - model.update(cx, |model, _| { - model.subscription = Some(subscription); - }); - server.send(proto::Ping {}); - done_rx.next().await.unwrap(); - } - - #[derive(Default)] - struct TestModel { - id: usize, - subscription: Option, - } -} diff --git a/crates/client2/src/telemetry.rs b/crates/client2/src/telemetry.rs deleted file mode 100644 index b303e68183..0000000000 --- a/crates/client2/src/telemetry.rs +++ /dev/null @@ -1,494 +0,0 @@ -use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; -use chrono::{DateTime, Utc}; -use futures::Future; -use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use serde::Serialize; -use settings::Settings; -use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; -use sysinfo::{ - CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, -}; -use tempfile::NamedTempFile; -use util::http::HttpClient; -use util::{channel::ReleaseChannel, TryFutureExt}; - -pub struct Telemetry { - http_client: Arc, - executor: BackgroundExecutor, - state: Mutex, -} - -struct TelemetryState { - metrics_id: Option>, // Per logged-in user - installation_id: Option>, // Per app installation (different for dev, nightly, preview, and stable) - session_id: Option>, // Per app launch - release_channel: Option<&'static str>, - app_metadata: AppMetadata, - architecture: &'static str, - clickhouse_events_queue: Vec, - flush_clickhouse_events_task: Option>, - log_file: Option, - is_staff: Option, - first_event_datetime: Option>, -} - -const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; - -lazy_static! { - static ref CLICKHOUSE_EVENTS_URL: String = - format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH); -} - -#[derive(Serialize, Debug)] -struct ClickhouseEventRequestBody { - token: &'static str, - installation_id: Option>, - session_id: Option>, - is_staff: Option, - app_version: Option, - os_name: &'static str, - os_version: Option, - architecture: &'static str, - release_channel: Option<&'static str>, - events: Vec, -} - -#[derive(Serialize, Debug)] -struct ClickhouseEventWrapper { - signed_in: bool, - #[serde(flatten)] - event: ClickhouseEvent, -} - -#[derive(Serialize, Debug)] -#[serde(rename_all = "snake_case")] -pub enum AssistantKind { - Panel, - Inline, -} - -#[derive(Serialize, Debug)] -#[serde(tag = "type")] -pub enum ClickhouseEvent { - Editor { - operation: &'static str, - file_extension: Option, - vim_mode: bool, - copilot_enabled: bool, - copilot_enabled_for_language: bool, - milliseconds_since_first_event: i64, - }, - Copilot { - suggestion_id: Option, - suggestion_accepted: bool, - file_extension: Option, - milliseconds_since_first_event: i64, - }, - Call { - operation: &'static str, - room_id: Option, - channel_id: Option, - milliseconds_since_first_event: i64, - }, - Assistant { - conversation_id: Option, - kind: AssistantKind, - model: &'static str, - milliseconds_since_first_event: i64, - }, - Cpu { - usage_as_percentage: f32, - core_count: u32, - milliseconds_since_first_event: i64, - }, - Memory { - memory_in_bytes: u64, - virtual_memory_in_bytes: u64, - milliseconds_since_first_event: i64, - }, - App { - operation: &'static str, - milliseconds_since_first_event: i64, - }, -} - -#[cfg(debug_assertions)] -const MAX_QUEUE_LEN: usize = 1; - -#[cfg(not(debug_assertions))] -const MAX_QUEUE_LEN: usize = 10; - -#[cfg(debug_assertions)] -const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); - -#[cfg(not(debug_assertions))] -const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); - -impl Telemetry { - pub fn new(client: Arc, cx: &mut AppContext) -> Arc { - let release_channel = if cx.has_global::() { - Some(cx.global::().display_name()) - } else { - None - }; - - // TODO: Replace all hardware stuff with nested SystemSpecs json - let this = Arc::new(Self { - http_client: client, - executor: cx.background_executor().clone(), - state: Mutex::new(TelemetryState { - app_metadata: cx.app_metadata(), - architecture: env::consts::ARCH, - release_channel, - installation_id: None, - metrics_id: None, - session_id: None, - clickhouse_events_queue: Default::default(), - flush_clickhouse_events_task: Default::default(), - log_file: None, - is_staff: None, - first_event_datetime: None, - }), - }); - - // We should only ever have one instance of Telemetry, leak the subscription to keep it alive - // rather than store in TelemetryState, complicating spawn as subscriptions are not Send - std::mem::forget(cx.on_app_quit({ - let this = this.clone(); - move |cx| this.shutdown_telemetry(cx) - })); - - this - } - - #[cfg(any(test, feature = "test-support"))] - fn shutdown_telemetry(self: &Arc, _: &mut AppContext) -> impl Future { - Task::ready(()) - } - - // Skip calling this function in tests. - // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings - #[cfg(not(any(test, feature = "test-support")))] - fn shutdown_telemetry(self: &Arc, cx: &mut AppContext) -> impl Future { - let telemetry_settings = TelemetrySettings::get_global(cx).clone(); - self.report_app_event(telemetry_settings, "close"); - Task::ready(()) - } - - pub fn log_file_path(&self) -> Option { - Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) - } - - pub fn start( - self: &Arc, - installation_id: Option, - session_id: String, - cx: &mut AppContext, - ) { - let mut state = self.state.lock(); - state.installation_id = installation_id.map(|id| id.into()); - state.session_id = Some(session_id.into()); - drop(state); - - let this = self.clone(); - cx.spawn(|cx| async move { - // Avoiding calling `System::new_all()`, as there have been crashes related to it - let refresh_kind = RefreshKind::new() - .with_memory() // For memory usage - .with_processes(ProcessRefreshKind::everything()) // For process usage - .with_cpu(CpuRefreshKind::everything()); // For core count - - let mut system = System::new_with_specifics(refresh_kind); - - // Avoiding calling `refresh_all()`, just update what we need - system.refresh_specifics(refresh_kind); - - 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_specifics(refresh_kind); - - 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 telemetry_settings = if let Ok(telemetry_settings) = - cx.update(|cx| *TelemetrySettings::get_global(cx)) - { - telemetry_settings - } else { - break; - }; - - this.report_memory_event( - telemetry_settings, - process.memory(), - process.virtual_memory(), - ); - this.report_cpu_event( - telemetry_settings, - process.cpu_usage(), - system.cpus().len() as u32, - ); - } - }) - .detach(); - } - - pub fn set_authenticated_user_info( - self: &Arc, - metrics_id: Option, - is_staff: bool, - cx: &AppContext, - ) { - if !TelemetrySettings::get_global(cx).metrics { - return; - } - - let mut state = self.state.lock(); - let metrics_id: Option> = metrics_id.map(|id| id.into()); - state.metrics_id = metrics_id.clone(); - state.is_staff = Some(is_staff); - drop(state); - } - - pub fn report_editor_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - file_extension: Option, - vim_mode: bool, - operation: &'static str, - copilot_enabled: bool, - copilot_enabled_for_language: bool, - ) { - let event = ClickhouseEvent::Editor { - file_extension, - vim_mode, - operation, - copilot_enabled, - copilot_enabled_for_language, - milliseconds_since_first_event: self.milliseconds_since_first_event(), - }; - - self.report_clickhouse_event(event, telemetry_settings, false) - } - - pub fn report_copilot_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - suggestion_id: Option, - suggestion_accepted: bool, - file_extension: Option, - ) { - let event = ClickhouseEvent::Copilot { - suggestion_id, - suggestion_accepted, - file_extension, - milliseconds_since_first_event: self.milliseconds_since_first_event(), - }; - - self.report_clickhouse_event(event, telemetry_settings, false) - } - - pub fn report_assistant_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - conversation_id: Option, - kind: AssistantKind, - model: &'static str, - ) { - let event = ClickhouseEvent::Assistant { - conversation_id, - kind, - model, - milliseconds_since_first_event: self.milliseconds_since_first_event(), - }; - - self.report_clickhouse_event(event, telemetry_settings, false) - } - - pub fn report_call_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - operation: &'static str, - room_id: Option, - channel_id: Option, - ) { - let event = ClickhouseEvent::Call { - operation, - room_id, - channel_id, - milliseconds_since_first_event: self.milliseconds_since_first_event(), - }; - - self.report_clickhouse_event(event, telemetry_settings, false) - } - - pub fn report_cpu_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - usage_as_percentage: f32, - core_count: u32, - ) { - let event = ClickhouseEvent::Cpu { - usage_as_percentage, - core_count, - milliseconds_since_first_event: self.milliseconds_since_first_event(), - }; - - self.report_clickhouse_event(event, telemetry_settings, false) - } - - pub fn report_memory_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - memory_in_bytes: u64, - virtual_memory_in_bytes: u64, - ) { - let event = ClickhouseEvent::Memory { - memory_in_bytes, - virtual_memory_in_bytes, - milliseconds_since_first_event: self.milliseconds_since_first_event(), - }; - - self.report_clickhouse_event(event, telemetry_settings, false) - } - - // app_events are called at app open and app close, so flush is set to immediately send - pub fn report_app_event( - self: &Arc, - telemetry_settings: TelemetrySettings, - operation: &'static str, - ) { - let event = ClickhouseEvent::App { - operation, - milliseconds_since_first_event: self.milliseconds_since_first_event(), - }; - - self.report_clickhouse_event(event, telemetry_settings, true) - } - - fn milliseconds_since_first_event(&self) -> i64 { - let mut state = self.state.lock(); - match state.first_event_datetime { - Some(first_event_datetime) => { - let now: DateTime = Utc::now(); - now.timestamp_millis() - first_event_datetime.timestamp_millis() - } - None => { - state.first_event_datetime = Some(Utc::now()); - 0 - } - } - } - - fn report_clickhouse_event( - self: &Arc, - event: ClickhouseEvent, - telemetry_settings: TelemetrySettings, - immediate_flush: bool, - ) { - if !telemetry_settings.metrics { - return; - } - - let mut state = self.state.lock(); - let signed_in = state.metrics_id.is_some(); - state - .clickhouse_events_queue - .push(ClickhouseEventWrapper { signed_in, event }); - - if state.installation_id.is_some() { - if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { - drop(state); - self.flush_clickhouse_events(); - } else { - let this = self.clone(); - let executor = self.executor.clone(); - state.flush_clickhouse_events_task = Some(self.executor.spawn(async move { - executor.timer(DEBOUNCE_INTERVAL).await; - this.flush_clickhouse_events(); - })); - } - } - } - - pub fn metrics_id(self: &Arc) -> Option> { - self.state.lock().metrics_id.clone() - } - - pub fn installation_id(self: &Arc) -> Option> { - self.state.lock().installation_id.clone() - } - - pub fn is_staff(self: &Arc) -> Option { - self.state.lock().is_staff - } - - fn flush_clickhouse_events(self: &Arc) { - let mut state = self.state.lock(); - state.first_event_datetime = None; - let mut events = mem::take(&mut state.clickhouse_events_queue); - state.flush_clickhouse_events_task.take(); - drop(state); - - let this = self.clone(); - self.executor - .spawn( - async move { - let mut json_bytes = Vec::new(); - - if let Some(file) = &mut this.state.lock().log_file { - let file = file.as_file_mut(); - for event in &mut events { - json_bytes.clear(); - serde_json::to_writer(&mut json_bytes, event)?; - file.write_all(&json_bytes)?; - file.write(b"\n")?; - } - } - - { - let state = this.state.lock(); - let request_body = 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_metadata - .app_version - .map(|version| version.to_string()), - os_name: state.app_metadata.os_name, - os_version: state - .app_metadata - .os_version - .map(|version| version.to_string()), - architecture: state.architecture, - - release_channel: state.release_channel, - events, - }; - json_bytes.clear(); - serde_json::to_writer(&mut json_bytes, &request_body)?; - } - - this.http_client - .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into()) - .await?; - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); - } -} diff --git a/crates/client2/src/test.rs b/crates/client2/src/test.rs deleted file mode 100644 index ce3ea740b6..0000000000 --- a/crates/client2/src/test.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; -use anyhow::{anyhow, Result}; -use futures::{stream::BoxStream, StreamExt}; -use gpui::{BackgroundExecutor, Context, Model, TestAppContext}; -use parking_lot::Mutex; -use rpc::{ - proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, - ConnectionId, Peer, Receipt, TypedEnvelope, -}; -use std::sync::Arc; - -pub struct FakeServer { - peer: Arc, - state: Arc>, - user_id: u64, - executor: BackgroundExecutor, -} - -#[derive(Default)] -struct FakeServerState { - incoming: Option>>, - connection_id: Option, - forbid_connections: bool, - auth_count: usize, - access_token: usize, -} - -impl FakeServer { - pub async fn for_client( - client_user_id: u64, - client: &Arc, - cx: &TestAppContext, - ) -> Self { - let server = Self { - peer: Peer::new(0), - state: Default::default(), - user_id: client_user_id, - executor: cx.executor(), - }; - - client - .override_authenticate({ - let state = Arc::downgrade(&server.state); - move |cx| { - let state = state.clone(); - cx.spawn(move |_| async move { - let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?; - let mut state = state.lock(); - state.auth_count += 1; - let access_token = state.access_token.to_string(); - Ok(Credentials { - user_id: client_user_id, - access_token, - }) - }) - } - }) - .override_establish_connection({ - let peer = Arc::downgrade(&server.peer); - let state = Arc::downgrade(&server.state); - move |credentials, cx| { - let peer = peer.clone(); - let state = state.clone(); - let credentials = credentials.clone(); - cx.spawn(move |cx| async move { - let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?; - let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?; - if state.lock().forbid_connections { - Err(EstablishConnectionError::Other(anyhow!( - "server is forbidding connections" - )))? - } - - assert_eq!(credentials.user_id, client_user_id); - - if credentials.access_token != state.lock().access_token.to_string() { - Err(EstablishConnectionError::Unauthorized)? - } - - let (client_conn, server_conn, _) = - Connection::in_memory(cx.background_executor().clone()); - let (connection_id, io, incoming) = - peer.add_test_connection(server_conn, cx.background_executor().clone()); - cx.background_executor().spawn(io).detach(); - { - let mut state = state.lock(); - state.connection_id = Some(connection_id); - state.incoming = Some(incoming); - } - peer.send( - connection_id, - proto::Hello { - peer_id: Some(connection_id.into()), - }, - ) - .unwrap(); - - Ok(client_conn) - }) - } - }); - - client - .authenticate_and_connect(false, &cx.to_async()) - .await - .unwrap(); - - server - } - - pub fn disconnect(&self) { - if self.state.lock().connection_id.is_some() { - self.peer.disconnect(self.connection_id()); - let mut state = self.state.lock(); - state.connection_id.take(); - state.incoming.take(); - } - } - - pub fn auth_count(&self) -> usize { - self.state.lock().auth_count - } - - pub fn roll_access_token(&self) { - self.state.lock().access_token += 1; - } - - pub fn forbid_connections(&self) { - self.state.lock().forbid_connections = true; - } - - pub fn allow_connections(&self) { - self.state.lock().forbid_connections = false; - } - - pub fn send(&self, message: T) { - self.peer.send(self.connection_id(), message).unwrap(); - } - - #[allow(clippy::await_holding_lock)] - pub async fn receive(&self) -> Result> { - self.executor.start_waiting(); - - loop { - let message = self - .state - .lock() - .incoming - .as_mut() - .expect("not connected") - .next() - .await - .ok_or_else(|| anyhow!("other half hung up"))?; - self.executor.finish_waiting(); - let type_name = message.payload_type_name(); - let message = message.into_any(); - - if message.is::>() { - return Ok(*message.downcast().unwrap()); - } - - if message.is::>() { - self.respond( - message - .downcast::>() - .unwrap() - .receipt(), - GetPrivateUserInfoResponse { - metrics_id: "the-metrics-id".into(), - staff: false, - flags: Default::default(), - }, - ); - continue; - } - - panic!( - "fake server received unexpected message type: {:?}", - type_name - ); - } - } - - pub fn respond(&self, receipt: Receipt, response: T::Response) { - self.peer.respond(receipt, response).unwrap() - } - - fn connection_id(&self) -> ConnectionId { - self.state.lock().connection_id.expect("not connected") - } - - pub async fn build_user_store( - &self, - client: Arc, - cx: &mut TestAppContext, - ) -> Model { - let user_store = cx.build_model(|cx| UserStore::new(client, cx)); - assert_eq!( - self.receive::() - .await - .unwrap() - .payload - .user_ids, - &[self.user_id] - ); - user_store - } -} - -impl Drop for FakeServer { - fn drop(&mut self) { - self.disconnect(); - } -} diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs deleted file mode 100644 index b08d423cae..0000000000 --- a/crates/client2/src/user.rs +++ /dev/null @@ -1,694 +0,0 @@ -use super::{proto, Client, Status, TypedEnvelope}; -use anyhow::{anyhow, Context, Result}; -use collections::{hash_map::Entry, HashMap, HashSet}; -use feature_flags::FeatureFlagAppExt; -use futures::{channel::mpsc, Future, StreamExt}; -use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task}; -use postage::{sink::Sink, watch}; -use rpc::proto::{RequestMessage, UsersResponse}; -use std::sync::{Arc, Weak}; -use text::ReplicaId; -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, - pub github_login: String, - pub avatar_uri: SharedString, -} - -#[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)) - } -} - -impl Ord for User { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.github_login.cmp(&other.github_login) - } -} - -impl PartialEq for User { - fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.github_login == other.github_login - } -} - -impl Eq for User {} - -#[derive(Debug, PartialEq)] -pub struct Contact { - pub user: Arc, - pub online: bool, - pub busy: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ContactRequestStatus { - None, - RequestSent, - RequestReceived, - RequestAccepted, -} - -pub struct UserStore { - users: HashMap>, - participant_indices: HashMap, - update_contacts_tx: mpsc::UnboundedSender, - current_user: watch::Receiver>>, - contacts: Vec>, - incoming_contact_requests: Vec>, - outgoing_contact_requests: Vec>, - pending_contact_requests: HashMap, - invite_info: Option, - client: Weak, - _maintain_contacts: Task<()>, - _maintain_current_user: Task>, -} - -#[derive(Clone)] -pub struct InviteInfo { - pub count: u32, - pub url: Arc, -} - -pub enum Event { - Contact { - user: Arc, - kind: ContactEventKind, - }, - ShowContacts, - ParticipantIndicesChanged, -} - -#[derive(Clone, Copy)] -pub enum ContactEventKind { - Requested, - Accepted, - Cancelled, -} - -impl EventEmitter for UserStore {} - -enum UpdateContacts { - Update(proto::UpdateContacts), - Wait(postage::barrier::Sender), - Clear(postage::barrier::Sender), -} - -impl UserStore { - pub fn new(client: Arc, cx: &mut ModelContext) -> Self { - let (mut current_user_tx, current_user_rx) = watch::channel(); - let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); - let rpc_subscriptions = vec![ - client.add_message_handler(cx.weak_model(), Self::handle_update_contacts), - client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info), - client.add_message_handler(cx.weak_model(), Self::handle_show_contacts), - ]; - Self { - users: Default::default(), - 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), - update_contacts_tx, - _maintain_contacts: cx.spawn(|this, mut cx| async move { - let _subscriptions = rpc_subscriptions; - while let Some(message) = update_contacts_rx.next().await { - if let Ok(task) = - this.update(&mut cx, |this, cx| this.update_contacts(message, cx)) - { - task.log_err().await; - } else { - break; - } - } - }), - _maintain_current_user: cx.spawn(|this, mut cx| async move { - let mut status = client.status(); - while let Some(status) = status.next().await { - match status { - Status::Connected { .. } => { - if let Some(user_id) = client.user_id() { - let fetch_user = if let Ok(fetch_user) = this - .update(&mut cx, |this, cx| { - this.get_user(user_id, cx).log_err() - }) { - fetch_user - } else { - break; - }; - let fetch_metrics_id = - client.request(proto::GetPrivateUserInfo {}).log_err(); - let (user, info) = futures::join!(fetch_user, fetch_metrics_id); - - cx.update(|cx| { - if let Some(info) = info { - cx.update_flags(info.staff, info.flags); - client.telemetry.set_authenticated_user_info( - Some(info.metrics_id.clone()), - info.staff, - cx, - ) - } - })?; - - current_user_tx.send(user).await.ok(); - - this.update(&mut cx, |_, cx| cx.notify())?; - } - } - Status::SignedOut => { - current_user_tx.send(None).await.ok(); - this.update(&mut cx, |this, cx| { - cx.notify(); - this.clear_contacts() - })? - .await; - } - Status::ConnectionLost => { - this.update(&mut cx, |this, cx| { - cx.notify(); - this.clear_contacts() - })? - .await; - } - _ => {} - } - } - Ok(()) - }), - pending_contact_requests: Default::default(), - } - } - - #[cfg(feature = "test-support")] - pub fn clear_cache(&mut self) { - self.users.clear(); - } - - async fn handle_update_invite_info( - this: Model, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.invite_info = Some(InviteInfo { - url: Arc::from(message.payload.url), - count: message.payload.count, - }); - cx.notify(); - })?; - Ok(()) - } - - async fn handle_show_contacts( - this: Model, - _: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?; - Ok(()) - } - - pub fn invite_info(&self) -> Option<&InviteInfo> { - self.invite_info.as_ref() - } - - async fn handle_update_contacts( - this: Model, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - this.update_contacts_tx - .unbounded_send(UpdateContacts::Update(message.payload)) - .unwrap(); - })?; - Ok(()) - } - - fn update_contacts( - &mut self, - message: UpdateContacts, - cx: &mut ModelContext, - ) -> Task> { - match message { - UpdateContacts::Wait(barrier) => { - drop(barrier); - Task::ready(Ok(())) - } - UpdateContacts::Clear(barrier) => { - self.contacts.clear(); - self.incoming_contact_requests.clear(); - self.outgoing_contact_requests.clear(); - drop(barrier); - Task::ready(Ok(())) - } - UpdateContacts::Update(message) => { - let mut user_ids = HashSet::default(); - for contact in &message.contacts { - user_ids.insert(contact.user_id); - } - user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id)); - user_ids.extend(message.outgoing_requests.iter()); - - let load_users = self.get_users(user_ids.into_iter().collect(), cx); - cx.spawn(|this, mut cx| async move { - load_users.await?; - - // Users are fetched in parallel above and cached in call to get_users - // No need to paralellize here - let mut updated_contacts = Vec::new(); - let this = this - .upgrade() - .ok_or_else(|| anyhow!("can't upgrade user store handle"))?; - for contact in message.contacts { - updated_contacts.push(Arc::new( - Contact::from_proto(contact, &this, &mut cx).await?, - )); - } - - let mut incoming_requests = Vec::new(); - for request in message.incoming_requests { - incoming_requests.push({ - this.update(&mut cx, |this, cx| { - this.get_user(request.requester_id, cx) - })? - .await? - }); - } - - let mut outgoing_requests = Vec::new(); - for requested_user_id in message.outgoing_requests { - outgoing_requests.push( - this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))? - .await?, - ); - } - - let removed_contacts = - HashSet::::from_iter(message.remove_contacts.iter().copied()); - let removed_incoming_requests = - HashSet::::from_iter(message.remove_incoming_requests.iter().copied()); - let removed_outgoing_requests = - HashSet::::from_iter(message.remove_outgoing_requests.iter().copied()); - - this.update(&mut cx, |this, cx| { - // Remove contacts - this.contacts - .retain(|contact| !removed_contacts.contains(&contact.user.id)); - // Update existing contacts and insert new ones - for updated_contact in updated_contacts { - match this.contacts.binary_search_by_key( - &&updated_contact.user.github_login, - |contact| &contact.user.github_login, - ) { - Ok(ix) => this.contacts[ix] = updated_contact, - Err(ix) => this.contacts.insert(ix, updated_contact), - } - } - - // Remove incoming contact requests - this.incoming_contact_requests.retain(|user| { - if removed_incoming_requests.contains(&user.id) { - cx.emit(Event::Contact { - user: user.clone(), - kind: ContactEventKind::Cancelled, - }); - false - } else { - true - } - }); - // Update existing incoming requests and insert new ones - for user in incoming_requests { - match this - .incoming_contact_requests - .binary_search_by_key(&&user.github_login, |contact| { - &contact.github_login - }) { - Ok(ix) => this.incoming_contact_requests[ix] = user, - Err(ix) => this.incoming_contact_requests.insert(ix, user), - } - } - - // Remove outgoing contact requests - this.outgoing_contact_requests - .retain(|user| !removed_outgoing_requests.contains(&user.id)); - // Update existing incoming requests and insert new ones - for request in outgoing_requests { - match this - .outgoing_contact_requests - .binary_search_by_key(&&request.github_login, |contact| { - &contact.github_login - }) { - Ok(ix) => this.outgoing_contact_requests[ix] = request, - Err(ix) => this.outgoing_contact_requests.insert(ix, request), - } - } - - cx.notify(); - })?; - - Ok(()) - }) - } - } - } - - pub fn contacts(&self) -> &[Arc] { - &self.contacts - } - - pub fn has_contact(&self, user: &Arc) -> bool { - self.contacts - .binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login) - .is_ok() - } - - pub fn incoming_contact_requests(&self) -> &[Arc] { - &self.incoming_contact_requests - } - - pub fn outgoing_contact_requests(&self) -> &[Arc] { - &self.outgoing_contact_requests - } - - pub fn is_contact_request_pending(&self, user: &User) -> bool { - self.pending_contact_requests.contains_key(&user.id) - } - - pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus { - if self - .contacts - .binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login) - .is_ok() - { - ContactRequestStatus::RequestAccepted - } else if self - .outgoing_contact_requests - .binary_search_by_key(&&user.github_login, |user| &user.github_login) - .is_ok() - { - ContactRequestStatus::RequestSent - } else if self - .incoming_contact_requests - .binary_search_by_key(&&user.github_login, |user| &user.github_login) - .is_ok() - { - ContactRequestStatus::RequestReceived - } else { - ContactRequestStatus::None - } - } - - pub fn request_contact( - &mut self, - responder_id: u64, - cx: &mut ModelContext, - ) -> Task> { - self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx) - } - - pub fn remove_contact( - &mut self, - user_id: u64, - cx: &mut ModelContext, - ) -> Task> { - self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx) - } - - pub fn has_incoming_contact_request(&self, user_id: u64) -> bool { - self.incoming_contact_requests - .iter() - .any(|user| user.id == user_id) - } - - pub fn respond_to_contact_request( - &mut self, - requester_id: u64, - accept: bool, - cx: &mut ModelContext, - ) -> Task> { - self.perform_contact_request( - requester_id, - proto::RespondToContactRequest { - requester_id, - response: if accept { - proto::ContactRequestResponse::Accept - } else { - proto::ContactRequestResponse::Decline - } as i32, - }, - cx, - ) - } - - pub fn dismiss_contact_request( - &mut self, - requester_id: u64, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.upgrade(); - cx.spawn(move |_, _| async move { - client - .ok_or_else(|| anyhow!("can't upgrade client reference"))? - .request(proto::RespondToContactRequest { - requester_id, - response: proto::ContactRequestResponse::Dismiss as i32, - }) - .await?; - Ok(()) - }) - } - - fn perform_contact_request( - &mut self, - user_id: u64, - request: T, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.upgrade(); - *self.pending_contact_requests.entry(user_id).or_insert(0) += 1; - cx.notify(); - - cx.spawn(move |this, mut cx| async move { - let response = client - .ok_or_else(|| anyhow!("can't upgrade client reference"))? - .request(request) - .await; - this.update(&mut cx, |this, cx| { - if let Entry::Occupied(mut request_count) = - this.pending_contact_requests.entry(user_id) - { - *request_count.get_mut() -= 1; - if *request_count.get() == 0 { - request_count.remove(); - } - } - cx.notify(); - })?; - response?; - Ok(()) - }) - } - - pub fn clear_contacts(&mut self) -> impl Future { - let (tx, mut rx) = postage::barrier::channel(); - self.update_contacts_tx - .unbounded_send(UpdateContacts::Clear(tx)) - .unwrap(); - async move { - rx.next().await; - } - } - - pub fn contact_updates_done(&mut self) -> impl Future { - let (tx, mut rx) = postage::barrier::channel(); - self.update_contacts_tx - .unbounded_send(UpdateContacts::Wait(tx)) - .unwrap(); - async move { - rx.next().await; - } - } - - pub fn get_users( - &mut self, - user_ids: Vec, - cx: &mut ModelContext, - ) -> Task>>> { - let mut user_ids_to_fetch = user_ids.clone(); - user_ids_to_fetch.retain(|id| !self.users.contains_key(id)); - - cx.spawn(|this, mut cx| async move { - if !user_ids_to_fetch.is_empty() { - this.update(&mut cx, |this, cx| { - this.load_users( - proto::GetUsers { - user_ids: user_ids_to_fetch, - }, - cx, - ) - })? - .await?; - } - - this.update(&mut cx, |this, _| { - user_ids - .iter() - .map(|user_id| { - this.users - .get(user_id) - .cloned() - .ok_or_else(|| anyhow!("user {} not found", user_id)) - }) - .collect() - })? - }) - } - - pub fn fuzzy_search_users( - &mut self, - query: String, - cx: &mut ModelContext, - ) -> Task>>> { - 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, - cx: &mut ModelContext, - ) -> Task>> { - if let Some(user) = self.users.get(&user_id).cloned() { - return Task::ready(Ok(user)); - } - - let load_users = self.get_users(vec![user_id], cx); - cx.spawn(move |this, mut cx| async move { - load_users.await?; - this.update(&mut cx, |this, _| { - this.users - .get(&user_id) - .cloned() - .ok_or_else(|| anyhow!("server responded with no users")) - })? - }) - } - - pub fn current_user(&self) -> Option> { - self.current_user.borrow().clone() - } - - pub fn watch_current_user(&self) -> watch::Receiver>> { - self.current_user.clone() - } - - fn load_users( - &mut self, - request: impl RequestMessage, - cx: &mut ModelContext, - ) -> Task>>> { - let client = self.client.clone(); - cx.spawn(|this, mut cx| async move { - if let Some(rpc) = client.upgrade() { - let response = rpc.request(request).await.context("error loading users")?; - let users = response - .users - .into_iter() - .map(|user| User::new(user)) - .collect::>(); - - this.update(&mut cx, |this, _| { - for user in &users { - this.users.insert(user.id, user.clone()); - } - }) - .ok(); - - Ok(users) - } else { - Ok(Vec::new()) - } - }) - } - - 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 { - fn new(message: proto::User) -> Arc { - Arc::new(User { - id: message.id, - github_login: message.github_login, - avatar_uri: message.avatar_url.into(), - }) - } -} - -impl Contact { - async fn from_proto( - contact: proto::Contact, - user_store: &Model, - cx: &mut AsyncAppContext, - ) -> Result { - let user = user_store - .update(cx, |user_store, cx| { - user_store.get_user(contact.user_id, cx) - })? - .await?; - Ok(Self { - user, - online: contact.online, - busy: contact.busy, - }) - } -} - -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, - }) - } -} diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fd5c183d2a..baf279d634 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.31.0" +version = "0.35.0" publish = false [[bin]] @@ -74,11 +74,13 @@ live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } node_runtime = { path = "../node_runtime" } notifications = { path = "../notifications", features = ["test-support"] } + project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } + collab_ui = { path = "../collab_ui", features = ["test-support"] } async-trait.workspace = true diff --git a/crates/collab/k8s/manifest.template.yml b/crates/collab/k8s/collab.template.yml similarity index 72% rename from crates/collab/k8s/manifest.template.yml rename to crates/collab/k8s/collab.template.yml index d4a7a7033e..120e5f592f 100644 --- a/crates/collab/k8s/manifest.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -13,6 +13,7 @@ metadata: annotations: service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} + service.beta.kubernetes.io/do-loadbalancer-disable-lets-encrypt-dns-records: "true" spec: type: LoadBalancer selector: @@ -23,25 +24,6 @@ spec: 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 @@ -138,40 +120,3 @@ 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/collab2/k8s/environments/preview.sh b/crates/collab/k8s/environments/nightly.sh similarity index 78% rename from crates/collab2/k8s/environments/preview.sh rename to crates/collab/k8s/environments/nightly.sh index 132a1ef53c..49c562437b 100644 --- a/crates/collab2/k8s/environments/preview.sh +++ b/crates/collab/k8s/environments/nightly.sh @@ -1,4 +1,4 @@ -ZED_ENVIRONMENT=preview +ZED_ENVIRONMENT=nightly RUST_LOG=info INVITE_LINK_PREFIX=https://zed.dev/invites/ DATABASE_MAX_CONNECTIONS=10 diff --git a/crates/collab/k8s/postgrest.template.yml b/crates/collab/k8s/postgrest.template.yml new file mode 100644 index 0000000000..f03c0ce33c --- /dev/null +++ b/crates/collab/k8s/postgrest.template.yml @@ -0,0 +1,56 @@ +--- +kind: Service +apiVersion: v1 +metadata: + namespace: ${ZED_KUBE_NAMESPACE} + name: postgrest + annotations: + service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} + service.beta.kubernetes.io/do-loadbalancer-disable-lets-encrypt-dns-records: "true" +spec: + type: LoadBalancer + selector: + app: postgrest + ports: + - name: web + protocol: TCP + port: 443 + targetPort: 8080 + +--- +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 775a4c1bbe..9bbbf88dac 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -161,7 +161,8 @@ CREATE TABLE "room_participants" ( "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, - "participant_index" INTEGER + "participant_index" INTEGER, + "role" TEXT ); 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"); diff --git a/crates/collab/migrations/20240103025509_add_role_to_room_participants.sql b/crates/collab/migrations/20240103025509_add_role_to_room_participants.sql new file mode 100644 index 0000000000..2748e00eba --- /dev/null +++ b/crates/collab/migrations/20240103025509_add_role_to_room_participants.sql @@ -0,0 +1 @@ +ALTER TABLE room_participants ADD COLUMN role TEXT; diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 8d286388cf..a28aeac9ab 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -117,12 +117,13 @@ struct CreateUserResponse { struct Panic { version: String, release_channel: String, + backtrace_hash: String, text: String, } #[instrument(skip(panic))] async fn trace_panic(panic: Json) -> Result<()> { - tracing::error!(version = %panic.version, release_channel = %panic.release_channel, text = %panic.text, "panic report"); + tracing::error!(version = %panic.version, release_channel = %panic.release_channel, backtrace_hash = %panic.backtrace_hash, text = %panic.text, "panic report"); Ok(()) } diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 5f0df90811..9f77225fb7 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -132,6 +132,30 @@ impl ChannelRole { Admin | Member | Banned => false, } } + + pub fn can_share_projects(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member => true, + Guest | Banned => false, + } + } + + pub fn can_edit_projects(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member => true, + Guest | Banned => false, + } + } + + pub fn can_read_projects(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member | Guest => true, + Banned => false, + } + } } impl From for ChannelRole { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 780fb783bc..9c28e998c9 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -129,51 +129,52 @@ impl Database { .await?, ); + debug_assert!( + self.channel_role_for_user(&channel, user_id, &*tx).await? == role + ); + } else if channel.visibility == ChannelVisibility::Public { + role = Some(ChannelRole::Guest); + let channel_to_join = self + .public_ancestors_including_self(&channel, &*tx) + .await? + .first() + .cloned() + .unwrap_or(channel.clone()); + + channel_member::Entity::insert(channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_to_join.id), + user_id: ActiveValue::Set(user_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Guest), + }) + .exec(&*tx) + .await?; + + accept_invite_result = Some( + self.calculate_membership_updated(&channel_to_join, user_id, &*tx) + .await?, + ); + debug_assert!( self.channel_role_for_user(&channel, user_id, &*tx).await? == role ); } } - if channel.visibility == ChannelVisibility::Public { - role = Some(ChannelRole::Guest); - let channel_to_join = self - .public_ancestors_including_self(&channel, &*tx) - .await? - .first() - .cloned() - .unwrap_or(channel.clone()); - - channel_member::Entity::insert(channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_to_join.id), - user_id: ActiveValue::Set(user_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Guest), - }) - .exec(&*tx) - .await?; - - accept_invite_result = Some( - self.calculate_membership_updated(&channel_to_join, user_id, &*tx) - .await?, - ); - - debug_assert!(self.channel_role_for_user(&channel, user_id, &*tx).await? == role); - } - if role.is_none() || role == Some(ChannelRole::Banned) { Err(anyhow!("not allowed"))? } + let role = role.unwrap(); let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); let room_id = self .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) .await?; - self.join_channel_room_internal(room_id, user_id, connection, &*tx) + self.join_channel_room_internal(room_id, user_id, connection, role, &*tx) .await - .map(|jr| (jr, accept_invite_result, role.unwrap())) + .map(|jr| (jr, accept_invite_result, role)) }) .await } diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 3e2c003378..6e1bf16309 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -46,6 +46,13 @@ impl Database { if participant.room_id != room_id { return Err(anyhow!("shared project on unexpected room"))?; } + if !participant + .role + .unwrap_or(ChannelRole::Member) + .can_share_projects() + { + return Err(anyhow!("guests cannot share projects"))?; + } let project = project::ActiveModel { room_id: ActiveValue::set(participant.room_id), @@ -770,13 +777,129 @@ impl Database { .await } - pub async fn project_collaborators( + pub async fn check_user_is_project_host( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result<()> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + project_collaborator::Entity::find() + .filter( + Condition::all() + .add(project_collaborator::Column::ProjectId.eq(project_id)) + .add(project_collaborator::Column::IsHost.eq(true)) + .add(project_collaborator::Column::ConnectionId.eq(connection_id.id)) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection_id.owner_id), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("failed to read project host"))?; + + Ok(()) + }) + .await + .map(|guard| guard.into_inner()) + } + + pub async fn host_for_read_only_project_request( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let current_participant = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if !current_participant + .role + .map_or(false, |role| role.can_read_projects()) + { + Err(anyhow!("not authorized to read projects"))?; + } + + let host = project_collaborator::Entity::find() + .filter( + project_collaborator::Column::ProjectId + .eq(project_id) + .and(project_collaborator::Column::IsHost.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("failed to read project host"))?; + + Ok(host.connection()) + }) + .await + .map(|guard| guard.into_inner()) + } + + pub async fn host_for_mutating_project_request( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let current_participant = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if !current_participant + .role + .map_or(false, |role| role.can_edit_projects()) + { + Err(anyhow!("not authorized to edit projects"))?; + } + + let host = project_collaborator::Entity::find() + .filter( + project_collaborator::Column::ProjectId + .eq(project_id) + .and(project_collaborator::Column::IsHost.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("failed to read project host"))?; + + Ok(host.connection()) + }) + .await + .map(|guard| guard.into_inner()) + } + + pub async fn project_collaborators_for_buffer_update( &self, project_id: ProjectId, connection_id: ConnectionId, ) -> Result>> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { + let current_participant = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if !current_participant + .role + .map_or(false, |role| role.can_edit_projects()) + { + Err(anyhow!("not authorized to edit projects"))?; + } + let collaborators = project_collaborator::Entity::find() .filter(project_collaborator::Column::ProjectId.eq(project_id)) .all(&*tx) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 40fdf5d58f..9a87f91b81 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -131,7 +131,12 @@ impl Database { connection.owner_id as i32, ))), participant_index: ActiveValue::set(Some(0)), - ..Default::default() + role: ActiveValue::set(Some(ChannelRole::Admin)), + + id: ActiveValue::NotSet, + location_kind: ActiveValue::NotSet, + location_project_id: ActiveValue::NotSet, + initial_project_id: ActiveValue::NotSet, } .insert(&*tx) .await?; @@ -151,6 +156,22 @@ impl Database { initial_project_id: Option, ) -> Result> { self.room_transaction(room_id, |tx| async move { + let caller = room_participant::Entity::find() + .filter( + room_participant::Column::UserId + .eq(calling_user_id) + .and(room_participant::Column::RoomId.eq(room_id)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not in the room"))?; + + let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) { + ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member, + ChannelRole::Guest => ChannelRole::Guest, + ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()), + }; + room_participant::ActiveModel { room_id: ActiveValue::set(room_id), user_id: ActiveValue::set(called_user_id), @@ -162,7 +183,13 @@ impl Database { calling_connection.owner_id as i32, ))), initial_project_id: ActiveValue::set(initial_project_id), - ..Default::default() + role: ActiveValue::set(Some(called_user_role)), + + id: ActiveValue::NotSet, + answering_connection_id: ActiveValue::NotSet, + answering_connection_server_id: ActiveValue::NotSet, + location_kind: ActiveValue::NotSet, + location_project_id: ActiveValue::NotSet, } .insert(&*tx) .await?; @@ -384,6 +411,7 @@ impl Database { room_id: RoomId, user_id: UserId, connection: ConnectionId, + role: ChannelRole, tx: &DatabaseTransaction, ) -> Result { let participant_index = self @@ -404,7 +432,11 @@ impl Database { connection.owner_id as i32, ))), participant_index: ActiveValue::Set(Some(participant_index)), - ..Default::default() + role: ActiveValue::set(Some(role)), + id: ActiveValue::NotSet, + location_kind: ActiveValue::NotSet, + location_project_id: ActiveValue::NotSet, + initial_project_id: ActiveValue::NotSet, }]) .on_conflict( OnConflict::columns([room_participant::Column::UserId]) @@ -413,6 +445,7 @@ impl Database { room_participant::Column::AnsweringConnectionServerId, room_participant::Column::AnsweringConnectionLost, room_participant::Column::ParticipantIndex, + room_participant::Column::Role, ]) .to_owned(), ) @@ -855,6 +888,14 @@ impl Database { .exec(&*tx) .await?; + follower::Entity::delete_many() + .filter( + Condition::all() + .add(follower::Column::FollowerConnectionId.eq(connection.id as i32)), + ) + .exec(&*tx) + .await?; + // Unshare projects. project::Entity::delete_many() .filter( @@ -1126,6 +1167,7 @@ impl Database { projects: Default::default(), location: Some(proto::ParticipantLocation { variant: location }), participant_index: participant_index as u32, + role: db_participant.role.unwrap_or(ChannelRole::Member).into(), }, ); } else { diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index 4c5b8cc11c..c562111e96 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -1,4 +1,4 @@ -use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; +use crate::db::{ChannelRole, ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; @@ -19,6 +19,7 @@ pub struct Model { pub calling_connection_id: i32, pub calling_connection_server_id: Option, pub participant_index: Option, + pub role: Option, } impl Model { diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index b6a89ff6f8..56e37abc1d 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -5,7 +5,7 @@ mod feature_flag_tests; mod message_tests; use super::*; -use gpui::executor::Background; +use gpui::BackgroundExecutor; use parking_lot::Mutex; use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; @@ -22,7 +22,7 @@ pub struct TestDb { } impl TestDb { - pub fn sqlite(background: Arc) -> Self { + pub fn sqlite(background: BackgroundExecutor) -> Self { let url = format!("sqlite::memory:"); let runtime = tokio::runtime::Builder::new_current_thread() .enable_io() @@ -59,7 +59,7 @@ impl TestDb { } } - pub fn postgres(background: Arc) -> Self { + pub fn postgres(background: BackgroundExecutor) -> Self { static LOCK: Mutex<()> = Mutex::new(()); let _guard = LOCK.lock(); @@ -108,17 +108,14 @@ impl TestDb { macro_rules! test_both_dbs { ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { #[gpui::test] - async fn $postgres_test_name() { - let test_db = crate::db::TestDb::postgres( - gpui::executor::Deterministic::new(0).build_background(), - ); + async fn $postgres_test_name(cx: &mut gpui::TestAppContext) { + let test_db = crate::db::TestDb::postgres(cx.executor().clone()); $test_name(test_db.db()).await; } #[gpui::test] - async fn $sqlite_test_name() { - let test_db = - crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background()); + async fn $sqlite_test_name(cx: &mut gpui::TestAppContext) { + let test_db = crate::db::TestDb::sqlite(cx.executor().clone()); $test_name(test_db.db()).await; } }; diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 324917bbdd..8a7a19ed3a 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc) { .await .unwrap(); - // Dag is: zed - projects - livestreaming - // Move to same parent should be a no-op assert!(db .move_channel(projects_id, Some(zed_id), user_id) diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index c4b82f8cec..5332f227ef 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -1,6 +1,6 @@ use super::*; use crate::test_both_dbs; -use gpui::executor::{Background, Deterministic}; +use gpui::TestAppContext; use pretty_assertions::{assert_eq, assert_ne}; use std::sync::Arc; use tests::TestDb; @@ -455,7 +455,7 @@ async fn test_project_count(db: &Arc) { .unwrap(); let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev") + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "test") .await .unwrap() .id, @@ -473,7 +473,7 @@ async fn test_project_count(db: &Arc) { room_id, user2.user_id, ConnectionId { owner_id, id: 1 }, - "dev", + "test", ) .await .unwrap(); @@ -509,8 +509,8 @@ fn test_fuzzy_like_string() { } #[gpui::test] -async fn test_fuzzy_search_users() { - let test_db = TestDb::postgres(build_background_executor()); +async fn test_fuzzy_search_users(cx: &mut TestAppContext) { + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); for (i, github_login) in [ "California", @@ -631,7 +631,3 @@ async fn test_non_matching_release_channels(db: &Arc) { assert!(result.is_ok()) } - -fn build_background_executor() -> Arc { - Deterministic::new(0).build_background() -} diff --git a/crates/collab/src/executor.rs b/crates/collab/src/executor.rs index cd3cc60d4a..81d5e977a6 100644 --- a/crates/collab/src/executor.rs +++ b/crates/collab/src/executor.rs @@ -1,10 +1,13 @@ use std::{future::Future, time::Duration}; +#[cfg(test)] +use gpui::BackgroundExecutor; + #[derive(Clone)] pub enum Executor { Production, #[cfg(test)] - Deterministic(std::sync::Arc), + Deterministic(BackgroundExecutor), } impl Executor { @@ -33,12 +36,4 @@ impl Executor { } } } - - pub fn record_backtrace(&self) { - match self { - Executor::Production => {} - #[cfg(test)] - Executor::Deterministic(background) => background.record_backtrace(), - } - } } diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 85216525b0..7dbf4513d9 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -88,7 +88,7 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} -#[derive(Default, Deserialize)] +#[derive(Deserialize)] pub struct Config { pub http_port: u16, pub database_url: String, @@ -100,7 +100,7 @@ pub struct Config { pub live_kit_secret: Option, pub rust_log: Option, pub log_json: Option, - pub zed_environment: String, + pub zed_environment: Arc, } #[derive(Default, Deserialize)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7e847e8bff..5301ca9a23 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -42,7 +42,7 @@ use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, UpdateChannelBufferCollaborators, + RequestMessage, ShareProject, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -66,7 +66,6 @@ use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{info_span, instrument, Instrument}; -use util::channel::RELEASE_CHANNEL_NAME; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); @@ -104,13 +103,14 @@ impl Response { #[derive(Clone)] struct Session { + zed_environment: Arc, user_id: UserId, connection_id: ConnectionId, db: Arc>, peer: Arc, connection_pool: Arc>, live_kit_client: Option>, - executor: Executor, + _executor: Executor, } impl Session { @@ -216,40 +216,45 @@ impl Server { .add_message_handler(update_language_server) .add_message_handler(update_diagnostic_summary) .add_message_handler(update_worktree_settings) - .add_message_handler(refresh_inlay_hints) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler( + forward_mutating_project_request::, + ) + .add_request_handler( + forward_mutating_project_request::, + ) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) .add_message_handler(create_buffer_for_peer) .add_request_handler(update_buffer) - .add_message_handler(update_buffer_file) - .add_message_handler(buffer_reloaded) - .add_message_handler(buffer_saved) - .add_request_handler(forward_project_request::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(get_users) .add_request_handler(fuzzy_search_users) .add_request_handler(request_contact) @@ -281,7 +286,6 @@ impl Server { .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_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version); @@ -609,10 +613,11 @@ impl Server { user_id, connection_id, db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))), + zed_environment: this.app_state.config.zed_environment.clone(), peer: this.peer.clone(), connection_pool: this.connection_pool.clone(), live_kit_client: this.app_state.live_kit_client.clone(), - executor: executor.clone(), + _executor: executor.clone() }; update_user_contacts(user_id, &session).await?; @@ -965,7 +970,7 @@ async fn create_room( session.user_id, session.connection_id, &live_kit_room, - RELEASE_CHANNEL_NAME.as_str(), + &session.zed_environment, ) .await?; @@ -999,7 +1004,7 @@ async fn join_room( room_id, session.user_id, session.connection_id, - RELEASE_CHANNEL_NAME.as_str(), + session.zed_environment.as_ref(), ) .await?; room_updated(&room.room, &session.peer); @@ -1693,10 +1698,6 @@ async fn update_worktree_settings( Ok(()) } -async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> { - broadcast_project_message(request.project_id, request, session).await -} - async fn start_language_server( request: proto::StartLanguageServer, session: Session, @@ -1723,7 +1724,6 @@ async fn update_language_server( request: proto::UpdateLanguageServer, session: Session, ) -> Result<()> { - session.executor.record_backtrace(); let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session .db() @@ -1742,7 +1742,7 @@ async fn update_language_server( Ok(()) } -async fn forward_project_request( +async fn forward_read_only_project_request( request: T, response: Response, session: Session, @@ -1750,26 +1750,38 @@ async fn forward_project_request( where T: EntityMessage + RequestMessage, { - session.executor.record_backtrace(); let project_id = ProjectId::from_proto(request.remote_entity_id()); - let host_connection_id = { - let collaborators = session - .db() - .await - .project_collaborators(project_id, session.connection_id) - .await?; - collaborators - .iter() - .find(|collaborator| collaborator.is_host) - .ok_or_else(|| anyhow!("host not found"))? - .connection_id - }; - + let host_connection_id = session + .db() + .await + .host_for_read_only_project_request(project_id, session.connection_id) + .await?; let payload = session .peer .forward_request(session.connection_id, host_connection_id, request) .await?; + response.send(payload)?; + Ok(()) +} +async fn forward_mutating_project_request( + request: T, + response: Response, + session: Session, +) -> Result<()> +where + T: EntityMessage + RequestMessage, +{ + let project_id = ProjectId::from_proto(request.remote_entity_id()); + let host_connection_id = session + .db() + .await + .host_for_mutating_project_request(project_id, session.connection_id) + .await?; + let payload = session + .peer + .forward_request(session.connection_id, host_connection_id, request) + .await?; response.send(payload)?; Ok(()) } @@ -1778,7 +1790,14 @@ async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, session: Session, ) -> Result<()> { - session.executor.record_backtrace(); + session + .db() + .await + .check_user_is_project_host( + ProjectId::from_proto(request.project_id), + session.connection_id, + ) + .await?; let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?; session .peer @@ -1791,15 +1810,15 @@ async fn update_buffer( response: Response, session: Session, ) -> Result<()> { - session.executor.record_backtrace(); let project_id = ProjectId::from_proto(request.project_id); let mut guest_connection_ids; let mut host_connection_id = None; + { let collaborators = session .db() .await - .project_collaborators(project_id, session.connection_id) + .project_collaborators_for_buffer_update(project_id, session.connection_id) .await?; guest_connection_ids = Vec::with_capacity(collaborators.len() - 1); for collaborator in collaborators.iter() { @@ -1812,7 +1831,6 @@ async fn update_buffer( } let host_connection_id = host_connection_id.ok_or_else(|| anyhow!("host not found"))?; - session.executor.record_backtrace(); broadcast( Some(session.connection_id), guest_connection_ids, @@ -1833,60 +1851,17 @@ async fn update_buffer( Ok(()) } -async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> { - broadcast_project_message(request.project_id, request, session).await -} - -async fn broadcast_project_message( - project_id: u64, +async fn broadcast_project_message_from_host>( request: T, session: Session, ) -> Result<()> { - let project_id = ProjectId::from_proto(project_id); + let project_id = ProjectId::from_proto(request.remote_entity_id()); let project_connection_ids = session .db() .await .project_connection_ids(project_id, session.connection_id) .await?; + broadcast( Some(session.connection_id), project_connection_ids.iter().copied(), @@ -2613,7 +2588,7 @@ async fn join_channel_internal( channel_id, session.user_id, session.connection_id, - RELEASE_CHANNEL_NAME.as_str(), + session.zed_environment.as_ref(), ) .await?; @@ -3115,25 +3090,6 @@ async fn mark_notification_as_read( 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 - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - async fn get_private_user_info( _request: proto::GetPrivateUserInfo, response: Response, diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index e8da66a75a..aca9329d5a 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,9 +1,11 @@ use call::Room; -use gpui::{ModelHandle, TestAppContext}; +use gpui::{Model, TestAppContext}; mod channel_buffer_tests; +mod channel_guest_tests; mod channel_message_tests; mod channel_tests; +mod editor_tests; mod following_tests; mod integration_tests; mod notification_tests; @@ -23,7 +25,7 @@ struct RoomParticipants { pending: Vec, } -fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomParticipants { +fn room_participants(room: &Model, cx: &mut TestAppContext) -> RoomParticipants { room.read_with(cx, |room, _| { let mut remote = room .remote_participants() @@ -41,6 +43,6 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP }) } -fn channel_id(room: &ModelHandle, cx: &mut TestAppContext) -> Option { +fn channel_id(room: &Model, cx: &mut TestAppContext) -> Option { cx.read(|cx| room.read(cx).channel_id()) } diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 5ca40a3c2d..19411ed892 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -4,25 +4,23 @@ use crate::{ }; use call::ActiveCall; use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL; -use client::ParticipantIndex; -use client::{Collaborator, UserId}; +use client::{Collaborator, ParticipantIndex, UserId}; use collab_ui::channel_view::ChannelView; use collections::HashMap; use editor::{Anchor, Editor, ToOffset}; use futures::future; -use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; +use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; -use std::{ops::Range, sync::Arc}; +use std::ops::Range; #[gpui::test] async fn test_core_channel_buffers( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -50,7 +48,7 @@ async fn test_core_channel_buffers( }); buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); - deterministic.run_until_parked(); + executor.run_until_parked(); // Client B joins the channel buffer let channel_buffer_b = client_b @@ -77,13 +75,13 @@ async fn test_core_channel_buffers( }); // Both A and B see the new edit - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); // Client A closes the channel buffer. cx_a.update(|_| drop(channel_buffer_a)); - deterministic.run_until_parked(); + executor.run_until_parked(); // Client B sees that client A is gone from the channel buffer. channel_buffer_b.read_with(cx_b, |buffer, _| { @@ -96,7 +94,7 @@ async fn test_core_channel_buffers( .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Sanity test, make sure we saw A rejoining channel_buffer_b.read_with(cx_b, |buffer, _| { @@ -109,7 +107,7 @@ async fn test_core_channel_buffers( // Client A loses connection. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); // Client B observes A disconnect channel_buffer_b.read_with(cx_b, |buffer, _| { @@ -123,13 +121,12 @@ async fn test_core_channel_buffers( #[gpui::test] async fn test_channel_notes_participant_indices( - deterministic: Arc, - mut cx_a: &mut TestAppContext, - mut cx_b: &mut TestAppContext, + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -157,9 +154,10 @@ async fn test_channel_notes_participant_indices( 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); + + let (workspace_a, mut cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, mut cx_b) = client_b.build_workspace(&project_b, cx_b); + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); // Clients A, B, and C open the channel notes let channel_view_a = cx_a @@ -184,7 +182,7 @@ async fn test_channel_notes_participant_indices( }); }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); channel_view_b.update(cx_b, |notes, cx| { notes.editor.update(cx, |editor, cx| { editor.move_down(&Default::default(), cx); @@ -194,7 +192,7 @@ async fn test_channel_notes_participant_indices( }); }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); channel_view_c.update(cx_c, |notes, cx| { notes.editor.update(cx, |editor, cx| { editor.move_down(&Default::default(), cx); @@ -207,7 +205,7 @@ async fn test_channel_notes_participant_indices( // Client A sees clients B and C without assigned colors, because they aren't // in a call together. - deterministic.run_until_parked(); + executor.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); @@ -223,7 +221,7 @@ async fn test_channel_notes_participant_indices( // Clients A and B see each other with two different assigned colors. Client C // still doesn't have a color. - deterministic.run_until_parked(); + executor.run_until_parked(); channel_view_a.update(cx_a, |notes, cx| { notes.editor.update(cx, |editor, cx| { assert_remote_selections( @@ -249,7 +247,7 @@ async fn test_channel_notes_participant_indices( .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); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); // Clients A and B open the same file. let editor_a = workspace_a @@ -279,7 +277,7 @@ async fn test_channel_notes_participant_indices( selections.select_ranges(vec![2..3]); }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); // Clients A and B see each other with the same colors as in the channel notes. editor_a.update(cx_a, |editor, cx| { @@ -314,11 +312,10 @@ fn assert_remote_selections( #[gpui::test] async fn test_multiple_handles_to_channel_buffer( - deterministic: Arc, + deterministic: BackgroundExecutor, cx_a: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(deterministic.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let channel_id = server @@ -340,7 +337,7 @@ async fn test_multiple_handles_to_channel_buffer( future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) .await .unwrap(); - let channel_buffer_model_id = channel_buffer.id(); + let channel_buffer_model_id = channel_buffer.entity_id(); assert_eq!(channel_buffer, channel_buffer_2); assert_eq!(channel_buffer, channel_buffer_3); @@ -364,7 +361,7 @@ async fn test_multiple_handles_to_channel_buffer( .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) .await .unwrap(); - assert_ne!(channel_buffer.id(), channel_buffer_model_id); + assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); channel_buffer.update(cx_a, |buffer, cx| { buffer.buffer().update(cx, |buffer, _| { assert_eq!(buffer.text(), "hello"); @@ -374,12 +371,11 @@ async fn test_multiple_handles_to_channel_buffer( #[gpui::test] async fn test_channel_buffer_disconnect( - deterministic: Arc, + deterministic: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(deterministic.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -397,6 +393,7 @@ async fn test_channel_buffer_disconnect( .update(cx_a, |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)) @@ -437,12 +434,11 @@ async fn test_channel_buffer_disconnect( #[gpui::test] async fn test_rejoin_channel_buffer( - deterministic: Arc, + deterministic: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(deterministic.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -518,13 +514,12 @@ async fn test_rejoin_channel_buffer( #[gpui::test] async fn test_channel_buffers_and_server_restarts( - deterministic: Arc, + deterministic: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(deterministic.clone()).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; @@ -606,13 +601,12 @@ async fn test_channel_buffers_and_server_restarts( #[gpui::test(iterations = 10)] async fn test_following_to_channel_notes_without_a_shared_project( - deterministic: Arc, + deterministic: BackgroundExecutor, 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 mut server = TestServer::start(deterministic.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -664,9 +658,9 @@ async fn test_following_to_channel_notes_without_a_shared_project( 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); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c); active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) @@ -691,7 +685,9 @@ async fn test_following_to_channel_notes_without_a_shared_project( // Client B follows client A. workspace_b .update(cx_b, |workspace, cx| { - workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + workspace + .start_following(client_a.peer_id().unwrap(), cx) + .unwrap() }) .await .unwrap(); @@ -699,7 +695,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( // 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| { + let channel_view_1_b = workspace_b.update(cx_b, |workspace, cx| { assert_eq!( workspace.leader_for_pane(workspace.active_pane()), Some(client_a.peer_id().unwrap()) @@ -710,7 +706,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .downcast::() .expect("active item is not a channel view") }); - channel_view_1_b.read_with(cx_b, |notes, cx| { + channel_view_1_b.update(cx_b, |notes, cx| { assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); let editor = notes.editor.read(cx); assert_eq!(editor.text(cx), "Hello from A."); @@ -718,17 +714,22 @@ async fn test_following_to_channel_notes_without_a_shared_project( }); // Client A opens the notes for channel 2. + eprintln!("opening -------------------->"); + 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| { + channel_view_2_a.update(cx_a, |notes, cx| { assert_eq!(notes.channel(cx).unwrap().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| { + + eprintln!("opening <--------------------"); + + let channel_view_2_b = workspace_b.update(cx_b, |workspace, cx| { assert_eq!( workspace.leader_for_pane(workspace.active_pane()), Some(client_a.peer_id().unwrap()) @@ -739,19 +740,18 @@ async fn test_following_to_channel_notes_without_a_shared_project( .downcast::() .expect("active item is not a channel view") }); - channel_view_2_b.read_with(cx_b, |notes, cx| { + channel_view_2_b.update(cx_b, |notes, cx| { assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); }); } #[gpui::test] async fn test_channel_buffer_changes( - deterministic: Arc, + deterministic: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(deterministic.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -778,7 +778,7 @@ async fn test_channel_buffer_changes( }); deterministic.run_until_parked(); - let has_buffer_changed = cx_b.read(|cx| { + let has_buffer_changed = cx_b.update(|cx| { client_b .channel_store() .read(cx) @@ -789,14 +789,14 @@ async fn test_channel_buffer_changes( // 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 (workspace_b, cx_b) = client_b.build_workspace(&project_b, 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| { + let has_buffer_changed = cx_b.update(|cx| { client_b .channel_store() .read(cx) @@ -826,7 +826,8 @@ async fn test_channel_buffer_changes( // 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); + server + .simulate_long_connection_interruption(client_b.peer_id().unwrap(), deterministic.clone()); let has_buffer_changed = cx_b.read(|cx| { client_b .channel_store() @@ -877,6 +878,6 @@ fn assert_collaborators(collaborators: &HashMap, ids: &[Op ); } -fn buffer_text(channel_buffer: &ModelHandle, cx: &mut TestAppContext) -> String { +fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { channel_buffer.read_with(cx, |buffer, _| buffer.text()) } diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs new file mode 100644 index 0000000000..32cc074ec9 --- /dev/null +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -0,0 +1,94 @@ +use crate::tests::TestServer; +use call::ActiveCall; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use rpc::proto; +use workspace::Workspace; + +#[gpui::test] +async fn test_channel_guests( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).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 []) + .await; + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + + client_a + .fs() + .insert_tree( + "/a", + serde_json::json!({ + "a.txt": "a-contents", + }), + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + + // Client A shares a project in the channel + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + cx_a.executor().run_until_parked(); + + // Client B joins channel A as a guest + cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) + .await + .unwrap(); + + // b should be following a in the shared project. + // B is a guest, + cx_a.executor().run_until_parked(); + + // todo!() the test window does not call activation handlers + // correctly yet, so this API does not work. + // let project_b = active_call_b.read_with(cx_b, |call, _| { + // call.location() + // .unwrap() + // .upgrade() + // .expect("should not be weak") + // }); + + let window_b = cx_b.update(|cx| cx.active_window().unwrap()); + let cx_b = &mut VisualTestContext::from_window(window_b, cx_b); + + let workspace_b = window_b + .downcast::() + .unwrap() + .root_view(cx_b) + .unwrap(); + let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone()); + + assert_eq!( + project_b.read_with(cx_b, |project, _| project.remote_id()), + Some(project_id), + ); + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + + assert!(project_b + .update(cx_b, |project, cx| { + let worktree_id = project.worktrees().next().unwrap().read(cx).id(); + project.create_entry((worktree_id, "b.txt"), false, cx) + }) + .await + .is_err()) +} diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 918eb053d3..f5da0e3ee6 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -1,20 +1,16 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use channel::{ChannelChat, ChannelMessageId, MessageParams}; -use collab_ui::chat_panel::ChatPanel; -use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext}; +use gpui::{BackgroundExecutor, Model, TestAppContext}; use rpc::Notification; -use std::sync::Arc; -use workspace::dock::Panel; #[gpui::test] async fn test_basic_channel_messages( - deterministic: Arc, + executor: BackgroundExecutor, 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 mut server = TestServer::start(executor.clone()).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; @@ -57,13 +53,13 @@ async fn test_basic_channel_messages( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); channel_chat_b .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap()) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); let channel_chat_c = client_c .channel_store() @@ -117,12 +113,11 @@ async fn test_basic_channel_messages( #[gpui::test] async fn test_rejoin_channel_chat( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -178,7 +173,7 @@ async fn test_rejoin_channel_chat( // Client A reconnects. server.allow_connections(); - deterministic.advance_clock(RECONNECT_TIMEOUT); + executor.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. @@ -189,13 +184,12 @@ async fn test_rejoin_channel_chat( #[gpui::test] async fn test_remove_channel_message( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -235,7 +229,7 @@ async fn test_remove_channel_message( .unwrap(); // Clients A and B see all of the messages. - deterministic.run_until_parked(); + executor.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); @@ -252,7 +246,7 @@ async fn test_remove_channel_message( .unwrap(); // Client B sees that the message is gone. - deterministic.run_until_parked(); + executor.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); @@ -267,146 +261,148 @@ async fn test_remove_channel_message( } #[track_caller] -fn assert_messages(chat: &ModelHandle, messages: &[&str], cx: &mut TestAppContext) { +fn assert_messages(chat: &Model, messages: &[&str], cx: &mut TestAppContext) { + // todo!(don't directly borrow here) assert_eq!( - chat.read_with(cx, |chat, _| chat - .messages() - .iter() - .map(|m| m.body.clone()) - .collect::>(),), + 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; +//todo!(collab_ui) +// #[gpui::test] +// async fn test_channel_message_changes( +// executor: BackgroundExecutor, +// cx_a: &mut TestAppContext, +// cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(&executor).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_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(); +// // 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(); +// channel_chat_a +// .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) +// .await +// .unwrap(); - deterministic.run_until_parked(); +// executor.run_until_parked(); - let b_has_messages = cx_b.read_with(|cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - .unwrap() - }); +// 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); +// 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, None, cx) - }) - .await - .unwrap(); +// // 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, None, cx) +// }) +// .await +// .unwrap(); - deterministic.run_until_parked(); +// executor.run_until_parked(); - let b_has_messages = cx_b.read_with(|cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - .unwrap() - }); +// 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); +// 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(); +// // 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(); +// executor.run_until_parked(); - let b_has_messages = cx_b.read_with(|cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - .unwrap() - }); +// 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); +// 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 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(); +// // 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(); +// executor.run_until_parked(); - let b_has_messages = cx_b.read_with(|cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - .unwrap() - }); +// 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); +// assert!(b_has_messages); - // Closing the chat should re-enable change tracking - cx_b.update(|_| drop(chat_panel_b)); +// // 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(); +// channel_chat_a +// .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) +// .await +// .unwrap(); - deterministic.run_until_parked(); +// executor.run_until_parked(); - let b_has_messages = cx_b.read_with(|cx| { - client_b - .channel_store() - .read(cx) - .has_new_messages(channel_id) - .unwrap() - }); +// 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); -} +// assert!(b_has_messages); +// } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index a33ded6492..49e7060301 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -7,7 +7,7 @@ use call::ActiveCall; use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::User; use futures::future::try_join_all; -use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext}; use rpc::{ proto::{self, ChannelRole}, RECEIVE_TIMEOUT, @@ -16,12 +16,11 @@ use std::sync::Arc; #[gpui::test] async fn test_core_channels( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -40,28 +39,30 @@ async fn test_core_channels( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channels( client_a.channel_store(), cx_a, &[ ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), depth: 0, role: ChannelRole::Admin, }, ExpectedChannel { id: channel_b_id, - name: "channel-b".to_string(), + name: "channel-b".into(), depth: 1, role: ChannelRole::Admin, }, ], ); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert!(channels.ordered_channels().collect::>().is_empty()) + cx_b.read(|cx| { + client_b.channel_store().read_with(cx, |channels, _| { + assert!(channels.ordered_channels().collect::>().is_empty()) + }) }); // Invite client B to channel A as client A. @@ -85,13 +86,13 @@ async fn test_core_channels( .unwrap(); // Client A sees that B has been invited. - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channel_invitations( client_b.channel_store(), cx_b, &[ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), depth: 0, role: ChannelRole::Member, }], @@ -129,7 +130,7 @@ async fn test_core_channels( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Client B now sees that they are a member of channel A and its existing subchannels. assert_channel_invitations(client_b.channel_store(), cx_b, &[]); @@ -139,13 +140,13 @@ async fn test_core_channels( &[ ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), role: ChannelRole::Member, depth: 0, }, ExpectedChannel { id: channel_b_id, - name: "channel-b".to_string(), + name: "channel-b".into(), role: ChannelRole::Member, depth: 1, }, @@ -160,26 +161,26 @@ async fn test_core_channels( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channels( client_b.channel_store(), cx_b, &[ ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), role: ChannelRole::Member, depth: 0, }, ExpectedChannel { id: channel_b_id, - name: "channel-b".to_string(), + name: "channel-b".into(), role: ChannelRole::Member, depth: 1, }, ExpectedChannel { id: channel_c_id, - name: "channel-c".to_string(), + name: "channel-c".into(), role: ChannelRole::Member, depth: 2, }, @@ -199,7 +200,7 @@ async fn test_core_channels( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Observe that client B is now an admin of channel A, and that // their admin priveleges extend to subchannels of channel A. @@ -210,19 +211,19 @@ async fn test_core_channels( &[ ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), depth: 0, role: ChannelRole::Admin, }, ExpectedChannel { id: channel_b_id, - name: "channel-b".to_string(), + name: "channel-b".into(), depth: 1, role: ChannelRole::Admin, }, ExpectedChannel { id: channel_c_id, - name: "channel-c".to_string(), + name: "channel-c".into(), depth: 2, role: ChannelRole::Admin, }, @@ -238,13 +239,13 @@ async fn test_core_channels( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channels( client_a.channel_store(), cx_a, &[ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), depth: 0, role: ChannelRole::Admin, }], @@ -254,7 +255,7 @@ async fn test_core_channels( cx_b, &[ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), depth: 0, role: ChannelRole::Admin, }], @@ -269,7 +270,7 @@ async fn test_core_channels( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Client A still has their channel assert_channels( @@ -277,7 +278,7 @@ async fn test_core_channels( cx_a, &[ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a".into(), depth: 0, role: ChannelRole::Admin, }], @@ -288,7 +289,7 @@ async fn test_core_channels( server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); server .app_state @@ -302,13 +303,13 @@ async fn test_core_channels( .unwrap(); server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); assert_channels( client_a.channel_store(), cx_a, &[ExpectedChannel { id: channel_a_id, - name: "channel-a-renamed".to_string(), + name: "channel-a-renamed".into(), depth: 0, role: ChannelRole::Admin, }], @@ -339,12 +340,11 @@ fn assert_members_eq( #[gpui::test] async fn test_joining_channel_ancestor_member( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -371,13 +371,12 @@ async fn test_joining_channel_ancestor_member( #[gpui::test] async fn test_channel_room( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -400,15 +399,18 @@ async fn test_channel_room( .unwrap(); // Give everyone a chance to observe user A joining - deterministic.run_until_parked(); - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); + executor.run_until_parked(); + let room_a = + cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); + cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); + cx_a.read(|cx| { + client_a.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); + }) }); assert_channels( @@ -416,23 +418,27 @@ async fn test_channel_room( cx_b, &[ExpectedChannel { id: zed_id, - name: "zed".to_string(), + name: "zed".into(), depth: 0, role: ChannelRole::Member, }], ); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); + cx_b.read(|cx| { + client_b.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); + }) }); - client_c.channel_store().read_with(cx_c, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); + cx_c.read(|cx| { + client_c.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); + }) }); active_call_b @@ -440,31 +446,38 @@ async fn test_channel_room( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); + cx_a.read(|cx| { + client_a.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }) }); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); + cx_b.read(|cx| { + client_b.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }) }); - client_c.channel_store().read_with(cx_c, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); + cx_c.read(|cx| { + client_c.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }) }); - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); + let room_a = + cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); + cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -473,8 +486,9 @@ async fn test_channel_room( } ); - let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); - room_b.read_with(cx_b, |room, _| assert!(room.is_connected())); + let room_b = + cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone())); + cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_b, cx_b), RoomParticipants { @@ -490,27 +504,33 @@ async fn test_channel_room( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_b.user_id().unwrap()], - ); + cx_a.read(|cx| { + client_a.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_b.user_id().unwrap()], + ); + }) }); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_b.user_id().unwrap()], - ); + cx_b.read(|cx| { + client_b.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_b.user_id().unwrap()], + ); + }) }); - client_c.channel_store().read_with(cx_c, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_b.user_id().unwrap()], - ); + cx_c.read(|cx| { + client_c.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_b.user_id().unwrap()], + ); + }) }); active_call_b @@ -518,18 +538,24 @@ async fn test_channel_room( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); + cx_a.read(|cx| { + client_a.channel_store().read_with(cx, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + }) }); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); + cx_b.read(|cx| { + client_b.channel_store().read_with(cx, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + }) }); - client_c.channel_store().read_with(cx_c, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); + cx_c.read(|cx| { + client_c.channel_store().read_with(cx, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + }) }); active_call_a @@ -542,10 +568,11 @@ async fn test_channel_room( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); + let room_a = + cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); + cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -554,8 +581,9 @@ async fn test_channel_room( } ); - let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); - room_b.read_with(cx_b, |room, _| assert!(room.is_connected())); + let room_b = + cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone())); + cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_b, cx_b), RoomParticipants { @@ -566,9 +594,8 @@ async fn test_channel_room( } #[gpui::test] -async fn test_channel_jumping(deterministic: Arc, cx_a: &mut TestAppContext) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; +async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppContext) { + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let zed_id = server @@ -586,14 +613,16 @@ async fn test_channel_jumping(deterministic: Arc, cx_a: &mut Test .unwrap(); // Give everything a chance to observe user A joining - deterministic.run_until_parked(); + executor.run_until_parked(); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); - assert_participants_eq(channels.channel_participants(rust_id), &[]); + cx_a.read(|cx| { + client_a.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); + assert_participants_eq(channels.channel_participants(rust_id), &[]); + }) }); active_call_a @@ -603,25 +632,26 @@ async fn test_channel_jumping(deterministic: Arc, cx_a: &mut Test .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); - assert_participants_eq( - channels.channel_participants(rust_id), - &[client_a.user_id().unwrap()], - ); + cx_a.read(|cx| { + client_a.channel_store().read_with(cx, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + assert_participants_eq( + channels.channel_participants(rust_id), + &[client_a.user_id().unwrap()], + ); + }) }); } #[gpui::test] async fn test_permissions_update_while_invited( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -642,7 +672,7 @@ async fn test_permissions_update_while_invited( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channel_invitations( client_b.channel_store(), @@ -650,7 +680,7 @@ async fn test_permissions_update_while_invited( &[ExpectedChannel { depth: 0, id: rust_id, - name: "rust".to_string(), + name: "rust".into(), role: ChannelRole::Member, }], ); @@ -670,7 +700,7 @@ async fn test_permissions_update_while_invited( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channel_invitations( client_b.channel_store(), @@ -678,7 +708,7 @@ async fn test_permissions_update_while_invited( &[ExpectedChannel { depth: 0, id: rust_id, - name: "rust".to_string(), + name: "rust".into(), role: ChannelRole::Member, }], ); @@ -687,12 +717,11 @@ async fn test_permissions_update_while_invited( #[gpui::test] async fn test_channel_rename( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -709,7 +738,7 @@ async fn test_channel_rename( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Client A sees the channel with its new name. assert_channels( @@ -718,7 +747,7 @@ async fn test_channel_rename( &[ExpectedChannel { depth: 0, id: rust_id, - name: "rust-archive".to_string(), + name: "rust-archive".into(), role: ChannelRole::Admin, }], ); @@ -730,7 +759,7 @@ async fn test_channel_rename( &[ExpectedChannel { depth: 0, id: rust_id, - name: "rust-archive".to_string(), + name: "rust-archive".into(), role: ChannelRole::Member, }], ); @@ -738,13 +767,12 @@ async fn test_channel_rename( #[gpui::test] async fn test_call_from_channel( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -778,47 +806,54 @@ async fn test_call_from_channel( .unwrap(); // Client B accepts the call. - deterministic.run_until_parked(); + executor.run_until_parked(); active_call_b .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); // Client B sees that they are now in the channel - deterministic.run_until_parked(); - active_call_b.read_with(cx_b, |call, cx| { - assert_eq!(call.channel_id(cx), Some(channel_id)); + executor.run_until_parked(); + cx_b.read(|cx| { + active_call_b.read_with(cx, |call, cx| { + assert_eq!(call.channel_id(cx), Some(channel_id)); + }) }); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_participants_eq( - channels.channel_participants(channel_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); + cx_b.read(|cx| { + client_b.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(channel_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }) }); // Clients A and C also see that client B is in the channel. - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_participants_eq( - channels.channel_participants(channel_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); + cx_a.read(|cx| { + client_a.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(channel_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }) }); - client_c.channel_store().read_with(cx_c, |channels, _| { - assert_participants_eq( - channels.channel_participants(channel_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); + cx_c.read(|cx| { + client_c.channel_store().read_with(cx, |channels, _| { + assert_participants_eq( + channels.channel_participants(channel_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }) }); } #[gpui::test] async fn test_lost_channel_creation( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -844,7 +879,7 @@ async fn test_lost_channel_creation( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Sanity check, B has the invitation assert_channel_invitations( @@ -853,7 +888,7 @@ async fn test_lost_channel_creation( &[ExpectedChannel { depth: 0, id: channel_id, - name: "x".to_string(), + name: "x".into(), role: ChannelRole::Member, }], ); @@ -867,7 +902,7 @@ async fn test_lost_channel_creation( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Make sure A sees their new channel assert_channels( @@ -877,13 +912,13 @@ async fn test_lost_channel_creation( ExpectedChannel { depth: 0, id: channel_id, - name: "x".to_string(), + name: "x".into(), role: ChannelRole::Admin, }, ExpectedChannel { depth: 1, id: subchannel_id, - name: "subchannel".to_string(), + name: "subchannel".into(), role: ChannelRole::Admin, }, ], @@ -898,7 +933,7 @@ async fn test_lost_channel_creation( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Client B should now see the channel assert_channels( @@ -908,13 +943,13 @@ async fn test_lost_channel_creation( ExpectedChannel { depth: 0, id: channel_id, - name: "x".to_string(), + name: "x".into(), role: ChannelRole::Member, }, ExpectedChannel { depth: 1, id: subchannel_id, - name: "subchannel".to_string(), + name: "subchannel".into(), role: ChannelRole::Member, }, ], @@ -923,14 +958,12 @@ async fn test_lost_channel_creation( #[gpui::test] async fn test_channel_link_notifications( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -953,7 +986,7 @@ async fn test_channel_link_notifications( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); client_b .channel_store() @@ -971,7 +1004,7 @@ async fn test_channel_link_notifications( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // we have an admin (a), member (b) and guest (c) all part of the zed channel. @@ -984,6 +1017,8 @@ async fn test_channel_link_notifications( .await .unwrap(); + executor.run_until_parked(); + // the new channel shows for b and not c assert_channels_list_shape( client_a.channel_store(), @@ -1021,7 +1056,7 @@ async fn test_channel_link_notifications( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // the new channel shows for b and c assert_channels_list_shape( @@ -1093,6 +1128,8 @@ async fn test_channel_link_notifications( .await .unwrap(); + executor.run_until_parked(); + // the members-only channel is still shown for c, but hidden for b assert_channels_list_shape( client_b.channel_store(), @@ -1104,9 +1141,8 @@ async fn test_channel_link_notifications( (helix_channel, 3), ], ); - client_b - .channel_store() - .read_with(cx_b, |channel_store, _| { + cx_b.read(|cx| { + client_b.channel_store().read_with(cx, |channel_store, _| { assert_eq!( channel_store .channel_for_id(vim_channel) @@ -1114,22 +1150,19 @@ async fn test_channel_link_notifications( .visibility, proto::ChannelVisibility::Members ) - }); + }) + }); assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); } #[gpui::test] async fn test_channel_membership_notifications( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_c").await; @@ -1160,7 +1193,7 @@ async fn test_channel_membership_notifications( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); client_b .channel_store() @@ -1178,7 +1211,7 @@ async fn test_channel_membership_notifications( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // we have an admin (a), and a guest (b) with access to all of zed, and membership in vim. assert_channels( @@ -1188,13 +1221,13 @@ async fn test_channel_membership_notifications( ExpectedChannel { depth: 0, id: zed_channel, - name: "zed".to_string(), + name: "zed".into(), role: ChannelRole::Guest, }, ExpectedChannel { depth: 1, id: vim_channel, - name: "vim".to_string(), + name: "vim".into(), role: ChannelRole::Member, }, ], @@ -1208,7 +1241,7 @@ async fn test_channel_membership_notifications( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channels( client_b.channel_store(), @@ -1217,13 +1250,13 @@ async fn test_channel_membership_notifications( ExpectedChannel { depth: 0, id: zed_channel, - name: "zed".to_string(), + name: "zed".into(), role: ChannelRole::Guest, }, ExpectedChannel { depth: 1, id: vim_channel, - name: "vim".to_string(), + name: "vim".into(), role: ChannelRole::Guest, }, ], @@ -1232,13 +1265,11 @@ async fn test_channel_membership_notifications( #[gpui::test] async fn test_guest_access( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -1281,7 +1312,7 @@ async fn test_guest_access( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channels_list_shape( client_a.channel_store(), cx_a, @@ -1314,19 +1345,17 @@ async fn test_guest_access( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]); } #[gpui::test] async fn test_invite_access( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -1365,7 +1394,7 @@ async fn test_invite_access( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); client_b.channel_store().update(cx_b, |channel_store, _| { assert!(channel_store.channel_for_id(channel_b_id).is_some()); @@ -1381,13 +1410,12 @@ async fn test_invite_access( #[gpui::test] async fn test_channel_moving( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, _cx_b: &mut TestAppContext, _cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -1448,64 +1476,68 @@ async fn test_channel_moving( struct ExpectedChannel { depth: usize, id: ChannelId, - name: String, + name: SharedString, role: ChannelRole, } #[track_caller] fn assert_channel_invitations( - channel_store: &ModelHandle, + channel_store: &Model, cx: &TestAppContext, expected_channels: &[ExpectedChannel], ) { - let actual = channel_store.read_with(cx, |store, _| { - store - .channel_invitations() - .iter() - .map(|channel| ExpectedChannel { - depth: 0, - name: channel.name.clone(), - id: channel.id, - role: channel.role, - }) - .collect::>() + let actual = cx.read(|cx| { + channel_store.read_with(cx, |store, _| { + store + .channel_invitations() + .iter() + .map(|channel| ExpectedChannel { + depth: 0, + name: channel.name.clone(), + id: channel.id, + role: channel.role, + }) + .collect::>() + }) }); assert_eq!(actual, expected_channels); } #[track_caller] fn assert_channels( - channel_store: &ModelHandle, + channel_store: &Model, cx: &TestAppContext, expected_channels: &[ExpectedChannel], ) { - let actual = channel_store.read_with(cx, |store, _| { - store - .ordered_channels() - .map(|(depth, channel)| ExpectedChannel { - depth, - name: channel.name.clone(), - id: channel.id, - role: channel.role, - }) - .collect::>() + let actual = cx.read(|cx| { + channel_store.read_with(cx, |store, _| { + store + .ordered_channels() + .map(|(depth, channel)| ExpectedChannel { + depth, + name: channel.name.clone().into(), + id: channel.id, + role: channel.role, + }) + .collect::>() + }) }); pretty_assertions::assert_eq!(actual, expected_channels); } #[track_caller] fn assert_channels_list_shape( - channel_store: &ModelHandle, + channel_store: &Model, cx: &TestAppContext, expected_channels: &[(u64, usize)], ) { - cx.foreground().run_until_parked(); - - let actual = channel_store.read_with(cx, |store, _| { - store - .ordered_channels() - .map(|(depth, channel)| (channel.id, depth)) - .collect::>() + let actual = cx.read(|cx| { + channel_store.read_with(cx, |store, _| { + store + .ordered_channels() + .map(|(depth, channel)| (channel.id, depth)) + .collect::>() + }) }); pretty_assertions::assert_eq!(actual, expected_channels); } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs new file mode 100644 index 0000000000..6f06e9f10f --- /dev/null +++ b/crates/collab/src/tests/editor_tests.rs @@ -0,0 +1,1884 @@ +use std::{ + path::Path, + sync::{ + atomic::{self, AtomicBool, AtomicUsize}, + Arc, + }, +}; + +use call::ActiveCall; +use editor::{ + test::editor_test_context::{AssertionContextManager, EditorTestContext}, + ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, + Undo, +}; +use futures::StreamExt; +use gpui::{TestAppContext, VisualContext, VisualTestContext}; +use indoc::indoc; +use language::{ + language_settings::{AllLanguageSettings, InlayHintSettings}, + tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, +}; +use rpc::RECEIVE_TIMEOUT; +use serde_json::json; +use settings::SettingsStore; +use text::Point; +use workspace::Workspace; + +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; + +#[gpui::test(iterations = 10)] +async fn test_host_disconnect( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).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; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + serde_json::json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + + let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().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; + cx_a.background_executor.run_until_parked(); + + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + + let workspace_b = + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); + let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); + + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "b.txt"), None, true, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + //TODO: focus + assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); + editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); + //todo(is_edited) + // assert!(workspace_b.is_edited(cx_b)); + + // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + cx_a.background_executor + .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); + + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); + + project_b.read_with(cx_b, |project, _| project.is_read_only()); + + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + + // Ensure client B's edited state is reset and that the whole window is blurred. + + workspace_b + .update(cx_b, |_, cx| { + assert_eq!(cx.focused(), None); + }) + .unwrap(); + // assert!(!workspace_b.is_edited(cx_b)); + + // Ensure client B is not prompted to save edits when closing window after disconnecting. + let can_close = workspace_b + .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) + .unwrap() + .await + .unwrap(); + assert!(can_close); + + // Allow client A to reconnect to the server. + server.allow_connections(); + cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT); + + // Client B calls client A again after they reconnected. + let active_call_b = cx_b.read(ActiveCall::global); + active_call_b + .update(cx_b, |call, cx| { + call.invite(client_a.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + cx_a.background_executor.run_until_parked(); + active_call_a + .update(cx_a, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + + active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Drop client A's connection again. We should still unshare it successfully. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + cx_a.background_executor + .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); +} + +#[gpui::test] +async fn test_newline_above_or_below_does_not_move_guest_cursor( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let executor = cx_a.executor(); + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs() + .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + 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; + + // Open a buffer as client A + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + let window_a = cx_a.add_empty_window(); + let editor_a = + window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); + + let mut editor_cx_a = EditorTestContext { + cx: VisualTestContext::from_window(window_a, cx_a), + window: window_a.into(), + editor: editor_a, + assertion_cx: AssertionContextManager::new(), + }; + + let window_b = cx_b.add_empty_window(); + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + + // Open a buffer as client B + let buffer_b = project_b + .update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + let editor_b = window_b.build_view(&mut cx_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b), cx) + }); + let mut editor_cx_b = EditorTestContext { + cx: cx_b, + window: window_b.into(), + editor: editor_b, + assertion_cx: AssertionContextManager::new(), + }; + + // Test newline above + editor_cx_a.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); + executor.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + ˇ + Some text + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + "}); + + // Test newline below + editor_cx_a.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); + executor.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + + Some text + ˇ + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + + "}); +} + +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).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); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + 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; + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_b = cx_b.add_empty_window(); + let editor_b = window_b.build_view(cx_b, |cx| { + Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.background_executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert!(!buffer.completion_triggers().is_empty()) + }); + + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + + // Type a completion trigger character as the guest. + editor_b.update(&mut cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(".", cx); + }); + cx_b.focus_view(&editor_b); + + // Receive a completion request as the host's language server. + // Return some completions from the host's language server. + cx_a.executor().start_waiting(); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + lsp::CompletionItem { + label: "second_method(…)".into(), + detail: Some("fn(&mut self, C) -> D".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "second_method()".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + ]))) + }) + .next() + .await + .unwrap(); + cx_a.executor().finish_waiting(); + + // Open the buffer on the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + cx_a.executor().run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a. }") + }); + + // Confirm a completion on the guest. + + editor_b.update(&mut cx_b, |editor, cx| { + assert!(editor.context_menu_visible()); + editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); + assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); + }); + + // Return a resolved completion from the host's language server. + // The resolved completion has an additional text edit. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!(params.label, "first_method(…)"); + Ok(lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + new_text: "use d::SomeTrait;\n".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + }]), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }) + }, + ); + + // The additional edit is applied. + cx_a.executor().run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); + + buffer_b.read_with(&mut cx_b, |buffer, _| { + assert_eq!( + buffer.text(), + "use d::SomeTrait;\nfn main() { a.first_method() }" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_code_actions( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).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_b.update(editor::init); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", + "other.rs": "pub fn foo() -> usize { 4 }", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Join the project as client B. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let mut requests = fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(0, 0)); + assert_eq!(params.range.end, lsp::Position::new(0, 0)); + Ok(None) + }); + cx_a.background_executor + .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; + + // Move cursor to a location that contains code actions. + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) + }); + }); + cx_b.focus_view(&editor_b); + + let mut requests = fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(1, 31)); + assert_eq!(params.range.end, lsp::Position::new(1, 31)); + + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + data: Some(json!({ + "codeActionParams": { + "range": { + "start": {"line": 1, "column": 31}, + "end": {"line": 1, "column": 31}, + } + } + })), + ..Default::default() + }, + )])) + }); + cx_a.background_executor + .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; + + // Toggle code actions and wait for them to display. + editor_b.update(cx_b, |editor, cx| { + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: false, + }, + cx, + ); + }); + cx_a.background_executor.run_until_parked(); + + editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible())); + + fake_language_server.remove_request_handler::(); + + // Confirming the code action will trigger a resolve request. + let confirm_action = editor_b + .update(cx_b, |editor, cx| { + Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx) + }) + .unwrap(); + fake_language_server.handle_request::( + |_, _| async move { + Ok(lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + ..Default::default() + }) + }, + ); + + // After the action is confirmed, an editor containing both modified files is opened. + confirm_action.await.unwrap(); + + let code_action_editor = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + code_action_editor.update(cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" + ); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).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_b.update(editor::init); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + 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, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "one.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + + // Move cursor to a location that can be renamed. + let prepare_rename = editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([7..7])); + editor.rename(&Rename, cx).unwrap() + }); + + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + )))) + }) + .next() + .await + .unwrap(); + prepare_rename.await.unwrap(); + editor_b.update(cx_b, |editor, cx| { + use editor::ToOffset; + let rename = editor.pending_rename().unwrap(); + let buffer = editor.buffer().read(cx).snapshot(cx); + assert_eq!( + rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), + 6..9 + ); + rename.editor.update(cx, |rename_editor, cx| { + rename_editor.buffer().update(cx, |rename_buffer, cx| { + rename_buffer.edit([(0..3, "THREE")], None, cx); + }); + }); + }); + + let confirm_rename = editor_b.update(cx_b, |editor, cx| { + Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap() + }); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///dir/one.rs" + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 6) + ); + assert_eq!(params.new_name, "THREE"); + Ok(Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/dir/one.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + "THREE".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/dir/two.rs").unwrap(), + vec![ + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + "THREE".to_string(), + ), + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + "THREE".to_string(), + ), + ], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + })) + }) + .next() + .await + .unwrap(); + confirm_rename.await.unwrap(); + + let rename_editor = workspace_b.update(cx_b, |workspace, cx| { + workspace.active_item_as::(cx).unwrap() + }); + + rename_editor.update(cx_b, |editor, cx| { + assert_eq!( + editor.text(cx), + "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" + ); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" + ); + editor.redo(&Redo, cx); + assert_eq!( + editor.text(cx), + "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" + ); + }); + + // Ensure temporary rename edits cannot be undone/redone. + editor_b.update(cx_b, |editor, cx| { + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "const THREE: usize = 1;"); + }) +} + +#[gpui::test(iterations = 10)] +async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + 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_b.update(editor::init); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-language-server", + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/dir", + json!({ + "main.rs": "const ONE: usize = 1;", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + + let _buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.start_progress("the-token").await; + fake_language_server.notify::(lsp::ProgressParams { + token: lsp::NumberOrString::String("the-token".to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( + lsp::WorkDoneProgressReport { + message: Some("the-message".to_string()), + ..Default::default() + }, + )), + }); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message" + ); + }); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + executor.run_until_parked(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + project_b.read_with(cx_b, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + }); + + fake_language_server.notify::(lsp::ProgressParams { + token: lsp::NumberOrString::String("the-token".to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( + lsp::WorkDoneProgressReport { + message: Some("the-message-2".to_string()), + ..Default::default() + }, + )), + }); + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message-2" + ); + }); + + project_b.read_with(cx_b, |project, _| { + let status = project.language_server_statuses().next().unwrap(); + assert_eq!(status.name, "the-language-server"); + assert_eq!(status.pending_work.len(), 1); + assert_eq!( + status.pending_work["the-token"].message.as_ref().unwrap(), + "the-message-2" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_share_project( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let window_b = cx_b.add_empty_window(); + let mut server = TestServer::start(executor.clone()).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; + server + .make_contacts(&mut [(&client_a, cx_a), (&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); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + ".gitignore": "ignored-dir", + "a.txt": "a-contents", + "b.txt": "b-contents", + "ignored-dir": { + "c.txt": "", + "d.txt": "", + } + }), + ) + .await; + + // Invite client B to collaborate on a project + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) + }) + .await + .unwrap(); + + // Join that project as client B + + let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + executor.run_until_parked(); + let call = incoming_call_b.borrow().clone().unwrap(); + assert_eq!(call.calling_user.github_login, "user_a"); + let initial_project = call.initial_project.unwrap(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + let client_b_peer_id = client_b.peer_id().unwrap(); + let project_b = client_b + .build_remote_project(initial_project.id, cx_b) + .await; + + let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); + + executor.run_until_parked(); + + project_a.read_with(cx_a, |project, _| { + let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); + assert_eq!(client_b_collaborator.replica_id, replica_id_b); + }); + + project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap().read(cx); + assert_eq!( + worktree.paths().map(AsRef::as_ref).collect::>(), + [ + Path::new(".gitignore"), + Path::new("a.txt"), + Path::new("b.txt"), + Path::new("ignored-dir"), + ] + ); + }); + + project_b + .update(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap(); + let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); + project.expand_entry(worktree_id, entry.id, cx).unwrap() + }) + .await + .unwrap(); + + project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees().next().unwrap().read(cx); + assert_eq!( + worktree.paths().map(AsRef::as_ref).collect::>(), + [ + Path::new(".gitignore"), + Path::new("a.txt"), + Path::new("b.txt"), + Path::new("ignored-dir"), + Path::new("ignored-dir/c.txt"), + Path::new("ignored-dir/d.txt"), + ] + ); + }); + + // Open the same file as client B and client A. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .await + .unwrap(); + + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); + + project_a.read_with(cx_a, |project, cx| { + assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) + }); + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .await + .unwrap(); + + let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); + + // Client A sees client B's selection + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX) + .count() + == 1 + }); + + // Edit the buffer as client B and see that edit as client A. + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + editor_b.update(&mut cx_b, |editor, cx| editor.handle_input("ok, ", cx)); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "ok, b-contents") + }); + + // Client B can invite client C on a project shared by client A. + active_call_b + .update(&mut cx_b, |call, cx| { + call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) + }) + .await + .unwrap(); + + let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); + executor.run_until_parked(); + let call = incoming_call_c.borrow().clone().unwrap(); + assert_eq!(call.calling_user.github_login, "user_b"); + let initial_project = call.initial_project.unwrap(); + active_call_c + .update(cx_c, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + let _project_c = client_c + .build_remote_project(initial_project.id, cx_c) + .await; + + // Client B closes the editor, and client A sees client B's selections removed. + cx_b.update(move |_| drop(editor_b)); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + buffer + .snapshot() + .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX) + .count() + == 0 + }); +} + +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_host_to_guest( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + 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); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + 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; + + // Open a file in an editor as the host. + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_a = cx_a.add_empty_window(); + let editor_a = window_a + .update(cx_a, |_, cx| { + cx.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) + }) + .unwrap(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~<".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }, + ); + + // Open the buffer on the guest and see that the formattings worked + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + + let mut cx_a = VisualTestContext::from_window(window_a, cx_a); + // Type a on type formatting trigger character as the guest. + cx_a.focus_view(&editor_a); + editor_a.update(&mut cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(">", cx); + }); + + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a>~< }") + }); + + // Undo should remove LSP edits first + editor_a.update(&mut cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a>~< }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a> }"); + }); + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a> }") + }); + + editor_a.update(&mut cx_a, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a> }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); + + buffer_b.read_with(cx_b, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + +#[gpui::test(iterations = 10)] +async fn test_on_input_format_from_guest_to_host( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + 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); + + // Set up a fake language server. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + client_a.language_registry().add(Arc::new(language)); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + 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; + + // Open a file in an editor as the guest. + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + let window_b = cx_b.add_empty_window(); + let editor_b = window_b.build_view(cx_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) + }); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + executor.run_until_parked(); + let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + // Type a on type formatting trigger character as the guest. + cx_b.focus_view(&editor_b); + editor_b.update(&mut cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(":", cx); + }); + + // Receive an OnTypeFormatting request as the host's language server. + // Return some formattings from the host's language server. + executor.start_waiting(); + fake_language_server + .handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "~:".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), + }])) + }) + .next() + .await + .unwrap(); + executor.finish_waiting(); + + // Open the buffer on the host and see that the formattings worked + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .await + .unwrap(); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a:~: }") + }); + + // Undo should remove LSP edits first + editor_b.update(&mut cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a:~: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a: }"); + }); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a: }") + }); + + editor_b.update(&mut cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "fn main() { a: }"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "fn main() { a }"); + }); + executor.run_until_parked(); + + buffer_a.read_with(cx_a, |buffer, _| { + assert_eq!(buffer.text(), "fn main() { a }") + }); +} + +#[gpui::test(iterations = 10)] +async fn test_mutual_editor_inlay_hint_cache_update( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + 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); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); + + // Client A opens a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .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(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + executor.start_waiting(); + + // The host opens a rust file. + let _buffer_a = project_a + .update(cx_a, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Set up the language server to return an additional inlay hint on each request. + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); + fake_language_server + .handle_request::(move |params, _| { + let task_edits_made = Arc::clone(&closure_edits_made); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let edits_made = task_edits_made.load(atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); + + executor.run_until_parked(); + + let initial_edit = edits_made.load(atomic::Ordering::Acquire); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Host should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Host editor update the cache version after every cache/view change", + ); + }); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![initial_edit.to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Guest editor update the cache version after every cache/view change" + ); + }); + + let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); + editor.handle_input(":", cx); + }); + cx_b.focus_view(&editor_b); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_client_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 2); + }); + + let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("a change to increment both buffers' versions", cx); + }); + cx_a.focus_view(&editor_a); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_host_edit.to_string()], + extract_hint_labels(editor), + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), 3); + }); + + let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Host should react to /refresh LSP request" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Host should accepted all edits and bump its cache version every time" + ); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![after_special_edit_for_refresh.to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 4, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_inlay_hint_refresh_is_forwarded( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let executor = cx_a.executor(); + 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); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + }); + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", + "other.rs": "// Test file", + }), + ) + .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, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + cx_a.background_executor.start_waiting(); + + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let other_hints = Arc::new(AtomicBool::new(false)); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let closure_other_hints = Arc::clone(&other_hints); + fake_language_server + .handle_request::(move |params, _| { + let task_other_hints = Arc::clone(&closure_other_hints); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let other_hints = task_other_hints.load(atomic::Ordering::Acquire); + let character = if other_hints { 0 } else { 2 }; + let label = if other_hints { + "other hint" + } else { + "initial hint" + }; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, character), + label: lsp::InlayHintLabel::String(label.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await + .unwrap(); + executor.finish_waiting(); + + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get no hints due to them turned off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates" + ); + }); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["initial hint".to_string()], + extract_hint_labels(editor), + "Client should get its first hints when opens an editor" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 1, + "Should update cache verison after first hints" + ); + }); + + other_hints.fetch_or(true, atomic::Ordering::Release); + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + executor.run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "Host should get nop hints due to them turned off, even after the /refresh" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 0, + "Turned off hints should not generate version updates, again" + ); + }); + + executor.run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["other hint".to_string()], + extract_hint_labels(editor), + "Guest should get a /refresh LSP request propagated by host despite host hints are off" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version(), + 2, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} + +fn extract_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), + } + } + labels +} diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 97509d730f..0486e29461 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -2,13 +2,16 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; -use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; +use gpui::{ + point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext, + VisualTestContext, +}; +use language::Capability; use live_kit_client::MacOSDisplay; use project::project_settings::ProjectSettings; use rpc::proto::PeerId; use serde_json::json; use settings::SettingsStore; -use std::{borrow::Cow, sync::Arc}; use workspace::{ dock::{test::TestPanel, DockPosition}, item::{test::TestItem, ItemHandle as _}, @@ -18,15 +21,13 @@ use 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 executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).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; @@ -72,13 +73,11 @@ async fn test_basic_following( .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); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); // Client A opens some editors. - let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + let pane_a = workspace_a.update(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) @@ -127,15 +126,10 @@ async fn test_basic_following( }); // 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(); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); - cx_c.foreground().run_until_parked(); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + cx_c.executor().run_until_parked(); + let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { workspace .active_item(cx) .unwrap() @@ -147,37 +141,32 @@ async fn test_basic_following( Some((worktree_id, "2.txt").into()) ); assert_eq!( - editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + editor_b2.update(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)), + editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), vec![3..2] ); - cx_c.foreground().run_until_parked(); + executor.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); + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); active_call_c .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) .await .unwrap(); + let weak_project_c = project_c.downgrade(); 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(); + workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx)); - cx_d.foreground().run_until_parked(); + cx_d.executor().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); + let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d); active_call_d .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) .await @@ -185,7 +174,7 @@ async fn test_basic_following( drop(project_d); // All clients see that clients B and C are following client A. - cx_c.foreground().run_until_parked(); + cx_c.executor().run_until_parked(); for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { assert_eq!( followers_by_leader(project_id, cx), @@ -200,7 +189,7 @@ async fn test_basic_following( }); // All clients see that clients B is following client A. - cx_c.foreground().run_until_parked(); + cx_c.executor().run_until_parked(); for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { assert_eq!( followers_by_leader(project_id, cx), @@ -210,15 +199,10 @@ async fn test_basic_following( } // Client C re-follows client A. - workspace_c - .update(cx_c, |workspace, cx| { - workspace.follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); + 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(); + cx_c.executor().run_until_parked(); for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { assert_eq!( followers_by_leader(project_id, cx), @@ -228,21 +212,12 @@ async fn test_basic_following( } // Client D follows client B, then switches to following client C. - workspace_d - .update(cx_d, |workspace, cx| { - workspace.follow(peer_id_b, cx).unwrap() - }) - .await - .unwrap(); - workspace_d - .update(cx_d, |workspace, cx| { - workspace.follow(peer_id_c, cx).unwrap() - }) - .await - .unwrap(); + workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx)); + cx_a.executor().run_until_parked(); + workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx)); // All clients see that D is following C - cx_d.foreground().run_until_parked(); + cx_a.executor().run_until_parked(); for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { assert_eq!( followers_by_leader(project_id, cx), @@ -255,12 +230,25 @@ async fn test_basic_following( } // Client C closes the project. - window_c.remove(cx_c); - cx_c.drop_last(workspace_c); + let weak_workspace_c = workspace_c.downgrade(); + workspace_c.update(cx_c, |workspace, cx| { + workspace.close_window(&Default::default(), cx); + }); + cx_c.update(|_| { + drop(workspace_c); + }); + cx_b.executor().run_until_parked(); + // are you sure you want to leave the call? + cx_c.simulate_prompt_answer(0); + cx_b.executor().run_until_parked(); + executor.run_until_parked(); + + weak_workspace_c.assert_dropped(); + weak_project_c.assert_dropped(); // 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, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + executor.run_until_parked(); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] { assert_eq!( followers_by_leader(project_id, cx), &[(peer_id_a, vec![peer_id_b]),], @@ -272,13 +260,16 @@ async fn test_basic_following( 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()); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); }); // When client A opens a multibuffer, client B does so as well. - let multibuffer_a = cx_a.add_model(|cx| { + let multibuffer_a = cx_a.new_model(|cx| { let buffer_a1 = project_a.update(cx, |project, cx| { project .get_open_buffer(&(worktree_id, "1.txt").into(), cx) @@ -289,7 +280,7 @@ async fn test_basic_following( .get_open_buffer(&(worktree_id, "2.txt").into(), cx) .unwrap() }); - let mut result = MultiBuffer::new(0); + let mut result = MultiBuffer::new(0, Capability::ReadWrite); result.push_excerpts( buffer_a1, [ExcerptRange { @@ -310,12 +301,12 @@ async fn test_basic_following( }); 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)); + cx.new_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| { + executor.run_until_parked(); + let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { workspace .active_item(cx) .unwrap() @@ -323,8 +314,8 @@ async fn test_basic_following( .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)), + multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), + multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)), ); // When client A navigates back and forth, client B does so as well. @@ -334,9 +325,12 @@ async fn test_basic_following( }) .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()); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); }); workspace_a @@ -345,9 +339,12 @@ async fn test_basic_following( }) .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()); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b2.item_id() + ); }); workspace_a @@ -356,30 +353,34 @@ async fn test_basic_following( }) .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()); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_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| { + executor.run_until_parked(); + cx_b.background_executor.run_until_parked(); + editor_b1.update(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")); + executor.run_until_parked(); + editor_b1.update(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); + editor.set_scroll_position(point(0., 100.), cx); }); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| { + executor.run_until_parked(); + editor_b1.update(cx_b, |editor, cx| { assert_eq!(editor.selections.ranges(cx), &[3..3]); }); @@ -390,32 +391,28 @@ async fn test_basic_following( workspace_a.update(cx_a, |workspace, cx| { workspace.activate_item(&editor_a2, cx) }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace + workspace_b.update(cx_b, |workspace, cx| workspace .active_item(cx) .unwrap() - .id()), - editor_b1.id() + .item_id()), + editor_b1.item_id() ); // Client A starts following client B. - workspace_a - .update(cx_a, |workspace, cx| { - workspace.follow(peer_id_b, cx).unwrap() - }) - .await - .unwrap(); + workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx)); + executor.run_until_parked(); assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), Some(peer_id_b) ); assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace + workspace_a.update(cx_a, |workspace, cx| workspace .active_item(cx) .unwrap() - .id()), - editor_a1.id() + .item_id()), + editor_a1.item_id() ); // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. @@ -433,8 +430,8 @@ async fn test_basic_following( }) .await .unwrap(); - deterministic.run_until_parked(); - let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { + executor.run_until_parked(); + let shared_screen = workspace_a.update(cx_a, |workspace, cx| { workspace .active_item(cx) .expect("no active item") @@ -447,83 +444,86 @@ async fn test_basic_following( .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()) + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_a1.item_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| { + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_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)); + let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx)); workspace_b.update(cx_b, |workspace, cx| { workspace.add_panel(panel, cx); workspace.toggle_panel_focus::(cx); }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace + workspace_a.update(cx_a, |workspace, cx| workspace .active_item(cx) .unwrap() - .id()), - shared_screen.id() + .item_id()), + shared_screen.item_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| { + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_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()); + let unfollowable_item = cx_b.new_view(|cx| TestItem::new(cx)); 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(); + executor.run_until_parked(); assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace + workspace_a.update(cx_a, |workspace, cx| workspace .active_item(cx) .unwrap() - .id()), - shared_screen.id() + .item_id()), + shared_screen.item_id() ); // Following interrupts when client B disconnects. client_b.disconnect(&cx_b.to_async()); - deterministic.advance_clock(RECONNECT_TIMEOUT); + executor.advance_clock(RECONNECT_TIMEOUT); assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), None ); } #[gpui::test] async fn test_following_tab_order( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -562,13 +562,13 @@ async fn test_following_tab_order( .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_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let pane_a = workspace_a.update(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 (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); - let client_b_id = project_a.read_with(cx_a, |project, _| { + let client_b_id = project_a.update(cx_a, |project, _| { project.collaborators().values().next().unwrap().peer_id }); @@ -586,7 +586,7 @@ async fn test_following_tab_order( .await .unwrap(); - let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { + let pane_paths = |pane: &View, cx: &mut VisualTestContext| { pane.update(cx, |pane, cx| { pane.items() .map(|item| { @@ -605,12 +605,8 @@ async fn test_following_tab_order( 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(); + workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); + executor.run_until_parked(); //Open just 2 on client B workspace_b @@ -619,7 +615,7 @@ async fn test_following_tab_order( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.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"]); @@ -632,20 +628,16 @@ async fn test_following_tab_order( .await .unwrap(); assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); - deterministic.run_until_parked(); + executor.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; +async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -688,7 +680,7 @@ async fn test_peers_following_each_other( .unwrap(); // Client A opens a file. - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -699,7 +691,7 @@ async fn test_peers_following_each_other( .unwrap(); // Client B opens a different file. - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), None, true, cx) @@ -713,26 +705,22 @@ async fn test_peers_following_each_other( 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_a.update(cx_a, |workspace, cx| { + workspace.follow(client_b.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); 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(); + workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); // 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(); + executor.run_until_parked(); // Both clients see the other client's focused file in their right pane. assert_eq!( @@ -780,7 +768,7 @@ async fn test_peers_following_each_other( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // Both client's see the other client open the new file, but keep their // focus on their own active pane. @@ -825,7 +813,7 @@ async fn test_peers_following_each_other( // 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(); + executor.run_until_parked(); // Client B sees that client A is now looking at the same file as them. assert_eq!( @@ -871,7 +859,7 @@ async fn test_peers_following_each_other( // 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(); + executor.run_until_parked(); // Client A sees that client B is now looking at the same file as them. assert_eq!( @@ -921,7 +909,7 @@ async fn test_peers_following_each_other( pane.activate_prev_item(true, cx); }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); // Both clients see that client B is looking at that previous file. assert_eq!( @@ -975,7 +963,7 @@ async fn test_peers_following_each_other( }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); // Both clients see that Client B is looking at the previous tab. assert_eq!( @@ -1015,20 +1003,17 @@ async fn test_peers_following_each_other( ); // Client B follows client A again. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() - }) - .await - .unwrap(); - + workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); // 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(); + executor.run_until_parked(); // Client B follows client A into those tabs. assert_eq!( @@ -1072,7 +1057,7 @@ async fn test_peers_following_each_other( pane.activate_prev_item(true, cx); }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( pane_summaries(&workspace_a, cx_a), @@ -1119,7 +1104,7 @@ async fn test_peers_following_each_other( pane.activate_prev_item(true, cx); }); }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( pane_summaries(&workspace_a, cx_a), @@ -1164,15 +1149,10 @@ async fn test_peers_following_each_other( } #[gpui::test(iterations = 10)] -async fn test_auto_unfollowing( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - +async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { // 2 clients connect to a server. - let mut server = TestServer::start(&deterministic).await; + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -1212,8 +1192,9 @@ async fn test_auto_unfollowing( .await .unwrap(); - // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let _editor_a1 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -1224,22 +1205,17 @@ async fn test_auto_unfollowing( .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, _| { + let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.update(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(); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), Some(leader_id) ); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { workspace .active_item(cx) .unwrap() @@ -1250,56 +1226,44 @@ async fn test_auto_unfollowing( // 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)), + workspace_b.update(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(); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + workspace_b.update(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)), + workspace_b.update(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(); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + workspace_b.update(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) + editor.set_scroll_position(point(0., 3.), cx) }); assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + workspace_b.update(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(); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), Some(leader_id) ); @@ -1308,13 +1272,13 @@ async fn test_auto_unfollowing( 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)), + workspace_b.update(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)), + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), Some(leader_id) ); @@ -1326,20 +1290,18 @@ async fn test_auto_unfollowing( .await .unwrap(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + workspace_b.update(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 executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -1352,38 +1314,34 @@ async fn test_peers_simultaneously_following_each_other( 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 (workspace_a, cx_a) = client_a.build_workspace(&project_a, 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); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - deterministic.run_until_parked(); - let client_a_id = project_b.read_with(cx_b, |project, _| { + executor.run_until_parked(); + let client_a_id = project_b.update(cx_b, |project, _| { project.collaborators().values().next().unwrap().peer_id }); - let client_b_id = project_a.read_with(cx_a, |project, _| { + let client_b_id = project_a.update(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() - }); + workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx)); + executor.run_until_parked(); - futures::try_join!(a_follow_b, b_follow_a).unwrap(); - workspace_a.read_with(cx_a, |workspace, _| { + workspace_a.update(cx_a, |workspace, _| { assert_eq!( workspace.leader_for_pane(workspace.active_pane()), Some(client_b_id) ); }); - workspace_b.read_with(cx_b, |workspace, _| { + workspace_b.update(cx_b, |workspace, _| { assert_eq!( workspace.leader_for_pane(workspace.active_pane()), Some(client_a_id) @@ -1392,11 +1350,7 @@ async fn test_peers_simultaneously_following_each_other( } #[gpui::test(iterations = 10)] -async fn test_following_across_workspaces( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { +async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { // a and b join a channel/call // a shares project 1 // b shares project 2 @@ -1405,8 +1359,8 @@ async fn test_following_across_workspaces( // 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 executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).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); @@ -1443,8 +1397,8 @@ async fn test_following_across_workspaces( 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); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, 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)); @@ -1470,40 +1424,46 @@ async fn test_following_across_workspaces( .await .unwrap(); - deterministic.run_until_parked(); + executor.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() + workspace.follow(client_a.peer_id().unwrap(), cx) }); - deterministic.run_until_parked(); - let workspace_b_project_a = cx_b + executor.run_until_parked(); + let window_b_project_a = cx_b .windows() .iter() - .max_by_key(|window| window.id()) + .max_by_key(|window| window.window_id()) .unwrap() + .clone(); + + let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b); + + let workspace_b_project_a = window_b_project_a .downcast::() .unwrap() - .root(cx_b); + .root(cx_b) + .unwrap(); // assert that b is following a in project a in w.rs - workspace_b_project_a.update(cx_b, |workspace, cx| { + workspace_b_project_a.update(&mut cx_b2, |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")); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("w.rs") + ); }); // TODO: in app code, this would be done by the collab_ui. active_call_b - .update(cx_b, |call, cx| { + .update(&mut cx_b2, |call, cx| { let project = workspace_b_project_a.read(cx).project().clone(); call.set_location(Some(&project), cx) }) @@ -1515,26 +1475,26 @@ async fn test_following_across_workspaces( // b moves to x.rs in a's project, and a follows workspace_b_project_a - .update(cx_b, |workspace, cx| { + .update(&mut cx_b2, |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| { + executor.run_until_parked(); + workspace_b_project_a.update(&mut cx_b2, |workspace, cx| { let item = workspace.active_item(cx).unwrap(); - assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("x.rs") + ); }); workspace_a.update(cx_a, |workspace, cx| { - workspace - .follow(client_b.peer_id().unwrap(), cx) - .unwrap() - .detach() + workspace.follow(client_b.peer_id().unwrap(), cx) }); - deterministic.run_until_parked(); + executor.run_until_parked(); workspace_a.update(cx_a, |workspace, cx| { assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); assert_eq!( @@ -1542,7 +1502,7 @@ async fn test_following_across_workspaces( 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")); + assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs"); }); // b moves to y.rs in b's project, a is still following but can't yet see @@ -1567,7 +1527,7 @@ async fn test_following_across_workspaces( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_a).len(), 1); cx_a.update(|cx| { workspace::join_remote_project( @@ -1580,19 +1540,23 @@ async fn test_following_across_workspaces( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_a).len(), 0); - let workspace_a_project_b = cx_a + let window_a_project_b = cx_a .windows() .iter() - .max_by_key(|window| window.id()) + .max_by_key(|window| window.window_id()) .unwrap() + .clone(); + let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a); + let workspace_a_project_b = window_a_project_b .downcast::() .unwrap() - .root(cx_a); + .root(cx_a) + .unwrap(); - workspace_a_project_b.update(cx_a, |workspace, cx| { + workspace_a_project_b.update(cx_a2, |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!( @@ -1600,24 +1564,25 @@ async fn test_following_across_workspaces( 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")); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("y.rs") + ); }); } #[gpui::test] async fn test_following_into_excluded_file( - deterministic: Arc, mut cx_a: &mut TestAppContext, mut cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; for cx in [&mut cx_a, &mut cx_b] { cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global::(|store, cx| { store.update_user_settings::(cx, |project_settings| { project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); }); @@ -1629,6 +1594,7 @@ async fn test_following_into_excluded_file( .await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); + let peer_id_a = client_a.peer_id().unwrap(); cx_a.update(editor::init); cx_b.update(editor::init); @@ -1663,11 +1629,8 @@ async fn test_following_into_excluded_file( .await .unwrap(); - let window_a = client_a.build_workspace(&project_a, cx_a); - let workspace_a = window_a.root(cx_a); - let peer_id_a = client_a.peer_id().unwrap(); - let window_b = client_b.build_workspace(&project_b, cx_b); - let workspace_b = window_b.root(cx_b); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); // Client A opens editors for a regular file and an excluded file. let editor_for_regular = workspace_a @@ -1703,14 +1666,10 @@ async fn test_following_into_excluded_file( }); // When client B starts following client A, currently visible file is replicated - workspace_b - .update(cx_b, |workspace, cx| { - workspace.follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + executor.run_until_parked(); - let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| { + let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { workspace .active_item(cx) .unwrap() @@ -1722,7 +1681,7 @@ async fn test_following_into_excluded_file( Some((worktree_id, ".git/COMMIT_EDITMSG").into()) ); assert_eq!( - editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), vec![18..17] ); @@ -1730,7 +1689,7 @@ async fn test_following_into_excluded_file( editor_for_excluded_b.update(cx_b, |editor, cx| { editor.handle_input("\nCo-Authored-By: B ", cx); }); - deterministic.run_until_parked(); + executor.run_until_parked(); editor_for_excluded_a.update(cx_a, |editor, cx| { assert_eq!( editor.text(cx), @@ -1741,18 +1700,16 @@ async fn test_following_into_excluded_file( fn visible_push_notifications( cx: &mut TestAppContext, -) -> Vec> { +) -> 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) - } - }); + window + .update(cx, |window, _| { + if let Ok(handle) = window.downcast::() { + ret.push(handle) + } + }) + .unwrap(); } ret } @@ -1788,8 +1745,8 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec }) } -fn pane_summaries(workspace: &ViewHandle, cx: &mut TestAppContext) -> Vec { - workspace.read_with(cx, |workspace, cx| { +fn pane_summaries(workspace: &View, cx: &mut VisualTestContext) -> Vec { + workspace.update(cx, |workspace, cx| { let active_pane = workspace.active_pane(); workspace .panes() diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ad4c59e377..a21235b6f3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -5,18 +5,13 @@ use crate::{ use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; -use editor::{ - test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, - ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, -}; use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; -use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, TestAppContext}; -use indoc::indoc; +use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext}; use language::{ - language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, - tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, + language_settings::{AllLanguageSettings, Formatter}, + tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, + LineEnding, OffsetRangeExt, Point, Rope, }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; @@ -24,6 +19,7 @@ use project::{ search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, }; use rand::prelude::*; +use rpc::proto::ChannelRole; use serde_json::json; use settings::SettingsStore; use std::{ @@ -32,12 +28,11 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::{ - atomic::{self, AtomicBool, AtomicUsize, Ordering::SeqCst}, + atomic::{AtomicBool, Ordering::SeqCst}, Arc, }, }; use unindent::Unindent as _; -use workspace::Workspace; #[ctor::ctor] fn init_logger() { @@ -48,14 +43,13 @@ fn init_logger() { #[gpui::test(iterations = 10)] async fn test_basic_calls( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_b2: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -76,7 +70,7 @@ async fn test_basic_calls( .await .unwrap(); let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -86,6 +80,7 @@ async fn test_basic_calls( ); // 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"); @@ -93,8 +88,9 @@ async fn test_basic_calls( // User B connects via another client and also receives a ring on the newly-connected client. let _client_b2 = server.create_client(cx_b2, "user_b").await; let active_call_b2 = cx_b2.read(ActiveCall::global); + let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming()); - deterministic.run_until_parked(); + executor.run_until_parked(); let call_b2 = incoming_call_b2.next().await.unwrap().unwrap(); assert_eq!(call_b2.calling_user.github_login, "user_a"); @@ -103,10 +99,11 @@ async fn test_basic_calls( .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()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -123,6 +120,7 @@ async fn test_basic_calls( ); // Call user C from client B. + let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); active_call_b .update(cx_b, |call, cx| { @@ -131,7 +129,7 @@ async fn test_basic_calls( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -153,7 +151,7 @@ async fn test_basic_calls( active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap()); assert!(incoming_call_c.next().await.unwrap().is_none()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -177,7 +175,7 @@ async fn test_basic_calls( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -201,9 +199,10 @@ async fn test_basic_calls( .await .unwrap(); assert!(incoming_call_c.next().await.unwrap().is_none()); + let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -240,13 +239,14 @@ async fn test_basic_calls( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // User B observes the remote screen sharing track. assert_eq!(events_b.borrow().len(), 1); let event_b = events_b.borrow().first().unwrap().clone(); if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b { assert_eq!(participant_id, client_a.peer_id().unwrap()); + room_b.read_with(cx_b, |room, _| { assert_eq!( room.remote_participants()[&client_a.user_id().unwrap()] @@ -264,6 +264,7 @@ async fn test_basic_calls( let event_c = events_c.borrow().first().unwrap().clone(); if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c { assert_eq!(participant_id, client_a.peer_id().unwrap()); + room_c.read_with(cx_c, |room, _| { assert_eq!( room.remote_participants()[&client_a.user_id().unwrap()] @@ -285,7 +286,7 @@ async fn test_basic_calls( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -315,8 +316,10 @@ async fn test_basic_calls( .test_live_kit_server .disconnect_client(client_b.user_id().unwrap().to_string()) .await; - deterministic.run_until_parked(); + executor.run_until_parked(); + active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none())); + active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none())); assert_eq!( room_participants(&room_a, cx_a), @@ -343,14 +346,13 @@ async fn test_basic_calls( #[gpui::test(iterations = 10)] async fn test_calling_multiple_users_simultaneously( - deterministic: Arc, + executor: BackgroundExecutor, 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 mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -381,7 +383,7 @@ async fn test_calling_multiple_users_simultaneously( c_invite.await.unwrap(); let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -397,7 +399,7 @@ async fn test_calling_multiple_users_simultaneously( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -418,10 +420,12 @@ async fn test_calling_multiple_users_simultaneously( accept_c.await.unwrap(); accept_d.await.unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); + let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone()); assert_eq!( room_participants(&room_a, cx_a), @@ -471,13 +475,12 @@ async fn test_calling_multiple_users_simultaneously( #[gpui::test(iterations = 10)] async fn test_joining_channels_and_calling_multiple_users_simultaneously( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -515,7 +518,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( join_channel_2.await.unwrap(); let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(channel_id(&room_a, cx_a), Some(channel_2)); @@ -543,7 +546,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( join_channel.await.unwrap(); let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), @@ -579,20 +582,19 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( c_invite.await.unwrap(); active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); } #[gpui::test(iterations = 10)] async fn test_room_uniqueness( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_a2: &mut TestAppContext, cx_b: &mut TestAppContext, cx_b2: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let _client_a2 = server.create_client(cx_a2, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -623,9 +625,11 @@ async fn test_room_uniqueness( }) .await .unwrap_err(); + active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none())); // User B receives the call from user A. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); let call_b1 = incoming_call_b.next().await.unwrap().unwrap(); assert_eq!(call_b1.calling_user.github_login, "user_a"); @@ -651,6 +655,7 @@ async fn test_room_uniqueness( }) .await .unwrap_err(); + active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none())); // User B joins the room and calling them after they've joined still fails. @@ -672,6 +677,7 @@ async fn test_room_uniqueness( }) .await .unwrap_err(); + active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none())); // Client C can successfully call client B after client B leaves the room. @@ -679,26 +685,25 @@ async fn test_room_uniqueness( .update(cx_b, |call, cx| call.hang_up(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); active_call_c .update(cx_c, |call, cx| { call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); let call_b2 = incoming_call_b.next().await.unwrap().unwrap(); assert_eq!(call_b2.calling_user.github_login, "user_c"); } #[gpui::test(iterations = 10)] async fn test_client_disconnecting_from_room( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -715,17 +720,20 @@ async fn test_client_disconnecting_from_room( }) .await .unwrap(); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); // User B receives the call and joins the room. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); incoming_call_b.next().await.unwrap().unwrap(); 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()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -743,8 +751,8 @@ async fn test_client_disconnecting_from_room( // User A automatically reconnects to the room upon disconnection. server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT); - deterministic.run_until_parked(); + executor.advance_clock(RECEIVE_TIMEOUT); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -763,8 +771,10 @@ async fn test_client_disconnecting_from_room( // When user A disconnects, both client A and B clear their room on the active call. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none())); + active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none())); assert_eq!( room_participants(&room_a, cx_a), @@ -783,7 +793,7 @@ async fn test_client_disconnecting_from_room( // Allow user A to reconnect to the server. server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT); // Call user B again from client A. active_call_a @@ -792,17 +802,20 @@ async fn test_client_disconnecting_from_room( }) .await .unwrap(); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); // User B receives the call and joins the room. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); incoming_call_b.next().await.unwrap().unwrap(); 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()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -824,7 +837,7 @@ async fn test_client_disconnecting_from_room( .test_live_kit_server .disconnect_client(client_b.user_id().unwrap().to_string()) .await; - deterministic.run_until_parked(); + executor.run_until_parked(); active_call_a.update(cx_a, |call, _| assert!(call.room().is_none())); active_call_b.update(cx_b, |call, _| assert!(call.room().is_none())); assert_eq!( @@ -845,14 +858,13 @@ async fn test_client_disconnecting_from_room( #[gpui::test(iterations = 10)] async fn test_server_restarts( - deterministic: Arc, + executor: BackgroundExecutor, 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 mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; client_a .fs() @@ -898,31 +910,37 @@ async fn test_server_restarts( }) .await .unwrap(); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); // User B receives the call and joins the room. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); assert!(incoming_call_b.next().await.unwrap().is_some()); 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()); // User C receives the call and joins the room. + let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); assert!(incoming_call_c.next().await.unwrap().is_some()); active_call_c .update(cx_c, |call, cx| call.accept_incoming(cx)) .await .unwrap(); + let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); // User D receives the call but doesn't join the room yet. + let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming()); assert!(incoming_call_d.next().await.unwrap().is_some()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -950,7 +968,7 @@ async fn test_server_restarts( // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room. client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - deterministic.advance_clock(RECONNECT_TIMEOUT); + executor.advance_clock(RECONNECT_TIMEOUT); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -979,7 +997,8 @@ async fn test_server_restarts( .update(cx_d, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone()); assert_eq!( room_participants(&room_a, cx_a), @@ -1024,7 +1043,7 @@ async fn test_server_restarts( // The server finishes restarting, cleaning up stale connections. server.start().await.unwrap(); - deterministic.advance_clock(CLEANUP_TIMEOUT); + executor.advance_clock(CLEANUP_TIMEOUT); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -1059,7 +1078,7 @@ async fn test_server_restarts( .update(cx_d, |call, cx| call.hang_up(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -1098,9 +1117,10 @@ async fn test_server_restarts( .unwrap(); // User D receives the call but doesn't join the room yet. + let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming()); assert!(incoming_call_d.next().await.unwrap().is_some()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -1123,7 +1143,7 @@ async fn test_server_restarts( client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - deterministic.advance_clock(RECONNECT_TIMEOUT); + executor.advance_clock(RECONNECT_TIMEOUT); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -1145,19 +1165,18 @@ async fn test_server_restarts( // The server finishes restarting, cleaning up stale connections and canceling the // call to user D because the room has become empty. server.start().await.unwrap(); - deterministic.advance_clock(CLEANUP_TIMEOUT); + executor.advance_clock(CLEANUP_TIMEOUT); assert!(incoming_call_d.next().await.unwrap().is_none()); } #[gpui::test(iterations = 10)] async fn test_calls_on_multiple_connections( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b1: &mut TestAppContext, cx_b2: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b1 = server.create_client(cx_b1, "user_b").await; let client_b2 = server.create_client(cx_b2, "user_b").await; @@ -1168,7 +1187,9 @@ async fn test_calls_on_multiple_connections( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b1 = cx_b1.read(ActiveCall::global); let active_call_b2 = cx_b2.read(ActiveCall::global); + let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming()); + let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming()); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); @@ -1180,14 +1201,14 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); // User B declines the call on one of the two connections, causing both connections // to stop ringing. active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); @@ -1198,7 +1219,7 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); @@ -1208,13 +1229,13 @@ async fn test_calls_on_multiple_connections( .update(cx_b2, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); // User B disconnects the client that is not on the call. Everything should be fine. client_b1.disconnect(&cx_b1.to_async()); - deterministic.advance_clock(RECEIVE_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT); client_b1 .authenticate_and_connect(false, &cx_b1.to_async()) .await @@ -1225,14 +1246,14 @@ async fn test_calls_on_multiple_connections( .update(cx_b2, |call, cx| call.hang_up(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); active_call_a .update(cx_a, |call, cx| { call.invite(client_b1.user_id().unwrap(), None, cx) }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); @@ -1243,7 +1264,7 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); @@ -1254,7 +1275,7 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); @@ -1263,7 +1284,7 @@ async fn test_calls_on_multiple_connections( .update(cx_a, |call, cx| call.hang_up(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); @@ -1274,27 +1295,27 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); // User A disconnects, causing both connections to stop ringing. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); // User A reconnects automatically, then calls user B again. server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT); active_call_a .update(cx_a, |call, cx| { call.invite(client_b1.user_id().unwrap(), None, cx) }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); @@ -1302,187 +1323,19 @@ async fn test_calls_on_multiple_connections( server.forbid_connections(); server.disconnect_client(client_b1.peer_id().unwrap()); server.disconnect_client(client_b2.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none())); } -#[gpui::test(iterations = 10)] -async fn test_share_project( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - deterministic.forbid_parking(); - let window_b = cx_b.add_window(|_| EmptyView); - 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; - server - .make_contacts(&mut [(&client_a, cx_a), (&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); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - ".gitignore": "ignored-dir", - "a.txt": "a-contents", - "b.txt": "b-contents", - "ignored-dir": { - "c.txt": "", - "d.txt": "", - } - }), - ) - .await; - - // Invite client B to collaborate on a project - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) - }) - .await - .unwrap(); - - // Join that project as client B - let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); - deterministic.run_until_parked(); - let call = incoming_call_b.borrow().clone().unwrap(); - assert_eq!(call.calling_user.github_login, "user_a"); - let initial_project = call.initial_project.unwrap(); - active_call_b - .update(cx_b, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - let client_b_peer_id = client_b.peer_id().unwrap(); - let project_b = client_b - .build_remote_project(initial_project.id, cx_b) - .await; - let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); - - deterministic.run_until_parked(); - project_a.read_with(cx_a, |project, _| { - let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); - assert_eq!(client_b_collaborator.replica_id, replica_id_b); - }); - project_b.read_with(cx_b, |project, cx| { - let worktree = project.worktrees(cx).next().unwrap().read(cx); - assert_eq!( - worktree.paths().map(AsRef::as_ref).collect::>(), - [ - Path::new(".gitignore"), - Path::new("a.txt"), - Path::new("b.txt"), - Path::new("ignored-dir"), - ] - ); - }); - - project_b - .update(cx_b, |project, cx| { - let worktree = project.worktrees(cx).next().unwrap(); - let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); - project.expand_entry(worktree_id, entry.id, cx).unwrap() - }) - .await - .unwrap(); - project_b.read_with(cx_b, |project, cx| { - let worktree = project.worktrees(cx).next().unwrap().read(cx); - assert_eq!( - worktree.paths().map(AsRef::as_ref).collect::>(), - [ - Path::new(".gitignore"), - Path::new("a.txt"), - Path::new("b.txt"), - Path::new("ignored-dir"), - Path::new("ignored-dir/c.txt"), - Path::new("ignored-dir/d.txt"), - ] - ); - }); - - // Open the same file as client B and client A. - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) - .await - .unwrap(); - buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); - project_a.read_with(cx_a, |project, cx| { - assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) - }); - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) - .await - .unwrap(); - - let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); - - // Client A sees client B's selection - deterministic.run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - buffer - .snapshot() - .remote_selections_in_range(Anchor::MIN..Anchor::MAX) - .count() - == 1 - }); - - // Edit the buffer as client B and see that edit as client A. - editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); - deterministic.run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "ok, b-contents") - }); - - // Client B can invite client C on a project shared by client A. - active_call_b - .update(cx_b, |call, cx| { - call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) - }) - .await - .unwrap(); - - let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); - deterministic.run_until_parked(); - let call = incoming_call_c.borrow().clone().unwrap(); - assert_eq!(call.calling_user.github_login, "user_b"); - let initial_project = call.initial_project.unwrap(); - active_call_c - .update(cx_c, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - let _project_c = client_c - .build_remote_project(initial_project.id, cx_c) - .await; - - // Client B closes the editor, and client A sees client B's selections removed. - cx_b.update(move |_| drop(editor_b)); - deterministic.run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - buffer - .snapshot() - .remote_selections_in_range(Anchor::MIN..Anchor::MAX) - .count() - == 0 - }); -} - #[gpui::test(iterations = 10)] async fn test_unshare_project( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -1509,9 +1362,11 @@ async fn test_unshare_project( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); + + let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); let project_b = client_b.build_remote_project(project_id, cx_b).await; - deterministic.run_until_parked(); + executor.run_until_parked(); + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b @@ -1524,8 +1379,9 @@ async fn test_unshare_project( .update(cx_b, |call, cx| call.hang_up(cx)) .await .unwrap(); - deterministic.run_until_parked(); - assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + executor.run_until_parked(); + + assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected())); // Client C opens the project. let project_c = client_c.build_remote_project(project_id, cx_c).await; @@ -1534,9 +1390,11 @@ async fn test_unshare_project( project_a .update(cx_a, |project, cx| project.unshare(cx)) .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - assert!(project_c.read_with(cx_c, |project, _| project.is_read_only())); + + assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected())); // Client C can open the project again after client A re-shares. let project_id = active_call_a @@ -1544,7 +1402,8 @@ async fn test_unshare_project( .await .unwrap(); let project_c2 = client_c.build_remote_project(project_id, cx_c).await; - deterministic.run_until_parked(); + executor.run_until_parked(); + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_c2 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) @@ -1556,130 +1415,23 @@ async fn test_unshare_project( .update(cx_a, |call, cx| call.hang_up(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); + project_c2.read_with(cx_c, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); assert!(project.collaborators().is_empty()); }); } -#[gpui::test(iterations = 10)] -async fn test_host_disconnect( - 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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().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; - deterministic.run_until_parked(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - - let window_b = - cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); - let workspace_b = window_b.root(cx_b); - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "b.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); - editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); - assert!(window_b.is_edited(cx_b)); - - // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); - project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - project_b.read_with(cx_b, |project, _| project.is_read_only()); - assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - - // Ensure client B's edited state is reset and that the whole window is blurred. - window_b.read_with(cx_b, |cx| { - assert_eq!(cx.focused_view_id(), None); - }); - assert!(!window_b.is_edited(cx_b)); - - // Ensure client B is not prompted to save edits when closing window after disconnecting. - let can_close = workspace_b - .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) - .await - .unwrap(); - assert!(can_close); - - // Allow client A to reconnect to the server. - server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT); - - // Client B calls client A again after they reconnected. - let active_call_b = cx_b.read(ActiveCall::global); - active_call_b - .update(cx_b, |call, cx| { - call.invite(client_a.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - active_call_a - .update(cx_a, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - - active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Drop client A's connection again. We should still unshare it successfully. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); -} - #[gpui::test(iterations = 10)] async fn test_project_reconnect( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -1739,8 +1491,7 @@ async fn test_project_reconnect( let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await; let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await; let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await; - let worktree_a1 = - project_a1.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); + let worktree_a1 = project_a1.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); let project1_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx)) .await @@ -1757,7 +1508,7 @@ async fn test_project_reconnect( let project_b1 = client_b.build_remote_project(project1_id, cx_b).await; let project_b2 = client_b.build_remote_project(project2_id, cx_b).await; let project_b3 = client_b.build_remote_project(project3_id, cx_b).await; - deterministic.run_until_parked(); + executor.run_until_parked(); let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| { assert!(worktree.as_local().unwrap().is_shared()); @@ -1769,12 +1520,14 @@ async fn test_project_reconnect( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| { assert!(tree.as_local().unwrap().is_shared()); tree.id() }); - deterministic.run_until_parked(); + executor.run_until_parked(); + project_b1.read_with(cx_b, |project, cx| { assert!(project.worktree_for_id(worktree2_id, cx).is_some()) }); @@ -1791,15 +1544,18 @@ async fn test_project_reconnect( // Drop client A's connection. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT); + project_a1.read_with(cx_a, |project, _| { assert!(project.is_shared()); assert_eq!(project.collaborators().len(), 1); }); + project_b1.read_with(cx_b, |project, _| { - assert!(!project.is_read_only()); + assert!(!project.is_disconnected()); assert_eq!(project.collaborators().len(), 1); }); + worktree_a1.read_with(cx_a, |tree, _| { assert!(tree.as_local().unwrap().is_shared()) }); @@ -1842,11 +1598,12 @@ async fn test_project_reconnect( worktree_a3 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; + let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| { assert!(!tree.as_local().unwrap().is_shared()); tree.id() }); - deterministic.run_until_parked(); + executor.run_until_parked(); // While client A is disconnected, close project 2 cx_a.update(|_| drop(project_a2)); @@ -1854,7 +1611,7 @@ async fn test_project_reconnect( // While client A is disconnected, mutate a buffer on both the host and the guest. buffer_a1.update(cx_a, |buf, cx| buf.edit([(0..0, "W")], None, cx)); buffer_b1.update(cx_b, |buf, cx| buf.edit([(1..1, "Z")], None, cx)); - deterministic.run_until_parked(); + executor.run_until_parked(); // Client A reconnects. Their project is re-shared, and client B re-joins it. server.allow_connections(); @@ -1862,7 +1619,8 @@ async fn test_project_reconnect( .authenticate_and_connect(false, &cx_a.to_async()) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + project_a1.read_with(cx_a, |project, cx| { assert!(project.is_shared()); assert!(worktree_a1.read(cx).as_local().unwrap().is_shared()); @@ -1894,8 +1652,9 @@ async fn test_project_reconnect( vec!["w.txt", "x.txt", "y.txt"] ); }); + project_b1.read_with(cx_b, |project, cx| { - assert!(!project.is_read_only()); + assert!(!project.is_disconnected()); assert_eq!( project .worktree_for_id(worktree1_id, cx) @@ -1928,15 +1687,19 @@ async fn test_project_reconnect( vec!["w.txt", "x.txt", "y.txt"] ); }); - project_b2.read_with(cx_b, |project, _| assert!(project.is_read_only())); - project_b3.read_with(cx_b, |project, _| assert!(!project.is_read_only())); + + project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected())); + + project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected())); + buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ")); + buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WaZ")); // Drop client B's connection. server.forbid_connections(); server.disconnect_client(client_b.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT); // While client B is disconnected, add and remove files from client A's project client_a @@ -1956,7 +1719,8 @@ async fn test_project_reconnect( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| { assert!(tree.as_local().unwrap().is_shared()); tree.id() @@ -1964,12 +1728,12 @@ async fn test_project_reconnect( project_a1.update(cx_a, |project, cx| { project.remove_worktree(worktree3_id, cx) }); - deterministic.run_until_parked(); + executor.run_until_parked(); // While client B is disconnected, mutate a buffer on both the host and the guest. buffer_a1.update(cx_a, |buf, cx| buf.edit([(1..1, "X")], None, cx)); buffer_b1.update(cx_b, |buf, cx| buf.edit([(2..2, "Y")], None, cx)); - deterministic.run_until_parked(); + executor.run_until_parked(); // While disconnected, close project 3 cx_a.update(|_| drop(project_a3)); @@ -1980,9 +1744,10 @@ async fn test_project_reconnect( .authenticate_and_connect(false, &cx_b.to_async()) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + project_b1.read_with(cx_b, |project, cx| { - assert!(!project.is_read_only()); + assert!(!project.is_disconnected()); assert_eq!( project .worktree_for_id(worktree1_id, cx) @@ -2015,19 +1780,21 @@ async fn test_project_reconnect( vec!["z.txt"] ); }); - project_b3.read_with(cx_b, |project, _| assert!(project.is_read_only())); + + project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected())); + buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ")); + buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WXaYZ")); } #[gpui::test(iterations = 10)] async fn test_active_call_events( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; client_a.fs().insert_tree("/a", json!({})).await; @@ -2049,7 +1816,7 @@ async fn test_active_call_events( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]); assert_eq!( mem::take(&mut *events_b.borrow_mut()), @@ -2057,7 +1824,7 @@ async fn test_active_call_events( owner: Arc::new(User { id: client_a.user_id().unwrap(), github_login: "user_a".to_string(), - avatar: None, + avatar_uri: "avatar_a".into(), }), project_id: project_a_id, worktree_root_names: vec!["a".to_string()], @@ -2068,14 +1835,14 @@ async fn test_active_call_events( .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( mem::take(&mut *events_a.borrow_mut()), vec![room::Event::RemoteProjectShared { owner: Arc::new(User { id: client_b.user_id().unwrap(), github_login: "user_b".to_string(), - avatar: None, + avatar_uri: "avatar_b".into(), }), project_id: project_b_id, worktree_root_names: vec!["b".to_string()] @@ -2089,7 +1856,7 @@ async fn test_active_call_events( .await .unwrap(); assert_eq!(project_b_id_2, project_b_id); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]); assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); } @@ -2111,12 +1878,11 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc>> #[gpui::test(iterations = 10)] async fn test_room_location( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; client_a.fs().insert_tree("/a", json!({})).await; @@ -2153,9 +1919,11 @@ async fn test_room_location( server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), @@ -2171,7 +1939,7 @@ async fn test_room_location( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), @@ -2192,7 +1960,7 @@ async fn test_room_location( .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), @@ -2213,7 +1981,7 @@ async fn test_room_location( .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), @@ -2239,7 +2007,7 @@ async fn test_room_location( .update(cx_b, |call, cx| call.set_location(None, cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), @@ -2257,7 +2025,7 @@ async fn test_room_location( ); fn participant_locations( - room: &ModelHandle, + room: &Model, cx: &TestAppContext, ) -> Vec<(String, ParticipantLocation)> { room.read_with(cx, |room, _| { @@ -2276,13 +2044,12 @@ async fn test_room_location( #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -2324,7 +2091,8 @@ async fn test_propagate_saves_and_fs_changes( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap()); + + let worktree_a = project_a.read_with(cx_a, |p, _| p.worktrees().next().unwrap()); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await @@ -2333,8 +2101,10 @@ async fn test_propagate_saves_and_fs_changes( // Join that worktree as clients B and C. let project_b = client_b.build_remote_project(project_id, cx_b).await; let project_c = client_c.build_remote_project(project_id, cx_c).await; - let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap()); - let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap()); + + let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap()); + + let worktree_c = project_c.read_with(cx_c, |p, _| p.worktrees().next().unwrap()); // Open and edit a buffer as both guests B and C. let buffer_b = project_b @@ -2345,9 +2115,11 @@ async fn test_propagate_saves_and_fs_changes( .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx)) .await .unwrap(); + buffer_b.read_with(cx_b, |buffer, _| { assert_eq!(&*buffer.language().unwrap().name(), "Rust"); }); + buffer_c.read_with(cx_c, |buffer, _| { assert_eq!(&*buffer.language().unwrap().name(), "Rust"); }); @@ -2360,19 +2132,23 @@ async fn test_propagate_saves_and_fs_changes( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, ")); buffer_a.update(cx_a, |buf, cx| { buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx) }); - deterministic.run_until_parked(); + executor.run_until_parked(); + buffer_a.read_with(cx_a, |buf, _| { assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a"); }); + buffer_b.read_with(cx_b, |buf, _| { assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a"); }); + buffer_c.read_with(cx_c, |buf, _| { assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a"); }); @@ -2388,9 +2164,12 @@ async fn test_propagate_saves_and_fs_changes( "hi-a, i-am-c, i-am-b, i-am-a" ); - deterministic.run_until_parked(); + executor.run_until_parked(); + buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty())); + buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty())); + buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty())); // Make changes on host's file system, see those changes on guest worktrees. @@ -2409,7 +2188,7 @@ async fn test_propagate_saves_and_fs_changes( .await .unwrap(); client_a.fs().insert_file("/a/file4", "4".into()).await; - deterministic.run_until_parked(); + executor.run_until_parked(); worktree_a.read_with(cx_a, |tree, _| { assert_eq!( @@ -2419,6 +2198,7 @@ async fn test_propagate_saves_and_fs_changes( ["file1.js", "file3", "file4"] ) }); + worktree_b.read_with(cx_b, |tree, _| { assert_eq!( tree.paths() @@ -2427,6 +2207,7 @@ async fn test_propagate_saves_and_fs_changes( ["file1.js", "file3", "file4"] ) }); + worktree_c.read_with(cx_c, |tree, _| { assert_eq!( tree.paths() @@ -2437,14 +2218,17 @@ async fn test_propagate_saves_and_fs_changes( }); // Ensure buffer files are updated as well. + buffer_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js")); assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); }); + buffer_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js")); assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); }); + buffer_c.read_with(cx_c, |buffer, _| { assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js")); assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); @@ -2453,11 +2237,13 @@ async fn test_propagate_saves_and_fs_changes( let new_buffer_a = project_a .update(cx_a, |p, cx| p.create_buffer("", None, cx)) .unwrap(); + let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()); let new_buffer_b = project_b .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx)) .await .unwrap(); + new_buffer_b.read_with(cx_b, |buffer, _| { assert!(buffer.file().is_none()); }); @@ -2472,7 +2258,8 @@ async fn test_propagate_saves_and_fs_changes( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); + new_buffer_b.read_with(cx_b, |buffer_b, _| { assert_eq!( buffer_b.file().unwrap().path().as_ref(), @@ -2488,12 +2275,11 @@ async fn test_propagate_saves_and_fs_changes( #[gpui::test(iterations = 10)] async fn test_git_diff_base_change( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -2558,9 +2344,10 @@ async fn test_git_diff_base_change( .unwrap(); // Wait for it to catch up to the new diff - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test diffing + buffer_local_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( @@ -2578,9 +2365,10 @@ async fn test_git_diff_base_change( .unwrap(); // Wait remote buffer to catch up to the new diff - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test diffing + buffer_remote_a.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( @@ -2597,9 +2385,10 @@ async fn test_git_diff_base_change( ); // Wait for buffer_local_a to receive it - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test new diffing + buffer_local_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); @@ -2612,6 +2401,7 @@ async fn test_git_diff_base_change( }); // Smoke test B + buffer_remote_a.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( @@ -2648,9 +2438,10 @@ async fn test_git_diff_base_change( .unwrap(); // Wait for it to catch up to the new diff - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test diffing + buffer_local_b.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( @@ -2668,9 +2459,10 @@ async fn test_git_diff_base_change( .unwrap(); // Wait remote buffer to catch up to the new diff - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test diffing + buffer_remote_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( @@ -2687,9 +2479,10 @@ async fn test_git_diff_base_change( ); // Wait for buffer_local_b to receive it - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test new diffing + buffer_local_b.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); println!("{:?}", buffer.as_rope().to_string()); @@ -2711,6 +2504,7 @@ async fn test_git_diff_base_change( }); // Smoke test B + buffer_remote_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( @@ -2724,13 +2518,12 @@ async fn test_git_diff_base_change( #[gpui::test] async fn test_git_branch_name( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -2763,7 +2556,7 @@ async fn test_git_branch_name( .set_branch_name(Path::new("/dir/.git"), Some("branch-1")); // Wait for it to catch up to the new branch - deterministic.run_until_parked(); + executor.run_until_parked(); #[track_caller] fn assert_branch(branch_name: Option>, project: &Project, cx: &AppContext) { @@ -2776,9 +2569,11 @@ async fn test_git_branch_name( } // Smoke test branch reading + project_local.read_with(cx_a, |project, cx| { assert_branch(Some("branch-1"), project, cx) }); + project_remote.read_with(cx_b, |project, cx| { assert_branch(Some("branch-1"), project, cx) }); @@ -2788,18 +2583,21 @@ async fn test_git_branch_name( .set_branch_name(Path::new("/dir/.git"), Some("branch-2")); // Wait for buffer_local_a to receive it - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test branch reading + project_local.read_with(cx_a, |project, cx| { assert_branch(Some("branch-2"), project, cx) }); + project_remote.read_with(cx_b, |project, cx| { assert_branch(Some("branch-2"), project, cx) }); let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; - deterministic.run_until_parked(); + executor.run_until_parked(); + project_remote_c.read_with(cx_c, |project, cx| { assert_branch(Some("branch-2"), project, cx) }); @@ -2807,13 +2605,12 @@ async fn test_git_branch_name( #[gpui::test] async fn test_git_status_sync( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -2856,7 +2653,7 @@ async fn test_git_status_sync( let project_remote = client_b.build_remote_project(project_id, cx_b).await; // Wait for it to catch up to the new status - deterministic.run_until_parked(); + executor.run_until_parked(); #[track_caller] fn assert_status( @@ -2874,10 +2671,12 @@ async fn test_git_status_sync( } // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); + project_remote.read_with(cx_b, |project, cx| { assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); @@ -2892,9 +2691,10 @@ async fn test_git_status_sync( ); // Wait for buffer_local_a to receive it - deterministic.run_until_parked(); + executor.run_until_parked(); // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { assert_status( &Path::new(A_TXT), @@ -2909,6 +2709,7 @@ async fn test_git_status_sync( cx, ); }); + project_remote.read_with(cx_b, |project, cx| { assert_status( &Path::new(A_TXT), @@ -2926,7 +2727,7 @@ async fn test_git_status_sync( // And synchronization while joining let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; - deterministic.run_until_parked(); + executor.run_until_parked(); project_remote_c.read_with(cx_c, |project, cx| { assert_status( @@ -2946,12 +2747,11 @@ async fn test_git_status_sync( #[gpui::test(iterations = 10)] async fn test_fs_operations( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -2976,8 +2776,9 @@ async fn test_fs_operations( .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); - let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); + let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); + + let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap()); let entry = project_b .update(cx_b, |project, cx| { @@ -2986,6 +2787,7 @@ async fn test_fs_operations( .await .unwrap() .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -2995,6 +2797,7 @@ async fn test_fs_operations( ["a.txt", "b.txt", "c.txt"] ); }); + worktree_b.read_with(cx_b, |worktree, _| { assert_eq!( worktree @@ -3010,7 +2813,9 @@ async fn test_fs_operations( project.rename_entry(entry.id, Path::new("d.txt"), cx) }) .await + .unwrap() .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -3020,6 +2825,7 @@ async fn test_fs_operations( ["a.txt", "b.txt", "d.txt"] ); }); + worktree_b.read_with(cx_b, |worktree, _| { assert_eq!( worktree @@ -3037,6 +2843,7 @@ async fn test_fs_operations( .await .unwrap() .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -3046,6 +2853,7 @@ async fn test_fs_operations( ["DIR", "a.txt", "b.txt", "d.txt"] ); }); + worktree_b.read_with(cx_b, |worktree, _| { assert_eq!( worktree @@ -3061,19 +2869,23 @@ async fn test_fs_operations( project.create_entry((worktree_id, "DIR/e.txt"), false, cx) }) .await + .unwrap() .unwrap(); project_b .update(cx_b, |project, cx| { project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx) }) .await + .unwrap() .unwrap(); project_b .update(cx_b, |project, cx| { project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) }) .await + .unwrap() .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -3091,6 +2903,7 @@ async fn test_fs_operations( ] ); }); + worktree_b.read_with(cx_b, |worktree, _| { assert_eq!( worktree @@ -3114,7 +2927,9 @@ async fn test_fs_operations( project.copy_entry(entry.id, Path::new("f.txt"), cx) }) .await + .unwrap() .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -3133,6 +2948,7 @@ async fn test_fs_operations( ] ); }); + worktree_b.read_with(cx_b, |worktree, _| { assert_eq!( worktree @@ -3158,7 +2974,7 @@ async fn test_fs_operations( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( @@ -3169,6 +2985,7 @@ async fn test_fs_operations( ["a.txt", "b.txt", "d.txt", "f.txt"] ); }); + worktree_b.read_with(cx_b, |worktree, _| { assert_eq!( worktree @@ -3185,6 +3002,7 @@ async fn test_fs_operations( }) .await .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( worktree @@ -3194,6 +3012,7 @@ async fn test_fs_operations( ["a.txt", "b.txt", "f.txt"] ); }); + worktree_b.read_with(cx_b, |worktree, _| { assert_eq!( worktree @@ -3207,12 +3026,11 @@ async fn test_fs_operations( #[gpui::test(iterations = 10)] async fn test_local_settings( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -3242,7 +3060,7 @@ async fn test_local_settings( ) .await; let (project_a, _) = client_a.build_local_project("/dir", cx_a).await; - deterministic.run_until_parked(); + executor.run_until_parked(); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await @@ -3250,12 +3068,15 @@ async fn test_local_settings( // As client B, join that project and observe the local settings. let project_b = client_b.build_remote_project(project_id, cx_b).await; - let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); - deterministic.run_until_parked(); + + let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap()); + executor.run_until_parked(); cx_b.read(|cx| { let store = cx.global::(); assert_eq!( - store.local_settings(worktree_b.id()).collect::>(), + store + .local_settings(worktree_b.read(cx).id().to_usize()) + .collect::>(), &[ (Path::new("").into(), r#"{"tab_size":2}"#.to_string()), (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), @@ -3268,11 +3089,13 @@ async fn test_local_settings( .fs() .insert_file("/dir/.zed/settings.json", r#"{}"#.into()) .await; - deterministic.run_until_parked(); + executor.run_until_parked(); cx_b.read(|cx| { let store = cx.global::(); assert_eq!( - store.local_settings(worktree_b.id()).collect::>(), + store + .local_settings(worktree_b.read(cx).id().to_usize()) + .collect::>(), &[ (Path::new("").into(), r#"{}"#.to_string()), (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), @@ -3295,11 +3118,13 @@ async fn test_local_settings( .fs() .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) .await; - deterministic.run_until_parked(); + executor.run_until_parked(); cx_b.read(|cx| { let store = cx.global::(); assert_eq!( - store.local_settings(worktree_b.id()).collect::>(), + store + .local_settings(worktree_b.read(cx).id().to_usize()) + .collect::>(), &[ (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()), @@ -3321,15 +3146,17 @@ async fn test_local_settings( .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // As client B, reconnect and see the changed settings. server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT); cx_b.read(|cx| { let store = cx.global::(); assert_eq!( - store.local_settings(worktree_b.id()).collect::>(), + store + .local_settings(worktree_b.read(cx).id().to_usize()) + .collect::>(), &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),] ) }); @@ -3337,12 +3164,11 @@ async fn test_local_settings( #[gpui::test(iterations = 10)] async fn test_buffer_conflict_after_save( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -3373,6 +3199,7 @@ async fn test_buffer_conflict_after_save( .unwrap(); buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx)); + buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -3384,13 +3211,15 @@ async fn test_buffer_conflict_after_save( }) .await .unwrap(); - cx_a.foreground().forbid_parking(); + buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty())); + buffer_b.read_with(cx_b, |buf, _| { assert!(!buf.has_conflict()); }); buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx)); + buffer_b.read_with(cx_b, |buf, _| { assert!(buf.is_dirty()); assert!(!buf.has_conflict()); @@ -3399,12 +3228,11 @@ async fn test_buffer_conflict_after_save( #[gpui::test(iterations = 10)] async fn test_buffer_reloading( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -3433,6 +3261,7 @@ async fn test_buffer_reloading( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); + buffer_b.read_with(cx_b, |buf, _| { assert!(!buf.is_dirty()); assert!(!buf.has_conflict()); @@ -3445,7 +3274,9 @@ async fn test_buffer_reloading( .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows) .await .unwrap(); - cx_a.foreground().run_until_parked(); + + executor.run_until_parked(); + buffer_b.read_with(cx_b, |buf, _| { assert_eq!(buf.text(), new_contents.to_string()); assert!(!buf.is_dirty()); @@ -3456,12 +3287,11 @@ async fn test_buffer_reloading( #[gpui::test(iterations = 10)] async fn test_editing_while_guest_opens_buffer( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -3487,124 +3317,29 @@ async fn test_editing_while_guest_opens_buffer( .unwrap(); // Start opening the same buffer as client B - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer); // Edit the buffer as client A while client B is still opening it. - cx_b.background().simulate_random_delay().await; + cx_b.executor().simulate_random_delay().await; buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx)); - cx_b.background().simulate_random_delay().await; + cx_b.executor().simulate_random_delay().await; buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx)); let text = buffer_a.read_with(cx_a, |buf, _| buf.text()); let buffer_b = buffer_b.await.unwrap(); - cx_a.foreground().run_until_parked(); + executor.run_until_parked(); + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text)); } -#[gpui::test] -async fn test_newline_above_or_below_does_not_move_guest_cursor( - 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); - - client_a - .fs() - .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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; - - // Open a buffer as client A - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - let window_a = cx_a.add_window(|_| EmptyView); - let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); - let mut editor_cx_a = EditorTestContext { - cx: cx_a, - window: window_a.into(), - editor: editor_a, - }; - - // Open a buffer as client B - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - let window_b = cx_b.add_window(|_| EmptyView); - let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); - let mut editor_cx_b = EditorTestContext { - cx: cx_b, - window: window_b.into(), - editor: editor_b, - }; - - // Test newline above - editor_cx_a.set_selections_state(indoc! {" - Some textˇ - "}); - editor_cx_b.set_selections_state(indoc! {" - Some textˇ - "}); - editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); - deterministic.run_until_parked(); - editor_cx_a.assert_editor_state(indoc! {" - ˇ - Some text - "}); - editor_cx_b.assert_editor_state(indoc! {" - - Some textˇ - "}); - - // Test newline below - editor_cx_a.set_selections_state(indoc! {" - - Some textˇ - "}); - editor_cx_b.set_selections_state(indoc! {" - - Some textˇ - "}); - editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); - deterministic.run_until_parked(); - editor_cx_a.assert_editor_state(indoc! {" - - Some text - ˇ - "}); - editor_cx_b.assert_editor_state(indoc! {" - - Some textˇ - - "}); -} - #[gpui::test(iterations = 10)] async fn test_leaving_worktree_while_opening_buffer( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -3624,30 +3359,29 @@ async fn test_leaving_worktree_while_opening_buffer( let project_b = client_b.build_remote_project(project_id, cx_b).await; // See that a guest has joined as client A. - cx_a.foreground().run_until_parked(); + executor.run_until_parked(); + project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1)); // Begin opening a buffer as client B, but leave the project before the open completes. - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer); cx_b.update(|_| drop(project_b)); drop(buffer_b); // See that the guest has left. - cx_a.foreground().run_until_parked(); + executor.run_until_parked(); + project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty())); } #[gpui::test(iterations = 10)] async fn test_canceling_buffer_opening( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -3680,7 +3414,7 @@ async fn test_canceling_buffer_opening( let buffer_b = project_b.update(cx_b, |p, cx| { p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx) }); - deterministic.simulate_random_delay().await; + executor.simulate_random_delay().await; drop(buffer_b); // Try opening the same buffer again as client B, and ensure we can @@ -3691,18 +3425,18 @@ async fn test_canceling_buffer_opening( }) .await .unwrap(); + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc")); } #[gpui::test(iterations = 10)] async fn test_leaving_project( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -3730,13 +3464,16 @@ async fn test_leaving_project( let project_c = client_c.build_remote_project(project_id, cx_c).await; // Client A sees that a guest has joined. - deterministic.run_until_parked(); + executor.run_until_parked(); + project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 2); }); + project_b1.read_with(cx_b, |project, _| { assert_eq!(project.collaborators().len(), 2); }); + project_c.read_with(cx_c, |project, _| { assert_eq!(project.collaborators().len(), 2); }); @@ -3744,54 +3481,64 @@ async fn test_leaving_project( // Client B opens a buffer. let buffer_b1 = project_b1 .update(cx_b, |project, cx| { - let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id(); + let worktree_id = project.worktrees().next().unwrap().read(cx).id(); project.open_buffer((worktree_id, "a.txt"), cx) }) .await .unwrap(); + buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); // Drop client B's project and ensure client A and client C observe client B leaving. cx_b.update(|_| drop(project_b1)); - deterministic.run_until_parked(); + executor.run_until_parked(); + project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 1); }); + project_c.read_with(cx_c, |project, _| { assert_eq!(project.collaborators().len(), 1); }); // Client B re-joins the project and can open buffers as before. let project_b2 = client_b.build_remote_project(project_id, cx_b).await; - deterministic.run_until_parked(); + executor.run_until_parked(); + project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 2); }); + project_b2.read_with(cx_b, |project, _| { assert_eq!(project.collaborators().len(), 2); }); + project_c.read_with(cx_c, |project, _| { assert_eq!(project.collaborators().len(), 2); }); let buffer_b2 = project_b2 .update(cx_b, |project, cx| { - let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id(); + let worktree_id = project.worktrees().next().unwrap().read(cx).id(); project.open_buffer((worktree_id, "a.txt"), cx) }) .await .unwrap(); + buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); // Drop client B's connection and ensure client A and client C observe client B leaving. client_b.disconnect(&cx_b.to_async()); - deterministic.advance_clock(RECONNECT_TIMEOUT); + executor.advance_clock(RECONNECT_TIMEOUT); + project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 1); }); + project_b2.read_with(cx_b, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); }); + project_c.read_with(cx_c, |project, _| { assert_eq!(project.collaborators().len(), 1); }); @@ -3803,7 +3550,8 @@ async fn test_leaving_project( client_b.app_state.client.clone(), client_b.user_store().clone(), client_b.language_registry().clone(), - FakeFs::new(cx.background()), + FakeFs::new(cx.background_executor().clone()), + ChannelRole::Member, cx, ) }) @@ -3814,28 +3562,30 @@ async fn test_leaving_project( client_c.wait_for_current_user(cx_c).await; server.forbid_connections(); server.disconnect_client(client_c.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - deterministic.run_until_parked(); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.run_until_parked(); + project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 0); }); + project_b2.read_with(cx_b, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); }); + project_c.read_with(cx_c, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); }); } #[gpui::test(iterations = 10)] async fn test_collaborating_with_diagnostics( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).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; @@ -3925,9 +3675,10 @@ async fn test_collaborating_with_diagnostics( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Wait for server to see the diagnostics update. - deterministic.run_until_parked(); + executor.run_until_parked(); // Ensure client B observes the new diagnostics. + project_b.read_with(cx_b, |project, cx| { assert_eq!( project.diagnostic_summaries(false, cx).collect::>(), @@ -3964,7 +3715,7 @@ async fn test_collaborating_with_diagnostics( .detach(); }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( project_c_diagnostic_summaries.borrow().as_slice(), &[( @@ -4004,7 +3755,8 @@ async fn test_collaborating_with_diagnostics( ); // Clients B and C get the updated summaries - deterministic.run_until_parked(); + executor.run_until_parked(); + project_b.read_with(cx_b, |project, cx| { assert_eq!( project.diagnostic_summaries(false, cx).collect::>(), @@ -4021,6 +3773,7 @@ async fn test_collaborating_with_diagnostics( )] ); }); + project_c.read_with(cx_c, |project, cx| { assert_eq!( project.diagnostic_summaries(false, cx).collect::>(), @@ -4039,11 +3792,8 @@ async fn test_collaborating_with_diagnostics( }); // Open the file with the errors on client B. They should be present. - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); buffer_b.read_with(cx_b, |buffer, _| { assert_eq!( @@ -4084,19 +3834,22 @@ async fn test_collaborating_with_diagnostics( diagnostics: vec![], }, ); - deterministic.run_until_parked(); + executor.run_until_parked(); + project_a.read_with(cx_a, |project, cx| { assert_eq!( project.diagnostic_summaries(false, cx).collect::>(), [] ) }); + project_b.read_with(cx_b, |project, cx| { assert_eq!( project.diagnostic_summaries(false, cx).collect::>(), [] ) }); + project_c.read_with(cx_c, |project, cx| { assert_eq!( project.diagnostic_summaries(false, cx).collect::>(), @@ -4107,12 +3860,11 @@ async fn test_collaborating_with_diagnostics( #[gpui::test(iterations = 10)] async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -4236,200 +3988,17 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( } }); - deterministic.run_until_parked(); + executor.run_until_parked(); assert!(disk_based_diagnostics_finished.load(SeqCst)); } -#[gpui::test(iterations = 10)] -async fn test_collaborating_with_completion( - 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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - 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; - - // Open a file in an editor as the guest. - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - let window_b = cx_b.add_window(|_| EmptyView); - let editor_b = window_b.add_view(cx_b, |cx| { - Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) - }); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - cx_a.foreground().run_until_parked(); - buffer_b.read_with(cx_b, |buffer, _| { - assert!(!buffer.completion_triggers().is_empty()) - }); - - // Type a completion trigger character as the guest. - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(".", cx); - cx.focus(&editor_b); - }); - - // Receive a completion request as the host's language server. - // Return some completions from the host's language server. - cx_a.foreground().start_waiting(); - fake_language_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "first_method(…)".into(), - detail: Some("fn(&mut self, B) -> C".into()), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - new_text: "first_method($1)".to_string(), - range: lsp::Range::new( - lsp::Position::new(0, 14), - lsp::Position::new(0, 14), - ), - })), - insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - ..Default::default() - }, - lsp::CompletionItem { - label: "second_method(…)".into(), - detail: Some("fn(&mut self, C) -> D".into()), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - new_text: "second_method()".to_string(), - range: lsp::Range::new( - lsp::Position::new(0, 14), - lsp::Position::new(0, 14), - ), - })), - insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - ..Default::default() - }, - ]))) - }) - .next() - .await - .unwrap(); - cx_a.foreground().finish_waiting(); - - // Open the buffer on the host. - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - cx_a.foreground().run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a. }") - }); - - // Confirm a completion on the guest. - editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); - editor_b.update(cx_b, |editor, cx| { - editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); - assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); - }); - - // Return a resolved completion from the host's language server. - // The resolved completion has an additional text edit. - fake_language_server.handle_request::( - |params, _| async move { - assert_eq!(params.label, "first_method(…)"); - Ok(lsp::CompletionItem { - label: "first_method(…)".into(), - detail: Some("fn(&mut self, B) -> C".into()), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - new_text: "first_method($1)".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), - })), - additional_text_edits: Some(vec![lsp::TextEdit { - new_text: "use d::SomeTrait;\n".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), - }]), - insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - ..Default::default() - }) - }, - ); - - // The additional edit is applied. - cx_a.foreground().run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!( - buffer.text(), - "use d::SomeTrait;\nfn main() { a.first_method() }" - ); - }); - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!( - buffer.text(), - "use d::SomeTrait;\nfn main() { a.first_method() }" - ); - }); -} - #[gpui::test(iterations = 10)] async fn test_reloading_buffer_manually( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -4453,11 +4022,8 @@ async fn test_reloading_buffer_manually( let project_b = client_b.build_remote_project(project_id, cx_b).await; - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); buffer_b.update(cx_b, |buffer, cx| { buffer.edit([(4..7, "six")], None, cx); buffer.edit([(10..11, "6")], None, cx); @@ -4465,7 +4031,8 @@ async fn test_reloading_buffer_manually( assert!(buffer.is_dirty()); assert!(!buffer.has_conflict()); }); - cx_a.foreground().run_until_parked(); + executor.run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;")); client_a @@ -4477,8 +4044,10 @@ async fn test_reloading_buffer_manually( ) .await .unwrap(); - cx_a.foreground().run_until_parked(); + executor.run_until_parked(); + buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict())); + buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict())); project_b @@ -4487,11 +4056,13 @@ async fn test_reloading_buffer_manually( }) .await .unwrap(); + buffer_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.text(), "let seven = 7;"); assert!(!buffer.is_dirty()); assert!(!buffer.has_conflict()); }); + buffer_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.text(), "let seven = 7;"); assert!(!buffer.is_dirty()); @@ -4516,11 +4087,12 @@ async fn test_reloading_buffer_manually( #[gpui::test(iterations = 10)] async fn test_formatting_buffer( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - let mut server = TestServer::start(&deterministic).await; + executor.allow_parking(); + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -4555,11 +4127,8 @@ async fn test_formatting_buffer( .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| async move { @@ -4624,11 +4193,11 @@ async fn test_formatting_buffer( #[gpui::test(iterations = 10)] async fn test_prettier_formatting_buffer( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -4667,22 +4236,16 @@ async fn test_prettier_formatting_buffer( .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; - let buffer_a = cx_a - .background() - .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); + let buffer_a = cx_a.executor().spawn(open_buffer).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; - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); cx_a.update(|cx| { cx.update_global(|store: &mut SettingsStore, cx| { @@ -4716,8 +4279,8 @@ async fn test_prettier_formatting_buffer( }) .await .unwrap(); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + executor.run_until_parked(); assert_eq!( buffer_b.read_with(cx_b, |buffer, _| buffer.text()), buffer_text.to_string() + "\n" + prettier_format_suffix, @@ -4735,8 +4298,8 @@ async fn test_prettier_formatting_buffer( }) .await .unwrap(); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + executor.run_until_parked(); assert_eq!( buffer_b.read_with(cx_b, |buffer, _| buffer.text()), buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix, @@ -4746,12 +4309,11 @@ async fn test_prettier_formatting_buffer( #[gpui::test(iterations = 10)] async fn test_definition( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -4794,11 +4356,8 @@ async fn test_definition( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file on client B. - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); // Request the definition of a symbol as the guest. let fake_language_server = fake_language_servers.next().await.unwrap(); @@ -4817,7 +4376,7 @@ async fn test_definition( .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_1.len(), 1); - assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); + assert_eq!(project_b.read(cx).worktrees().count(), 2); let target_buffer = definitions_1[0].target.buffer.read(cx); assert_eq!( target_buffer.text(), @@ -4846,7 +4405,7 @@ async fn test_definition( .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_2.len(), 1); - assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); + assert_eq!(project_b.read(cx).worktrees().count(), 2); let target_buffer = definitions_2[0].target.buffer.read(cx); assert_eq!( target_buffer.text(), @@ -4894,12 +4453,11 @@ async fn test_definition( #[gpui::test(iterations = 10)] async fn test_references( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -4942,11 +4500,8 @@ async fn test_references( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file on client B. - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); // Request references to a symbol as the guest. let fake_language_server = fake_language_servers.next().await.unwrap(); @@ -4977,7 +4532,7 @@ async fn test_references( .unwrap(); cx_b.read(|cx| { assert_eq!(references.len(), 3); - assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); + assert_eq!(project_b.read(cx).worktrees().count(), 2); let two_buffer = references[0].buffer.read(cx); let three_buffer = references[2].buffer.read(cx); @@ -4999,12 +4554,11 @@ async fn test_references( #[gpui::test(iterations = 10)] async fn test_project_search( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -5086,12 +4640,11 @@ async fn test_project_search( #[gpui::test(iterations = 10)] async fn test_document_highlights( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -5129,11 +4682,8 @@ async fn test_document_highlights( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file on client B. - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))) - .await - .unwrap(); + let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_b).await.unwrap(); // Request document highlights as the guest. let fake_language_server = fake_language_servers.next().await.unwrap(); @@ -5172,6 +4722,7 @@ async fn test_document_highlights( .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)) .await .unwrap(); + buffer_b.read_with(cx_b, |buffer, _| { let snapshot = buffer.snapshot(); @@ -5192,12 +4743,11 @@ async fn test_document_highlights( #[gpui::test(iterations = 10)] async fn test_lsp_hover( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -5235,11 +4785,8 @@ async fn test_lsp_hover( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file as the guest - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))) - .await - .unwrap(); + let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)); + let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); // Request hover information as the guest. let fake_language_server = fake_language_servers.next().await.unwrap(); @@ -5278,6 +4825,7 @@ async fn test_lsp_hover( .await .unwrap() .unwrap(); + buffer_b.read_with(cx_b, |buffer, _| { let snapshot = buffer.snapshot(); assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29); @@ -5301,12 +4849,11 @@ async fn test_lsp_hover( #[gpui::test(iterations = 10)] async fn test_project_symbols( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -5351,11 +4898,9 @@ async fn test_project_symbols( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Cause the language server to start. - let _buffer = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx))) - .await - .unwrap(); + let open_buffer_task = + project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)); + let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| async move { @@ -5390,10 +4935,11 @@ async fn test_project_symbols( }) .await .unwrap(); - buffer_b_2.read_with(cx_b, |buffer, _| { + + buffer_b_2.read_with(cx_b, |buffer, cx| { assert_eq!( - buffer.file().unwrap().path().as_ref(), - Path::new("../crate-2/two.rs") + buffer.file().unwrap().full_path(cx), + Path::new("/code/crate-2/two.rs") ); }); @@ -5411,13 +4957,12 @@ async fn test_project_symbols( #[gpui::test(iterations = 10)] async fn test_open_buffer_while_getting_definition_pointing_to_it( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, mut rng: StdRng, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server @@ -5454,11 +4999,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let buffer_b1 = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) - .await - .unwrap(); + let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); + let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| async move { @@ -5486,540 +5028,15 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( assert_eq!(definitions[0].target.buffer, buffer_b2); } -#[gpui::test(iterations = 10)] -async fn test_collaborating_with_code_actions( - 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_b.update(editor::init); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", - "other.rs": "pub fn foo() -> usize { 4 }", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Join the project as client B. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - let window_b = - cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); - let workspace_b = window_b.root(cx_b); - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let mut fake_language_server = fake_language_servers.next().await.unwrap(); - let mut requests = fake_language_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!(params.range.start, lsp::Position::new(0, 0)); - assert_eq!(params.range.end, lsp::Position::new(0, 0)); - Ok(None) - }); - deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); - requests.next().await; - - // Move cursor to a location that contains code actions. - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) - }); - cx.focus(&editor_b); - }); - - let mut requests = fake_language_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!(params.range.start, lsp::Position::new(1, 31)); - assert_eq!(params.range.end, lsp::Position::new(1, 31)); - - Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "Inline into all callers".to_string(), - edit: Some(lsp::WorkspaceEdit { - changes: Some( - [ - ( - lsp::Url::from_file_path("/a/main.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(1, 22), - lsp::Position::new(1, 34), - ), - "4".to_string(), - )], - ), - ( - lsp::Url::from_file_path("/a/other.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 27), - ), - "".to_string(), - )], - ), - ] - .into_iter() - .collect(), - ), - ..Default::default() - }), - data: Some(json!({ - "codeActionParams": { - "range": { - "start": {"line": 1, "column": 31}, - "end": {"line": 1, "column": 31}, - } - } - })), - ..Default::default() - }, - )])) - }); - deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); - requests.next().await; - - // Toggle code actions and wait for them to display. - editor_b.update(cx_b, |editor, cx| { - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from_indicator: false, - }, - cx, - ); - }); - cx_a.foreground().run_until_parked(); - editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); - - fake_language_server.remove_request_handler::(); - - // Confirming the code action will trigger a resolve request. - let confirm_action = workspace_b - .update(cx_b, |workspace, cx| { - Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx) - }) - .unwrap(); - fake_language_server.handle_request::( - |_, _| async move { - Ok(lsp::CodeAction { - title: "Inline into all callers".to_string(), - edit: Some(lsp::WorkspaceEdit { - changes: Some( - [ - ( - lsp::Url::from_file_path("/a/main.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(1, 22), - lsp::Position::new(1, 34), - ), - "4".to_string(), - )], - ), - ( - lsp::Url::from_file_path("/a/other.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 27), - ), - "".to_string(), - )], - ), - ] - .into_iter() - .collect(), - ), - ..Default::default() - }), - ..Default::default() - }) - }, - ); - - // After the action is confirmed, an editor containing both modified files is opened. - confirm_action.await.unwrap(); - let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - code_action_editor.update(cx_b, |editor, cx| { - assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); - editor.undo(&Undo, cx); - assert_eq!( - editor.text(cx), - "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" - ); - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_collaborating_with_renames( - 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_b.update(editor::init); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;" - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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 window_b = - cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); - let workspace_b = window_b.root(cx_b); - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "one.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let fake_language_server = fake_language_servers.next().await.unwrap(); - - // Move cursor to a location that can be renamed. - let prepare_rename = editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([7..7])); - editor.rename(&Rename, cx).unwrap() - }); - - fake_language_server - .handle_request::(|params, _| async move { - assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); - assert_eq!(params.position, lsp::Position::new(0, 7)); - Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( - lsp::Position::new(0, 6), - lsp::Position::new(0, 9), - )))) - }) - .next() - .await - .unwrap(); - prepare_rename.await.unwrap(); - editor_b.update(cx_b, |editor, cx| { - use editor::ToOffset; - let rename = editor.pending_rename().unwrap(); - let buffer = editor.buffer().read(cx).snapshot(cx); - assert_eq!( - rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), - 6..9 - ); - rename.editor.update(cx, |rename_editor, cx| { - rename_editor.buffer().update(cx, |rename_buffer, cx| { - rename_buffer.edit([(0..3, "THREE")], None, cx); - }); - }); - }); - - let confirm_rename = workspace_b.update(cx_b, |workspace, cx| { - Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() - }); - fake_language_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri.as_str(), - "file:///dir/one.rs" - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 6) - ); - assert_eq!(params.new_name, "THREE"); - Ok(Some(lsp::WorkspaceEdit { - changes: Some( - [ - ( - lsp::Url::from_file_path("/dir/one.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - "THREE".to_string(), - )], - ), - ( - lsp::Url::from_file_path("/dir/two.rs").unwrap(), - vec![ - lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 24), - lsp::Position::new(0, 27), - ), - "THREE".to_string(), - ), - lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 35), - lsp::Position::new(0, 38), - ), - "THREE".to_string(), - ), - ], - ), - ] - .into_iter() - .collect(), - ), - ..Default::default() - })) - }) - .next() - .await - .unwrap(); - confirm_rename.await.unwrap(); - - let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - rename_editor.update(cx_b, |editor, cx| { - assert_eq!( - editor.text(cx), - "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" - ); - editor.undo(&Undo, cx); - assert_eq!( - editor.text(cx), - "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" - ); - editor.redo(&Redo, cx); - assert_eq!( - editor.text(cx), - "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" - ); - }); - - // Ensure temporary rename edits cannot be undone/redone. - editor_b.update(cx_b, |editor, cx| { - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "const ONE: usize = 1;"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "const ONE: usize = 1;"); - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "const THREE: usize = 1;"); - }) -} - -#[gpui::test(iterations = 10)] -async fn test_language_server_statuses( - 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_b.update(editor::init); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-language-server", - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - "main.rs": "const ONE: usize = 1;", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - - let _buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.start_progress("the-token").await; - fake_language_server.notify::(lsp::ProgressParams { - token: lsp::NumberOrString::String("the-token".to_string()), - value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( - lsp::WorkDoneProgressReport { - message: Some("the-message".to_string()), - ..Default::default() - }, - )), - }); - deterministic.run_until_parked(); - project_a.read_with(cx_a, |project, _| { - let status = project.language_server_statuses().next().unwrap(); - assert_eq!(status.name, "the-language-server"); - assert_eq!(status.pending_work.len(), 1); - assert_eq!( - status.pending_work["the-token"].message.as_ref().unwrap(), - "the-message" - ); - }); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - deterministic.run_until_parked(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - project_b.read_with(cx_b, |project, _| { - let status = project.language_server_statuses().next().unwrap(); - assert_eq!(status.name, "the-language-server"); - }); - - fake_language_server.notify::(lsp::ProgressParams { - token: lsp::NumberOrString::String("the-token".to_string()), - value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( - lsp::WorkDoneProgressReport { - message: Some("the-message-2".to_string()), - ..Default::default() - }, - )), - }); - deterministic.run_until_parked(); - project_a.read_with(cx_a, |project, _| { - let status = project.language_server_statuses().next().unwrap(); - assert_eq!(status.name, "the-language-server"); - assert_eq!(status.pending_work.len(), 1); - assert_eq!( - status.pending_work["the-token"].message.as_ref().unwrap(), - "the-message-2" - ); - }); - project_b.read_with(cx_b, |project, _| { - let status = project.language_server_statuses().next().unwrap(); - assert_eq!(status.name, "the-language-server"); - assert_eq!(status.pending_work.len(), 1); - assert_eq!( - status.pending_work["the-token"].message.as_ref().unwrap(), - "the-message-2" - ); - }); -} - #[gpui::test(iterations = 10)] async fn test_contacts( - deterministic: Arc, + executor: BackgroundExecutor, 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 mut server = TestServer::start(executor.clone()).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; @@ -6032,7 +5049,7 @@ async fn test_contacts( let active_call_c = cx_c.read(ActiveCall::global); let _active_call_d = cx_d.read(ActiveCall::global); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6058,7 +5075,7 @@ async fn test_contacts( server.disconnect_client(client_c.peer_id().unwrap()); server.forbid_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); assert_eq!( contacts(&client_a, cx_a), [ @@ -6082,7 +5099,7 @@ async fn test_contacts( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6112,7 +5129,7 @@ async fn test_contacts( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6140,7 +5157,7 @@ async fn test_contacts( server .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)]) .await; - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6169,7 +5186,7 @@ async fn test_contacts( ); active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6203,7 +5220,7 @@ async fn test_contacts( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6235,7 +5252,7 @@ async fn test_contacts( .update(cx_a, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6269,7 +5286,7 @@ async fn test_contacts( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6301,7 +5318,7 @@ async fn test_contacts( .update(cx_a, |call, cx| call.hang_up(cx)) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6335,7 +5352,7 @@ async fn test_contacts( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), [ @@ -6365,7 +5382,7 @@ async fn test_contacts( server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); assert_eq!(contacts(&client_a, cx_a), []); assert_eq!( contacts(&client_b, cx_b), @@ -6395,7 +5412,7 @@ async fn test_contacts( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( contacts(&client_b, cx_b), [ @@ -6430,7 +5447,7 @@ async fn test_contacts( #[gpui::test(iterations = 10)] async fn test_contact_requests( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_a2: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -6438,10 +5455,8 @@ async fn test_contact_requests( cx_c: &mut TestAppContext, cx_c2: &mut TestAppContext, ) { - deterministic.forbid_parking(); - // Connect to a server as 3 clients. - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_a2 = server.create_client(cx_a2, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -6468,7 +5483,7 @@ async fn test_contact_requests( }) .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // All users see the pending request appear in all their clients. assert_eq!( @@ -6500,7 +5515,7 @@ async fn test_contact_requests( disconnect_and_reconnect(&client_a, cx_a).await; disconnect_and_reconnect(&client_b, cx_b).await; disconnect_and_reconnect(&client_c, cx_c).await; - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( client_a.summarize_contacts(cx_a).outgoing_requests, &["user_b"] @@ -6523,7 +5538,7 @@ async fn test_contact_requests( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // User B sees user A as their contact now in all client, and the incoming request from them is removed. let contacts_b = client_b.summarize_contacts(cx_b); @@ -6545,7 +5560,7 @@ async fn test_contact_requests( disconnect_and_reconnect(&client_a, cx_a).await; disconnect_and_reconnect(&client_b, cx_b).await; disconnect_and_reconnect(&client_c, cx_c).await; - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]); assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]); assert_eq!( @@ -6567,7 +5582,7 @@ async fn test_contact_requests( .await .unwrap(); - deterministic.run_until_parked(); + executor.run_until_parked(); // User B doesn't see user C as their contact, and the incoming request from them is removed. let contacts_b = client_b.summarize_contacts(cx_b); @@ -6589,7 +5604,7 @@ async fn test_contact_requests( disconnect_and_reconnect(&client_a, cx_a).await; disconnect_and_reconnect(&client_b, cx_b).await; disconnect_and_reconnect(&client_c, cx_c).await; - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]); assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]); assert!(client_b @@ -6614,12 +5629,11 @@ async fn test_contact_requests( #[gpui::test(iterations = 10)] async fn test_join_call_after_screen_was_shared( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -6637,8 +5651,9 @@ async fn test_join_call_after_screen_was_shared( }) .await .unwrap(); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -6648,6 +5663,7 @@ async fn test_join_call_after_screen_was_shared( ); // 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"); @@ -6673,10 +5689,11 @@ async fn test_join_call_after_screen_was_shared( .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()); - deterministic.run_until_parked(); + executor.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { @@ -6693,6 +5710,7 @@ async fn test_join_call_after_screen_was_shared( ); // Ensure User B sees User A's screenshare. + room_b.read_with(cx_b, |room, _| { assert_eq!( room.remote_participants() @@ -6704,754 +5722,3 @@ async fn test_join_call_after_screen_was_shared( ); }); } - -#[gpui::test(iterations = 10)] -async fn test_on_input_format_from_host_to_guest( - 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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - 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; - - // Open a file in an editor as the host. - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - let window_a = cx_a.add_window(|_| EmptyView); - let editor_a = window_a.add_view(cx_a, |cx| { - Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) - }); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - cx_b.foreground().run_until_parked(); - - // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. - fake_language_server.handle_request::( - |params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "~<".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), - }])) - }, - ); - - // Open the buffer on the guest and see that the formattings worked - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - - // Type a on type formatting trigger character as the guest. - editor_a.update(cx_a, |editor, cx| { - cx.focus(&editor_a); - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(">", cx); - }); - - cx_b.foreground().run_until_parked(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a>~< }") - }); - - // Undo should remove LSP edits first - editor_a.update(cx_a, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a>~< }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a> }"); - }); - cx_b.foreground().run_until_parked(); - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a> }") - }); - - editor_a.update(cx_a, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a> }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a }"); - }); - cx_b.foreground().run_until_parked(); - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a }") - }); -} - -#[gpui::test(iterations = 10)] -async fn test_on_input_format_from_guest_to_host( - 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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "// Test file", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - 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; - - // Open a file in an editor as the guest. - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - let window_b = cx_b.add_window(|_| EmptyView); - let editor_b = window_b.add_view(cx_b, |cx| { - Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) - }); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - cx_a.foreground().run_until_parked(); - // Type a on type formatting trigger character as the guest. - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(":", cx); - cx.focus(&editor_b); - }); - - // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. - cx_a.foreground().start_waiting(); - fake_language_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "~:".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), - }])) - }) - .next() - .await - .unwrap(); - cx_a.foreground().finish_waiting(); - - // Open the buffer on the host and see that the formattings worked - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) - .await - .unwrap(); - cx_a.foreground().run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a:~: }") - }); - - // Undo should remove LSP edits first - editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a:~: }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a: }"); - }); - cx_a.foreground().run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a: }") - }); - - editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.text(cx), "fn main() { a: }"); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "fn main() { a }"); - }); - cx_a.foreground().run_until_parked(); - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "fn main() { a }") - }); -} - -#[gpui::test(iterations = 10)] -async fn test_mutual_editor_inlay_hint_cache_update( - 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); - - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - }) - }); - }); - }); - cx_b.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - }) - }); - }); - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); - - // Client A opens a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", - "other.rs": "// Test file", - }), - ) - .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(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - cx_a.foreground().start_waiting(); - - // The host opens a rust file. - let _buffer_a = project_a - .update(cx_a, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - let fake_language_server = fake_language_servers.next().await.unwrap(); - let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Set up the language server to return an additional inlay hint on each request. - let edits_made = Arc::new(AtomicUsize::new(0)); - let closure_edits_made = Arc::clone(&edits_made); - fake_language_server - .handle_request::(move |params, _| { - let task_edits_made = Arc::clone(&closure_edits_made); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let edits_made = task_edits_made.load(atomic::Ordering::Acquire); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, edits_made as u32), - label: lsp::InlayHintLabel::String(edits_made.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - - deterministic.run_until_parked(); - - let initial_edit = edits_made.load(atomic::Ordering::Acquire); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![initial_edit.to_string()], - extract_hint_labels(editor), - "Host should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Host editor update the cache version after every cache/view change", - ); - }); - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - deterministic.run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![initial_edit.to_string()], - extract_hint_labels(editor), - "Client should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Guest editor update the cache version after every cache/view change" - ); - }); - - let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); - editor.handle_input(":", cx); - cx.focus(&editor_b); - }); - - deterministic.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_client_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_client_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 2); - }); - - let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input("a change to increment both buffers' versions", cx); - cx.focus(&editor_a); - }); - - deterministic.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_host_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_host_edit.to_string()], - extract_hint_labels(editor), - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), 3); - }); - - let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - fake_language_server - .request::(()) - .await - .expect("inlay refresh request failed"); - - deterministic.run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), - "Host should react to /refresh LSP request" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Host should accepted all edits and bump its cache version every time" - ); - }); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 4, - "Guest should accepted all edits and bump its cache version every time" - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_inlay_hint_refresh_is_forwarded( - 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); - - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: false, - show_type_hints: false, - show_parameter_hints: false, - show_other_hints: false, - }) - }); - }); - }); - cx_b.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - }) - }); - }); - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - client_b.language_registry().add(language); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", - "other.rs": "// Test file", - }), - ) - .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 workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - cx_a.foreground().start_waiting(); - cx_b.foreground().start_waiting(); - - let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let other_hints = Arc::new(AtomicBool::new(false)); - let fake_language_server = fake_language_servers.next().await.unwrap(); - let closure_other_hints = Arc::clone(&other_hints); - fake_language_server - .handle_request::(move |params, _| { - let task_other_hints = Arc::clone(&closure_other_hints); - async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - let other_hints = task_other_hints.load(atomic::Ordering::Acquire); - let character = if other_hints { 0 } else { 2 }; - let label = if other_hints { - "other hint" - } else { - "initial hint" - }; - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, character), - label: lsp::InlayHintLabel::String(label.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - cx_a.foreground().finish_waiting(); - cx_b.foreground().finish_waiting(); - - cx_a.foreground().run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert!( - extract_hint_labels(editor).is_empty(), - "Host should get no hints due to them turned off" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates" - ); - }); - - cx_b.foreground().run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec!["initial hint".to_string()], - extract_hint_labels(editor), - "Client should get its first hints when opens an editor" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 1, - "Should update cache verison after first hints" - ); - }); - - other_hints.fetch_or(true, atomic::Ordering::Release); - fake_language_server - .request::(()) - .await - .expect("inlay refresh request failed"); - cx_a.foreground().run_until_parked(); - editor_a.update(cx_a, |editor, _| { - assert!( - extract_hint_labels(editor).is_empty(), - "Host should get nop hints due to them turned off, even after the /refresh" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 0, - "Turned off hints should not generate version updates, again" - ); - }); - - cx_b.foreground().run_until_parked(); - editor_b.update(cx_b, |editor, _| { - assert_eq!( - vec!["other hint".to_string()], - extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host despite host hints are off" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.version(), - 2, - "Guest should accepted all edits and bump its cache version every time" - ); - }); -} - -fn extract_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for hint in editor.inlay_hint_cache().hints() { - match hint.label { - project::InlayHintLabel::String(s) => labels.push(s), - _ => unreachable!(), - } - } - labels -} diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs index 1114470449..f6066e6409 100644 --- a/crates/collab/src/tests/notification_tests.rs +++ b/crates/collab/src/tests/notification_tests.rs @@ -1,18 +1,19 @@ -use crate::tests::TestServer; -use gpui::{executor::Deterministic, TestAppContext}; +use std::sync::Arc; + +use gpui::{BackgroundExecutor, TestAppContext}; use notifications::NotificationEvent; use parking_lot::Mutex; use rpc::{proto, Notification}; -use std::sync::Arc; + +use crate::tests::TestServer; #[gpui::test] async fn test_notifications( - deterministic: Arc, + executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -42,7 +43,7 @@ async fn test_notifications( // Client B receives a contact request notification and responds to the // request, accepting it. - deterministic.run_until_parked(); + executor.run_until_parked(); client_b.notification_store().update(cx_b, |store, cx| { assert_eq!(store.notification_count(), 1); assert_eq!(store.unread_notification_count(), 1); @@ -72,7 +73,7 @@ async fn test_notifications( }); // Client B sees the notification is now read, and that they responded. - deterministic.run_until_parked(); + executor.run_until_parked(); client_b.notification_store().read_with(cx_b, |store, _| { assert_eq!(store.notification_count(), 1); assert_eq!(store.unread_notification_count(), 0); @@ -127,7 +128,7 @@ async fn test_notifications( // Client B receives a channel invitation notification and responds to the // invitation, accepting it. - deterministic.run_until_parked(); + executor.run_until_parked(); client_b.notification_store().update(cx_b, |store, cx| { assert_eq!(store.notification_count(), 2); assert_eq!(store.unread_notification_count(), 1); @@ -147,7 +148,7 @@ async fn test_notifications( }); // Client B sees the notification is now read, and that they responded. - deterministic.run_until_parked(); + executor.run_until_parked(); client_b.notification_store().read_with(cx_b, |store, _| { assert_eq!(store.notification_count(), 2); assert_eq!(store.unread_notification_count(), 0); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 38bc3f7c12..f980f7d908 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -3,10 +3,14 @@ use crate::db::ChannelRole; use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; use anyhow::Result; use async_trait::async_trait; -use gpui::{executor::Deterministic, TestAppContext}; +use gpui::{BackgroundExecutor, SharedString, TestAppContext}; use rand::prelude::*; use serde_derive::{Deserialize, Serialize}; -use std::{ops::Range, rc::Rc, sync::Arc}; +use std::{ + ops::{Deref, DerefMut, Range}, + rc::Rc, + sync::Arc, +}; use text::Bias; #[gpui::test( @@ -15,10 +19,10 @@ use text::Bias; )] async fn test_random_channel_buffers( cx: &mut TestAppContext, - deterministic: Arc, + executor: BackgroundExecutor, rng: StdRng, ) { - run_randomized_test::(cx, deterministic, rng).await; + run_randomized_test::(cx, executor, rng).await; } struct RandomChannelBufferTest; @@ -26,13 +30,13 @@ struct RandomChannelBufferTest; #[derive(Clone, Serialize, Deserialize)] enum ChannelBufferOperation { JoinChannelNotes { - channel_name: String, + channel_name: SharedString, }, LeaveChannelNotes { - channel_name: String, + channel_name: SharedString, }, EditChannelNotes { - channel_name: String, + channel_name: SharedString, edits: Vec<(Range, Arc)>, }, Noop, @@ -69,11 +73,11 @@ impl RandomizedTest for RandomChannelBufferTest { cx: &TestAppContext, ) -> ChannelBufferOperation { let channel_store = client.channel_store().clone(); - let channel_buffers = client.channel_buffers(); + let mut channel_buffers = client.channel_buffers(); // When signed out, we can't do anything unless a channel buffer is // already open. - if channel_buffers.is_empty() + if channel_buffers.deref_mut().is_empty() && channel_store.read_with(cx, |store, _| store.channel_count() == 0) { return ChannelBufferOperation::Noop; @@ -97,7 +101,7 @@ impl RandomizedTest for RandomChannelBufferTest { } 30..=40 => { - if let Some(buffer) = channel_buffers.iter().choose(rng) { + if let Some(buffer) = channel_buffers.deref().iter().choose(rng) { let channel_name = buffer.read_with(cx, |b, cx| b.channel(cx).unwrap().name.clone()); break ChannelBufferOperation::LeaveChannelNotes { channel_name }; @@ -105,7 +109,7 @@ impl RandomizedTest for RandomChannelBufferTest { } _ => { - if let Some(buffer) = channel_buffers.iter().choose(rng) { + if let Some(buffer) = channel_buffers.deref().iter().choose(rng) { break buffer.read_with(cx, |b, cx| { let channel_name = b.channel(cx).unwrap().name.clone(); let edits = b @@ -147,13 +151,13 @@ impl RandomizedTest for RandomChannelBufferTest { "{}: opening notes for channel {channel_name}", client.username ); - client.channel_buffers().insert(buffer.await?); + client.channel_buffers().deref_mut().insert(buffer.await?); } ChannelBufferOperation::LeaveChannelNotes { channel_name } => { let buffer = cx.update(|cx| { let mut left_buffer = Err(TestError::Inapplicable); - client.channel_buffers().retain(|buffer| { + client.channel_buffers().deref_mut().retain(|buffer| { if buffer.read(cx).channel(cx).unwrap().name == channel_name { left_buffer = Ok(buffer.clone()); false @@ -179,6 +183,7 @@ impl RandomizedTest for RandomChannelBufferTest { .read(|cx| { client .channel_buffers() + .deref() .iter() .find(|buffer| { buffer.read(cx).channel(cx).unwrap().name == channel_name @@ -215,13 +220,6 @@ impl RandomizedTest for RandomChannelBufferTest { Ok(()) } - async fn on_client_added(client: &Rc, cx: &mut TestAppContext) { - let channel_store = client.channel_store(); - while channel_store.read_with(cx, |store, _| store.channel_count() == 0) { - channel_store.next_notification(cx).await; - } - } - async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { let channels = server.app_state.db.all_channels().await.unwrap(); @@ -229,6 +227,7 @@ impl RandomizedTest for RandomChannelBufferTest { client_cx.update(|cx| { client .channel_buffers() + .deref_mut() .retain(|b| b.read(cx).is_connected()); }); } @@ -252,6 +251,7 @@ impl RandomizedTest for RandomChannelBufferTest { client_cx.read(|cx| { if let Some(channel_buffer) = client .channel_buffers() + .deref() .iter() .find(|b| b.read(cx).channel_id == channel_id.to_proto()) { diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index f839333c95..53d47eb6b5 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1,5 +1,5 @@ -use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; -use crate::db::UserId; +use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; +use crate::{db::UserId, tests::run_randomized_test}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use call::ActiveCall; @@ -7,7 +7,7 @@ use collections::{BTreeMap, HashMap}; use editor::Bias; use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt; -use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use gpui::{BackgroundExecutor, Model, TestAppContext}; use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; use lsp::FakeLanguageServer; use pretty_assertions::assert_eq; @@ -18,7 +18,7 @@ use rand::{ }; use serde::{Deserialize, Serialize}; use std::{ - ops::Range, + ops::{Deref, Range}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -31,10 +31,10 @@ use util::ResultExt; )] async fn test_random_project_collaboration( cx: &mut TestAppContext, - deterministic: Arc, + executor: BackgroundExecutor, rng: StdRng, ) { - run_randomized_test::(cx, deterministic, rng).await; + run_randomized_test::(cx, executor, rng).await; } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -295,7 +295,7 @@ impl RandomizedTest for ProjectCollaborationTest { let is_local = project.read_with(cx, |project, _| project.is_local()); let worktree = project.read_with(cx, |project, cx| { project - .worktrees(cx) + .worktrees() .filter(|worktree| { let worktree = worktree.read(cx); worktree.is_visible() @@ -417,7 +417,7 @@ impl RandomizedTest for ProjectCollaborationTest { 81.. => { let worktree = project.read_with(cx, |project, cx| { project - .worktrees(cx) + .worktrees() .filter(|worktree| { let worktree = worktree.read(cx); worktree.is_visible() @@ -624,7 +624,7 @@ impl RandomizedTest for ProjectCollaborationTest { room.join_project( project_id, client.language_registry().clone(), - FakeFs::new(cx.background().clone()), + FakeFs::new(cx.background_executor().clone()), cx, ) })) @@ -782,6 +782,7 @@ impl RandomizedTest for ProjectCollaborationTest { .map_err(|err| anyhow!("save request failed: {:?}", err))?; assert!(buffer .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() }) + .expect("App should not be dropped") .observed_all(&requested_version)); anyhow::Ok(()) }); @@ -817,30 +818,30 @@ impl RandomizedTest for ProjectCollaborationTest { use futures::{FutureExt as _, TryFutureExt as _}; let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left)); - let request = cx.foreground().spawn(project.update(cx, |project, cx| { - match kind { - LspRequestKind::Rename => project - .prepare_rename(buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Completion => project - .completions(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::CodeAction => project - .code_actions(&buffer, offset..offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Definition => project - .definition(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Highlights => project - .document_highlights(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - } - })); + + let process_lsp_request = project.update(cx, |project, cx| match kind { + LspRequestKind::Rename => project + .prepare_rename(buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::Completion => project + .completions(&buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::CodeAction => project + .code_actions(&buffer, offset..offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::Definition => project + .definition(&buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + LspRequestKind::Highlights => project + .document_highlights(&buffer, offset, cx) + .map_ok(|_| ()) + .boxed(), + }); + let request = cx.foreground_executor().spawn(process_lsp_request); if detach { request.detach(); } else { @@ -874,7 +875,7 @@ impl RandomizedTest for ProjectCollaborationTest { ) }); drop(project); - let search = cx.background().spawn(async move { + let search = cx.executor().spawn(async move { let mut results = HashMap::default(); while let Some((buffer, ranges)) = search.next().await { results.entry(buffer).or_insert(ranges); @@ -1075,12 +1076,12 @@ impl RandomizedTest for ProjectCollaborationTest { fake_server.handle_request::({ let fs = fs.clone(); move |_, cx| { - let background = cx.background(); + let background = cx.background_executor(); let mut rng = background.rng(); let count = rng.gen_range::(1..3); let files = fs.as_fake().files(); let files = (0..count) - .map(|_| files.choose(&mut *rng).unwrap().clone()) + .map(|_| files.choose(&mut rng).unwrap().clone()) .collect::>(); async move { log::info!("LSP: Returning definitions in files {:?}", &files); @@ -1100,7 +1101,7 @@ impl RandomizedTest for ProjectCollaborationTest { fake_server.handle_request::( move |_, cx| { let mut highlights = Vec::new(); - let background = cx.background(); + let background = cx.background_executor(); let mut rng = background.rng(); let highlight_count = rng.gen_range(1..=5); @@ -1148,12 +1149,12 @@ impl RandomizedTest for ProjectCollaborationTest { Some((project, cx)) }); - if !guest_project.is_read_only() { + if !guest_project.is_disconnected() { if let Some((host_project, host_cx)) = host_project { let host_worktree_snapshots = host_project.read_with(host_cx, |host_project, cx| { host_project - .worktrees(cx) + .worktrees() .map(|worktree| { let worktree = worktree.read(cx); (worktree.id(), worktree.snapshot()) @@ -1161,7 +1162,7 @@ impl RandomizedTest for ProjectCollaborationTest { .collect::>() }); let guest_worktree_snapshots = guest_project - .worktrees(cx) + .worktrees() .map(|worktree| { let worktree = worktree.read(cx); (worktree.id(), worktree.snapshot()) @@ -1218,7 +1219,7 @@ impl RandomizedTest for ProjectCollaborationTest { } } - for buffer in guest_project.opened_buffers(cx) { + for buffer in guest_project.opened_buffers() { let buffer = buffer.read(cx); assert_eq!( buffer.deferred_ops_len(), @@ -1235,7 +1236,7 @@ impl RandomizedTest for ProjectCollaborationTest { let buffers = client.buffers().clone(); for (guest_project, guest_buffers) in &buffers { let project_id = if guest_project.read_with(client_cx, |project, _| { - project.is_local() || project.is_read_only() + project.is_local() || project.is_disconnected() }) { continue; } else { @@ -1268,8 +1269,8 @@ impl RandomizedTest for ProjectCollaborationTest { for guest_buffer in guest_buffers { let buffer_id = guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id()); - let host_buffer = host_project.read_with(host_cx, |project, cx| { - project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| { + let host_buffer = host_project.read_with(host_cx, |project, _| { + project.buffer_for_id(buffer_id).unwrap_or_else(|| { panic!( "host does not have buffer for guest:{}, peer:{:?}, id:{}", client.username, @@ -1457,10 +1458,10 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation fn buffer_for_full_path( client: &TestClient, - project: &ModelHandle, + project: &Model, full_path: &PathBuf, cx: &TestAppContext, -) -> Option> { +) -> Option> { client .buffers_for_project(project) .iter() @@ -1476,18 +1477,18 @@ fn project_for_root_name( client: &TestClient, root_name: &str, cx: &TestAppContext, -) -> Option> { - if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) { +) -> Option> { + if let Some(ix) = project_ix_for_root_name(&*client.local_projects().deref(), root_name, cx) { return Some(client.local_projects()[ix].clone()); } - if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) { + if let Some(ix) = project_ix_for_root_name(&*client.remote_projects().deref(), root_name, cx) { return Some(client.remote_projects()[ix].clone()); } None } fn project_ix_for_root_name( - projects: &[ModelHandle], + projects: &[Model], root_name: &str, cx: &TestAppContext, ) -> Option { @@ -1499,7 +1500,7 @@ fn project_ix_for_root_name( }) } -fn root_name_for_project(project: &ModelHandle, cx: &TestAppContext) -> String { +fn root_name_for_project(project: &Model, cx: &TestAppContext) -> String { project.read_with(cx, |project, cx| { project .visible_worktrees(cx) @@ -1512,7 +1513,7 @@ fn root_name_for_project(project: &ModelHandle, cx: &TestAppContext) -> } fn project_path_for_full_path( - project: &ModelHandle, + project: &Model, full_path: &Path, cx: &TestAppContext, ) -> Option { @@ -1520,7 +1521,7 @@ fn project_path_for_full_path( let root_name = components.next().unwrap().as_os_str().to_str().unwrap(); let path = components.as_path().into(); let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees(cx).find_map(|worktree| { + project.worktrees().find_map(|worktree| { let worktree = worktree.read(cx); if worktree.root_name() == root_name { Some(worktree.id()) @@ -1533,7 +1534,7 @@ fn project_path_for_full_path( } async fn ensure_project_shared( - project: &ModelHandle, + project: &Model, client: &TestClient, cx: &mut TestAppContext, ) { @@ -1566,9 +1567,10 @@ async fn ensure_project_shared( } } -fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option> { +fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option> { client .local_projects() + .deref() .iter() .chain(client.remote_projects().iter()) .choose(rng) diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index 1cec945282..69bec62460 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -5,7 +5,7 @@ use crate::{ }; use async_trait::async_trait; use futures::StreamExt; -use gpui::{executor::Deterministic, Task, TestAppContext}; +use gpui::{BackgroundExecutor, Task, TestAppContext}; use parking_lot::Mutex; use rand::prelude::*; use rpc::RECEIVE_TIMEOUT; @@ -115,18 +115,17 @@ pub trait RandomizedTest: 'static + Sized { async fn initialize(server: &mut TestServer, users: &[UserTestPlan]); - async fn on_client_added(client: &Rc, cx: &mut TestAppContext); + async fn on_client_added(_client: &Rc, _cx: &mut TestAppContext) {} async fn on_quiesce(server: &mut TestServer, client: &mut [(Rc, TestAppContext)]); } pub async fn run_randomized_test( cx: &mut TestAppContext, - deterministic: Arc, + executor: BackgroundExecutor, rng: StdRng, ) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let mut server = TestServer::start(executor.clone()).await; let plan = TestPlan::::new(&mut server, rng).await; LAST_PLAN.lock().replace({ @@ -144,7 +143,7 @@ pub async fn run_randomized_test( applied.store(true, SeqCst); let did_apply = TestPlan::apply_server_operation( plan.clone(), - deterministic.clone(), + executor.clone(), &mut server, &mut clients, &mut client_tasks, @@ -159,14 +158,14 @@ pub async fn run_randomized_test( } drop(operation_channels); - deterministic.start_waiting(); + executor.start_waiting(); futures::future::join_all(client_tasks).await; - deterministic.finish_waiting(); + executor.finish_waiting(); - deterministic.run_until_parked(); + executor.run_until_parked(); T::on_quiesce(&mut server, &mut clients).await; - for (client, mut cx) in clients { + for (client, cx) in clients { cx.update(|cx| { let store = cx.remove_global::(); cx.clear_globals(); @@ -174,7 +173,7 @@ pub async fn run_randomized_test( drop(client); }); } - deterministic.run_until_parked(); + executor.run_until_parked(); if let Some(path) = &*PLAN_SAVE_PATH { eprintln!("saved test plan to path {:?}", path); @@ -450,7 +449,7 @@ impl TestPlan { async fn apply_server_operation( plan: Arc>, - deterministic: Arc, + deterministic: BackgroundExecutor, server: &mut TestServer, clients: &mut Vec<(Rc, TestAppContext)>, client_tasks: &mut Vec>, @@ -471,28 +470,18 @@ impl TestPlan { username = user.username.clone(); }; log::info!("adding new connection for {}", username); - let next_entity_id = (user_id.0 * 10_000) as usize; - let mut client_cx = TestAppContext::new( - cx.foreground_platform(), - cx.platform(), - deterministic.build_foreground(user_id.0 as usize), - deterministic.build_background(), - cx.font_cache(), - cx.leak_detector(), - next_entity_id, - cx.function_name.clone(), - ); + + let mut client_cx = cx.new_app(); let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded(); let client = Rc::new(server.create_client(&mut client_cx, &username).await); operation_channels.push(operation_tx); clients.push((client.clone(), client_cx.clone())); - client_tasks.push(client_cx.foreground().spawn(Self::simulate_client( - plan.clone(), - client, - operation_rx, - client_cx, - ))); + + let foreground_executor = client_cx.foreground_executor().clone(); + let simulate_client = + Self::simulate_client(plan.clone(), client, operation_rx, client_cx); + client_tasks.push(foreground_executor.spawn(simulate_client)); log::info!("added connection for {}", username); } @@ -514,7 +503,7 @@ impl TestPlan { .collect::>(); assert_eq!(user_connection_ids.len(), 1); let removed_peer_id = user_connection_ids[0].into(); - let (client, mut client_cx) = clients.remove(client_ix); + let (client, client_cx) = clients.remove(client_ix); let client_task = client_tasks.remove(client_ix); operation_channels.remove(client_ix); server.forbid_connections(); @@ -529,7 +518,7 @@ impl TestPlan { for project in client.remote_projects().iter() { project.read_with(&client_cx, |project, _| { assert!( - project.is_read_only(), + project.is_disconnected(), "project {:?} should be read only", project.remote_id() ) @@ -647,7 +636,7 @@ impl TestPlan { log::error!("{} error: {}", client.username, error); } } - cx.background().simulate_random_delay().await; + cx.executor().simulate_random_delay().await; } log::info!("{}: done", client.username); } diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 245f34ebac..034a85961f 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -2,7 +2,7 @@ use crate::{ db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - AppState, + AppState, Config, }; use anyhow::anyhow; use call::ActiveCall; @@ -13,9 +13,10 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; +use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; + use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; @@ -46,17 +47,17 @@ pub struct TestServer { pub struct TestClient { pub username: String, pub app_state: Arc, - channel_store: ModelHandle, - notification_store: ModelHandle, + channel_store: Model, + notification_store: Model, state: RefCell, } #[derive(Default)] struct TestClientState { - local_projects: Vec>, - remote_projects: Vec>, - buffers: HashMap, HashSet>>, - channel_buffers: HashSet>, + local_projects: Vec>, + remote_projects: Vec>, + buffers: HashMap, HashSet>>, + channel_buffers: HashSet>, } pub struct ContactsSummary { @@ -66,22 +67,22 @@ pub struct ContactsSummary { } impl TestServer { - pub async fn start(deterministic: &Arc) -> Self { + pub async fn start(deterministic: BackgroundExecutor) -> Self { static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0); let use_postgres = env::var("USE_POSTGRES").ok(); let use_postgres = use_postgres.as_deref(); let test_db = if use_postgres == Some("true") || use_postgres == Some("1") { - TestDb::postgres(deterministic.build_background()) + TestDb::postgres(deterministic.clone()) } else { - TestDb::sqlite(deterministic.build_background()) + TestDb::sqlite(deterministic.clone()) }; let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst); let live_kit_server = live_kit_client::TestServer::create( format!("http://livekit.{}.test", live_kit_server_id), format!("devkey-{}", live_kit_server_id), format!("secret-{}", live_kit_server_id), - deterministic.build_background(), + deterministic.clone(), ) .unwrap(); let app_state = Self::build_app_state(&test_db, &live_kit_server).await; @@ -93,7 +94,7 @@ impl TestServer { let server = Server::new( epoch, app_state.clone(), - Executor::Deterministic(deterministic.build_background()), + Executor::Deterministic(deterministic.clone()), ); server.start().await.unwrap(); // Advance clock to ensure the server's cleanup task is finished. @@ -124,8 +125,8 @@ impl TestServer { if cx.has_global::() { panic!("Same cx used to create two test clients") } - - cx.set_global(SettingsStore::test(cx)); + let settings = SettingsStore::test(cx); + cx.set_global(settings); }); let http = FakeHttpClient::with_404_response(); @@ -148,7 +149,7 @@ impl TestServer { .user_id }; let client_name = name.to_string(); - let mut client = cx.read(|cx| Client::new(http.clone(), cx)); + let mut client = cx.update(|cx| Client::new(http.clone(), cx)); let server = self.server.clone(); let db = self.app_state.db.clone(); let connection_killers = self.connection_killers.clone(); @@ -182,20 +183,20 @@ impl TestServer { ))) } else { let (client_conn, server_conn, killed) = - Connection::in_memory(cx.background()); + Connection::in_memory(cx.background_executor().clone()); let (connection_id_tx, connection_id_rx) = oneshot::channel(); let user = db .get_user_by_id(user_id) .await .expect("retrieving user failed") .unwrap(); - cx.background() + cx.background_executor() .spawn(server.handle_connection( server_conn, client_name, user, Some(connection_id_tx), - Executor::Deterministic(cx.background()), + Executor::Deterministic(cx.background_executor().clone()), )) .detach(); let connection_id = connection_id_rx.await.unwrap(); @@ -207,11 +208,11 @@ 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 fs = FakeFs::new(cx.executor()); + let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); + let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); let mut language_registry = LanguageRegistry::test(); - language_registry.set_executor(cx.background()); + language_registry.set_executor(cx.executor()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), @@ -219,13 +220,11 @@ impl TestServer { languages: Arc::new(language_registry), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _, _| Task::ready(Ok(())), - background_actions: || &[], node_runtime: FakeNodeRuntime::new(), }); cx.update(|cx| { - theme::init((), cx); + theme::init(theme::LoadThemes::JustBase, cx); Project::init(&client, cx); client::init(&client, cx); language::init(cx); @@ -264,7 +263,7 @@ impl TestServer { pub fn simulate_long_connection_interruption( &self, peer_id: PeerId, - deterministic: &Arc, + deterministic: BackgroundExecutor, ) { self.forbid_connections(); self.disconnect_client(peer_id); @@ -295,7 +294,7 @@ impl TestServer { }) .await .unwrap(); - cx_a.foreground().run_until_parked(); + cx_a.executor().run_until_parked(); client_b .app_state .user_store @@ -338,7 +337,7 @@ impl TestServer { .await .unwrap(); - admin_cx.foreground().run_until_parked(); + admin_cx.executor().run_until_parked(); member_cx .read(ChannelStore::global) @@ -399,7 +398,7 @@ impl TestServer { .await .unwrap(); - cx_b.foreground().run_until_parked(); + cx_b.executor().run_until_parked(); let active_call_b = cx_b.read(ActiveCall::global); active_call_b .update(*cx_b, |call, cx| call.accept_incoming(cx)) @@ -415,7 +414,19 @@ impl TestServer { Arc::new(AppState { db: test_db.db().clone(), live_kit_client: Some(Arc::new(fake_server.create_api_client())), - config: Default::default(), + config: Config { + http_port: 0, + database_url: "".into(), + database_max_connections: 0, + api_token: "".into(), + invite_link_prefix: "".into(), + live_kit_server: None, + live_kit_key: None, + live_kit_secret: None, + rust_log: None, + log_json: None, + zed_environment: "test".into(), + }, }) } } @@ -448,15 +459,15 @@ impl TestClient { self.app_state.fs.as_fake() } - pub fn channel_store(&self) -> &ModelHandle { + pub fn channel_store(&self) -> &Model { &self.channel_store } - pub fn notification_store(&self) -> &ModelHandle { + pub fn notification_store(&self) -> &Model { &self.notification_store } - pub fn user_store(&self) -> &ModelHandle { + pub fn user_store(&self) -> &Model { &self.app_state.user_store } @@ -491,30 +502,26 @@ impl TestClient { .await; } - pub fn local_projects<'a>(&'a self) -> impl Deref>> + 'a { + pub fn local_projects<'a>(&'a self) -> impl Deref>> + 'a { Ref::map(self.state.borrow(), |state| &state.local_projects) } - pub fn remote_projects<'a>(&'a self) -> impl Deref>> + 'a { + pub fn remote_projects<'a>(&'a self) -> impl Deref>> + 'a { Ref::map(self.state.borrow(), |state| &state.remote_projects) } - pub fn local_projects_mut<'a>( - &'a self, - ) -> impl DerefMut>> + 'a { + pub fn local_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects) } - pub fn remote_projects_mut<'a>( - &'a self, - ) -> impl DerefMut>> + 'a { + pub fn remote_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects) } pub fn buffers_for_project<'a>( &'a self, - project: &ModelHandle, - ) -> impl DerefMut>> + 'a { + project: &Model, + ) -> impl DerefMut>> + 'a { RefMut::map(self.state.borrow_mut(), |state| { state.buffers.entry(project.clone()).or_default() }) @@ -522,14 +529,14 @@ impl TestClient { pub fn buffers<'a>( &'a self, - ) -> impl DerefMut, HashSet>>> + 'a + ) -> impl DerefMut, HashSet>>> + 'a { RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers) } pub fn channel_buffers<'a>( &'a self, - ) -> impl DerefMut>> + 'a { + ) -> impl DerefMut>> + 'a { RefMut::map(self.state.borrow_mut(), |state| &mut state.channel_buffers) } @@ -559,7 +566,7 @@ impl TestClient { &self, root_path: impl AsRef, cx: &mut TestAppContext, - ) -> (ModelHandle, WorktreeId) { + ) -> (Model, WorktreeId) { let project = self.build_empty_local_project(cx); let (worktree, _) = project .update(cx, |p, cx| { @@ -573,7 +580,7 @@ impl TestClient { (project, worktree.read_with(cx, |tree, _| tree.id())) } - pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle { + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model { cx.update(|cx| { Project::local( self.client().clone(), @@ -590,7 +597,7 @@ impl TestClient { &self, host_project_id: u64, guest_cx: &mut TestAppContext, - ) -> ModelHandle { + ) -> Model { let active_call = guest_cx.read(ActiveCall::global); let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone()); room.update(guest_cx, |room, cx| { @@ -605,12 +612,12 @@ impl TestClient { .unwrap() } - pub fn build_workspace( - &self, - project: &ModelHandle, - cx: &mut TestAppContext, - ) -> WindowHandle { - cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) + pub fn build_workspace<'a>( + &'a self, + project: &Model, + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } } diff --git a/crates/collab2/.env.toml b/crates/collab2/.env.toml deleted file mode 100644 index 01866012ea..0000000000 --- a/crates/collab2/.env.toml +++ /dev/null @@ -1,12 +0,0 @@ -DATABASE_URL = "postgres://postgres@localhost/zed" -DATABASE_MAX_CONNECTIONS = 5 -HTTP_PORT = 8080 -API_TOKEN = "secret" -INVITE_LINK_PREFIX = "http://localhost:3000/invites/" -ZED_ENVIRONMENT = "development" -LIVE_KIT_SERVER = "http://localhost:7880" -LIVE_KIT_KEY = "devkey" -LIVE_KIT_SECRET = "secret" - -# RUST_LOG=info -# LOG_JSON=true diff --git a/crates/collab2/Cargo.toml b/crates/collab2/Cargo.toml deleted file mode 100644 index 06443afdbc..0000000000 --- a/crates/collab2/Cargo.toml +++ /dev/null @@ -1,99 +0,0 @@ -[package] -authors = ["Nathan Sobo "] -default-run = "collab2" -edition = "2021" -name = "collab2" -version = "0.28.0" -publish = false - -[[bin]] -name = "collab2" - -[[bin]] -name = "seed2" -required-features = ["seed-support"] - -[dependencies] -clock = { path = "../clock" } -collections = { path = "../collections" } -live_kit_server = { path = "../live_kit_server" } -text = { package = "text2", path = "../text2" } -rpc = { package = "rpc2", path = "../rpc2" } -util = { path = "../util" } - -anyhow.workspace = true -async-tungstenite = "0.16" -axum = { version = "0.5", features = ["json", "headers", "ws"] } -axum-extra = { version = "0.3", features = ["erased-json"] } -base64 = "0.13" -clap = { version = "3.1", features = ["derive"], optional = true } -dashmap = "5.4" -envy = "0.4.2" -futures.workspace = true -hyper = "0.14" -lazy_static.workspace = true -lipsum = { version = "0.8", optional = true } -log.workspace = true -nanoid = "0.4" -parking_lot.workspace = true -prometheus = "0.13" -prost.workspace = true -rand.workspace = true -reqwest = { version = "0.11", features = ["json"], optional = true } -scrypt = "0.7" -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.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } -time.workspace = true -tokio = { version = "1", features = ["full"] } -tokio-tungstenite = "0.17" -tonic = "0.6" -tower = "0.4" -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 = { package = "audio2", path = "../audio2" } -collections = { path = "../collections", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -call = { package = "call2", path = "../call2", features = ["test-support"] } -client = { package = "client2", path = "../client2", features = ["test-support"] } -channel = { package = "channel2", path = "../channel2" } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -language = { package = "language2", path = "../language2", features = ["test-support"] } -fs = { package = "fs2", path = "../fs2", features = ["test-support"] } -git = { package = "git3", path = "../git3", features = ["test-support"] } -live_kit_client = { package = "live_kit_client2", path = "../live_kit_client2", features = ["test-support"] } -lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } -node_runtime = { path = "../node_runtime" } -notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] } - -project = { package = "project2", path = "../project2", features = ["test-support"] } -rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } -settings = { package = "settings2", path = "../settings2", features = ["test-support"] } -theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } - -collab_ui = { path = "../collab_ui2", package = "collab_ui2", 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 = { version = "0.12.x", features = ["sqlx-sqlite"] } -serde_json.workspace = true -sqlx = { version = "0.7", features = ["sqlite"] } -unindent.workspace = true - -[features] -seed-support = ["clap", "lipsum", "reqwest"] diff --git a/crates/collab2/README.md b/crates/collab2/README.md deleted file mode 100644 index d766324255..0000000000 --- a/crates/collab2/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Zed Server - -This crate is what we run at https://collab.zed.dev. - -It contains our back-end logic for collaboration, to which we connect from the Zed client via a websocket after authenticating via https://zed.dev, which is a separate repo running on Vercel. diff --git a/crates/collab2/admin_api.conf b/crates/collab2/admin_api.conf deleted file mode 100644 index 5d3b0e65b7..0000000000 --- a/crates/collab2/admin_api.conf +++ /dev/null @@ -1,4 +0,0 @@ -db-uri = "postgres://postgres@localhost/zed" -server-port = 8081 -jwt-secret = "the-postgrest-jwt-secret-for-authorization" -log-level = "info" diff --git a/crates/collab2/basic.conf b/crates/collab2/basic.conf deleted file mode 100644 index c6db392dba..0000000000 --- a/crates/collab2/basic.conf +++ /dev/null @@ -1,12 +0,0 @@ - -[Interface] -PrivateKey = B5Fp/yVfP0QYlb+YJv9ea+EMI1mWODPD3akh91cVjvc= -Address = fdaa:0:2ce3:a7b:bea:0:a:2/120 -DNS = fdaa:0:2ce3::3 - -[Peer] -PublicKey = RKAYPljEJiuaELNDdQIEJmQienT9+LRISfIHwH45HAw= -AllowedIPs = fdaa:0:2ce3::/48 -Endpoint = ord1.gateway.6pn.dev:51820 -PersistentKeepalive = 15 - diff --git a/crates/collab2/k8s/environments/production.sh b/crates/collab2/k8s/environments/production.sh deleted file mode 100644 index cb1d4b4de7..0000000000 --- a/crates/collab2/k8s/environments/production.sh +++ /dev/null @@ -1,4 +0,0 @@ -ZED_ENVIRONMENT=production -RUST_LOG=info -INVITE_LINK_PREFIX=https://zed.dev/invites/ -DATABASE_MAX_CONNECTIONS=85 diff --git a/crates/collab2/k8s/environments/staging.sh b/crates/collab2/k8s/environments/staging.sh deleted file mode 100644 index b9689ccb19..0000000000 --- a/crates/collab2/k8s/environments/staging.sh +++ /dev/null @@ -1,4 +0,0 @@ -ZED_ENVIRONMENT=staging -RUST_LOG=info -INVITE_LINK_PREFIX=https://staging.zed.dev/invites/ -DATABASE_MAX_CONNECTIONS=5 diff --git a/crates/collab2/k8s/manifest.template.yml b/crates/collab2/k8s/manifest.template.yml deleted file mode 100644 index d4a7a7033e..0000000000 --- a/crates/collab2/k8s/manifest.template.yml +++ /dev/null @@ -1,177 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: ${ZED_KUBE_NAMESPACE} - ---- -kind: Service -apiVersion: v1 -metadata: - namespace: ${ZED_KUBE_NAMESPACE} - name: collab - 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: collab - ports: - - name: web - 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 -metadata: - namespace: ${ZED_KUBE_NAMESPACE} - name: collab - -spec: - replicas: 1 - selector: - matchLabels: - app: collab - template: - metadata: - labels: - app: collab - annotations: - ad.datadoghq.com/collab.check_names: | - ["openmetrics"] - ad.datadoghq.com/collab.init_configs: | - [{}] - ad.datadoghq.com/collab.instances: | - [ - { - "openmetrics_endpoint": "http://%%host%%:%%port%%/metrics", - "namespace": "collab_${ZED_KUBE_NAMESPACE}", - "metrics": [".*"] - } - ] - spec: - containers: - - name: collab - image: "${ZED_IMAGE_ID}" - args: - - serve - ports: - - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 5 - readinessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 1 - periodSeconds: 1 - env: - - name: HTTP_PORT - value: "8080" - - name: DATABASE_URL - valueFrom: - secretKeyRef: - name: database - key: url - - name: DATABASE_MAX_CONNECTIONS - value: "${DATABASE_MAX_CONNECTIONS}" - - name: API_TOKEN - valueFrom: - secretKeyRef: - name: api - key: token - - name: LIVE_KIT_SERVER - valueFrom: - secretKeyRef: - name: livekit - key: server - - name: LIVE_KIT_KEY - valueFrom: - secretKeyRef: - name: livekit - key: key - - name: LIVE_KIT_SECRET - valueFrom: - secretKeyRef: - name: livekit - key: secret - - name: INVITE_LINK_PREFIX - value: ${INVITE_LINK_PREFIX} - - name: RUST_BACKTRACE - value: "1" - - name: RUST_LOG - value: ${RUST_LOG} - - name: LOG_JSON - value: "true" - - name: ZED_ENVIRONMENT - value: ${ZED_ENVIRONMENT} - securityContext: - capabilities: - # 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/collab2/k8s/migrate.template.yml b/crates/collab2/k8s/migrate.template.yml deleted file mode 100644 index c890d7b330..0000000000 --- a/crates/collab2/k8s/migrate.template.yml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - namespace: ${ZED_KUBE_NAMESPACE} - name: ${ZED_MIGRATE_JOB_NAME} -spec: - template: - spec: - restartPolicy: Never - containers: - - name: migrator - imagePullPolicy: Always - image: ${ZED_IMAGE_ID} - args: - - migrate - env: - - name: DATABASE_URL - valueFrom: - secretKeyRef: - name: database - key: url diff --git a/crates/collab2/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab2/migrations.sqlite/20221109000000_test_schema.sql deleted file mode 100644 index 775a4c1bbe..0000000000 --- a/crates/collab2/migrations.sqlite/20221109000000_test_schema.sql +++ /dev/null @@ -1,344 +0,0 @@ -CREATE TABLE "users" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "github_login" VARCHAR, - "admin" BOOLEAN, - "email_address" VARCHAR(255) DEFAULT NULL, - "invite_code" VARCHAR(64), - "invite_count" INTEGER NOT NULL DEFAULT 0, - "inviter_id" INTEGER REFERENCES users (id), - "connected_once" BOOLEAN NOT NULL DEFAULT false, - "created_at" TIMESTAMP NOT NULL DEFAULT now, - "metrics_id" TEXT, - "github_user_id" INTEGER -); -CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login"); -CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code"); -CREATE INDEX "index_users_on_email_address" ON "users" ("email_address"); -CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"); - -CREATE TABLE "access_tokens" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "user_id" INTEGER REFERENCES users (id), - "hash" VARCHAR(128) -); -CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id"); - -CREATE TABLE "contacts" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "user_id_a" INTEGER REFERENCES users (id) NOT NULL, - "user_id_b" INTEGER REFERENCES users (id) NOT NULL, - "a_to_b" BOOLEAN NOT NULL, - "should_notify" BOOLEAN NOT NULL, - "accepted" BOOLEAN NOT NULL -); -CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b"); -CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); - -CREATE TABLE "rooms" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "live_kit_room" VARCHAR NOT NULL, - "enviroment" VARCHAR, - "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE -); -CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); - -CREATE TABLE "projects" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL, - "host_user_id" INTEGER REFERENCES users (id) NOT NULL, - "host_connection_id" INTEGER, - "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE, - "unregistered" BOOLEAN NOT NULL DEFAULT FALSE -); -CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id"); -CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id"); - -CREATE TABLE "worktrees" ( - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "id" INTEGER NOT NULL, - "root_name" VARCHAR NOT NULL, - "abs_path" VARCHAR NOT NULL, - "visible" BOOL NOT NULL, - "scan_id" INTEGER NOT NULL, - "is_complete" BOOL NOT NULL DEFAULT FALSE, - "completed_scan_id" INTEGER NOT NULL, - PRIMARY KEY(project_id, id) -); -CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id"); - -CREATE TABLE "worktree_entries" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INTEGER NOT NULL, - "scan_id" INTEGER NOT NULL, - "id" INTEGER NOT NULL, - "is_dir" BOOL NOT NULL, - "path" VARCHAR NOT NULL, - "inode" INTEGER NOT NULL, - "mtime_seconds" INTEGER NOT NULL, - "mtime_nanos" INTEGER NOT NULL, - "is_symlink" BOOL NOT NULL, - "is_external" BOOL NOT NULL, - "is_ignored" BOOL NOT NULL, - "is_deleted" BOOL NOT NULL, - "git_status" INTEGER, - PRIMARY KEY(project_id, worktree_id, id), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id"); -CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id"); - -CREATE TABLE "worktree_repositories" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INTEGER NOT NULL, - "work_directory_id" INTEGER NOT NULL, - "branch" VARCHAR, - "scan_id" INTEGER NOT NULL, - "is_deleted" BOOL NOT NULL, - PRIMARY KEY(project_id, worktree_id, work_directory_id), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, - FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id"); -CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); - -CREATE TABLE "worktree_settings_files" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INTEGER NOT NULL, - "path" VARCHAR NOT NULL, - "content" TEXT, - PRIMARY KEY(project_id, worktree_id, path), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); -CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id"); - -CREATE TABLE "worktree_diagnostic_summaries" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INTEGER NOT NULL, - "path" VARCHAR NOT NULL, - "language_server_id" INTEGER NOT NULL, - "error_count" INTEGER NOT NULL, - "warning_count" INTEGER NOT NULL, - PRIMARY KEY(project_id, worktree_id, path), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id"); -CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id"); - -CREATE TABLE "language_servers" ( - "id" INTEGER NOT NULL, - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "name" VARCHAR NOT NULL, - PRIMARY KEY(project_id, id) -); -CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id"); - -CREATE TABLE "project_collaborators" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "connection_id" INTEGER NOT NULL, - "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, - "user_id" INTEGER NOT NULL, - "replica_id" INTEGER NOT NULL, - "is_host" BOOLEAN NOT NULL -); -CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id"); -CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id"); -CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id"); -CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id"); - -CREATE TABLE "room_participants" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "room_id" INTEGER NOT NULL REFERENCES rooms (id), - "user_id" INTEGER NOT NULL REFERENCES users (id), - "answering_connection_id" INTEGER, - "answering_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE, - "answering_connection_lost" BOOLEAN NOT NULL, - "location_kind" INTEGER, - "location_project_id" INTEGER, - "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, - "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"); -CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id"); -CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id"); -CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id"); -CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id"); - -CREATE TABLE "servers" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "environment" VARCHAR NOT NULL -); - -CREATE TABLE "followers" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE, - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, - "leader_connection_id" INTEGER NOT NULL, - "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, - "follower_connection_id" INTEGER NOT NULL -); -CREATE UNIQUE INDEX - "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" -ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id"); -CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); - -CREATE TABLE "channels" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "visibility" VARCHAR NOT NULL, - "parent_path" TEXT -); - -CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path"); - -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_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce"); - -CREATE TABLE "channel_message_mentions" ( - "message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE, - "start_offset" INTEGER NOT NULL, - "end_offset" INTEGER NOT NULL, - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - PRIMARY KEY(message_id, start_offset) -); - -CREATE TABLE "channel_members" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "admin" BOOLEAN NOT NULL DEFAULT false, - "role" VARCHAR, - "accepted" BOOLEAN NOT NULL DEFAULT false, - "updated_at" TIMESTAMP NOT NULL DEFAULT now -); - -CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); - -CREATE TABLE "buffers" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "epoch" INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); - -CREATE TABLE "buffer_operations" ( - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, - "epoch" INTEGER NOT NULL, - "replica_id" INTEGER NOT NULL, - "lamport_timestamp" INTEGER NOT NULL, - "value" BLOB NOT NULL, - PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) -); - -CREATE TABLE "buffer_snapshots" ( - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, - "epoch" INTEGER NOT NULL, - "text" TEXT NOT NULL, - "operation_serialization_version" INTEGER NOT NULL, - PRIMARY KEY(buffer_id, epoch) -); - -CREATE TABLE "channel_buffer_collaborators" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "connection_id" INTEGER NOT NULL, - "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, - "connection_lost" BOOLEAN NOT NULL DEFAULT false, - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "replica_id" INTEGER NOT NULL -); - -CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); -CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); -CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); - - -CREATE TABLE "feature_flags" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "flag" TEXT NOT NULL UNIQUE -); - -CREATE INDEX "index_feature_flags" ON "feature_flags" ("id"); - - -CREATE TABLE "user_features" ( - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE, - PRIMARY KEY (user_id, feature_id) -); - -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"); - -CREATE TABLE "notification_kinds" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "name" VARCHAR NOT NULL -); - -CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name"); - -CREATE TABLE "notifications" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "created_at" TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, - "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), - "entity_id" INTEGER, - "content" TEXT, - "is_read" BOOLEAN NOT NULL DEFAULT FALSE, - "response" BOOLEAN -); - -CREATE INDEX - "index_notifications_on_recipient_id_is_read_kind_entity_id" - ON "notifications" - ("recipient_id", "is_read", "kind", "entity_id"); diff --git a/crates/collab2/migrations/20210527024318_initial_schema.sql b/crates/collab2/migrations/20210527024318_initial_schema.sql deleted file mode 100644 index 4b06531848..0000000000 --- a/crates/collab2/migrations/20210527024318_initial_schema.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE IF NOT EXISTS "sessions" ( - "id" VARCHAR NOT NULL PRIMARY KEY, - "expires" TIMESTAMP WITH TIME ZONE NULL, - "session" TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS "users" ( - "id" SERIAL PRIMARY KEY, - "github_login" VARCHAR, - "admin" BOOLEAN -); - -CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login"); - -CREATE TABLE IF NOT EXISTS "signups" ( - "id" SERIAL PRIMARY KEY, - "github_login" VARCHAR, - "email_address" VARCHAR, - "about" TEXT -); diff --git a/crates/collab2/migrations/20210607190313_create_access_tokens.sql b/crates/collab2/migrations/20210607190313_create_access_tokens.sql deleted file mode 100644 index 60745a98ba..0000000000 --- a/crates/collab2/migrations/20210607190313_create_access_tokens.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE IF NOT EXISTS "access_tokens" ( - "id" SERIAL PRIMARY KEY, - "user_id" INTEGER REFERENCES users (id), - "hash" VARCHAR(128) -); - -CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id"); diff --git a/crates/collab2/migrations/20210805175147_create_chat_tables.sql b/crates/collab2/migrations/20210805175147_create_chat_tables.sql deleted file mode 100644 index 5bba4689d9..0000000000 --- a/crates/collab2/migrations/20210805175147_create_chat_tables.sql +++ /dev/null @@ -1,46 +0,0 @@ -CREATE TABLE IF NOT EXISTS "orgs" ( - "id" SERIAL PRIMARY KEY, - "name" VARCHAR NOT NULL, - "slug" VARCHAR NOT NULL -); - -CREATE UNIQUE INDEX "index_orgs_slug" ON "orgs" ("slug"); - -CREATE TABLE IF NOT EXISTS "org_memberships" ( - "id" SERIAL PRIMARY KEY, - "org_id" INTEGER REFERENCES orgs (id) NOT NULL, - "user_id" INTEGER REFERENCES users (id) NOT NULL, - "admin" BOOLEAN NOT NULL -); - -CREATE INDEX "index_org_memberships_user_id" ON "org_memberships" ("user_id"); -CREATE UNIQUE INDEX "index_org_memberships_org_id_and_user_id" ON "org_memberships" ("org_id", "user_id"); - -CREATE TABLE IF NOT EXISTS "channels" ( - "id" SERIAL PRIMARY KEY, - "owner_id" INTEGER NOT NULL, - "owner_is_user" BOOLEAN NOT NULL, - "name" VARCHAR NOT NULL -); - -CREATE UNIQUE INDEX "index_channels_owner_and_name" ON "channels" ("owner_is_user", "owner_id", "name"); - -CREATE TABLE IF NOT EXISTS "channel_memberships" ( - "id" SERIAL PRIMARY KEY, - "channel_id" INTEGER REFERENCES channels (id) NOT NULL, - "user_id" INTEGER REFERENCES users (id) NOT NULL, - "admin" BOOLEAN NOT NULL -); - -CREATE INDEX "index_channel_memberships_user_id" ON "channel_memberships" ("user_id"); -CREATE UNIQUE INDEX "index_channel_memberships_channel_id_and_user_id" ON "channel_memberships" ("channel_id", "user_id"); - -CREATE TABLE IF NOT EXISTS "channel_messages" ( - "id" SERIAL PRIMARY KEY, - "channel_id" INTEGER REFERENCES channels (id) NOT NULL, - "sender_id" INTEGER REFERENCES users (id) NOT NULL, - "body" TEXT NOT NULL, - "sent_at" TIMESTAMP -); - -CREATE INDEX "index_channel_messages_channel_id" ON "channel_messages" ("channel_id"); diff --git a/crates/collab2/migrations/20210916123647_add_nonce_to_channel_messages.sql b/crates/collab2/migrations/20210916123647_add_nonce_to_channel_messages.sql deleted file mode 100644 index ee4d4aa319..0000000000 --- a/crates/collab2/migrations/20210916123647_add_nonce_to_channel_messages.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE "channel_messages" -ADD "nonce" UUID NOT NULL DEFAULT gen_random_uuid(); - -CREATE UNIQUE INDEX "index_channel_messages_nonce" ON "channel_messages" ("nonce"); diff --git a/crates/collab2/migrations/20210920192001_add_interests_to_signups.sql b/crates/collab2/migrations/20210920192001_add_interests_to_signups.sql deleted file mode 100644 index 2457abfc75..0000000000 --- a/crates/collab2/migrations/20210920192001_add_interests_to_signups.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE "signups" - ADD "wants_releases" BOOLEAN, - ADD "wants_updates" BOOLEAN, - ADD "wants_community" BOOLEAN; \ No newline at end of file diff --git a/crates/collab2/migrations/20220421165757_drop_signups.sql b/crates/collab2/migrations/20220421165757_drop_signups.sql deleted file mode 100644 index d7cd6e204c..0000000000 --- a/crates/collab2/migrations/20220421165757_drop_signups.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS "signups"; diff --git a/crates/collab2/migrations/20220505144506_add_trigram_index_to_users.sql b/crates/collab2/migrations/20220505144506_add_trigram_index_to_users.sql deleted file mode 100644 index 3d6fd3179a..0000000000 --- a/crates/collab2/migrations/20220505144506_add_trigram_index_to_users.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE INDEX trigram_index_users_on_github_login ON users USING GIN(github_login gin_trgm_ops); diff --git a/crates/collab2/migrations/20220506130724_create_contacts.sql b/crates/collab2/migrations/20220506130724_create_contacts.sql deleted file mode 100644 index 56beb70fd0..0000000000 --- a/crates/collab2/migrations/20220506130724_create_contacts.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS "contacts" ( - "id" SERIAL PRIMARY KEY, - "user_id_a" INTEGER REFERENCES users (id) NOT NULL, - "user_id_b" INTEGER REFERENCES users (id) NOT NULL, - "a_to_b" BOOLEAN NOT NULL, - "should_notify" BOOLEAN NOT NULL, - "accepted" BOOLEAN NOT NULL -); - -CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b"); -CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); diff --git a/crates/collab2/migrations/20220518151305_add_invites_to_users.sql b/crates/collab2/migrations/20220518151305_add_invites_to_users.sql deleted file mode 100644 index 2ac89b649e..0000000000 --- a/crates/collab2/migrations/20220518151305_add_invites_to_users.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE users -ADD email_address VARCHAR(255) DEFAULT NULL, -ADD invite_code VARCHAR(64), -ADD invite_count INTEGER NOT NULL DEFAULT 0, -ADD inviter_id INTEGER REFERENCES users (id), -ADD connected_once BOOLEAN NOT NULL DEFAULT false, -ADD created_at TIMESTAMP NOT NULL DEFAULT NOW(); - -CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code"); diff --git a/crates/collab2/migrations/20220523232954_allow_user_deletes.sql b/crates/collab2/migrations/20220523232954_allow_user_deletes.sql deleted file mode 100644 index ddf3f6f9bd..0000000000 --- a/crates/collab2/migrations/20220523232954_allow_user_deletes.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE contacts DROP CONSTRAINT contacts_user_id_a_fkey; -ALTER TABLE contacts DROP CONSTRAINT contacts_user_id_b_fkey; -ALTER TABLE contacts ADD CONSTRAINT contacts_user_id_a_fkey FOREIGN KEY (user_id_a) REFERENCES users(id) ON DELETE CASCADE; -ALTER TABLE contacts ADD CONSTRAINT contacts_user_id_b_fkey FOREIGN KEY (user_id_b) REFERENCES users(id) ON DELETE CASCADE; -ALTER TABLE users DROP CONSTRAINT users_inviter_id_fkey; -ALTER TABLE users ADD CONSTRAINT users_inviter_id_fkey FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/crates/collab2/migrations/20220620211403_create_projects.sql b/crates/collab2/migrations/20220620211403_create_projects.sql deleted file mode 100644 index d813c9f7a1..0000000000 --- a/crates/collab2/migrations/20220620211403_create_projects.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TABLE IF NOT EXISTS "projects" ( - "id" SERIAL PRIMARY KEY, - "host_user_id" INTEGER REFERENCES users (id) NOT NULL, - "unregistered" BOOLEAN NOT NULL DEFAULT false -); - -CREATE TABLE IF NOT EXISTS "worktree_extensions" ( - "id" SERIAL PRIMARY KEY, - "project_id" INTEGER REFERENCES projects (id) NOT NULL, - "worktree_id" INTEGER NOT NULL, - "extension" VARCHAR(255), - "count" INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS "project_activity_periods" ( - "id" SERIAL PRIMARY KEY, - "duration_millis" INTEGER NOT NULL, - "ended_at" TIMESTAMP NOT NULL, - "user_id" INTEGER REFERENCES users (id) NOT NULL, - "project_id" INTEGER REFERENCES projects (id) NOT NULL -); - -CREATE INDEX "index_project_activity_periods_on_ended_at" ON "project_activity_periods" ("ended_at"); -CREATE UNIQUE INDEX "index_worktree_extensions_on_project_id_and_worktree_id_and_extension" ON "worktree_extensions" ("project_id", "worktree_id", "extension"); \ No newline at end of file diff --git a/crates/collab2/migrations/20220913211150_create_signups.sql b/crates/collab2/migrations/20220913211150_create_signups.sql deleted file mode 100644 index 19559b747c..0000000000 --- a/crates/collab2/migrations/20220913211150_create_signups.sql +++ /dev/null @@ -1,27 +0,0 @@ -CREATE TABLE IF NOT EXISTS "signups" ( - "id" SERIAL PRIMARY KEY, - "email_address" VARCHAR NOT NULL, - "email_confirmation_code" VARCHAR(64) NOT NULL, - "email_confirmation_sent" BOOLEAN NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "device_id" VARCHAR, - "user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, - "inviting_user_id" INTEGER REFERENCES users (id) ON DELETE SET NULL, - - "platform_mac" BOOLEAN NOT NULL, - "platform_linux" BOOLEAN NOT NULL, - "platform_windows" BOOLEAN NOT NULL, - "platform_unknown" BOOLEAN NOT NULL, - - "editor_features" VARCHAR[], - "programming_languages" VARCHAR[] -); - -CREATE UNIQUE INDEX "index_signups_on_email_address" ON "signups" ("email_address"); -CREATE INDEX "index_signups_on_email_confirmation_sent" ON "signups" ("email_confirmation_sent"); - -ALTER TABLE "users" - ADD "github_user_id" INTEGER; - -CREATE INDEX "index_users_on_email_address" ON "users" ("email_address"); -CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"); diff --git a/crates/collab2/migrations/20220929182110_add_metrics_id.sql b/crates/collab2/migrations/20220929182110_add_metrics_id.sql deleted file mode 100644 index 665d6323bf..0000000000 --- a/crates/collab2/migrations/20220929182110_add_metrics_id.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "users" - ADD "metrics_id" uuid NOT NULL DEFAULT gen_random_uuid(); diff --git a/crates/collab2/migrations/20221111092550_reconnection_support.sql b/crates/collab2/migrations/20221111092550_reconnection_support.sql deleted file mode 100644 index 3289f6bbdd..0000000000 --- a/crates/collab2/migrations/20221111092550_reconnection_support.sql +++ /dev/null @@ -1,90 +0,0 @@ -CREATE TABLE IF NOT EXISTS "rooms" ( - "id" SERIAL PRIMARY KEY, - "live_kit_room" VARCHAR NOT NULL -); - -ALTER TABLE "projects" - ADD "room_id" INTEGER REFERENCES rooms (id), - ADD "host_connection_id" INTEGER, - ADD "host_connection_epoch" UUID; -CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch"); - -CREATE TABLE "worktrees" ( - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "id" INT8 NOT NULL, - "root_name" VARCHAR NOT NULL, - "abs_path" VARCHAR NOT NULL, - "visible" BOOL NOT NULL, - "scan_id" INT8 NOT NULL, - "is_complete" BOOL NOT NULL, - PRIMARY KEY(project_id, id) -); -CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id"); - -CREATE TABLE "worktree_entries" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INT8 NOT NULL, - "id" INT8 NOT NULL, - "is_dir" BOOL NOT NULL, - "path" VARCHAR NOT NULL, - "inode" INT8 NOT NULL, - "mtime_seconds" INT8 NOT NULL, - "mtime_nanos" INTEGER NOT NULL, - "is_symlink" BOOL NOT NULL, - "is_ignored" BOOL NOT NULL, - PRIMARY KEY(project_id, worktree_id, id), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id"); -CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id"); - -CREATE TABLE "worktree_diagnostic_summaries" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INT8 NOT NULL, - "path" VARCHAR NOT NULL, - "language_server_id" INT8 NOT NULL, - "error_count" INTEGER NOT NULL, - "warning_count" INTEGER NOT NULL, - PRIMARY KEY(project_id, worktree_id, path), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id"); -CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id"); - -CREATE TABLE "language_servers" ( - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "id" INT8 NOT NULL, - "name" VARCHAR NOT NULL, - PRIMARY KEY(project_id, id) -); -CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id"); - -CREATE TABLE "project_collaborators" ( - "id" SERIAL PRIMARY KEY, - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "connection_id" INTEGER NOT NULL, - "connection_epoch" UUID NOT NULL, - "user_id" INTEGER NOT NULL, - "replica_id" INTEGER NOT NULL, - "is_host" BOOLEAN NOT NULL -); -CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id"); -CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id"); -CREATE INDEX "index_project_collaborators_on_connection_epoch" ON "project_collaborators" ("connection_epoch"); - -CREATE TABLE "room_participants" ( - "id" SERIAL PRIMARY KEY, - "room_id" INTEGER NOT NULL REFERENCES rooms (id), - "user_id" INTEGER NOT NULL REFERENCES users (id), - "answering_connection_id" INTEGER, - "answering_connection_epoch" UUID, - "location_kind" INTEGER, - "location_project_id" INTEGER, - "initial_project_id" INTEGER, - "calling_user_id" INTEGER NOT NULL REFERENCES users (id), - "calling_connection_id" INTEGER NOT NULL, - "calling_connection_epoch" UUID NOT NULL -); -CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id"); -CREATE INDEX "index_room_participants_on_answering_connection_epoch" ON "room_participants" ("answering_connection_epoch"); -CREATE INDEX "index_room_participants_on_calling_connection_epoch" ON "room_participants" ("calling_connection_epoch"); diff --git a/crates/collab2/migrations/20221125192125_add_added_to_mailing_list_to_signups.sql b/crates/collab2/migrations/20221125192125_add_added_to_mailing_list_to_signups.sql deleted file mode 100644 index b154396df1..0000000000 --- a/crates/collab2/migrations/20221125192125_add_added_to_mailing_list_to_signups.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "signups" - ADD "added_to_mailing_list" BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/crates/collab2/migrations/20221207165001_add_connection_lost_to_room_participants.sql b/crates/collab2/migrations/20221207165001_add_connection_lost_to_room_participants.sql deleted file mode 100644 index ed0cf972bc..0000000000 --- a/crates/collab2/migrations/20221207165001_add_connection_lost_to_room_participants.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE "room_participants" - ADD "answering_connection_lost" BOOLEAN NOT NULL DEFAULT FALSE; - -CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_epoch" ON "project_collaborators" ("project_id", "connection_id", "connection_epoch"); -CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id"); -CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_epoch" ON "room_participants" ("answering_connection_id", "answering_connection_epoch"); diff --git a/crates/collab2/migrations/20221213125710_index_room_participants_on_room_id.sql b/crates/collab2/migrations/20221213125710_index_room_participants_on_room_id.sql deleted file mode 100644 index f40ca81906..0000000000 --- a/crates/collab2/migrations/20221213125710_index_room_participants_on_room_id.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id"); diff --git a/crates/collab2/migrations/20221214144346_change_epoch_from_uuid_to_integer.sql b/crates/collab2/migrations/20221214144346_change_epoch_from_uuid_to_integer.sql deleted file mode 100644 index 5e02f76ce2..0000000000 --- a/crates/collab2/migrations/20221214144346_change_epoch_from_uuid_to_integer.sql +++ /dev/null @@ -1,30 +0,0 @@ -CREATE TABLE servers ( - id SERIAL PRIMARY KEY, - environment VARCHAR NOT NULL -); - -DROP TABLE worktree_extensions; -DROP TABLE project_activity_periods; -DELETE from projects; -ALTER TABLE projects - DROP COLUMN host_connection_epoch, - ADD COLUMN host_connection_server_id INTEGER REFERENCES servers (id) ON DELETE CASCADE; -CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id"); -CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id"); - -DELETE FROM project_collaborators; -ALTER TABLE project_collaborators - DROP COLUMN connection_epoch, - ADD COLUMN connection_server_id INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE; -CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id"); -CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id"); - -DELETE FROM room_participants; -ALTER TABLE room_participants - DROP COLUMN answering_connection_epoch, - DROP COLUMN calling_connection_epoch, - ADD COLUMN answering_connection_server_id INTEGER REFERENCES servers (id) ON DELETE CASCADE, - ADD COLUMN calling_connection_server_id INTEGER REFERENCES servers (id) ON DELETE SET NULL; -CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id"); -CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id"); -CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id"); diff --git a/crates/collab2/migrations/20221219181850_project_reconnection_support.sql b/crates/collab2/migrations/20221219181850_project_reconnection_support.sql deleted file mode 100644 index 6efef5571c..0000000000 --- a/crates/collab2/migrations/20221219181850_project_reconnection_support.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE "worktree_entries" - ADD COLUMN "scan_id" INT8, - ADD COLUMN "is_deleted" BOOL; diff --git a/crates/collab2/migrations/20230103200902_replace_is_completed_with_completed_scan_id.sql b/crates/collab2/migrations/20230103200902_replace_is_completed_with_completed_scan_id.sql deleted file mode 100644 index 1894d888b9..0000000000 --- a/crates/collab2/migrations/20230103200902_replace_is_completed_with_completed_scan_id.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE worktrees - ALTER COLUMN is_complete SET DEFAULT FALSE, - ADD COLUMN completed_scan_id INT8; diff --git a/crates/collab2/migrations/20230202155735_followers.sql b/crates/collab2/migrations/20230202155735_followers.sql deleted file mode 100644 index c82d6ba3bd..0000000000 --- a/crates/collab2/migrations/20230202155735_followers.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS "followers" ( - "id" SERIAL PRIMARY KEY, - "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE, - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, - "leader_connection_id" INTEGER NOT NULL, - "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, - "follower_connection_id" INTEGER NOT NULL -); - -CREATE UNIQUE INDEX - "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" -ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id"); - -CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); diff --git a/crates/collab2/migrations/20230508211523_add-repository-entries.sql b/crates/collab2/migrations/20230508211523_add-repository-entries.sql deleted file mode 100644 index 1e59347939..0000000000 --- a/crates/collab2/migrations/20230508211523_add-repository-entries.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE "worktree_repositories" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INT8 NOT NULL, - "work_directory_id" INT8 NOT NULL, - "scan_id" INT8 NOT NULL, - "branch" VARCHAR, - "is_deleted" BOOL NOT NULL, - PRIMARY KEY(project_id, worktree_id, work_directory_id), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, - FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id"); -CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); diff --git a/crates/collab2/migrations/20230511004019_add_repository_statuses.sql b/crates/collab2/migrations/20230511004019_add_repository_statuses.sql deleted file mode 100644 index 862561c686..0000000000 --- a/crates/collab2/migrations/20230511004019_add_repository_statuses.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE "worktree_repository_statuses" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INT8 NOT NULL, - "work_directory_id" INT8 NOT NULL, - "repo_path" VARCHAR NOT NULL, - "status" INT8 NOT NULL, - "scan_id" INT8 NOT NULL, - "is_deleted" BOOL NOT NULL, - PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, - FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id"); -CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); -CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); diff --git a/crates/collab2/migrations/20230529164700_add_worktree_settings_files.sql b/crates/collab2/migrations/20230529164700_add_worktree_settings_files.sql deleted file mode 100644 index 973a40af0f..0000000000 --- a/crates/collab2/migrations/20230529164700_add_worktree_settings_files.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE "worktree_settings_files" ( - "project_id" INTEGER NOT NULL, - "worktree_id" INT8 NOT NULL, - "path" VARCHAR NOT NULL, - "content" TEXT NOT NULL, - PRIMARY KEY(project_id, worktree_id, path), - FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE -); -CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); -CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id"); diff --git a/crates/collab2/migrations/20230605191135_remove_repository_statuses.sql b/crates/collab2/migrations/20230605191135_remove_repository_statuses.sql deleted file mode 100644 index 3e5f907c44..0000000000 --- a/crates/collab2/migrations/20230605191135_remove_repository_statuses.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "worktree_entries" -ADD "git_status" INT8; diff --git a/crates/collab2/migrations/20230616134535_add_is_external_to_worktree_entries.sql b/crates/collab2/migrations/20230616134535_add_is_external_to_worktree_entries.sql deleted file mode 100644 index e4348af0cc..0000000000 --- a/crates/collab2/migrations/20230616134535_add_is_external_to_worktree_entries.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "worktree_entries" -ADD "is_external" BOOL NOT NULL DEFAULT FALSE; diff --git a/crates/collab2/migrations/20230727150500_add_channels.sql b/crates/collab2/migrations/20230727150500_add_channels.sql deleted file mode 100644 index df981838bf..0000000000 --- a/crates/collab2/migrations/20230727150500_add_channels.sql +++ /dev/null @@ -1,30 +0,0 @@ -DROP TABLE "channel_messages"; -DROP TABLE "channel_memberships"; -DROP TABLE "org_memberships"; -DROP TABLE "orgs"; -DROP TABLE "channels"; - -CREATE TABLE "channels" ( - "id" SERIAL PRIMARY KEY, - "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now() -); - -CREATE TABLE "channel_paths" ( - "id_path" VARCHAR NOT NULL PRIMARY KEY, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE -); -CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id"); - -CREATE TABLE "channel_members" ( - "id" SERIAL PRIMARY KEY, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "admin" BOOLEAN NOT NULL DEFAULT false, - "accepted" BOOLEAN NOT NULL DEFAULT false, - "updated_at" TIMESTAMP NOT NULL DEFAULT now() -); - -CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); - -ALTER TABLE rooms ADD COLUMN "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE; diff --git a/crates/collab2/migrations/20230819154600_add_channel_buffers.sql b/crates/collab2/migrations/20230819154600_add_channel_buffers.sql deleted file mode 100644 index 5e6e7ce339..0000000000 --- a/crates/collab2/migrations/20230819154600_add_channel_buffers.sql +++ /dev/null @@ -1,40 +0,0 @@ -CREATE TABLE "buffers" ( - "id" SERIAL PRIMARY KEY, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "epoch" INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); - -CREATE TABLE "buffer_operations" ( - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, - "epoch" INTEGER NOT NULL, - "replica_id" INTEGER NOT NULL, - "lamport_timestamp" INTEGER NOT NULL, - "value" BYTEA NOT NULL, - PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) -); - -CREATE TABLE "buffer_snapshots" ( - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, - "epoch" INTEGER NOT NULL, - "text" TEXT NOT NULL, - "operation_serialization_version" INTEGER NOT NULL, - PRIMARY KEY(buffer_id, epoch) -); - -CREATE TABLE "channel_buffer_collaborators" ( - "id" SERIAL PRIMARY KEY, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "connection_id" INTEGER NOT NULL, - "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, - "connection_lost" BOOLEAN NOT NULL DEFAULT FALSE, - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "replica_id" INTEGER NOT NULL -); - -CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); -CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); -CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab2/migrations/20230825190322_add_server_feature_flags.sql b/crates/collab2/migrations/20230825190322_add_server_feature_flags.sql deleted file mode 100644 index fffde54a20..0000000000 --- a/crates/collab2/migrations/20230825190322_add_server_feature_flags.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE "feature_flags" ( - "id" SERIAL PRIMARY KEY, - "flag" VARCHAR(255) NOT NULL UNIQUE -); - -CREATE UNIQUE INDEX "index_feature_flags" ON "feature_flags" ("id"); - -CREATE TABLE "user_features" ( - "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - "feature_id" INTEGER NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, - PRIMARY KEY (user_id, feature_id) -); - -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"); diff --git a/crates/collab2/migrations/20230907114200_add_channel_messages.sql b/crates/collab2/migrations/20230907114200_add_channel_messages.sql deleted file mode 100644 index abe7753ca6..0000000000 --- a/crates/collab2/migrations/20230907114200_add_channel_messages.sql +++ /dev/null @@ -1,19 +0,0 @@ -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/collab2/migrations/20230925210437_add_channel_changes.sql b/crates/collab2/migrations/20230925210437_add_channel_changes.sql deleted file mode 100644 index 250a9ac731..0000000000 --- a/crates/collab2/migrations/20230925210437_add_channel_changes.sql +++ /dev/null @@ -1,19 +0,0 @@ -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/collab2/migrations/20230926102500_add_participant_index_to_room_participants.sql b/crates/collab2/migrations/20230926102500_add_participant_index_to_room_participants.sql deleted file mode 100644 index 1493119e2a..0000000000 --- a/crates/collab2/migrations/20230926102500_add_participant_index_to_room_participants.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE room_participants ADD COLUMN participant_index INTEGER; diff --git a/crates/collab2/migrations/20231004130100_create_notifications.sql b/crates/collab2/migrations/20231004130100_create_notifications.sql deleted file mode 100644 index 93c282c631..0000000000 --- a/crates/collab2/migrations/20231004130100_create_notifications.sql +++ /dev/null @@ -1,22 +0,0 @@ -CREATE TABLE "notification_kinds" ( - "id" SERIAL PRIMARY KEY, - "name" VARCHAR NOT NULL -); - -CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name"); - -CREATE TABLE notifications ( - "id" SERIAL PRIMARY KEY, - "created_at" TIMESTAMP NOT NULL DEFAULT now(), - "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), - "entity_id" INTEGER, - "content" TEXT, - "is_read" BOOLEAN NOT NULL DEFAULT FALSE, - "response" BOOLEAN -); - -CREATE INDEX - "index_notifications_on_recipient_id_is_read_kind_entity_id" - ON "notifications" - ("recipient_id", "is_read", "kind", "entity_id"); diff --git a/crates/collab2/migrations/20231009181554_add_release_channel_to_rooms.sql b/crates/collab2/migrations/20231009181554_add_release_channel_to_rooms.sql deleted file mode 100644 index 8f3a704add..0000000000 --- a/crates/collab2/migrations/20231009181554_add_release_channel_to_rooms.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE rooms ADD COLUMN enviroment TEXT; diff --git a/crates/collab2/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql b/crates/collab2/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql deleted file mode 100644 index 21ec4cfbb7..0000000000 --- a/crates/collab2/migrations/20231010114600_add_unique_index_on_rooms_channel_id.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); diff --git a/crates/collab2/migrations/20231011214412_add_guest_role.sql b/crates/collab2/migrations/20231011214412_add_guest_role.sql deleted file mode 100644 index 1713547158..0000000000 --- a/crates/collab2/migrations/20231011214412_add_guest_role.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE channel_members ADD COLUMN role TEXT; -UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END; - -ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members'; diff --git a/crates/collab2/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql b/crates/collab2/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql deleted file mode 100644 index be535ff7fa..0000000000 --- a/crates/collab2/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Add migration script here - -ALTER TABLE projects - DROP CONSTRAINT projects_room_id_fkey, - ADD CONSTRAINT projects_room_id_fkey - FOREIGN KEY (room_id) - REFERENCES rooms (id) - ON DELETE CASCADE; diff --git a/crates/collab2/migrations/20231018102700_create_mentions.sql b/crates/collab2/migrations/20231018102700_create_mentions.sql deleted file mode 100644 index 221a1748cf..0000000000 --- a/crates/collab2/migrations/20231018102700_create_mentions.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE "channel_message_mentions" ( - "message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE, - "start_offset" INTEGER NOT NULL, - "end_offset" INTEGER NOT NULL, - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - PRIMARY KEY(message_id, start_offset) -); - --- We use 'on conflict update' with this index, so it should be per-user. -CREATE UNIQUE INDEX "index_channel_messages_on_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce"); -DROP INDEX "index_channel_messages_on_nonce"; diff --git a/crates/collab2/migrations/20231024085546_move_channel_paths_to_channels_table.sql b/crates/collab2/migrations/20231024085546_move_channel_paths_to_channels_table.sql deleted file mode 100644 index d9fc6c8722..0000000000 --- a/crates/collab2/migrations/20231024085546_move_channel_paths_to_channels_table.sql +++ /dev/null @@ -1,12 +0,0 @@ -ALTER TABLE channels ADD COLUMN parent_path TEXT; - -UPDATE channels -SET parent_path = substr( - channel_paths.id_path, - 2, - length(channel_paths.id_path) - length('/' || channel_paths.channel_id::text || '/') -) -FROM channel_paths -WHERE channel_paths.channel_id = channels.id; - -CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path"); diff --git a/crates/collab2/src/api.rs b/crates/collab2/src/api.rs deleted file mode 100644 index a84fcf328b..0000000000 --- a/crates/collab2/src/api.rs +++ /dev/null @@ -1,184 +0,0 @@ -use crate::{ - auth, - db::{User, UserId}, - rpc, AppState, Error, Result, -}; -use anyhow::anyhow; -use axum::{ - body::Body, - extract::{Path, Query}, - http::{self, Request, StatusCode}, - middleware::{self, Next}, - response::IntoResponse, - routing::{get, post}, - Extension, Json, Router, -}; -use axum_extra::response::ErasedJson; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tower::ServiceBuilder; -use tracing::instrument; - -pub fn routes(rpc_server: Arc, state: Arc) -> Router { - Router::new() - .route("/user", get(get_authenticated_user)) - .route("/users/:id/access_tokens", post(create_access_token)) - .route("/panic", post(trace_panic)) - .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) - .layer( - ServiceBuilder::new() - .layer(Extension(state)) - .layer(Extension(rpc_server)) - .layer(middleware::from_fn(validate_api_token)), - ) -} - -pub async fn validate_api_token(req: Request, next: Next) -> impl IntoResponse { - let token = req - .headers() - .get(http::header::AUTHORIZATION) - .and_then(|header| header.to_str().ok()) - .ok_or_else(|| { - Error::Http( - StatusCode::BAD_REQUEST, - "missing authorization header".to_string(), - ) - })? - .strip_prefix("token ") - .ok_or_else(|| { - Error::Http( - StatusCode::BAD_REQUEST, - "invalid authorization header".to_string(), - ) - })?; - - let state = req.extensions().get::>().unwrap(); - - if token != state.config.api_token { - Err(Error::Http( - StatusCode::UNAUTHORIZED, - "invalid authorization token".to_string(), - ))? - } - - Ok::<_, Error>(next.run(req).await) -} - -#[derive(Debug, Deserialize)] -struct AuthenticatedUserParams { - github_user_id: Option, - github_login: String, - github_email: Option, -} - -#[derive(Debug, Serialize)] -struct AuthenticatedUserResponse { - user: User, - metrics_id: String, -} - -async fn get_authenticated_user( - Query(params): Query, - Extension(app): Extension>, -) -> Result> { - let user = app - .db - .get_or_create_user_by_github_account( - ¶ms.github_login, - params.github_user_id, - params.github_email.as_deref(), - ) - .await? - .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?; - let metrics_id = app.db.get_user_metrics_id(user.id).await?; - return Ok(Json(AuthenticatedUserResponse { user, metrics_id })); -} - -#[derive(Deserialize, Debug)] -struct CreateUserParams { - github_user_id: i32, - github_login: String, - email_address: String, - email_confirmation_code: Option, - #[serde(default)] - admin: bool, - #[serde(default)] - invite_count: i32, -} - -#[derive(Serialize, Debug)] -struct CreateUserResponse { - user: User, - signup_device_id: Option, - metrics_id: String, -} - -#[derive(Debug, Deserialize)] -struct Panic { - version: String, - text: String, -} - -#[instrument(skip(panic))] -async fn trace_panic(panic: Json) -> Result<()> { - tracing::error!(version = %panic.version, text = %panic.text, "panic report"); - Ok(()) -} - -async fn get_rpc_server_snapshot( - Extension(rpc_server): Extension>, -) -> Result { - Ok(ErasedJson::pretty(rpc_server.snapshot().await)) -} - -#[derive(Deserialize)] -struct CreateAccessTokenQueryParams { - public_key: String, - impersonate: Option, -} - -#[derive(Serialize)] -struct CreateAccessTokenResponse { - user_id: UserId, - encrypted_access_token: String, -} - -async fn create_access_token( - Path(user_id): Path, - Query(params): Query, - Extension(app): Extension>, -) -> Result> { - let user = app - .db - .get_user_by_id(user_id) - .await? - .ok_or_else(|| anyhow!("user not found"))?; - - let mut user_id = user.id; - if let Some(impersonate) = params.impersonate { - if user.admin { - if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? { - user_id = impersonated_user.id; - } else { - return Err(Error::Http( - StatusCode::UNPROCESSABLE_ENTITY, - format!("user {impersonate} does not exist"), - )); - } - } else { - return Err(Error::Http( - StatusCode::UNAUTHORIZED, - "you do not have permission to impersonate other users".to_string(), - )); - } - } - - let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?; - let encrypted_access_token = - auth::encrypt_access_token(&access_token, params.public_key.clone())?; - - Ok(Json(CreateAccessTokenResponse { - user_id, - encrypted_access_token, - })) -} diff --git a/crates/collab2/src/auth.rs b/crates/collab2/src/auth.rs deleted file mode 100644 index 9ce602c577..0000000000 --- a/crates/collab2/src/auth.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::{ - db::{self, AccessTokenId, Database, UserId}, - AppState, Error, Result, -}; -use anyhow::{anyhow, Context}; -use axum::{ - http::{self, Request, StatusCode}, - middleware::Next, - response::IntoResponse, -}; -use lazy_static::lazy_static; -use prometheus::{exponential_buckets, register_histogram, Histogram}; -use rand::thread_rng; -use scrypt::{ - password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, - Scrypt, -}; -use serde::{Deserialize, Serialize}; -use std::{sync::Arc, time::Instant}; - -lazy_static! { - static ref METRIC_ACCESS_TOKEN_HASHING_TIME: Histogram = register_histogram!( - "access_token_hashing_time", - "time spent hashing access tokens", - exponential_buckets(10.0, 2.0, 10).unwrap(), - ) - .unwrap(); -} - -pub async fn validate_header(mut req: Request, next: Next) -> impl IntoResponse { - let mut auth_header = req - .headers() - .get(http::header::AUTHORIZATION) - .and_then(|header| header.to_str().ok()) - .ok_or_else(|| { - Error::Http( - StatusCode::UNAUTHORIZED, - "missing authorization header".to_string(), - ) - })? - .split_whitespace(); - - let user_id = UserId(auth_header.next().unwrap_or("").parse().map_err(|_| { - Error::Http( - StatusCode::BAD_REQUEST, - "missing user id in authorization header".to_string(), - ) - })?); - - let access_token = auth_header.next().ok_or_else(|| { - Error::Http( - StatusCode::BAD_REQUEST, - "missing access token in authorization header".to_string(), - ) - })?; - - let state = req.extensions().get::>().unwrap(); - let credentials_valid = if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") { - state.config.api_token == admin_token - } else { - verify_access_token(&access_token, user_id, &state.db) - .await - .unwrap_or(false) - }; - - if credentials_valid { - let user = state - .db - .get_user_by_id(user_id) - .await? - .ok_or_else(|| anyhow!("user {} not found", user_id))?; - req.extensions_mut().insert(user); - Ok::<_, Error>(next.run(req).await) - } else { - Err(Error::Http( - StatusCode::UNAUTHORIZED, - "invalid credentials".to_string(), - )) - } -} - -const MAX_ACCESS_TOKENS_TO_STORE: usize = 8; - -#[derive(Serialize, Deserialize)] -struct AccessTokenJson { - version: usize, - id: AccessTokenId, - token: String, -} - -pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result { - const VERSION: usize = 1; - let access_token = rpc::auth::random_token(); - let access_token_hash = - hash_access_token(&access_token).context("failed to hash access token")?; - let id = db - .create_access_token(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE) - .await?; - Ok(serde_json::to_string(&AccessTokenJson { - version: VERSION, - id, - token: access_token, - })?) -} - -fn hash_access_token(token: &str) -> Result { - // Avoid slow hashing in debug mode. - let params = if cfg!(debug_assertions) { - scrypt::Params::new(1, 1, 1).unwrap() - } else { - scrypt::Params::new(14, 8, 1).unwrap() - }; - - Ok(Scrypt - .hash_password( - token.as_bytes(), - None, - params, - &SaltString::generate(thread_rng()), - ) - .map_err(anyhow::Error::new)? - .to_string()) -} - -pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result { - let native_app_public_key = - rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?; - let encrypted_access_token = native_app_public_key - .encrypt_string(access_token) - .context("failed to encrypt access token with public key")?; - Ok(encrypted_access_token) -} - -pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc) -> Result { - let token: AccessTokenJson = serde_json::from_str(&token)?; - - let db_token = db.get_access_token(token.id).await?; - if db_token.user_id != user_id { - return Err(anyhow!("no such access token"))?; - } - - let db_hash = PasswordHash::new(&db_token.hash).map_err(anyhow::Error::new)?; - let t0 = Instant::now(); - let is_valid = Scrypt - .verify_password(token.token.as_bytes(), &db_hash) - .is_ok(); - let duration = t0.elapsed(); - log::info!("hashed access token in {:?}", duration); - METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64); - Ok(is_valid) -} diff --git a/crates/collab2/src/bin/dotenv2.rs b/crates/collab2/src/bin/dotenv2.rs deleted file mode 100644 index c093bcb6e9..0000000000 --- a/crates/collab2/src/bin/dotenv2.rs +++ /dev/null @@ -1,20 +0,0 @@ -use anyhow::anyhow; -use std::fs; - -fn main() -> anyhow::Result<()> { - let env: toml::map::Map = toml::de::from_str( - &fs::read_to_string("./.env.toml").map_err(|_| anyhow!("no .env.toml file found"))?, - )?; - - for (key, value) in env { - let value = match value { - toml::Value::String(value) => value, - toml::Value::Integer(value) => value.to_string(), - toml::Value::Float(value) => value.to_string(), - _ => panic!("unsupported TOML value in .env.toml for key {}", key), - }; - println!("export {}=\"{}\"", key, value); - } - - Ok(()) -} diff --git a/crates/collab2/src/bin/seed2.rs b/crates/collab2/src/bin/seed2.rs deleted file mode 100644 index a7127bbb77..0000000000 --- a/crates/collab2/src/bin/seed2.rs +++ /dev/null @@ -1,107 +0,0 @@ -use collab2::{db, executor::Executor}; -use db::{ConnectOptions, Database}; -use serde::{de::DeserializeOwned, Deserialize}; -use std::fmt::Write; - -#[derive(Debug, Deserialize)] -struct GitHubUser { - id: i32, - login: String, - email: Option, -} - -#[tokio::main] -async fn main() { - let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); - let db = Database::new(ConnectOptions::new(database_url), Executor::Production) - .await - .expect("failed to connect to postgres database"); - let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var"); - let client = reqwest::Client::new(); - - let mut current_user = - fetch_github::(&client, &github_token, "https://api.github.com/user").await; - current_user - .email - .get_or_insert_with(|| "placeholder@example.com".to_string()); - let staff_users = fetch_github::>( - &client, - &github_token, - "https://api.github.com/orgs/zed-industries/teams/staff/members", - ) - .await; - - let mut zed_users = Vec::new(); - zed_users.push((current_user, true)); - zed_users.extend(staff_users.into_iter().map(|user| (user, true))); - - let user_count = db - .get_all_users(0, 200) - .await - .expect("failed to load users from db") - .len(); - if user_count < 100 { - let mut last_user_id = None; - for _ in 0..10 { - let mut uri = "https://api.github.com/users?per_page=100".to_string(); - if let Some(last_user_id) = last_user_id { - write!(&mut uri, "&since={}", last_user_id).unwrap(); - } - let users = fetch_github::>(&client, &github_token, &uri).await; - if let Some(last_user) = users.last() { - last_user_id = Some(last_user.id); - zed_users.extend(users.into_iter().map(|user| (user, false))); - } else { - break; - } - } - } - - for (github_user, admin) in zed_users { - if db - .get_user_by_github_login(&github_user.login) - .await - .expect("failed to fetch user") - .is_none() - { - if admin { - db.create_user( - &format!("{}@zed.dev", github_user.login), - admin, - db::NewUserParams { - github_login: github_user.login, - github_user_id: github_user.id, - }, - ) - .await - .expect("failed to insert user"); - } else { - db.get_or_create_user_by_github_account( - &github_user.login, - Some(github_user.id), - github_user.email.as_deref(), - ) - .await - .expect("failed to insert user"); - } - } - } -} - -async fn fetch_github( - client: &reqwest::Client, - access_token: &str, - url: &str, -) -> T { - let response = client - .get(url) - .bearer_auth(&access_token) - .header("user-agent", "zed") - .send() - .await - .expect(&format!("failed to fetch '{}'", url)); - response - .json() - .await - .expect(&format!("failed to deserialize github user from '{}'", url)) -} diff --git a/crates/collab2/src/db.rs b/crates/collab2/src/db.rs deleted file mode 100644 index df33416a46..0000000000 --- a/crates/collab2/src/db.rs +++ /dev/null @@ -1,672 +0,0 @@ -#[cfg(test)] -pub mod tests; - -#[cfg(test)] -pub use tests::TestDb; - -mod ids; -mod queries; -mod tables; - -use crate::{executor::Executor, Error, Result}; -use anyhow::anyhow; -use collections::{BTreeMap, HashMap, HashSet}; -use dashmap::DashMap; -use futures::StreamExt; -use rand::{prelude::StdRng, Rng, SeedableRng}; -use rpc::{ - proto::{self}, - ConnectionId, -}; -use sea_orm::{ - entity::prelude::*, - sea_query::{Alias, Expr, OnConflict}, - ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr, - FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement, - TransactionTrait, -}; -use serde::{Deserialize, Serialize}; -use sqlx::{ - migrate::{Migrate, Migration, MigrationSource}, - Connection, -}; -use std::{ - fmt::Write as _, - future::Future, - marker::PhantomData, - ops::{Deref, DerefMut}, - path::Path, - rc::Rc, - sync::Arc, - time::Duration, -}; -use tables::*; -use tokio::sync::{Mutex, OwnedMutexGuard}; - -pub use ids::*; -pub use sea_orm::ConnectOptions; -pub use tables::user::Model as User; - -pub struct Database { - options: ConnectOptions, - pool: DatabaseConnection, - rooms: DashMap>>, - rng: Mutex, - executor: Executor, - notification_kinds_by_id: HashMap, - notification_kinds_by_name: HashMap, - #[cfg(test)] - runtime: Option, -} - -// The `Database` type has so many methods that its impl blocks are split into -// separate files in the `queries` folder. -impl Database { - pub async fn new(options: ConnectOptions, executor: Executor) -> Result { - sqlx::any::install_default_drivers(); - Ok(Self { - options: options.clone(), - pool: sea_orm::Database::connect(options).await?, - rooms: DashMap::with_capacity(16384), - rng: Mutex::new(StdRng::seed_from_u64(0)), - notification_kinds_by_id: HashMap::default(), - notification_kinds_by_name: HashMap::default(), - executor, - #[cfg(test)] - runtime: None, - }) - } - - #[cfg(test)] - pub fn reset(&self) { - self.rooms.clear(); - } - - pub async fn migrate( - &self, - migrations_path: &Path, - ignore_checksum_mismatch: bool, - ) -> anyhow::Result> { - let migrations = MigrationSource::resolve(migrations_path) - .await - .map_err(|err| anyhow!("failed to load migrations: {err:?}"))?; - - let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?; - - connection.ensure_migrations_table().await?; - let applied_migrations: HashMap<_, _> = connection - .list_applied_migrations() - .await? - .into_iter() - .map(|m| (m.version, m)) - .collect(); - - let mut new_migrations = Vec::new(); - for migration in migrations { - match applied_migrations.get(&migration.version) { - Some(applied_migration) => { - if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch - { - Err(anyhow!( - "checksum mismatch for applied migration {}", - migration.description - ))?; - } - } - None => { - let elapsed = connection.apply(&migration).await?; - new_migrations.push((migration, elapsed)); - } - } - } - - Ok(new_migrations) - } - - pub async fn initialize_static_data(&mut self) -> Result<()> { - self.initialize_notification_kinds().await?; - Ok(()) - } - - pub async fn transaction(&self, f: F) -> Result - where - F: Send + Fn(TransactionHandle) -> Fut, - Fut: Send + Future>, - { - let body = async { - let mut i = 0; - loop { - let (tx, result) = self.with_transaction(&f).await?; - match result { - Ok(result) => match tx.commit().await.map_err(Into::into) { - Ok(()) => return Ok(result), - Err(error) => { - if !self.retry_on_serialization_error(&error, i).await { - return Err(error); - } - } - }, - Err(error) => { - tx.rollback().await?; - if !self.retry_on_serialization_error(&error, i).await { - return Err(error); - } - } - } - i += 1; - } - }; - - self.run(body).await - } - - async fn optional_room_transaction(&self, f: F) -> Result>> - where - F: Send + Fn(TransactionHandle) -> Fut, - Fut: Send + Future>>, - { - let body = async { - let mut i = 0; - loop { - let (tx, result) = self.with_transaction(&f).await?; - match result { - Ok(Some((room_id, data))) => { - let lock = self.rooms.entry(room_id).or_default().clone(); - let _guard = lock.lock_owned().await; - match tx.commit().await.map_err(Into::into) { - Ok(()) => { - return Ok(Some(RoomGuard { - data, - _guard, - _not_send: PhantomData, - })); - } - Err(error) => { - if !self.retry_on_serialization_error(&error, i).await { - return Err(error); - } - } - } - } - Ok(None) => match tx.commit().await.map_err(Into::into) { - Ok(()) => return Ok(None), - Err(error) => { - if !self.retry_on_serialization_error(&error, i).await { - return Err(error); - } - } - }, - Err(error) => { - tx.rollback().await?; - if !self.retry_on_serialization_error(&error, i).await { - return Err(error); - } - } - } - i += 1; - } - }; - - self.run(body).await - } - - async fn room_transaction(&self, room_id: RoomId, f: F) -> Result> - where - F: Send + Fn(TransactionHandle) -> Fut, - Fut: Send + Future>, - { - let body = async { - let mut i = 0; - loop { - let lock = self.rooms.entry(room_id).or_default().clone(); - let _guard = lock.lock_owned().await; - let (tx, result) = self.with_transaction(&f).await?; - match result { - Ok(data) => match tx.commit().await.map_err(Into::into) { - Ok(()) => { - return Ok(RoomGuard { - data, - _guard, - _not_send: PhantomData, - }); - } - Err(error) => { - if !self.retry_on_serialization_error(&error, i).await { - return Err(error); - } - } - }, - Err(error) => { - tx.rollback().await?; - if !self.retry_on_serialization_error(&error, i).await { - return Err(error); - } - } - } - i += 1; - } - }; - - self.run(body).await - } - - async fn with_transaction(&self, f: &F) -> Result<(DatabaseTransaction, Result)> - where - F: Send + Fn(TransactionHandle) -> Fut, - Fut: Send + Future>, - { - let tx = self - .pool - .begin_with_config(Some(IsolationLevel::Serializable), None) - .await?; - - let mut tx = Arc::new(Some(tx)); - let result = f(TransactionHandle(tx.clone())).await; - let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else { - return Err(anyhow!( - "couldn't complete transaction because it's still in use" - ))?; - }; - - Ok((tx, result)) - } - - async fn run(&self, future: F) -> Result - where - F: Future>, - { - #[cfg(test)] - { - if let Executor::Deterministic(executor) = &self.executor { - executor.simulate_random_delay().await; - } - - self.runtime.as_ref().unwrap().block_on(future) - } - - #[cfg(not(test))] - { - future.await - } - } - - async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool { - // If the error is due to a failure to serialize concurrent transactions, then retry - // this transaction after a delay. With each subsequent retry, double the delay duration. - // Also vary the delay randomly in order to ensure different database connections retry - // at different times. - if is_serialization_error(error) { - let base_delay = 4_u64 << prev_attempt_count.min(16); - let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0); - log::info!( - "retrying transaction after serialization error. delay: {} ms.", - randomized_delay - ); - self.executor - .sleep(Duration::from_millis(randomized_delay as u64)) - .await; - true - } else { - false - } - } -} - -fn is_serialization_error(error: &Error) -> bool { - const SERIALIZATION_FAILURE_CODE: &'static str = "40001"; - match error { - Error::Database( - DbErr::Exec(sea_orm::RuntimeErr::SqlxError(error)) - | DbErr::Query(sea_orm::RuntimeErr::SqlxError(error)), - ) if error - .as_database_error() - .and_then(|error| error.code()) - .as_deref() - == Some(SERIALIZATION_FAILURE_CODE) => - { - true - } - _ => false, - } -} - -pub struct TransactionHandle(Arc>); - -impl Deref for TransactionHandle { - type Target = DatabaseTransaction; - - fn deref(&self) -> &Self::Target { - self.0.as_ref().as_ref().unwrap() - } -} - -pub struct RoomGuard { - data: T, - _guard: OwnedMutexGuard<()>, - _not_send: PhantomData>, -} - -impl Deref for RoomGuard { - type Target = T; - - fn deref(&self) -> &T { - &self.data - } -} - -impl DerefMut for RoomGuard { - fn deref_mut(&mut self) -> &mut T { - &mut self.data - } -} - -impl RoomGuard { - pub fn into_inner(self) -> T { - self.data - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Contact { - Accepted { user_id: UserId, busy: bool }, - Outgoing { user_id: UserId }, - Incoming { user_id: UserId }, -} - -impl Contact { - pub fn user_id(&self) -> UserId { - match self { - Contact::Accepted { user_id, .. } => *user_id, - Contact::Outgoing { user_id } => *user_id, - Contact::Incoming { user_id, .. } => *user_id, - } - } -} - -pub type NotificationBatch = Vec<(UserId, proto::Notification)>; - -pub struct CreatedChannelMessage { - pub message_id: MessageId, - pub participant_connection_ids: Vec, - pub channel_members: Vec, - pub notifications: NotificationBatch, -} - -#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] -pub struct Invite { - pub email_address: String, - pub email_confirmation_code: String, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct NewSignup { - pub email_address: String, - pub platform_mac: bool, - pub platform_windows: bool, - pub platform_linux: bool, - pub editor_features: Vec, - pub programming_languages: Vec, - pub device_id: Option, - pub added_to_mailing_list: bool, - pub created_at: Option, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)] -pub struct WaitlistSummary { - pub count: i64, - pub linux_count: i64, - pub mac_count: i64, - pub windows_count: i64, - pub unknown_count: i64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct NewUserParams { - pub github_login: String, - pub github_user_id: i32, -} - -#[derive(Debug)] -pub struct NewUserResult { - pub user_id: UserId, - pub metrics_id: String, - pub inviting_user_id: Option, - pub signup_device_id: Option, -} - -#[derive(Debug)] -pub struct MoveChannelResult { - pub participants_to_update: HashMap, - pub participants_to_remove: HashSet, - pub moved_channels: HashSet, -} - -#[derive(Debug)] -pub struct RenameChannelResult { - pub channel: Channel, - pub participants_to_update: HashMap, -} - -#[derive(Debug)] -pub struct CreateChannelResult { - pub channel: Channel, - pub participants_to_update: Vec<(UserId, ChannelsForUser)>, -} - -#[derive(Debug)] -pub struct SetChannelVisibilityResult { - pub participants_to_update: HashMap, - pub participants_to_remove: HashSet, - pub channels_to_remove: Vec, -} - -#[derive(Debug)] -pub struct MembershipUpdated { - pub channel_id: ChannelId, - pub new_channels: ChannelsForUser, - pub removed_channels: Vec, -} - -#[derive(Debug)] -pub enum SetMemberRoleResult { - InviteUpdated(Channel), - MembershipUpdated(MembershipUpdated), -} - -#[derive(Debug)] -pub struct InviteMemberResult { - pub channel: Channel, - pub notifications: NotificationBatch, -} - -#[derive(Debug)] -pub struct RespondToChannelInvite { - pub membership_update: Option, - pub notifications: NotificationBatch, -} - -#[derive(Debug)] -pub struct RemoveChannelMemberResult { - pub membership_update: MembershipUpdated, - pub notification_id: Option, -} - -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct Channel { - pub id: ChannelId, - pub name: String, - pub visibility: ChannelVisibility, - pub role: ChannelRole, - pub parent_path: Vec, -} - -impl Channel { - fn from_model(value: channel::Model, role: ChannelRole) -> Self { - Channel { - id: value.id, - visibility: value.visibility, - name: value.clone().name, - role, - parent_path: value.ancestors().collect(), - } - } - - pub fn to_proto(&self) -> proto::Channel { - proto::Channel { - id: self.id.to_proto(), - name: self.name.clone(), - visibility: self.visibility.into(), - role: self.role.into(), - parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(), - } - } -} - -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct ChannelMember { - pub role: ChannelRole, - pub user_id: UserId, - pub kind: proto::channel_member::Kind, -} - -impl ChannelMember { - pub fn to_proto(&self) -> proto::ChannelMember { - proto::ChannelMember { - role: self.role.into(), - user_id: self.user_id.to_proto(), - kind: self.kind.into(), - } - } -} - -#[derive(Debug, PartialEq)] -pub struct ChannelsForUser { - pub channels: Vec, - pub channel_participants: HashMap>, - pub unseen_buffer_changes: Vec, - pub channel_messages: Vec, -} - -#[derive(Debug)] -pub struct RejoinedChannelBuffer { - pub buffer: proto::RejoinedChannelBuffer, - pub old_connection_id: ConnectionId, -} - -#[derive(Clone)] -pub struct JoinRoom { - pub room: proto::Room, - pub channel_id: Option, - pub channel_members: Vec, -} - -pub struct RejoinedRoom { - pub room: proto::Room, - pub rejoined_projects: Vec, - pub reshared_projects: Vec, - pub channel_id: Option, - pub channel_members: Vec, -} - -pub struct ResharedProject { - pub id: ProjectId, - pub old_connection_id: ConnectionId, - pub collaborators: Vec, - pub worktrees: Vec, -} - -pub struct RejoinedProject { - pub id: ProjectId, - pub old_connection_id: ConnectionId, - pub collaborators: Vec, - pub worktrees: Vec, - pub language_servers: Vec, -} - -#[derive(Debug)] -pub struct RejoinedWorktree { - pub id: u64, - pub abs_path: String, - pub root_name: String, - pub visible: bool, - pub updated_entries: Vec, - pub removed_entries: Vec, - pub updated_repositories: Vec, - pub removed_repositories: Vec, - pub diagnostic_summaries: Vec, - pub settings_files: Vec, - pub scan_id: u64, - pub completed_scan_id: u64, -} - -pub struct LeftRoom { - pub room: proto::Room, - pub channel_id: Option, - pub channel_members: Vec, - pub left_projects: HashMap, - pub canceled_calls_to_user_ids: Vec, - pub deleted: bool, -} - -pub struct RefreshedRoom { - pub room: proto::Room, - pub channel_id: Option, - pub channel_members: Vec, - pub stale_participant_user_ids: Vec, - pub canceled_calls_to_user_ids: Vec, -} - -pub struct RefreshedChannelBuffer { - pub connection_ids: Vec, - pub collaborators: Vec, -} - -pub struct Project { - pub collaborators: Vec, - pub worktrees: BTreeMap, - pub language_servers: Vec, -} - -pub struct ProjectCollaborator { - pub connection_id: ConnectionId, - pub user_id: UserId, - pub replica_id: ReplicaId, - pub is_host: bool, -} - -impl ProjectCollaborator { - pub fn to_proto(&self) -> proto::Collaborator { - proto::Collaborator { - peer_id: Some(self.connection_id.into()), - replica_id: self.replica_id.0 as u32, - user_id: self.user_id.to_proto(), - } - } -} - -#[derive(Debug)] -pub struct LeftProject { - pub id: ProjectId, - pub host_user_id: UserId, - pub host_connection_id: ConnectionId, - pub connection_ids: Vec, -} - -pub struct Worktree { - pub id: u64, - pub abs_path: String, - pub root_name: String, - pub visible: bool, - pub entries: Vec, - pub repository_entries: BTreeMap, - pub diagnostic_summaries: Vec, - pub settings_files: Vec, - pub scan_id: u64, - pub completed_scan_id: u64, -} - -#[derive(Debug)] -pub struct WorktreeSettingsFile { - pub path: String, - pub content: String, -} diff --git a/crates/collab2/src/db/ids.rs b/crates/collab2/src/db/ids.rs deleted file mode 100644 index 5f0df90811..0000000000 --- a/crates/collab2/src/db/ids.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::Result; -use rpc::proto; -use sea_orm::{entity::prelude::*, DbErr}; -use serde::{Deserialize, Serialize}; - -macro_rules! id_type { - ($name:ident) => { - #[derive( - Clone, - Copy, - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - DeriveValueType, - )] - #[serde(transparent)] - pub struct $name(pub i32); - - impl $name { - #[allow(unused)] - pub const MAX: Self = Self(i32::MAX); - - #[allow(unused)] - pub fn from_proto(value: u64) -> Self { - Self(value as i32) - } - - #[allow(unused)] - pub fn to_proto(self) -> u64 { - self.0 as u64 - } - } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - self.0.fmt(f) - } - } - - impl sea_orm::TryFromU64 for $name { - fn try_from_u64(n: u64) -> Result { - Ok(Self(n.try_into().map_err(|_| { - DbErr::ConvertFromU64(concat!( - "error converting ", - stringify!($name), - " to u64" - )) - })?)) - } - } - - impl sea_orm::sea_query::Nullable for $name { - fn null() -> Value { - Value::Int(None) - } - } - }; -} - -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); -id_type!(RoomParticipantId); -id_type!(ProjectId); -id_type!(ProjectCollaboratorId); -id_type!(ReplicaId); -id_type!(ServerId); -id_type!(SignupId); -id_type!(UserId); -id_type!(ChannelBufferCollaboratorId); -id_type!(FlagId); -id_type!(NotificationId); -id_type!(NotificationKindId); - -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] -#[sea_orm(rs_type = "String", db_type = "String(None)")] -pub enum ChannelRole { - #[sea_orm(string_value = "admin")] - Admin, - #[sea_orm(string_value = "member")] - #[default] - Member, - #[sea_orm(string_value = "guest")] - Guest, - #[sea_orm(string_value = "banned")] - Banned, -} - -impl ChannelRole { - pub fn should_override(&self, other: Self) -> bool { - use ChannelRole::*; - match self { - Admin => matches!(other, Member | Banned | Guest), - Member => matches!(other, Banned | Guest), - Banned => matches!(other, Guest), - Guest => false, - } - } - - pub fn max(&self, other: Self) -> Self { - if self.should_override(other) { - *self - } else { - other - } - } - - pub fn can_see_all_descendants(&self) -> bool { - use ChannelRole::*; - match self { - Admin | Member => true, - Guest | Banned => false, - } - } - - pub fn can_only_see_public_descendants(&self) -> bool { - use ChannelRole::*; - match self { - Guest => true, - Admin | Member | Banned => false, - } - } -} - -impl From for ChannelRole { - fn from(value: proto::ChannelRole) -> Self { - match value { - proto::ChannelRole::Admin => ChannelRole::Admin, - proto::ChannelRole::Member => ChannelRole::Member, - proto::ChannelRole::Guest => ChannelRole::Guest, - proto::ChannelRole::Banned => ChannelRole::Banned, - } - } -} - -impl Into for ChannelRole { - fn into(self) -> proto::ChannelRole { - match self { - ChannelRole::Admin => proto::ChannelRole::Admin, - ChannelRole::Member => proto::ChannelRole::Member, - ChannelRole::Guest => proto::ChannelRole::Guest, - ChannelRole::Banned => proto::ChannelRole::Banned, - } - } -} - -impl Into for ChannelRole { - fn into(self) -> i32 { - let proto: proto::ChannelRole = self.into(); - proto.into() - } -} - -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] -#[sea_orm(rs_type = "String", db_type = "String(None)")] -pub enum ChannelVisibility { - #[sea_orm(string_value = "public")] - Public, - #[sea_orm(string_value = "members")] - #[default] - Members, -} - -impl From for ChannelVisibility { - fn from(value: proto::ChannelVisibility) -> Self { - match value { - proto::ChannelVisibility::Public => ChannelVisibility::Public, - proto::ChannelVisibility::Members => ChannelVisibility::Members, - } - } -} - -impl Into for ChannelVisibility { - fn into(self) -> proto::ChannelVisibility { - match self { - ChannelVisibility::Public => proto::ChannelVisibility::Public, - ChannelVisibility::Members => proto::ChannelVisibility::Members, - } - } -} - -impl Into for ChannelVisibility { - fn into(self) -> i32 { - let proto: proto::ChannelVisibility = self.into(); - proto.into() - } -} diff --git a/crates/collab2/src/db/queries.rs b/crates/collab2/src/db/queries.rs deleted file mode 100644 index 629e26f1a9..0000000000 --- a/crates/collab2/src/db/queries.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::*; - -pub mod access_tokens; -pub mod buffers; -pub mod channels; -pub mod contacts; -pub mod messages; -pub mod notifications; -pub mod projects; -pub mod rooms; -pub mod servers; -pub mod users; diff --git a/crates/collab2/src/db/queries/access_tokens.rs b/crates/collab2/src/db/queries/access_tokens.rs deleted file mode 100644 index 589b6483df..0000000000 --- a/crates/collab2/src/db/queries/access_tokens.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::*; -use sea_orm::sea_query::Query; - -impl Database { - pub async fn create_access_token( - &self, - user_id: UserId, - access_token_hash: &str, - max_access_token_count: usize, - ) -> Result { - self.transaction(|tx| async { - let tx = tx; - - let token = access_token::ActiveModel { - user_id: ActiveValue::set(user_id), - hash: ActiveValue::set(access_token_hash.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - - access_token::Entity::delete_many() - .filter( - access_token::Column::Id.in_subquery( - Query::select() - .column(access_token::Column::Id) - .from(access_token::Entity) - .and_where(access_token::Column::UserId.eq(user_id)) - .order_by(access_token::Column::Id, sea_orm::Order::Desc) - .limit(10000) - .offset(max_access_token_count as u64) - .to_owned(), - ), - ) - .exec(&*tx) - .await?; - Ok(token.id) - }) - .await - } - - pub async fn get_access_token( - &self, - access_token_id: AccessTokenId, - ) -> Result { - self.transaction(|tx| async move { - Ok(access_token::Entity::find_by_id(access_token_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such access token"))?) - }) - .await - } -} diff --git a/crates/collab2/src/db/queries/buffers.rs b/crates/collab2/src/db/queries/buffers.rs deleted file mode 100644 index 9eddb1f618..0000000000 --- a/crates/collab2/src/db/queries/buffers.rs +++ /dev/null @@ -1,1078 +0,0 @@ -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, - channel_id: ChannelId, - user_id: UserId, - connection: ConnectionId, - ) -> Result { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &tx) - .await?; - - let buffer = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(buffer::Entity) - .one(&*tx) - .await?; - - let buffer = if let Some(buffer) = buffer { - buffer - } else { - let buffer = buffer::ActiveModel { - channel_id: ActiveValue::Set(channel_id), - ..Default::default() - } - .insert(&*tx) - .await?; - buffer_snapshot::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(0), - text: ActiveValue::Set(String::new()), - operation_serialization_version: ActiveValue::Set( - storage::SERIALIZATION_VERSION, - ), - } - .insert(&*tx) - .await?; - buffer - }; - - // Join the collaborators - let mut collaborators = channel_buffer_collaborator::Entity::find() - .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) - .all(&*tx) - .await?; - let replica_ids = collaborators - .iter() - .map(|c| c.replica_id) - .collect::>(); - let mut replica_id = ReplicaId(0); - while replica_ids.contains(&replica_id) { - replica_id.0 += 1; - } - let collaborator = channel_buffer_collaborator::ActiveModel { - channel_id: ActiveValue::Set(channel_id), - connection_id: ActiveValue::Set(connection.id as i32), - connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::Set(user_id), - replica_id: ActiveValue::Set(replica_id), - ..Default::default() - } - .insert(&*tx) - .await?; - collaborators.push(collaborator); - - 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(), - replica_id: replica_id.to_proto() as u32, - base_text, - operations, - epoch: buffer.epoch as u64, - collaborators: collaborators - .into_iter() - .map(|collaborator| proto::Collaborator { - peer_id: Some(collaborator.connection().into()), - user_id: collaborator.user_id.to_proto(), - replica_id: collaborator.replica_id.0 as u32, - }) - .collect(), - }) - }) - .await - } - - pub async fn rejoin_channel_buffers( - &self, - buffers: &[proto::ChannelBufferVersion], - user_id: UserId, - connection_id: ConnectionId, - ) -> Result> { - self.transaction(|tx| async move { - let mut results = Vec::new(); - for client_buffer in buffers { - let channel = self - .get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &*tx) - .await?; - if self - .check_user_is_channel_participant(&channel, user_id, &*tx) - .await - .is_err() - { - log::info!("user is not a member of channel"); - continue; - } - - let buffer = self.get_channel_buffer(channel.id, &*tx).await?; - let mut collaborators = channel_buffer_collaborator::Entity::find() - .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel.id)) - .all(&*tx) - .await?; - - // If the buffer epoch hasn't changed since the client lost - // connection, then the client's buffer can be syncronized with - // the server's buffer. - if buffer.epoch as u64 != client_buffer.epoch { - log::info!("can't rejoin buffer, epoch has changed"); - continue; - } - - // Find the collaborator record for this user's previous lost - // connection. Update it with the new connection id. - let server_id = ServerId(connection_id.owner_id as i32); - let Some(self_collaborator) = collaborators.iter_mut().find(|c| { - c.user_id == user_id - && (c.connection_lost || c.connection_server_id != server_id) - }) else { - log::info!("can't rejoin buffer, no previous collaborator found"); - continue; - }; - let old_connection_id = self_collaborator.connection(); - *self_collaborator = channel_buffer_collaborator::ActiveModel { - id: ActiveValue::Unchanged(self_collaborator.id), - connection_id: ActiveValue::Set(connection_id.id as i32), - connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)), - connection_lost: ActiveValue::Set(false), - ..Default::default() - } - .update(&*tx) - .await?; - - let client_version = version_from_wire(&client_buffer.version); - let serialization_version = self - .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx) - .await?; - - let mut rows = buffer_operation::Entity::find() - .filter( - buffer_operation::Column::BufferId - .eq(buffer.id) - .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), - ) - .stream(&*tx) - .await?; - - // Find the server's version vector and any operations - // that the client has not seen. - let mut server_version = clock::Global::new(); - let mut operations = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - let timestamp = clock::Lamport { - replica_id: row.replica_id as u16, - value: row.lamport_timestamp as u32, - }; - server_version.observe(timestamp); - if !client_version.observed(timestamp) { - operations.push(proto::Operation { - variant: Some(operation_from_storage(row, serialization_version)?), - }) - } - } - - results.push(RejoinedChannelBuffer { - old_connection_id, - buffer: proto::RejoinedChannelBuffer { - channel_id: client_buffer.channel_id, - version: version_to_wire(&server_version), - operations, - collaborators: collaborators - .into_iter() - .map(|collaborator| proto::Collaborator { - peer_id: Some(collaborator.connection().into()), - user_id: collaborator.user_id.to_proto(), - replica_id: collaborator.replica_id.0 as u32, - }) - .collect(), - }, - }); - } - - Ok(results) - }) - .await - } - - pub async fn clear_stale_channel_buffer_collaborators( - &self, - channel_id: ChannelId, - server_id: ServerId, - ) -> Result { - self.transaction(|tx| async move { - 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 collaborators = Vec::new(); - let mut collaborator_ids_to_remove = Vec::new(); - 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 { - collaborator_ids_to_remove.push(db_collaborator.id); - } - } - - channel_buffer_collaborator::Entity::delete_many() - .filter(channel_buffer_collaborator::Column::Id.is_in(collaborator_ids_to_remove)) - .exec(&*tx) - .await?; - - Ok(RefreshedChannelBuffer { - connection_ids, - collaborators, - }) - }) - .await - } - - pub async fn leave_channel_buffer( - &self, - channel_id: ChannelId, - connection: ConnectionId, - ) -> Result { - self.transaction(|tx| async move { - self.leave_channel_buffer_internal(channel_id, connection, &*tx) - .await - }) - .await - } - - pub async fn 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> { - self.transaction(|tx| async move { - #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] - enum QueryChannelIds { - ChannelId, - } - - let channel_ids: Vec = channel_buffer_collaborator::Entity::find() - .select_only() - .column(channel_buffer_collaborator::Column::ChannelId) - .filter(Condition::all().add( - channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32), - )) - .into_values::<_, QueryChannelIds>() - .all(&*tx) - .await?; - - let mut result = Vec::new(); - for channel_id in channel_ids { - let left_channel_buffer = self - .leave_channel_buffer_internal(channel_id, connection, &*tx) - .await?; - result.push(left_channel_buffer); - } - - Ok(result) - }) - .await - } - - pub async fn leave_channel_buffer_internal( - &self, - channel_id: ChannelId, - connection: ConnectionId, - tx: &DatabaseTransaction, - ) -> Result { - let result = channel_buffer_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) - .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32)) - .add( - channel_buffer_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("not a collaborator on this project"))?; - } - - let mut collaborators = Vec::new(); - let mut connections = Vec::new(); - let mut rows = channel_buffer_collaborator::Entity::find() - .filter( - Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), - ) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - 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 collaborators.is_empty() { - self.snapshot_channel_buffer(channel_id, &tx).await?; - } - - Ok(LeftChannelBuffer { - channel_id, - collaborators, - connections, - }) - } - - pub async fn get_channel_buffer_collaborators( - &self, - channel_id: ChannelId, - ) -> Result> { - self.transaction(|tx| async move { - 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<( - Vec, - Vec, - i32, - Vec, - )> { - self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_member(&channel, user, &*tx) - .await?; - - let buffer = buffer::Entity::find() - .filter(buffer::Column::ChannelId.eq(channel_id)) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such buffer"))?; - - let serialization_version = self - .get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx) - .await?; - - let operations = operations - .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_participants(&channel, &*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([ - buffer_operation::Column::BufferId, - buffer_operation::Column::Epoch, - buffer_operation::Column::LamportTimestamp, - buffer_operation::Column::ReplicaId, - ]) - .do_nothing() - .to_owned(), - ) - .exec(&*tx) - .await?; - } else { - channel_members = Vec::new(); - max_version = Vec::new(); - } - - let mut connections = Vec::new(); - let mut rows = channel_buffer_collaborator::Entity::find() - .filter( - Condition::all() - .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), - ) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - connections.push(ConnectionId { - id: row.connection_id as u32, - owner_id: row.connection_server_id.0 as u32, - }); - } - - Ok((connections, 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, - epoch: i32, - tx: &DatabaseTransaction, - ) -> Result { - Ok(buffer_snapshot::Entity::find() - .filter(buffer_snapshot::Column::BufferId.eq(buffer_id)) - .filter(buffer_snapshot::Column::Epoch.eq(epoch)) - .select_only() - .column(buffer_snapshot::Column::OperationSerializationVersion) - .into_values::<_, QueryOperationSerializationVersion>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("missing buffer snapshot"))?) - } - - pub async fn get_channel_buffer( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result { - Ok(channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(buffer::Entity) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such buffer"))?) - } - - async fn get_buffer_state( - &self, - buffer: &buffer::Model, - tx: &DatabaseTransaction, - ) -> Result<( - String, - Vec, - Option, - )> { - let id = buffer.id; - let (base_text, version) = if buffer.epoch > 0 { - let snapshot = buffer_snapshot::Entity::find() - .filter( - buffer_snapshot::Column::BufferId - .eq(id) - .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such snapshot"))?; - - let version = snapshot.operation_serialization_version; - (snapshot.text, version) - } else { - (String::new(), storage::SERIALIZATION_VERSION) - }; - - let mut rows = buffer_operation::Entity::find() - .filter( - buffer_operation::Column::BufferId - .eq(id) - .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), - ) - .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)?), - }); - } - - Ok((base_text, operations, last_row)) - } - - async fn snapshot_channel_buffer( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result<()> { - let buffer = self.get_channel_buffer(channel_id, tx).await?; - let (base_text, operations, _) = self.get_buffer_state(&buffer, tx).await?; - if operations.is_empty() { - return Ok(()); - } - - let mut text_buffer = text::Buffer::new(0, 0, base_text); - text_buffer - .apply_ops(operations.into_iter().filter_map(operation_from_wire)) - .unwrap(); - - let base_text = text_buffer.text(); - let epoch = buffer.epoch + 1; - - buffer_snapshot::Model { - buffer_id: buffer.id, - epoch, - text: base_text, - operation_serialization_version: storage::SERIALIZATION_VERSION, - } - .into_active_model() - .insert(tx) - .await?; - - buffer::ActiveModel { - id: ActiveValue::Unchanged(buffer.id), - epoch: ActiveValue::Set(epoch), - ..Default::default() - } - .save(tx) - .await?; - - Ok(()) - } - - 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( - operation: &proto::Operation, - buffer: &buffer::Model, - _format: i32, -) -> Option { - let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? { - proto::operation::Variant::Edit(operation) => ( - operation.replica_id, - operation.lamport_timestamp, - storage::Operation { - version: version_to_storage(&operation.version), - is_undo: false, - edit_ranges: operation - .ranges - .iter() - .map(|range| storage::Range { - start: range.start, - end: range.end, - }) - .collect(), - edit_texts: operation.new_text.clone(), - undo_counts: Vec::new(), - }, - ), - proto::operation::Variant::Undo(operation) => ( - operation.replica_id, - operation.lamport_timestamp, - storage::Operation { - version: version_to_storage(&operation.version), - is_undo: true, - edit_ranges: Vec::new(), - edit_texts: Vec::new(), - undo_counts: operation - .counts - .iter() - .map(|entry| storage::UndoCount { - replica_id: entry.replica_id, - lamport_timestamp: entry.lamport_timestamp, - count: entry.count, - }) - .collect(), - }, - ), - _ => None?, - }; - - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(replica_id as i32), - lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32), - value: ActiveValue::Set(value.encode_to_vec()), - }) -} - -fn operation_from_storage( - row: buffer_operation::Model, - _format_version: i32, -) -> Result { - let operation = - storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?; - let version = version_from_storage(&operation.version); - Ok(if operation.is_undo { - proto::operation::Variant::Undo(proto::operation::Undo { - replica_id: row.replica_id as u32, - lamport_timestamp: row.lamport_timestamp as u32, - version, - counts: operation - .undo_counts - .iter() - .map(|entry| proto::UndoCount { - replica_id: entry.replica_id, - lamport_timestamp: entry.lamport_timestamp, - count: entry.count, - }) - .collect(), - }) - } else { - proto::operation::Variant::Edit(proto::operation::Edit { - replica_id: row.replica_id as u32, - lamport_timestamp: row.lamport_timestamp as u32, - version, - ranges: operation - .edit_ranges - .into_iter() - .map(|range| proto::Range { - start: range.start, - end: range.end, - }) - .collect(), - new_text: operation.edit_texts, - }) - }) -} - -fn version_to_storage(version: &Vec) -> Vec { - version - .iter() - .map(|entry| storage::VectorClockEntry { - replica_id: entry.replica_id, - timestamp: entry.timestamp, - }) - .collect() -} - -fn version_from_storage(version: &Vec) -> Vec { - version - .iter() - .map(|entry| proto::VectorClockEntry { - replica_id: entry.replica_id, - timestamp: entry.timestamp, - }) - .collect() -} - -// This is currently a manual copy of the deserialization code in the client's langauge crate -pub fn operation_from_wire(operation: proto::Operation) -> Option { - match operation.variant? { - proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { - timestamp: clock::Lamport { - replica_id: edit.replica_id as text::ReplicaId, - value: edit.lamport_timestamp, - }, - version: version_from_wire(&edit.version), - ranges: edit - .ranges - .into_iter() - .map(|range| { - text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize) - }) - .collect(), - new_text: edit.new_text.into_iter().map(Arc::from).collect(), - })), - proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo(UndoOperation { - timestamp: clock::Lamport { - replica_id: undo.replica_id as text::ReplicaId, - value: undo.lamport_timestamp, - }, - version: version_from_wire(&undo.version), - counts: undo - .counts - .into_iter() - .map(|c| { - ( - clock::Lamport { - replica_id: c.replica_id as text::ReplicaId, - value: c.lamport_timestamp, - }, - c.count, - ) - }) - .collect(), - })), - _ => None, - } -} - -fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global { - let mut version = clock::Global::new(); - for entry in message { - version.observe(clock::Lamport { - replica_id: entry.replica_id as text::ReplicaId, - value: entry.timestamp, - }); - } - version -} - -fn version_to_wire(version: &clock::Global) -> Vec { - let mut message = Vec::new(); - for entry in version.iter() { - message.push(proto::VectorClockEntry { - replica_id: entry.replica_id as u32, - timestamp: entry.value, - }); - } - message -} - -#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] -enum QueryOperationSerializationVersion { - OperationSerializationVersion, -} - -mod storage { - #![allow(non_snake_case)] - use prost::Message; - pub const SERIALIZATION_VERSION: i32 = 1; - - #[derive(Message)] - pub struct Operation { - #[prost(message, repeated, tag = "2")] - pub version: Vec, - #[prost(bool, tag = "3")] - pub is_undo: bool, - #[prost(message, repeated, tag = "4")] - pub edit_ranges: Vec, - #[prost(string, repeated, tag = "5")] - pub edit_texts: Vec, - #[prost(message, repeated, tag = "6")] - pub undo_counts: Vec, - } - - #[derive(Message)] - pub struct VectorClockEntry { - #[prost(uint32, tag = "1")] - pub replica_id: u32, - #[prost(uint32, tag = "2")] - pub timestamp: u32, - } - - #[derive(Message)] - pub struct Range { - #[prost(uint64, tag = "1")] - pub start: u64, - #[prost(uint64, tag = "2")] - pub end: u64, - } - - #[derive(Message)] - pub struct UndoCount { - #[prost(uint32, tag = "1")] - pub replica_id: u32, - #[prost(uint32, tag = "2")] - pub lamport_timestamp: u32, - #[prost(uint32, tag = "3")] - pub count: u32, - } -} diff --git a/crates/collab2/src/db/queries/channels.rs b/crates/collab2/src/db/queries/channels.rs deleted file mode 100644 index 780fb783bc..0000000000 --- a/crates/collab2/src/db/queries/channels.rs +++ /dev/null @@ -1,1319 +0,0 @@ -use super::*; -use rpc::proto::channel_member::Kind; -use sea_orm::TryGetableMany; - -impl Database { - #[cfg(test)] - pub async fn all_channels(&self) -> Result> { - self.transaction(move |tx| async move { - let mut channels = Vec::new(); - let mut rows = channel::Entity::find().stream(&*tx).await?; - while let Some(row) = rows.next().await { - let row = row?; - channels.push((row.id, row.name)); - } - Ok(channels) - }) - .await - } - - #[cfg(test)] - pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { - Ok(self - .create_channel(name, None, creator_id) - .await? - .channel - .id) - } - - #[cfg(test)] - pub async fn create_sub_channel( - &self, - name: &str, - parent: ChannelId, - creator_id: UserId, - ) -> Result { - Ok(self - .create_channel(name, Some(parent), creator_id) - .await? - .channel - .id) - } - - pub async fn create_channel( - &self, - name: &str, - parent_channel_id: Option, - admin_id: UserId, - ) -> Result { - let name = Self::sanitize_channel_name(name)?; - self.transaction(move |tx| async move { - let mut parent = None; - - if let Some(parent_channel_id) = parent_channel_id { - let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?; - self.check_user_is_channel_admin(&parent_channel, admin_id, &*tx) - .await?; - parent = Some(parent_channel); - } - - let channel = channel::ActiveModel { - id: ActiveValue::NotSet, - name: ActiveValue::Set(name.to_string()), - visibility: ActiveValue::Set(ChannelVisibility::Members), - parent_path: ActiveValue::Set( - parent - .as_ref() - .map_or(String::new(), |parent| parent.path()), - ), - } - .insert(&*tx) - .await?; - - let participants_to_update; - if let Some(parent) = &parent { - participants_to_update = self - .participants_to_notify_for_channel_change(parent, &*tx) - .await?; - } else { - participants_to_update = vec![]; - - channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel.id), - user_id: ActiveValue::Set(admin_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Admin), - } - .insert(&*tx) - .await?; - }; - - Ok(CreateChannelResult { - channel: Channel::from_model(channel, ChannelRole::Admin), - participants_to_update, - }) - }) - .await - } - - pub async fn join_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - connection: ConnectionId, - environment: &str, - ) -> Result<(JoinRoom, Option, ChannelRole)> { - self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - let mut role = self.channel_role_for_user(&channel, user_id, &*tx).await?; - - let mut accept_invite_result = None; - - if role.is_none() { - if let Some(invitation) = self - .pending_invite_for_channel(&channel, user_id, &*tx) - .await? - { - // note, this may be a parent channel - role = Some(invitation.role); - channel_member::Entity::update(channel_member::ActiveModel { - accepted: ActiveValue::Set(true), - ..invitation.into_active_model() - }) - .exec(&*tx) - .await?; - - accept_invite_result = Some( - self.calculate_membership_updated(&channel, user_id, &*tx) - .await?, - ); - - debug_assert!( - self.channel_role_for_user(&channel, user_id, &*tx).await? == role - ); - } - } - - if channel.visibility == ChannelVisibility::Public { - role = Some(ChannelRole::Guest); - let channel_to_join = self - .public_ancestors_including_self(&channel, &*tx) - .await? - .first() - .cloned() - .unwrap_or(channel.clone()); - - channel_member::Entity::insert(channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_to_join.id), - user_id: ActiveValue::Set(user_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Guest), - }) - .exec(&*tx) - .await?; - - accept_invite_result = Some( - self.calculate_membership_updated(&channel_to_join, user_id, &*tx) - .await?, - ); - - debug_assert!(self.channel_role_for_user(&channel, user_id, &*tx).await? == role); - } - - if role.is_none() || role == Some(ChannelRole::Banned) { - Err(anyhow!("not allowed"))? - } - - let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); - let room_id = self - .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) - .await?; - - self.join_channel_room_internal(room_id, user_id, connection, &*tx) - .await - .map(|jr| (jr, accept_invite_result, role.unwrap())) - }) - .await - } - - pub async fn set_channel_visibility( - &self, - channel_id: ChannelId, - visibility: ChannelVisibility, - admin_id: UserId, - ) -> Result { - self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - - self.check_user_is_channel_admin(&channel, admin_id, &*tx) - .await?; - - let previous_members = self - .get_channel_participant_details_internal(&channel, &*tx) - .await?; - - let mut model = channel.into_active_model(); - model.visibility = ActiveValue::Set(visibility); - let channel = model.update(&*tx).await?; - - let mut participants_to_update: HashMap = self - .participants_to_notify_for_channel_change(&channel, &*tx) - .await? - .into_iter() - .collect(); - - let mut channels_to_remove: Vec = vec![]; - let mut participants_to_remove: HashSet = HashSet::default(); - match visibility { - ChannelVisibility::Members => { - let all_descendents: Vec = self - .get_channel_descendants_including_self(vec![channel_id], &*tx) - .await? - .into_iter() - .map(|channel| channel.id) - .collect(); - - channels_to_remove = channel::Entity::find() - .filter( - channel::Column::Id - .is_in(all_descendents) - .and(channel::Column::Visibility.eq(ChannelVisibility::Public)), - ) - .all(&*tx) - .await? - .into_iter() - .map(|channel| channel.id) - .collect(); - - channels_to_remove.push(channel_id); - - for member in previous_members { - if member.role.can_only_see_public_descendants() { - participants_to_remove.insert(member.user_id); - } - } - } - ChannelVisibility::Public => { - if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? { - let parent_updates = self - .participants_to_notify_for_channel_change(&public_parent, &*tx) - .await?; - - for (user_id, channels) in parent_updates { - participants_to_update.insert(user_id, channels); - } - } - } - } - - Ok(SetChannelVisibilityResult { - participants_to_update, - participants_to_remove, - channels_to_remove, - }) - }) - .await - } - - pub async fn delete_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result<(Vec, Vec)> { - self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, user_id, &*tx) - .await?; - - let members_to_notify: Vec = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) - .select_only() - .column(channel_member::Column::UserId) - .distinct() - .into_values::<_, QueryUserIds>() - .all(&*tx) - .await?; - - let channels_to_remove = self - .get_channel_descendants_including_self(vec![channel.id], &*tx) - .await? - .into_iter() - .map(|channel| channel.id) - .collect::>(); - - channel::Entity::delete_many() - .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) - .exec(&*tx) - .await?; - - Ok((channels_to_remove, members_to_notify)) - }) - .await - } - - pub async fn invite_channel_member( - &self, - channel_id: ChannelId, - invitee_id: UserId, - inviter_id: UserId, - role: ChannelRole, - ) -> Result { - self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, inviter_id, &*tx) - .await?; - - channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_id), - user_id: ActiveValue::Set(invitee_id), - accepted: ActiveValue::Set(false), - role: ActiveValue::Set(role), - } - .insert(&*tx) - .await?; - - let channel = Channel::from_model(channel, role); - - let notifications = self - .create_notification( - invitee_id, - rpc::Notification::ChannelInvitation { - channel_id: channel_id.to_proto(), - channel_name: channel.name.clone(), - inviter_id: inviter_id.to_proto(), - }, - true, - &*tx, - ) - .await? - .into_iter() - .collect(); - - Ok(InviteMemberResult { - channel, - notifications, - }) - }) - .await - } - - fn sanitize_channel_name(name: &str) -> Result<&str> { - let new_name = name.trim().trim_start_matches('#'); - if new_name == "" { - Err(anyhow!("channel name can't be blank"))?; - } - Ok(new_name) - } - - pub async fn rename_channel( - &self, - channel_id: ChannelId, - admin_id: UserId, - new_name: &str, - ) -> Result { - self.transaction(move |tx| async move { - let new_name = Self::sanitize_channel_name(new_name)?.to_string(); - - let channel = self.get_channel_internal(channel_id, &*tx).await?; - let role = self - .check_user_is_channel_admin(&channel, admin_id, &*tx) - .await?; - - let mut model = channel.into_active_model(); - model.name = ActiveValue::Set(new_name.clone()); - let channel = model.update(&*tx).await?; - - let participants = self - .get_channel_participant_details_internal(&channel, &*tx) - .await?; - - Ok(RenameChannelResult { - channel: Channel::from_model(channel.clone(), role), - participants_to_update: participants - .iter() - .map(|participant| { - ( - participant.user_id, - Channel::from_model(channel.clone(), participant.role), - ) - }) - .collect(), - }) - }) - .await - } - - pub async fn respond_to_channel_invite( - &self, - channel_id: ChannelId, - user_id: UserId, - accept: bool, - ) -> Result { - self.transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - - let membership_update = if accept { - let rows_affected = channel_member::Entity::update_many() - .set(channel_member::ActiveModel { - accepted: ActiveValue::Set(accept), - ..Default::default() - }) - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Accepted.eq(false)), - ) - .exec(&*tx) - .await? - .rows_affected; - - if rows_affected == 0 { - Err(anyhow!("no such invitation"))?; - } - - Some( - self.calculate_membership_updated(&channel, user_id, &*tx) - .await?, - ) - } else { - let rows_affected = channel_member::Entity::delete_many() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Accepted.eq(false)), - ) - .exec(&*tx) - .await? - .rows_affected; - if rows_affected == 0 { - Err(anyhow!("no such invitation"))?; - } - - None - }; - - Ok(RespondToChannelInvite { - membership_update, - notifications: self - .mark_notification_as_read_with_response( - user_id, - &rpc::Notification::ChannelInvitation { - channel_id: channel_id.to_proto(), - channel_name: Default::default(), - inviter_id: Default::default(), - }, - accept, - &*tx, - ) - .await? - .into_iter() - .collect(), - }) - }) - .await - } - - async fn calculate_membership_updated( - &self, - channel: &channel::Model, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result { - let new_channels = self.get_user_channels(user_id, Some(channel), &*tx).await?; - let removed_channels = self - .get_channel_descendants_including_self(vec![channel.id], &*tx) - .await? - .into_iter() - .filter_map(|channel| { - if !new_channels.channels.iter().any(|c| c.id == channel.id) { - Some(channel.id) - } else { - None - } - }) - .collect::>(); - - Ok(MembershipUpdated { - channel_id: channel.id, - new_channels, - removed_channels, - }) - } - - pub async fn remove_channel_member( - &self, - channel_id: ChannelId, - member_id: UserId, - admin_id: UserId, - ) -> Result { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) - .await?; - - let result = channel_member::Entity::delete_many() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(member_id)), - ) - .exec(&*tx) - .await?; - - if result.rows_affected == 0 { - Err(anyhow!("no such member"))?; - } - - Ok(RemoveChannelMemberResult { - membership_update: self - .calculate_membership_updated(&channel, member_id, &*tx) - .await?, - notification_id: self - .remove_notification( - member_id, - rpc::Notification::ChannelInvitation { - channel_id: channel_id.to_proto(), - channel_name: Default::default(), - inviter_id: Default::default(), - }, - &*tx, - ) - .await?, - }) - }) - .await - } - - pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { - self.transaction(|tx| async move { - let mut role_for_channel: HashMap = HashMap::default(); - - let channel_invites = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(false)), - ) - .all(&*tx) - .await?; - - for invite in channel_invites { - role_for_channel.insert(invite.channel_id, invite.role); - } - - let channels = channel::Entity::find() - .filter(channel::Column::Id.is_in(role_for_channel.keys().copied())) - .all(&*tx) - .await?; - - let channels = channels - .into_iter() - .filter_map(|channel| { - let role = *role_for_channel.get(&channel.id)?; - Some(Channel::from_model(channel, role)) - }) - .collect(); - - Ok(channels) - }) - .await - } - - pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let tx = tx; - - self.get_user_channels(user_id, None, &tx).await - }) - .await - } - - pub async fn get_user_channels( - &self, - user_id: UserId, - ancestor_channel: Option<&channel::Model>, - tx: &DatabaseTransaction, - ) -> Result { - let channel_memberships = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(true)), - ) - .all(&*tx) - .await?; - - let descendants = self - .get_channel_descendants_including_self( - channel_memberships.iter().map(|m| m.channel_id), - &*tx, - ) - .await?; - - let mut roles_by_channel_id: HashMap = HashMap::default(); - for membership in channel_memberships.iter() { - roles_by_channel_id.insert(membership.channel_id, membership.role); - } - - let mut visible_channel_ids: HashSet = HashSet::default(); - - let channels: Vec = descendants - .into_iter() - .filter_map(|channel| { - let parent_role = channel - .parent_id() - .and_then(|parent_id| roles_by_channel_id.get(&parent_id)); - - let role = if let Some(parent_role) = parent_role { - let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) { - existing_role.max(*parent_role) - } else { - *parent_role - }; - roles_by_channel_id.insert(channel.id, role); - role - } else { - *roles_by_channel_id.get(&channel.id)? - }; - - let can_see_parent_paths = role.can_see_all_descendants() - || role.can_only_see_public_descendants() - && channel.visibility == ChannelVisibility::Public; - if !can_see_parent_paths { - return None; - } - - visible_channel_ids.insert(channel.id); - - if let Some(ancestor) = ancestor_channel { - if !channel - .ancestors_including_self() - .any(|id| id == ancestor.id) - { - return None; - } - } - - let mut channel = Channel::from_model(channel, role); - channel - .parent_path - .retain(|id| visible_channel_ids.contains(&id)); - - Some(channel) - }) - .collect(); - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryUserIdsAndChannelIds { - ChannelId, - UserId, - } - - let mut channel_participants: HashMap> = HashMap::default(); - { - let mut rows = room_participant::Entity::find() - .inner_join(room::Entity) - .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) - .select_only() - .column(room::Column::ChannelId) - .column(room_participant::Column::UserId) - .into_values::<_, QueryUserIdsAndChannelIds>() - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row: (ChannelId, UserId) = row?; - channel_participants.entry(row.0).or_default().push(row.1) - } - } - - let channel_ids = channels.iter().map(|c| c.id).collect::>(); - let channel_buffer_changes = self - .unseen_channel_buffer_changes(user_id, &channel_ids, &*tx) - .await?; - - let unseen_messages = self - .unseen_channel_messages(user_id, &channel_ids, &*tx) - .await?; - - Ok(ChannelsForUser { - channels, - channel_participants, - unseen_buffer_changes: channel_buffer_changes, - channel_messages: unseen_messages, - }) - } - - async fn participants_to_notify_for_channel_change( - &self, - new_parent: &channel::Model, - tx: &DatabaseTransaction, - ) -> Result> { - let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new(); - - let members = self - .get_channel_participant_details_internal(new_parent, &*tx) - .await?; - - for member in members.iter() { - if !member.role.can_see_all_descendants() { - continue; - } - results.push(( - member.user_id, - self.get_user_channels(member.user_id, Some(new_parent), &*tx) - .await?, - )) - } - - let public_parents = self - .public_ancestors_including_self(new_parent, &*tx) - .await?; - let public_parent = public_parents.last(); - - let Some(public_parent) = public_parent else { - return Ok(results); - }; - - // could save some time in the common case by skipping this if the - // new channel is not public and has no public descendants. - let public_members = if public_parent == new_parent { - members - } else { - self.get_channel_participant_details_internal(public_parent, &*tx) - .await? - }; - - for member in public_members { - if !member.role.can_only_see_public_descendants() { - continue; - }; - results.push(( - member.user_id, - self.get_user_channels(member.user_id, Some(public_parent), &*tx) - .await?, - )) - } - - Ok(results) - } - - pub async fn set_channel_member_role( - &self, - channel_id: ChannelId, - admin_id: UserId, - for_user: UserId, - role: ChannelRole, - ) -> Result { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) - .await?; - - let membership = channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(for_user)), - ) - .one(&*tx) - .await?; - - let Some(membership) = membership else { - Err(anyhow!("no such member"))? - }; - - let mut update = membership.into_active_model(); - update.role = ActiveValue::Set(role); - let updated = channel_member::Entity::update(update).exec(&*tx).await?; - - if updated.accepted { - Ok(SetMemberRoleResult::MembershipUpdated( - self.calculate_membership_updated(&channel, for_user, &*tx) - .await?, - )) - } else { - Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model( - channel, role, - ))) - } - }) - .await - } - - pub async fn get_channel_participant_details( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result> { - let (role, members) = self - .transaction(move |tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - let role = self - .check_user_is_channel_participant(&channel, user_id, &*tx) - .await?; - Ok(( - role, - self.get_channel_participant_details_internal(&channel, &*tx) - .await?, - )) - }) - .await?; - - if role == ChannelRole::Admin { - Ok(members - .into_iter() - .map(|channel_member| channel_member.to_proto()) - .collect()) - } else { - return Ok(members - .into_iter() - .filter_map(|member| { - if member.kind == proto::channel_member::Kind::Invitee { - return None; - } - Some(ChannelMember { - role: member.role, - user_id: member.user_id, - kind: proto::channel_member::Kind::Member, - }) - }) - .map(|channel_member| channel_member.to_proto()) - .collect()); - } - } - - async fn get_channel_participant_details_internal( - &self, - channel: &channel::Model, - tx: &DatabaseTransaction, - ) -> Result> { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryMemberDetails { - UserId, - Role, - IsDirectMember, - Accepted, - Visibility, - } - - let mut stream = channel_member::Entity::find() - .left_join(channel::Entity) - .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) - .select_only() - .column(channel_member::Column::UserId) - .column(channel_member::Column::Role) - .column_as( - channel_member::Column::ChannelId.eq(channel.id), - QueryMemberDetails::IsDirectMember, - ) - .column(channel_member::Column::Accepted) - .column(channel::Column::Visibility) - .into_values::<_, QueryMemberDetails>() - .stream(&*tx) - .await?; - - let mut user_details: HashMap = HashMap::default(); - - while let Some(user_membership) = stream.next().await { - let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( - UserId, - ChannelRole, - bool, - bool, - ChannelVisibility, - ) = user_membership?; - let kind = match (is_direct_member, is_invite_accepted) { - (true, true) => proto::channel_member::Kind::Member, - (true, false) => proto::channel_member::Kind::Invitee, - (false, true) => proto::channel_member::Kind::AncestorMember, - (false, false) => continue, - }; - - if channel_role == ChannelRole::Guest - && visibility != ChannelVisibility::Public - && channel.visibility != ChannelVisibility::Public - { - continue; - } - - if let Some(details_mut) = user_details.get_mut(&user_id) { - if channel_role.should_override(details_mut.role) { - details_mut.role = channel_role; - } - if kind == Kind::Member { - details_mut.kind = kind; - // the UI is going to be a bit confusing if you already have permissions - // that are greater than or equal to the ones you're being invited to. - } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember { - details_mut.kind = kind; - } - } else { - user_details.insert( - user_id, - ChannelMember { - user_id, - kind, - role: channel_role, - }, - ); - } - } - - Ok(user_details - .into_iter() - .map(|(_, details)| details) - .collect()) - } - - pub async fn get_channel_participants( - &self, - channel: &channel::Model, - tx: &DatabaseTransaction, - ) -> Result> { - let participants = self - .get_channel_participant_details_internal(channel, &*tx) - .await?; - Ok(participants - .into_iter() - .map(|member| member.user_id) - .collect()) - } - - pub async fn check_user_is_channel_admin( - &self, - channel: &channel::Model, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result { - let role = self.channel_role_for_user(channel, user_id, tx).await?; - match role { - Some(ChannelRole::Admin) => Ok(role.unwrap()), - Some(ChannelRole::Member) - | Some(ChannelRole::Banned) - | Some(ChannelRole::Guest) - | None => Err(anyhow!( - "user is not a channel admin or channel does not exist" - ))?, - } - } - - pub async fn check_user_is_channel_member( - &self, - channel: &channel::Model, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result { - let channel_role = self.channel_role_for_user(channel, user_id, tx).await?; - match channel_role { - Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()), - Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!( - "user is not a channel member or channel does not exist" - ))?, - } - } - - pub async fn check_user_is_channel_participant( - &self, - channel: &channel::Model, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result { - let role = self.channel_role_for_user(channel, user_id, tx).await?; - match role { - Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => { - Ok(role.unwrap()) - } - Some(ChannelRole::Banned) | None => Err(anyhow!( - "user is not a channel participant or channel does not exist" - ))?, - } - } - - pub async fn pending_invite_for_channel( - &self, - channel: &channel::Model, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result> { - let row = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) - .filter(channel_member::Column::UserId.eq(user_id)) - .filter(channel_member::Column::Accepted.eq(false)) - .one(&*tx) - .await?; - - Ok(row) - } - - pub async fn public_parent_channel( - &self, - channel: &channel::Model, - tx: &DatabaseTransaction, - ) -> Result> { - let mut path = self.public_ancestors_including_self(channel, &*tx).await?; - if path.last().unwrap().id == channel.id { - path.pop(); - } - Ok(path.pop()) - } - - pub async fn public_ancestors_including_self( - &self, - channel: &channel::Model, - tx: &DatabaseTransaction, - ) -> Result> { - let visible_channels = channel::Entity::find() - .filter(channel::Column::Id.is_in(channel.ancestors_including_self())) - .filter(channel::Column::Visibility.eq(ChannelVisibility::Public)) - .order_by_asc(channel::Column::ParentPath) - .all(&*tx) - .await?; - - Ok(visible_channels) - } - - pub async fn channel_role_for_user( - &self, - channel: &channel::Model, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result> { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelMembership { - ChannelId, - Role, - Visibility, - } - - let mut rows = channel_member::Entity::find() - .left_join(channel::Entity) - .filter( - channel_member::Column::ChannelId - .is_in(channel.ancestors_including_self()) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Accepted.eq(true)), - ) - .select_only() - .column(channel_member::Column::ChannelId) - .column(channel_member::Column::Role) - .column(channel::Column::Visibility) - .into_values::<_, QueryChannelMembership>() - .stream(&*tx) - .await?; - - let mut user_role: Option = None; - - let mut is_participant = false; - let mut current_channel_visibility = None; - - // note these channels are not iterated in any particular order, - // our current logic takes the highest permission available. - while let Some(row) = rows.next().await { - let (membership_channel, role, visibility): ( - ChannelId, - ChannelRole, - ChannelVisibility, - ) = row?; - - match role { - ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => { - if let Some(users_role) = user_role { - user_role = Some(users_role.max(role)); - } else { - user_role = Some(role) - } - } - ChannelRole::Guest if visibility == ChannelVisibility::Public => { - is_participant = true - } - ChannelRole::Guest => {} - } - if channel.id == membership_channel { - current_channel_visibility = Some(visibility); - } - } - // free up database connection - drop(rows); - - if is_participant && user_role.is_none() { - if current_channel_visibility.is_none() { - current_channel_visibility = channel::Entity::find() - .filter(channel::Column::Id.eq(channel.id)) - .one(&*tx) - .await? - .map(|channel| channel.visibility); - } - if current_channel_visibility == Some(ChannelVisibility::Public) { - user_role = Some(ChannelRole::Guest); - } - } - - Ok(user_role) - } - - // Get the descendants of the given set if channels, ordered by their - // path. - async fn get_channel_descendants_including_self( - &self, - channel_ids: impl IntoIterator, - tx: &DatabaseTransaction, - ) -> Result> { - let mut values = String::new(); - for id in channel_ids { - if !values.is_empty() { - values.push_str(", "); - } - write!(&mut values, "({})", id).unwrap(); - } - - if values.is_empty() { - return Ok(vec![]); - } - - let sql = format!( - r#" - SELECT DISTINCT - descendant_channels.*, - descendant_channels.parent_path || descendant_channels.id as full_path - FROM - channels parent_channels, channels descendant_channels - WHERE - descendant_channels.id IN ({values}) OR - ( - parent_channels.id IN ({values}) AND - descendant_channels.parent_path LIKE (parent_channels.parent_path || parent_channels.id || '/%') - ) - ORDER BY - full_path ASC - "# - ); - - Ok(channel::Entity::find() - .from_raw_sql(Statement::from_string( - self.pool.get_database_backend(), - sql, - )) - .all(tx) - .await?) - } - - /// Returns the channel with the given ID - pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - let role = self - .check_user_is_channel_participant(&channel, user_id, &*tx) - .await?; - - Ok(Channel::from_model(channel, role)) - }) - .await - } - - pub async fn get_channel_internal( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result { - Ok(channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such channel"))?) - } - - pub(crate) async fn get_or_create_channel_room( - &self, - channel_id: ChannelId, - live_kit_room: &str, - environment: &str, - tx: &DatabaseTransaction, - ) -> Result { - let room = room::Entity::find() - .filter(room::Column::ChannelId.eq(channel_id)) - .one(&*tx) - .await?; - - let room_id = if let Some(room) = room { - if let Some(env) = room.enviroment { - if &env != environment { - Err(anyhow!("must join using the {} release", env))?; - } - } - room.id - } else { - let result = room::Entity::insert(room::ActiveModel { - channel_id: ActiveValue::Set(Some(channel_id)), - live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - enviroment: ActiveValue::Set(Some(environment.to_string())), - ..Default::default() - }) - .exec(&*tx) - .await?; - - result.last_insert_id - }; - - Ok(room_id) - } - - /// Move a channel from one parent to another - pub async fn move_channel( - &self, - channel_id: ChannelId, - new_parent_id: Option, - admin_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) - .await?; - - let new_parent_path; - let new_parent_channel; - if let Some(new_parent_id) = new_parent_id { - let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; - self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) - .await?; - - if new_parent - .ancestors_including_self() - .any(|id| id == channel.id) - { - Err(anyhow!("cannot move a channel into one of its descendants"))?; - } - - new_parent_path = new_parent.path(); - new_parent_channel = Some(new_parent); - } else { - new_parent_path = String::new(); - new_parent_channel = None; - }; - - let previous_participants = self - .get_channel_participant_details_internal(&channel, &*tx) - .await?; - - let old_path = format!("{}{}/", channel.parent_path, channel.id); - let new_path = format!("{}{}/", new_parent_path, channel.id); - - if old_path == new_path { - return Ok(None); - } - - let mut model = channel.into_active_model(); - model.parent_path = ActiveValue::Set(new_parent_path); - let channel = model.update(&*tx).await?; - - if new_parent_channel.is_none() { - channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_id), - user_id: ActiveValue::Set(admin_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Admin), - } - .insert(&*tx) - .await?; - } - - let descendent_ids = - ChannelId::find_by_statement::(Statement::from_sql_and_values( - self.pool.get_database_backend(), - " - UPDATE channels SET parent_path = REPLACE(parent_path, $1, $2) - WHERE parent_path LIKE $3 || '%' - RETURNING id - ", - [old_path.clone().into(), new_path.into(), old_path.into()], - )) - .all(&*tx) - .await?; - - let participants_to_update: HashMap<_, _> = self - .participants_to_notify_for_channel_change( - new_parent_channel.as_ref().unwrap_or(&channel), - &*tx, - ) - .await? - .into_iter() - .collect(); - - let mut moved_channels: HashSet = HashSet::default(); - for id in descendent_ids { - moved_channels.insert(id); - } - moved_channels.insert(channel_id); - - let mut participants_to_remove: HashSet = HashSet::default(); - for participant in previous_participants { - if participant.kind == proto::channel_member::Kind::AncestorMember { - if !participants_to_update.contains_key(&participant.user_id) { - participants_to_remove.insert(participant.user_id); - } - } - } - - Ok(Some(MoveChannelResult { - participants_to_remove, - participants_to_update, - moved_channels, - })) - }) - .await - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -enum QueryIds { - Id, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -enum QueryUserIds { - UserId, -} diff --git a/crates/collab2/src/db/queries/contacts.rs b/crates/collab2/src/db/queries/contacts.rs deleted file mode 100644 index f31f1addbd..0000000000 --- a/crates/collab2/src/db/queries/contacts.rs +++ /dev/null @@ -1,353 +0,0 @@ -use super::*; - -impl Database { - pub async fn get_contacts(&self, user_id: UserId) -> Result> { - #[derive(Debug, FromQueryResult)] - struct ContactWithUserBusyStatuses { - user_id_a: UserId, - user_id_b: UserId, - a_to_b: bool, - accepted: bool, - user_a_busy: bool, - user_b_busy: bool, - } - - self.transaction(|tx| async move { - let user_a_participant = Alias::new("user_a_participant"); - let user_b_participant = Alias::new("user_b_participant"); - let mut db_contacts = contact::Entity::find() - .column_as( - Expr::col((user_a_participant.clone(), room_participant::Column::Id)) - .is_not_null(), - "user_a_busy", - ) - .column_as( - Expr::col((user_b_participant.clone(), room_participant::Column::Id)) - .is_not_null(), - "user_b_busy", - ) - .filter( - contact::Column::UserIdA - .eq(user_id) - .or(contact::Column::UserIdB.eq(user_id)), - ) - .join_as( - JoinType::LeftJoin, - contact::Relation::UserARoomParticipant.def(), - user_a_participant, - ) - .join_as( - JoinType::LeftJoin, - contact::Relation::UserBRoomParticipant.def(), - user_b_participant, - ) - .into_model::() - .stream(&*tx) - .await?; - - let mut contacts = Vec::new(); - while let Some(db_contact) = db_contacts.next().await { - let db_contact = db_contact?; - if db_contact.user_id_a == user_id { - if db_contact.accepted { - contacts.push(Contact::Accepted { - user_id: db_contact.user_id_b, - busy: db_contact.user_b_busy, - }); - } else if db_contact.a_to_b { - contacts.push(Contact::Outgoing { - user_id: db_contact.user_id_b, - }) - } else { - contacts.push(Contact::Incoming { - user_id: db_contact.user_id_b, - }); - } - } else if db_contact.accepted { - contacts.push(Contact::Accepted { - user_id: db_contact.user_id_a, - busy: db_contact.user_a_busy, - }); - } else if db_contact.a_to_b { - contacts.push(Contact::Incoming { - user_id: db_contact.user_id_a, - }); - } else { - contacts.push(Contact::Outgoing { - user_id: db_contact.user_id_a, - }); - } - } - - contacts.sort_unstable_by_key(|contact| contact.user_id()); - - Ok(contacts) - }) - .await - } - - pub async fn is_user_busy(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let participant = room_participant::Entity::find() - .filter(room_participant::Column::UserId.eq(user_id)) - .one(&*tx) - .await?; - Ok(participant.is_some()) - }) - .await - } - - pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { - self.transaction(|tx| async move { - let (id_a, id_b) = if user_id_1 < user_id_2 { - (user_id_1, user_id_2) - } else { - (user_id_2, user_id_1) - }; - - Ok(contact::Entity::find() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::Accepted.eq(true)), - ) - .one(&*tx) - .await? - .is_some()) - }) - .await - } - - pub async fn send_contact_request( - &self, - sender_id: UserId, - receiver_id: UserId, - ) -> Result { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if sender_id < receiver_id { - (sender_id, receiver_id, true) - } else { - (receiver_id, sender_id, false) - }; - - let rows_affected = contact::Entity::insert(contact::ActiveModel { - user_id_a: ActiveValue::set(id_a), - user_id_b: ActiveValue::set(id_b), - a_to_b: ActiveValue::set(a_to_b), - accepted: ActiveValue::set(false), - should_notify: ActiveValue::set(true), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB]) - .values([ - (contact::Column::Accepted, true.into()), - (contact::Column::ShouldNotify, false.into()), - ]) - .action_and_where( - contact::Column::Accepted.eq(false).and( - contact::Column::AToB - .eq(a_to_b) - .and(contact::Column::UserIdA.eq(id_b)) - .or(contact::Column::AToB - .ne(a_to_b) - .and(contact::Column::UserIdA.eq(id_a))), - ), - ) - .to_owned(), - ) - .exec_without_returning(&*tx) - .await?; - - if rows_affected == 0 { - Err(anyhow!("contact already requested"))?; - } - - Ok(self - .create_notification( - receiver_id, - rpc::Notification::ContactRequest { - sender_id: sender_id.to_proto(), - }, - true, - &*tx, - ) - .await? - .into_iter() - .collect()) - }) - .await - } - - /// Returns a bool indicating whether the removed contact had originally accepted or not - /// - /// Deletes the contact identified by the requester and responder ids, and then returns - /// whether the deleted contact had originally accepted or was a pending contact request. - /// - /// # Arguments - /// - /// * `requester_id` - The user that initiates this request - /// * `responder_id` - The user that will be removed - pub async fn remove_contact( - &self, - requester_id: UserId, - responder_id: UserId, - ) -> Result<(bool, Option)> { - self.transaction(|tx| async move { - let (id_a, id_b) = if responder_id < requester_id { - (responder_id, requester_id) - } else { - (requester_id, responder_id) - }; - - let contact = contact::Entity::find() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such contact"))?; - - contact::Entity::delete_by_id(contact.id).exec(&*tx).await?; - - let mut deleted_notification_id = None; - if !contact.accepted { - deleted_notification_id = self - .remove_notification( - responder_id, - rpc::Notification::ContactRequest { - sender_id: requester_id.to_proto(), - }, - &*tx, - ) - .await?; - } - - Ok((contact.accepted, deleted_notification_id)) - }) - .await - } - - pub async fn dismiss_contact_notification( - &self, - user_id: UserId, - contact_user_id: UserId, - ) -> Result<()> { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if user_id < contact_user_id { - (user_id, contact_user_id, true) - } else { - (contact_user_id, user_id, false) - }; - - let result = contact::Entity::update_many() - .set(contact::ActiveModel { - should_notify: ActiveValue::set(false), - ..Default::default() - }) - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and( - contact::Column::AToB - .eq(a_to_b) - .and(contact::Column::Accepted.eq(true)) - .or(contact::Column::AToB - .ne(a_to_b) - .and(contact::Column::Accepted.eq(false))), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("no such contact request"))? - } else { - Ok(()) - } - }) - .await - } - - pub async fn respond_to_contact_request( - &self, - responder_id: UserId, - requester_id: UserId, - accept: bool, - ) -> Result { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if responder_id < requester_id { - (responder_id, requester_id, false) - } else { - (requester_id, responder_id, true) - }; - let rows_affected = if accept { - let result = contact::Entity::update_many() - .set(contact::ActiveModel { - accepted: ActiveValue::set(true), - should_notify: ActiveValue::set(true), - ..Default::default() - }) - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::AToB.eq(a_to_b)), - ) - .exec(&*tx) - .await?; - result.rows_affected - } else { - let result = contact::Entity::delete_many() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::AToB.eq(a_to_b)) - .and(contact::Column::Accepted.eq(false)), - ) - .exec(&*tx) - .await?; - - result.rows_affected - }; - - if rows_affected == 0 { - Err(anyhow!("no such contact request"))? - } - - let mut notifications = Vec::new(); - notifications.extend( - self.mark_notification_as_read_with_response( - responder_id, - &rpc::Notification::ContactRequest { - sender_id: requester_id.to_proto(), - }, - accept, - &*tx, - ) - .await?, - ); - - if accept { - notifications.extend( - self.create_notification( - requester_id, - rpc::Notification::ContactRequestAccepted { - responder_id: responder_id.to_proto(), - }, - true, - &*tx, - ) - .await?, - ); - } - - Ok(notifications) - }) - .await - } -} diff --git a/crates/collab2/src/db/queries/messages.rs b/crates/collab2/src/db/queries/messages.rs deleted file mode 100644 index 47bb27df39..0000000000 --- a/crates/collab2/src/db/queries/messages.rs +++ /dev/null @@ -1,505 +0,0 @@ -use super::*; -use rpc::Notification; -use sea_orm::TryInsertResult; -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 { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, 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 { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, 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 rows = channel_message::Entity::find() - .filter(condition) - .order_by_desc(channel_message::Column::Id) - .limit(count as u64) - .all(&*tx) - .await?; - - self.load_channel_messages(rows, &*tx).await - }) - .await - } - - pub async fn get_channel_messages_by_id( - &self, - user_id: UserId, - message_ids: &[MessageId], - ) -> Result> { - self.transaction(|tx| async move { - let rows = channel_message::Entity::find() - .filter(channel_message::Column::Id.is_in(message_ids.iter().copied())) - .order_by_desc(channel_message::Column::Id) - .all(&*tx) - .await?; - - let mut channels = HashMap::::default(); - for row in &rows { - channels.insert( - row.channel_id, - self.get_channel_internal(row.channel_id, &*tx).await?, - ); - } - - for (_, channel) in channels { - self.check_user_is_channel_participant(&channel, user_id, &*tx) - .await?; - } - - let messages = self.load_channel_messages(rows, &*tx).await?; - Ok(messages) - }) - .await - } - - async fn load_channel_messages( - &self, - rows: Vec, - tx: &DatabaseTransaction, - ) -> Result> { - let mut messages = rows - .into_iter() - .map(|row| { - let nonce = row.nonce.as_u64_pair(); - 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, - mentions: vec![], - nonce: Some(proto::Nonce { - upper_half: nonce.0, - lower_half: nonce.1, - }), - } - }) - .collect::>(); - messages.reverse(); - - let mut mentions = channel_message_mention::Entity::find() - .filter(channel_message_mention::Column::MessageId.is_in(messages.iter().map(|m| m.id))) - .order_by_asc(channel_message_mention::Column::MessageId) - .order_by_asc(channel_message_mention::Column::StartOffset) - .stream(&*tx) - .await?; - - let mut message_ix = 0; - while let Some(mention) = mentions.next().await { - let mention = mention?; - let message_id = mention.message_id.to_proto(); - while let Some(message) = messages.get_mut(message_ix) { - if message.id < message_id { - message_ix += 1; - } else { - if message.id == message_id { - message.mentions.push(proto::ChatMention { - range: Some(proto::Range { - start: mention.start_offset as u64, - end: mention.end_offset as u64, - }), - user_id: mention.user_id.to_proto(), - }); - } - break; - } - } - } - - Ok(messages) - } - - pub async fn create_channel_message( - &self, - channel_id: ChannelId, - user_id: UserId, - body: &str, - mentions: &[proto::ChatMention], - timestamp: OffsetDateTime, - nonce: u128, - ) -> Result { - self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_participant(&channel, user_id, &*tx) - .await?; - - 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 result = 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::columns([ - channel_message::Column::SenderId, - channel_message::Column::Nonce, - ]) - .do_nothing() - .to_owned(), - ) - .do_nothing() - .exec(&*tx) - .await?; - - let message_id; - let mut notifications = Vec::new(); - match result { - TryInsertResult::Inserted(result) => { - message_id = result.last_insert_id; - let mentioned_user_ids = - mentions.iter().map(|m| m.user_id).collect::>(); - let mentions = mentions - .iter() - .filter_map(|mention| { - let range = mention.range.as_ref()?; - if !body.is_char_boundary(range.start as usize) - || !body.is_char_boundary(range.end as usize) - { - return None; - } - Some(channel_message_mention::ActiveModel { - message_id: ActiveValue::Set(message_id), - start_offset: ActiveValue::Set(range.start as i32), - end_offset: ActiveValue::Set(range.end as i32), - user_id: ActiveValue::Set(UserId::from_proto(mention.user_id)), - }) - }) - .collect::>(); - if !mentions.is_empty() { - channel_message_mention::Entity::insert_many(mentions) - .exec(&*tx) - .await?; - } - - for mentioned_user in mentioned_user_ids { - notifications.extend( - self.create_notification( - UserId::from_proto(mentioned_user), - rpc::Notification::ChannelMessageMention { - message_id: message_id.to_proto(), - sender_id: user_id.to_proto(), - channel_id: channel_id.to_proto(), - }, - false, - &*tx, - ) - .await?, - ); - } - - self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx) - .await?; - } - _ => { - message_id = channel_message::Entity::find() - .filter(channel_message::Column::Nonce.eq(Uuid::from_u128(nonce))) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("failed to insert message"))? - .id; - } - } - - let mut channel_members = self.get_channel_participants(&channel, &*tx).await?; - channel_members.retain(|member| !participant_user_ids.contains(member)); - - Ok(CreatedChannelMessage { - message_id, - participant_connection_ids, - channel_members, - notifications, - }) - }) - .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?; - let mut batch = NotificationBatch::default(); - batch.extend( - self.mark_notification_as_read( - user_id, - &Notification::ChannelMessageMention { - message_id: message_id.to_proto(), - sender_id: Default::default(), - channel_id: Default::default(), - }, - &*tx, - ) - .await?, - ); - Ok(batch) - }) - .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 { - let channel = self.get_channel_internal(channel_id, &*tx).await?; - if self - .check_user_is_channel_admin(&channel, user_id, &*tx) - .await - .is_ok() - { - let result = channel_message::Entity::delete_by_id(message_id) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("no such message"))?; - } - } else { - Err(anyhow!("operation could not be completed"))?; - } - } - - Ok(participant_connection_ids) - }) - .await - } -} diff --git a/crates/collab2/src/db/queries/notifications.rs b/crates/collab2/src/db/queries/notifications.rs deleted file mode 100644 index 6f2511c23e..0000000000 --- a/crates/collab2/src/db/queries/notifications.rs +++ /dev/null @@ -1,262 +0,0 @@ -use super::*; -use rpc::Notification; - -impl Database { - pub async fn initialize_notification_kinds(&mut self) -> Result<()> { - notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map( - |kind| notification_kind::ActiveModel { - name: ActiveValue::Set(kind.to_string()), - ..Default::default() - }, - )) - .on_conflict(OnConflict::new().do_nothing().to_owned()) - .exec_without_returning(&self.pool) - .await?; - - let mut rows = notification_kind::Entity::find().stream(&self.pool).await?; - while let Some(row) = rows.next().await { - let row = row?; - self.notification_kinds_by_name.insert(row.name, row.id); - } - - for name in Notification::all_variant_names() { - if let Some(id) = self.notification_kinds_by_name.get(*name).copied() { - self.notification_kinds_by_id.insert(id, name); - } - } - - Ok(()) - } - - pub async fn get_notifications( - &self, - recipient_id: UserId, - limit: usize, - before_id: Option, - ) -> Result> { - self.transaction(|tx| async move { - let mut result = Vec::new(); - let mut condition = - Condition::all().add(notification::Column::RecipientId.eq(recipient_id)); - - if let Some(before_id) = before_id { - condition = condition.add(notification::Column::Id.lt(before_id)); - } - - let mut rows = notification::Entity::find() - .filter(condition) - .order_by_desc(notification::Column::Id) - .limit(limit as u64) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - let kind = row.kind; - if let Some(proto) = model_to_proto(self, row) { - result.push(proto); - } else { - log::warn!("unknown notification kind {:?}", kind); - } - } - result.reverse(); - Ok(result) - }) - .await - } - - /// Create a notification. If `avoid_duplicates` is set to true, then avoid - /// creating a new notification if the given recipient already has an - /// unread notification with the given kind and entity id. - pub async fn create_notification( - &self, - recipient_id: UserId, - notification: Notification, - avoid_duplicates: bool, - tx: &DatabaseTransaction, - ) -> Result> { - if avoid_duplicates { - if self - .find_notification(recipient_id, ¬ification, tx) - .await? - .is_some() - { - return Ok(None); - } - } - - let proto = notification.to_proto(); - let kind = notification_kind_from_proto(self, &proto)?; - let model = notification::ActiveModel { - recipient_id: ActiveValue::Set(recipient_id), - kind: ActiveValue::Set(kind), - entity_id: ActiveValue::Set(proto.entity_id.map(|id| id as i32)), - content: ActiveValue::Set(proto.content.clone()), - ..Default::default() - } - .save(&*tx) - .await?; - - Ok(Some(( - recipient_id, - proto::Notification { - id: model.id.as_ref().to_proto(), - kind: proto.kind, - timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, - is_read: false, - response: None, - content: proto.content, - entity_id: proto.entity_id, - }, - ))) - } - - /// Remove an unread notification with the given recipient, kind and - /// entity id. - pub async fn remove_notification( - &self, - recipient_id: UserId, - notification: Notification, - tx: &DatabaseTransaction, - ) -> Result> { - let id = self - .find_notification(recipient_id, ¬ification, tx) - .await?; - if let Some(id) = id { - notification::Entity::delete_by_id(id).exec(tx).await?; - } - Ok(id) - } - - /// Populate the response for the notification with the given kind and - /// entity id. - pub async fn mark_notification_as_read_with_response( - &self, - recipient_id: UserId, - notification: &Notification, - response: bool, - tx: &DatabaseTransaction, - ) -> Result> { - self.mark_notification_as_read_internal(recipient_id, notification, Some(response), tx) - .await - } - - pub async fn mark_notification_as_read( - &self, - recipient_id: UserId, - notification: &Notification, - tx: &DatabaseTransaction, - ) -> Result> { - self.mark_notification_as_read_internal(recipient_id, notification, None, tx) - .await - } - - pub async fn mark_notification_as_read_by_id( - &self, - recipient_id: UserId, - notification_id: NotificationId, - ) -> Result { - self.transaction(|tx| async move { - let row = notification::Entity::update(notification::ActiveModel { - id: ActiveValue::Unchanged(notification_id), - recipient_id: ActiveValue::Unchanged(recipient_id), - is_read: ActiveValue::Set(true), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(model_to_proto(self, row) - .map(|notification| (recipient_id, notification)) - .into_iter() - .collect()) - }) - .await - } - - async fn mark_notification_as_read_internal( - &self, - recipient_id: UserId, - notification: &Notification, - response: Option, - tx: &DatabaseTransaction, - ) -> Result> { - if let Some(id) = self - .find_notification(recipient_id, notification, &*tx) - .await? - { - let row = notification::Entity::update(notification::ActiveModel { - id: ActiveValue::Unchanged(id), - recipient_id: ActiveValue::Unchanged(recipient_id), - is_read: ActiveValue::Set(true), - response: if let Some(response) = response { - ActiveValue::Set(Some(response)) - } else { - ActiveValue::NotSet - }, - ..Default::default() - }) - .exec(tx) - .await?; - Ok(model_to_proto(self, row).map(|notification| (recipient_id, notification))) - } else { - Ok(None) - } - } - - /// Find an unread notification by its recipient, kind and entity id. - async fn find_notification( - &self, - recipient_id: UserId, - notification: &Notification, - tx: &DatabaseTransaction, - ) -> Result> { - let proto = notification.to_proto(); - let kind = notification_kind_from_proto(self, &proto)?; - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryIds { - Id, - } - - Ok(notification::Entity::find() - .select_only() - .column(notification::Column::Id) - .filter( - Condition::all() - .add(notification::Column::RecipientId.eq(recipient_id)) - .add(notification::Column::IsRead.eq(false)) - .add(notification::Column::Kind.eq(kind)) - .add(if proto.entity_id.is_some() { - notification::Column::EntityId.eq(proto.entity_id) - } else { - notification::Column::EntityId.is_null() - }), - ) - .into_values::<_, QueryIds>() - .one(&*tx) - .await?) - } -} - -fn model_to_proto(this: &Database, row: notification::Model) -> Option { - let kind = this.notification_kinds_by_id.get(&row.kind)?; - Some(proto::Notification { - id: row.id.to_proto(), - kind: kind.to_string(), - timestamp: row.created_at.assume_utc().unix_timestamp() as u64, - is_read: row.is_read, - response: row.response, - content: row.content, - entity_id: row.entity_id.map(|id| id as u64), - }) -} - -fn notification_kind_from_proto( - this: &Database, - proto: &proto::Notification, -) -> Result { - Ok(this - .notification_kinds_by_name - .get(&proto.kind) - .copied() - .ok_or_else(|| anyhow!("invalid notification kind {:?}", proto.kind))?) -} diff --git a/crates/collab2/src/db/queries/projects.rs b/crates/collab2/src/db/queries/projects.rs deleted file mode 100644 index 3e2c003378..0000000000 --- a/crates/collab2/src/db/queries/projects.rs +++ /dev/null @@ -1,960 +0,0 @@ -use super::*; - -impl Database { - pub async fn project_count_excluding_admins(&self) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - Count, - } - - self.transaction(|tx| async move { - Ok(project::Entity::find() - .select_only() - .column_as(project::Column::Id.count(), QueryAs::Count) - .inner_join(user::Entity) - .filter(user::Column::Admin.eq(false)) - .into_values::<_, QueryAs>() - .one(&*tx) - .await? - .unwrap_or(0i64) as usize) - }) - .await - } - - pub async fn share_project( - &self, - room_id: RoomId, - connection: ConnectionId, - worktrees: &[proto::WorktreeMetadata], - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("could not find participant"))?; - if participant.room_id != room_id { - return Err(anyhow!("shared project on unexpected room"))?; - } - - let project = project::ActiveModel { - room_id: ActiveValue::set(participant.room_id), - host_user_id: ActiveValue::set(participant.user_id), - host_connection_id: ActiveValue::set(Some(connection.id as i32)), - host_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..Default::default() - } - .insert(&*tx) - .await?; - - if !worktrees.is_empty() { - worktree::Entity::insert_many(worktrees.iter().map(|worktree| { - worktree::ActiveModel { - id: ActiveValue::set(worktree.id as i64), - project_id: ActiveValue::set(project.id), - abs_path: ActiveValue::set(worktree.abs_path.clone()), - root_name: ActiveValue::set(worktree.root_name.clone()), - visible: ActiveValue::set(worktree.visible), - scan_id: ActiveValue::set(0), - completed_scan_id: ActiveValue::set(0), - } - })) - .exec(&*tx) - .await?; - } - - project_collaborator::ActiveModel { - project_id: ActiveValue::set(project.id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(ReplicaId(0)), - is_host: ActiveValue::set(true), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok((project.id, room)) - }) - .await - } - - pub async fn unshare_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result)>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project not found"))?; - if project.host_connection()? == connection { - project::Entity::delete(project.into_active_model()) - .exec(&*tx) - .await?; - let room = self.get_room(room_id, &tx).await?; - Ok((room, guest_connection_ids)) - } else { - Err(anyhow!("cannot unshare a project hosted by another user"))? - } - }) - .await - } - - pub async fn update_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - worktrees: &[proto::WorktreeMetadata], - ) -> Result)>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let project = project::Entity::find_by_id(project_id) - .filter( - Condition::all() - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId.eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - - self.update_project_worktrees(project.id, worktrees, &tx) - .await?; - - let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; - let room = self.get_room(project.room_id, &tx).await?; - Ok((room, guest_connection_ids)) - }) - .await - } - - pub(in crate::db) async fn update_project_worktrees( - &self, - project_id: ProjectId, - worktrees: &[proto::WorktreeMetadata], - tx: &DatabaseTransaction, - ) -> Result<()> { - if !worktrees.is_empty() { - worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel { - id: ActiveValue::set(worktree.id as i64), - project_id: ActiveValue::set(project_id), - abs_path: ActiveValue::set(worktree.abs_path.clone()), - root_name: ActiveValue::set(worktree.root_name.clone()), - visible: ActiveValue::set(worktree.visible), - scan_id: ActiveValue::set(0), - completed_scan_id: ActiveValue::set(0), - })) - .on_conflict( - OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id]) - .update_column(worktree::Column::RootName) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - worktree::Entity::delete_many() - .filter(worktree::Column::ProjectId.eq(project_id).and( - worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)), - )) - .exec(&*tx) - .await?; - - Ok(()) - } - - pub async fn update_worktree( - &self, - update: &proto::UpdateWorktree, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let worktree_id = update.worktree_id as i64; - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - // Ensure the update comes from the host. - let _project = project::Entity::find_by_id(project_id) - .filter( - Condition::all() - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId.eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - - // Update metadata. - worktree::Entity::update(worktree::ActiveModel { - id: ActiveValue::set(worktree_id), - project_id: ActiveValue::set(project_id), - root_name: ActiveValue::set(update.root_name.clone()), - scan_id: ActiveValue::set(update.scan_id as i64), - completed_scan_id: if update.is_last_update { - ActiveValue::set(update.scan_id as i64) - } else { - ActiveValue::default() - }, - abs_path: ActiveValue::set(update.abs_path.clone()), - ..Default::default() - }) - .exec(&*tx) - .await?; - - if !update.updated_entries.is_empty() { - worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| { - let mtime = entry.mtime.clone().unwrap_or_default(); - worktree_entry::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - id: ActiveValue::set(entry.id as i64), - is_dir: ActiveValue::set(entry.is_dir), - path: ActiveValue::set(entry.path.clone()), - inode: ActiveValue::set(entry.inode as i64), - mtime_seconds: ActiveValue::set(mtime.seconds as i64), - mtime_nanos: ActiveValue::set(mtime.nanos as i32), - is_symlink: ActiveValue::set(entry.is_symlink), - is_ignored: ActiveValue::set(entry.is_ignored), - is_external: ActiveValue::set(entry.is_external), - git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), - is_deleted: ActiveValue::set(false), - scan_id: ActiveValue::set(update.scan_id as i64), - } - })) - .on_conflict( - OnConflict::columns([ - worktree_entry::Column::ProjectId, - worktree_entry::Column::WorktreeId, - worktree_entry::Column::Id, - ]) - .update_columns([ - worktree_entry::Column::IsDir, - worktree_entry::Column::Path, - worktree_entry::Column::Inode, - worktree_entry::Column::MtimeSeconds, - worktree_entry::Column::MtimeNanos, - worktree_entry::Column::IsSymlink, - worktree_entry::Column::IsIgnored, - worktree_entry::Column::GitStatus, - worktree_entry::Column::ScanId, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - if !update.removed_entries.is_empty() { - worktree_entry::Entity::update_many() - .filter( - worktree_entry::Column::ProjectId - .eq(project_id) - .and(worktree_entry::Column::WorktreeId.eq(worktree_id)) - .and( - worktree_entry::Column::Id - .is_in(update.removed_entries.iter().map(|id| *id as i64)), - ), - ) - .set(worktree_entry::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - if !update.updated_repositories.is_empty() { - worktree_repository::Entity::insert_many(update.updated_repositories.iter().map( - |repository| worktree_repository::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - work_directory_id: ActiveValue::set(repository.work_directory_id as i64), - scan_id: ActiveValue::set(update.scan_id as i64), - branch: ActiveValue::set(repository.branch.clone()), - is_deleted: ActiveValue::set(false), - }, - )) - .on_conflict( - OnConflict::columns([ - worktree_repository::Column::ProjectId, - worktree_repository::Column::WorktreeId, - worktree_repository::Column::WorkDirectoryId, - ]) - .update_columns([ - worktree_repository::Column::ScanId, - worktree_repository::Column::Branch, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - if !update.removed_repositories.is_empty() { - worktree_repository::Entity::update_many() - .filter( - worktree_repository::Column::ProjectId - .eq(project_id) - .and(worktree_repository::Column::WorktreeId.eq(worktree_id)) - .and( - worktree_repository::Column::WorkDirectoryId - .is_in(update.removed_repositories.iter().map(|id| *id as i64)), - ), - ) - .set(worktree_repository::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn update_diagnostic_summary( - &self, - update: &proto::UpdateDiagnosticSummary, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let worktree_id = update.worktree_id as i64; - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let summary = update - .summary - .as_ref() - .ok_or_else(|| anyhow!("invalid summary"))?; - - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - // Update summary. - worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - path: ActiveValue::set(summary.path.clone()), - language_server_id: ActiveValue::set(summary.language_server_id as i64), - error_count: ActiveValue::set(summary.error_count as i32), - warning_count: ActiveValue::set(summary.warning_count as i32), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([ - worktree_diagnostic_summary::Column::ProjectId, - worktree_diagnostic_summary::Column::WorktreeId, - worktree_diagnostic_summary::Column::Path, - ]) - .update_columns([ - worktree_diagnostic_summary::Column::LanguageServerId, - worktree_diagnostic_summary::Column::ErrorCount, - worktree_diagnostic_summary::Column::WarningCount, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn start_language_server( - &self, - update: &proto::StartLanguageServer, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let server = update - .server - .as_ref() - .ok_or_else(|| anyhow!("invalid language server"))?; - - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - // Add the newly-started language server. - language_server::Entity::insert(language_server::ActiveModel { - project_id: ActiveValue::set(project_id), - id: ActiveValue::set(server.id as i64), - name: ActiveValue::set(server.name.clone()), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([ - language_server::Column::ProjectId, - language_server::Column::Id, - ]) - .update_column(language_server::Column::Name) - .to_owned(), - ) - .exec(&*tx) - .await?; - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn update_worktree_settings( - &self, - update: &proto::UpdateWorktreeSettings, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - if let Some(content) = &update.content { - worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { - project_id: ActiveValue::Set(project_id), - worktree_id: ActiveValue::Set(update.worktree_id as i64), - path: ActiveValue::Set(update.path.clone()), - content: ActiveValue::Set(content.clone()), - }) - .on_conflict( - OnConflict::columns([ - worktree_settings_file::Column::ProjectId, - worktree_settings_file::Column::WorktreeId, - worktree_settings_file::Column::Path, - ]) - .update_column(worktree_settings_file::Column::Content) - .to_owned(), - ) - .exec(&*tx) - .await?; - } else { - worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { - project_id: ActiveValue::Set(project_id), - worktree_id: ActiveValue::Set(update.worktree_id as i64), - path: ActiveValue::Set(update.path.clone()), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn join_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("must join a room first"))?; - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.room_id != participant.room_id { - return Err(anyhow!("no such project"))?; - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let replica_ids = collaborators - .iter() - .map(|c| c.replica_id) - .collect::>(); - let mut replica_id = ReplicaId(1); - while replica_ids.contains(&replica_id) { - replica_id.0 += 1; - } - let new_collaborator = project_collaborator::ActiveModel { - project_id: ActiveValue::set(project_id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(replica_id), - is_host: ActiveValue::set(false), - ..Default::default() - } - .insert(&*tx) - .await?; - collaborators.push(new_collaborator); - - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - let mut worktrees = db_worktrees - .into_iter() - .map(|db_worktree| { - ( - db_worktree.id as u64, - Worktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - entries: Default::default(), - repository_entries: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }, - ) - }) - .collect::>(); - - // Populate worktree entries. - { - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project_id)) - .add(worktree_entry::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { - worktree.entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Populate repository entries. - { - let mut db_repository_entries = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project_id)) - .add(worktree_repository::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_repository_entry) = db_repository_entries.next().await { - let db_repository_entry = db_repository_entry?; - if let Some(worktree) = - worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) - { - worktree.repository_entries.insert( - db_repository_entry.work_directory_id as u64, - proto::RepositoryEntry { - work_directory_id: db_repository_entry.work_directory_id as u64, - branch: db_repository_entry.branch, - }, - ); - } - } - } - - // Populate worktree diagnostic summaries. - { - let mut db_summaries = worktree_diagnostic_summary::Entity::find() - .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_summary) = db_summaries.next().await { - let db_summary = db_summary?; - if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { - worktree - .diagnostic_summaries - .push(proto::DiagnosticSummary { - path: db_summary.path, - language_server_id: db_summary.language_server_id as u64, - error_count: db_summary.error_count as u32, - warning_count: db_summary.warning_count as u32, - }); - } - } - } - - // Populate worktree settings files - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = - worktrees.get_mut(&(db_settings_file.worktree_id as u64)) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - // Populate language servers. - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await?; - - let project = Project { - collaborators: collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees, - language_servers: language_servers - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect(), - }; - Ok((project, replica_id as ReplicaId)) - }) - .await - } - - pub async fn leave_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let result = project_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add(project_collaborator::Column::ProjectId.eq(project_id)) - .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32)) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("not a collaborator on this project"))?; - } - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - let collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let connection_ids = collaborators - .into_iter() - .map(|collaborator| collaborator.connection()) - .collect(); - - follower::Entity::delete_many() - .filter( - Condition::any() - .add( - Condition::all() - .add(follower::Column::ProjectId.eq(Some(project_id))) - .add( - follower::Column::LeaderConnectionServerId - .eq(connection.owner_id), - ) - .add(follower::Column::LeaderConnectionId.eq(connection.id)), - ) - .add( - Condition::all() - .add(follower::Column::ProjectId.eq(Some(project_id))) - .add( - follower::Column::FollowerConnectionServerId - .eq(connection.owner_id), - ) - .add(follower::Column::FollowerConnectionId.eq(connection.id)), - ), - ) - .exec(&*tx) - .await?; - - let room = self.get_room(project.room_id, &tx).await?; - let left_project = LeftProject { - id: project_id, - host_user_id: project.host_user_id, - host_connection_id: project.host_connection()?, - connection_ids, - }; - Ok((room, left_project)) - }) - .await - } - - pub async fn project_collaborators( - &self, - project_id: ProjectId, - connection_id: ConnectionId, - ) -> Result>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .all(&*tx) - .await? - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect::>(); - - if collaborators - .iter() - .any(|collaborator| collaborator.connection_id == connection_id) - { - Ok(collaborators) - } else { - Err(anyhow!("no such project"))? - } - }) - .await - } - - pub async fn project_connection_ids( - &self, - project_id: ProjectId, - connection_id: ConnectionId, - ) -> Result>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let mut collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - - let mut connection_ids = HashSet::default(); - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - connection_ids.insert(collaborator.connection()); - } - - if connection_ids.contains(&connection_id) { - Ok(connection_ids) - } else { - Err(anyhow!("no such project"))? - } - }) - .await - } - - async fn project_guest_connection_ids( - &self, - project_id: ProjectId, - tx: &DatabaseTransaction, - ) -> Result> { - let mut collaborators = project_collaborator::Entity::find() - .filter( - project_collaborator::Column::ProjectId - .eq(project_id) - .and(project_collaborator::Column::IsHost.eq(false)), - ) - .stream(tx) - .await?; - - let mut guest_connection_ids = Vec::new(); - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - guest_connection_ids.push(collaborator.connection()); - } - Ok(guest_connection_ids) - } - - pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result { - self.transaction(|tx| async move { - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project {} not found", project_id))?; - Ok(project.room_id) - }) - .await - } - - pub async fn 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> { - self.room_transaction(room_id, |tx| async move { - follower::ActiveModel { - room_id: ActiveValue::set(room_id), - project_id: ActiveValue::set(project_id), - leader_connection_server_id: ActiveValue::set(ServerId( - leader_connection.owner_id as i32, - )), - leader_connection_id: ActiveValue::set(leader_connection.id as i32), - follower_connection_server_id: ActiveValue::set(ServerId( - follower_connection.owner_id as i32, - )), - follower_connection_id: ActiveValue::set(follower_connection.id as i32), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &*tx).await?; - Ok(room) - }) - .await - } - - pub async fn unfollow( - &self, - room_id: RoomId, - project_id: ProjectId, - leader_connection: ConnectionId, - follower_connection: ConnectionId, - ) -> Result> { - 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 - .eq(leader_connection.owner_id), - ) - .add(follower::Column::LeaderConnectionId.eq(leader_connection.id)) - .add( - follower::Column::FollowerConnectionServerId - .eq(follower_connection.owner_id), - ) - .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)), - ) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &*tx).await?; - Ok(room) - }) - .await - } -} diff --git a/crates/collab2/src/db/queries/rooms.rs b/crates/collab2/src/db/queries/rooms.rs deleted file mode 100644 index 40fdf5d58f..0000000000 --- a/crates/collab2/src/db/queries/rooms.rs +++ /dev/null @@ -1,1203 +0,0 @@ -use super::*; - -impl Database { - pub async fn clear_stale_room_participants( - &self, - room_id: RoomId, - new_server_id: ServerId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let stale_participant_filter = Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::AnsweringConnectionId.is_not_null()) - .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id)); - - let stale_participant_user_ids = room_participant::Entity::find() - .filter(stale_participant_filter.clone()) - .all(&*tx) - .await? - .into_iter() - .map(|participant| participant.user_id) - .collect::>(); - - // Delete participants who failed to reconnect and cancel their calls. - let mut canceled_calls_to_user_ids = Vec::new(); - room_participant::Entity::delete_many() - .filter(stale_participant_filter) - .exec(&*tx) - .await?; - let called_participants = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::CallingUserId - .is_in(stale_participant_user_ids.iter().copied()), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .all(&*tx) - .await?; - room_participant::Entity::delete_many() - .filter( - room_participant::Column::Id - .is_in(called_participants.iter().map(|participant| participant.id)), - ) - .exec(&*tx) - .await?; - canceled_calls_to_user_ids.extend( - called_participants - .into_iter() - .map(|participant| participant.user_id), - ); - - let (channel, room) = self.get_channel_room(room_id, &tx).await?; - let channel_members; - if let Some(channel) = &channel { - channel_members = self.get_channel_participants(channel, &tx).await?; - } else { - channel_members = Vec::new(); - - // Delete the room if it becomes empty. - if room.participants.is_empty() { - project::Entity::delete_many() - .filter(project::Column::RoomId.eq(room_id)) - .exec(&*tx) - .await?; - room::Entity::delete_by_id(room_id).exec(&*tx).await?; - } - }; - - Ok(RefreshedRoom { - room, - channel_id: channel.map(|channel| channel.id), - channel_members, - stale_participant_user_ids, - canceled_calls_to_user_ids, - }) - }) - .await - } - - pub async fn incoming_call_for_user( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - let pending_participant = room_participant::Entity::find() - .filter( - room_participant::Column::UserId - .eq(user_id) - .and(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .one(&*tx) - .await?; - - if let Some(pending_participant) = pending_participant { - let room = self.get_room(pending_participant.room_id, &tx).await?; - Ok(Self::build_incoming_call(&room, user_id)) - } else { - Ok(None) - } - }) - .await - } - - pub async fn create_room( - &self, - user_id: UserId, - connection: ConnectionId, - live_kit_room: &str, - release_channel: &str, - ) -> Result { - self.transaction(|tx| async move { - let room = room::ActiveModel { - live_kit_room: ActiveValue::set(live_kit_room.into()), - enviroment: ActiveValue::set(Some(release_channel.to_string())), - ..Default::default() - } - .insert(&*tx) - .await?; - room_participant::ActiveModel { - room_id: ActiveValue::set(room.id), - user_id: ActiveValue::set(user_id), - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(user_id), - calling_connection_id: ActiveValue::set(connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - participant_index: ActiveValue::set(Some(0)), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room.id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn call( - &self, - room_id: RoomId, - calling_user_id: UserId, - calling_connection: ConnectionId, - called_user_id: UserId, - initial_project_id: Option, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - room_participant::ActiveModel { - room_id: ActiveValue::set(room_id), - user_id: ActiveValue::set(called_user_id), - answering_connection_lost: ActiveValue::set(false), - 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( - calling_connection.owner_id as i32, - ))), - initial_project_id: ActiveValue::set(initial_project_id), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - let incoming_call = Self::build_incoming_call(&room, called_user_id) - .ok_or_else(|| anyhow!("failed to build incoming call"))?; - Ok((room, incoming_call)) - }) - .await - } - - pub async fn call_failed( - &self, - room_id: RoomId, - called_user_id: UserId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - room_participant::Entity::delete_many() - .filter( - room_participant::Column::RoomId - .eq(room_id) - .and(room_participant::Column::UserId.eq(called_user_id)), - ) - .exec(&*tx) - .await?; - let room = self.get_room(room_id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn decline_call( - &self, - expected_room_id: Option, - user_id: UserId, - ) -> Result>> { - self.optional_room_transaction(|tx| async move { - let mut filter = Condition::all() - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_null()); - if let Some(room_id) = expected_room_id { - filter = filter.add(room_participant::Column::RoomId.eq(room_id)); - } - let participant = room_participant::Entity::find() - .filter(filter) - .one(&*tx) - .await?; - - let participant = if let Some(participant) = participant { - participant - } else if expected_room_id.is_some() { - return Err(anyhow!("could not find call to decline"))?; - } else { - return Ok(None); - }; - - let room_id = participant.room_id; - room_participant::Entity::delete(participant.into_active_model()) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok(Some((room_id, room))) - }) - .await - } - - pub async fn cancel_call( - &self, - room_id: RoomId, - calling_connection: ConnectionId, - called_user_id: UserId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add(room_participant::Column::UserId.eq(called_user_id)) - .add(room_participant::Column::RoomId.eq(room_id)) - .add( - room_participant::Column::CallingConnectionId - .eq(calling_connection.id as i32), - ) - .add( - room_participant::Column::CallingConnectionServerId - .eq(calling_connection.owner_id as i32), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no call to cancel"))?; - - room_participant::Entity::delete(participant.into_active_model()) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn join_room( - &self, - room_id: RoomId, - user_id: UserId, - connection: ConnectionId, - enviroment: &str, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelIdAndEnviroment { - ChannelId, - Enviroment, - } - - let (channel_id, release_channel): (Option, Option) = - room::Entity::find() - .select_only() - .column(room::Column::ChannelId) - .column(room::Column::Enviroment) - .filter(room::Column::Id.eq(room_id)) - .into_values::<_, QueryChannelIdAndEnviroment>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such room"))?; - - if let Some(release_channel) = release_channel { - if &release_channel != enviroment { - Err(anyhow!("must join using the {} release", release_channel))?; - } - } - - if channel_id.is_some() { - Err(anyhow!("tried to join channel call directly"))? - } - - let participant_index = self - .get_next_participant_index_internal(room_id, &*tx) - .await?; - - let result = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .set(room_participant::ActiveModel { - 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, - ))), - answering_connection_lost: ActiveValue::set(false), - ..Default::default() - }) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("room does not exist or was already joined"))?; - } - - let room = self.get_room(room_id, &tx).await?; - Ok(JoinRoom { - room, - channel_id: None, - channel_members: vec![], - }) - }) - .await - } - - async fn get_next_participant_index_internal( - &self, - room_id: RoomId, - tx: &DatabaseTransaction, - ) -> Result { - #[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; - } - - Ok(participant_index) - } - - pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result> { - self.transaction(|tx| async move { - let room: Option = room::Entity::find() - .filter(room::Column::Id.eq(room_id)) - .one(&*tx) - .await?; - - Ok(room.and_then(|room| room.channel_id)) - }) - .await - } - - pub(crate) async fn join_channel_room_internal( - &self, - room_id: RoomId, - user_id: UserId, - connection: ConnectionId, - tx: &DatabaseTransaction, - ) -> Result { - let participant_index = self - .get_next_participant_index_internal(room_id, &*tx) - .await?; - - room_participant::Entity::insert_many([room_participant::ActiveModel { - room_id: ActiveValue::set(room_id), - user_id: ActiveValue::set(user_id), - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(user_id), - calling_connection_id: ActiveValue::set(connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - participant_index: ActiveValue::Set(Some(participant_index)), - ..Default::default() - }]) - .on_conflict( - OnConflict::columns([room_participant::Column::UserId]) - .update_columns([ - room_participant::Column::AnsweringConnectionId, - room_participant::Column::AnsweringConnectionServerId, - room_participant::Column::AnsweringConnectionLost, - room_participant::Column::ParticipantIndex, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - - let (channel, room) = self.get_channel_room(room_id, &tx).await?; - let channel = channel.ok_or_else(|| anyhow!("no channel for room"))?; - let channel_members = self.get_channel_participants(&channel, &*tx).await?; - Ok(JoinRoom { - room, - channel_id: Some(channel.id), - channel_members, - }) - } - - pub async fn rejoin_room( - &self, - rejoin_room: proto::RejoinRoom, - user_id: UserId, - connection: ConnectionId, - ) -> Result> { - let room_id = RoomId::from_proto(rejoin_room.id); - self.room_transaction(room_id, |tx| async { - let tx = tx; - let participant_update = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_not_null()) - .add( - Condition::any() - .add(room_participant::Column::AnsweringConnectionLost.eq(true)) - .add( - room_participant::Column::AnsweringConnectionServerId - .ne(connection.owner_id as i32), - ), - ), - ) - .set(room_participant::ActiveModel { - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - ..Default::default() - }) - .exec(&*tx) - .await?; - if participant_update.rows_affected == 0 { - return Err(anyhow!("room does not exist or was already joined"))?; - } - - let mut reshared_projects = Vec::new(); - for reshared_project in &rejoin_room.reshared_projects { - let project_id = ProjectId::from_proto(reshared_project.project_id); - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project does not exist"))?; - if project.host_user_id != user_id { - return Err(anyhow!("no such project"))?; - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let host_ix = collaborators - .iter() - .position(|collaborator| { - collaborator.user_id == user_id && collaborator.is_host - }) - .ok_or_else(|| anyhow!("host not found among collaborators"))?; - let host = collaborators.swap_remove(host_ix); - let old_connection_id = host.connection(); - - project::Entity::update(project::ActiveModel { - host_connection_id: ActiveValue::set(Some(connection.id as i32)), - host_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..project.into_active_model() - }) - .exec(&*tx) - .await?; - project_collaborator::Entity::update(project_collaborator::ActiveModel { - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - ..host.into_active_model() - }) - .exec(&*tx) - .await?; - - self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx) - .await?; - - reshared_projects.push(ResharedProject { - id: project_id, - old_connection_id, - collaborators: collaborators - .iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees: reshared_project.worktrees.clone(), - }); - } - - project::Entity::delete_many() - .filter( - Condition::all() - .add(project::Column::RoomId.eq(room_id)) - .add(project::Column::HostUserId.eq(user_id)) - .add( - project::Column::Id - .is_not_in(reshared_projects.iter().map(|project| project.id)), - ), - ) - .exec(&*tx) - .await?; - - let mut rejoined_projects = Vec::new(); - for rejoined_project in &rejoin_room.rejoined_projects { - let project_id = ProjectId::from_proto(rejoined_project.id); - let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else { - continue; - }; - - let mut worktrees = Vec::new(); - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - for db_worktree in db_worktrees { - let mut worktree = RejoinedWorktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - updated_entries: Default::default(), - removed_entries: Default::default(), - updated_repositories: Default::default(), - removed_repositories: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }; - - let rejoined_worktree = rejoined_project - .worktrees - .iter() - .find(|worktree| worktree.id == db_worktree.id as u64); - - // File entries - { - let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { - worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_entry::Column::IsDeleted.eq(false) - }; - - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project.id)) - .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) - .add(entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if db_entry.is_deleted { - worktree.removed_entries.push(db_entry.id as u64); - } else { - worktree.updated_entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Repository Entries - { - let repository_entry_filter = - if let Some(rejoined_worktree) = rejoined_worktree { - worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_repository::Column::IsDeleted.eq(false) - }; - - let mut db_repositories = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project.id)) - .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) - .add(repository_entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_repository) = db_repositories.next().await { - let db_repository = db_repository?; - if db_repository.is_deleted { - worktree - .removed_repositories - .push(db_repository.work_directory_id as u64); - } else { - worktree.updated_repositories.push(proto::RepositoryEntry { - work_directory_id: db_repository.work_directory_id as u64, - branch: db_repository.branch, - }); - } - } - } - - worktrees.push(worktree); - } - - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await? - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect::>(); - - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = worktrees - .iter_mut() - .find(|w| w.id == db_settings_file.worktree_id as u64) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let self_collaborator = if let Some(self_collaborator_ix) = collaborators - .iter() - .position(|collaborator| collaborator.user_id == user_id) - { - collaborators.swap_remove(self_collaborator_ix) - } else { - continue; - }; - let old_connection_id = self_collaborator.connection(); - project_collaborator::Entity::update(project_collaborator::ActiveModel { - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - ..self_collaborator.into_active_model() - }) - .exec(&*tx) - .await?; - - let collaborators = collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect::>(); - - rejoined_projects.push(RejoinedProject { - id: project_id, - old_connection_id, - collaborators, - worktrees, - language_servers, - }); - } - - let (channel, room) = self.get_channel_room(room_id, &tx).await?; - let channel_members = if let Some(channel) = &channel { - self.get_channel_participants(&channel, &tx).await? - } else { - Vec::new() - }; - - Ok(RejoinedRoom { - room, - channel_id: channel.map(|channel| channel.id), - channel_members, - rejoined_projects, - reshared_projects, - }) - }) - .await - } - - pub async fn leave_room( - &self, - connection: ConnectionId, - ) -> Result>> { - self.optional_room_transaction(|tx| async move { - let leaving_participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await?; - - if let Some(leaving_participant) = leaving_participant { - // Leave room. - let room_id = leaving_participant.room_id; - room_participant::Entity::delete_by_id(leaving_participant.id) - .exec(&*tx) - .await?; - - // Cancel pending calls initiated by the leaving user. - let called_participants = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::CallingUserId - .eq(leaving_participant.user_id), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .all(&*tx) - .await?; - room_participant::Entity::delete_many() - .filter( - room_participant::Column::Id - .is_in(called_participants.iter().map(|participant| participant.id)), - ) - .exec(&*tx) - .await?; - let canceled_calls_to_user_ids = called_participants - .into_iter() - .map(|participant| participant.user_id) - .collect(); - - // Detect left projects. - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryProjectIds { - ProjectId, - } - let project_ids: Vec = project_collaborator::Entity::find() - .select_only() - .column_as( - project_collaborator::Column::ProjectId, - QueryProjectIds::ProjectId, - ) - .filter( - Condition::all() - .add( - project_collaborator::Column::ConnectionId.eq(connection.id as i32), - ) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .into_values::<_, QueryProjectIds>() - .all(&*tx) - .await?; - let mut left_projects = HashMap::default(); - let mut collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.is_in(project_ids)) - .stream(&*tx) - .await?; - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - let left_project = - left_projects - .entry(collaborator.project_id) - .or_insert(LeftProject { - id: collaborator.project_id, - host_user_id: Default::default(), - connection_ids: Default::default(), - host_connection_id: Default::default(), - }); - - let collaborator_connection_id = collaborator.connection(); - if collaborator_connection_id != connection { - left_project.connection_ids.push(collaborator_connection_id); - } - - if collaborator.is_host { - left_project.host_user_id = collaborator.user_id; - left_project.host_connection_id = collaborator_connection_id; - } - } - drop(collaborators); - - // Leave projects. - project_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add( - project_collaborator::Column::ConnectionId.eq(connection.id as i32), - ) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - - // Unshare projects. - project::Entity::delete_many() - .filter( - Condition::all() - .add(project::Column::RoomId.eq(room_id)) - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - - let (channel, room) = self.get_channel_room(room_id, &tx).await?; - let deleted = if room.participants.is_empty() { - let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?; - result.rows_affected > 0 - } else { - false - }; - - let channel_members = if let Some(channel) = &channel { - self.get_channel_participants(channel, &tx).await? - } else { - Vec::new() - }; - let left_room = LeftRoom { - room, - channel_id: channel.map(|channel| channel.id), - channel_members, - left_projects, - canceled_calls_to_user_ids, - deleted, - }; - - if left_room.room.participants.is_empty() { - self.rooms.remove(&room_id); - } - - Ok(Some((room_id, left_room))) - } else { - Ok(None) - } - }) - .await - } - - pub async fn update_room_participant_location( - &self, - room_id: RoomId, - connection: ConnectionId, - location: proto::ParticipantLocation, - ) -> Result> { - self.room_transaction(room_id, |tx| async { - let tx = tx; - let location_kind; - let location_project_id; - match location - .variant - .as_ref() - .ok_or_else(|| anyhow!("invalid location"))? - { - proto::participant_location::Variant::SharedProject(project) => { - location_kind = 0; - location_project_id = Some(ProjectId::from_proto(project.id)); - } - proto::participant_location::Variant::UnsharedProject(_) => { - location_kind = 1; - location_project_id = None; - } - proto::participant_location::Variant::External(_) => { - location_kind = 2; - location_project_id = None; - } - } - - let result = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .set(room_participant::ActiveModel { - location_kind: ActiveValue::set(Some(location_kind)), - location_project_id: ActiveValue::set(location_project_id), - ..Default::default() - }) - .exec(&*tx) - .await?; - - if result.rows_affected == 1 { - let room = self.get_room(room_id, &tx).await?; - Ok(room) - } else { - Err(anyhow!("could not update room participant location"))? - } - }) - .await - } - - pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { - self.transaction(|tx| async move { - 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, - ) -> Option { - let pending_participant = room - .pending_participants - .iter() - .find(|participant| participant.user_id == called_user_id.to_proto())?; - - Some(proto::IncomingCall { - room_id: room.id, - calling_user_id: pending_participant.calling_user_id, - participant_user_ids: room - .participants - .iter() - .map(|participant| participant.user_id) - .collect(), - initial_project: room.participants.iter().find_map(|participant| { - let initial_project_id = pending_participant.initial_project_id?; - participant - .projects - .iter() - .find(|project| project.id == initial_project_id) - .cloned() - }), - }) - } - - pub async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { - let (_, room) = self.get_channel_room(room_id, tx).await?; - Ok(room) - } - - 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, - tx: &DatabaseTransaction, - ) -> Result<(Option, proto::Room)> { - let db_room = room::Entity::find_by_id(room_id) - .one(tx) - .await? - .ok_or_else(|| anyhow!("could not find room"))?; - - let mut db_participants = db_room - .find_related(room_participant::Entity) - .stream(tx) - .await?; - let mut participants = HashMap::default(); - let mut pending_participants = Vec::new(); - while let Some(db_participant) = db_participants.next().await { - let db_participant = db_participant?; - if let ( - Some(answering_connection_id), - 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, - ) { - (Some(0), Some(project_id)) => { - Some(proto::participant_location::Variant::SharedProject( - proto::participant_location::SharedProject { - id: project_id.to_proto(), - }, - )) - } - (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject( - Default::default(), - )), - _ => Some(proto::participant_location::Variant::External( - Default::default(), - )), - }; - - let answering_connection = ConnectionId { - owner_id: answering_connection_server_id.0 as u32, - id: answering_connection_id as u32, - }; - participants.insert( - answering_connection, - proto::Participant { - user_id: db_participant.user_id.to_proto(), - peer_id: Some(answering_connection.into()), - projects: Default::default(), - location: Some(proto::ParticipantLocation { variant: location }), - participant_index: participant_index as u32, - }, - ); - } else { - pending_participants.push(proto::PendingParticipant { - user_id: db_participant.user_id.to_proto(), - calling_user_id: db_participant.calling_user_id.to_proto(), - initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()), - }); - } - } - drop(db_participants); - - let mut db_projects = db_room - .find_related(project::Entity) - .find_with_related(worktree::Entity) - .stream(tx) - .await?; - - while let Some(row) = db_projects.next().await { - let (db_project, db_worktree) = row?; - let host_connection = db_project.host_connection()?; - if let Some(participant) = participants.get_mut(&host_connection) { - let project = if let Some(project) = participant - .projects - .iter_mut() - .find(|project| project.id == db_project.id.to_proto()) - { - project - } else { - participant.projects.push(proto::ParticipantProject { - id: db_project.id.to_proto(), - worktree_root_names: Default::default(), - }); - participant.projects.last_mut().unwrap() - }; - - if let Some(db_worktree) = db_worktree { - if db_worktree.visible { - project.worktree_root_names.push(db_worktree.root_name); - } - } - } - } - drop(db_projects); - - let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; - let mut followers = Vec::new(); - while let Some(db_follower) = db_followers.next().await { - let db_follower = db_follower?; - followers.push(proto::Follower { - leader_id: Some(db_follower.leader_connection().into()), - follower_id: Some(db_follower.follower_connection().into()), - project_id: db_follower.project_id.to_proto(), - }); - } - drop(db_followers); - - let channel = if let Some(channel_id) = db_room.channel_id { - Some(self.get_channel_internal(channel_id, &*tx).await?) - } else { - None - }; - - Ok(( - channel, - proto::Room { - id: db_room.id.to_proto(), - live_kit_room: db_room.live_kit_room, - participants: participants.into_values().collect(), - pending_participants, - followers, - }, - )) - } -} diff --git a/crates/collab2/src/db/queries/servers.rs b/crates/collab2/src/db/queries/servers.rs deleted file mode 100644 index e5ceee8887..0000000000 --- a/crates/collab2/src/db/queries/servers.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::*; - -impl Database { - pub async fn create_server(&self, environment: &str) -> Result { - self.transaction(|tx| async move { - let server = server::ActiveModel { - environment: ActiveValue::set(environment.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - Ok(server.id) - }) - .await - } - - pub async fn stale_server_resource_ids( - &self, - environment: &str, - new_server_id: ServerId, - ) -> Result<(Vec, Vec)> { - self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryRoomIds { - RoomId, - } - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelIds { - ChannelId, - } - - let stale_server_epochs = self - .stale_server_ids(environment, new_server_id, &tx) - .await?; - let room_ids = room_participant::Entity::find() - .select_only() - .column(room_participant::Column::RoomId) - .distinct() - .filter( - room_participant::Column::AnsweringConnectionServerId - .is_in(stale_server_epochs.iter().copied()), - ) - .into_values::<_, QueryRoomIds>() - .all(&*tx) - .await?; - let channel_ids = channel_buffer_collaborator::Entity::find() - .select_only() - .column(channel_buffer_collaborator::Column::ChannelId) - .distinct() - .filter( - channel_buffer_collaborator::Column::ConnectionServerId - .is_in(stale_server_epochs.iter().copied()), - ) - .into_values::<_, QueryChannelIds>() - .all(&*tx) - .await?; - - Ok((room_ids, channel_ids)) - }) - .await - } - - pub async fn delete_stale_servers( - &self, - environment: &str, - new_server_id: ServerId, - ) -> Result<()> { - self.transaction(|tx| async move { - server::Entity::delete_many() - .filter( - Condition::all() - .add(server::Column::Environment.eq(environment)) - .add(server::Column::Id.ne(new_server_id)), - ) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - async fn stale_server_ids( - &self, - environment: &str, - new_server_id: ServerId, - tx: &DatabaseTransaction, - ) -> Result> { - let stale_servers = server::Entity::find() - .filter( - Condition::all() - .add(server::Column::Environment.eq(environment)) - .add(server::Column::Id.ne(new_server_id)), - ) - .all(&*tx) - .await?; - Ok(stale_servers.into_iter().map(|server| server.id).collect()) - } -} diff --git a/crates/collab2/src/db/queries/users.rs b/crates/collab2/src/db/queries/users.rs deleted file mode 100644 index 27e64e2598..0000000000 --- a/crates/collab2/src/db/queries/users.rs +++ /dev/null @@ -1,259 +0,0 @@ -use super::*; - -impl Database { - pub async fn create_user( - &self, - email_address: &str, - admin: bool, - params: NewUserParams, - ) -> Result { - self.transaction(|tx| async { - let tx = tx; - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(Some(email_address.into())), - github_login: ActiveValue::set(params.github_login.clone()), - github_user_id: ActiveValue::set(Some(params.github_user_id)), - admin: ActiveValue::set(admin), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .on_conflict( - OnConflict::column(user::Column::GithubLogin) - .update_column(user::Column::GithubLogin) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - Ok(NewUserResult { - user_id: user.id, - metrics_id: user.metrics_id.to_string(), - signup_device_id: None, - inviting_user_id: None, - }) - }) - .await - } - - pub async fn get_user_by_id(&self, id: UserId) -> Result> { - self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) }) - .await - } - - pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { - self.transaction(|tx| async { - let tx = tx; - Ok(user::Entity::find() - .filter(user::Column::Id.is_in(ids.iter().copied())) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(&*tx) - .await?) - }) - .await - } - - pub async fn get_or_create_user_by_github_account( - &self, - github_login: &str, - github_user_id: Option, - github_email: Option<&str>, - ) -> Result> { - self.transaction(|tx| async move { - let tx = &*tx; - if let Some(github_user_id) = github_user_id { - if let Some(user_by_github_user_id) = user::Entity::find() - .filter(user::Column::GithubUserId.eq(github_user_id)) - .one(tx) - .await? - { - let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); - user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); - Ok(Some(user_by_github_user_id.update(tx).await?)) - } else if let Some(user_by_github_login) = user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await? - { - let mut user_by_github_login = user_by_github_login.into_active_model(); - user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id)); - Ok(Some(user_by_github_login.update(tx).await?)) - } else { - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(github_email.map(|email| email.into())), - github_login: ActiveValue::set(github_login.into()), - github_user_id: ActiveValue::set(Some(github_user_id)), - admin: ActiveValue::set(false), - invite_count: ActiveValue::set(0), - invite_code: ActiveValue::set(None), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .exec_with_returning(&*tx) - .await?; - Ok(Some(user)) - } - } else { - Ok(user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await?) - } - }) - .await - } - - pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .order_by_asc(user::Column::GithubLogin) - .limit(limit as u64) - .offset(page as u64 * limit as u64) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_user_metrics_id(&self, id: UserId) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - MetricsId, - } - - self.transaction(|tx| async move { - let metrics_id: Uuid = user::Entity::find_by_id(id) - .select_only() - .column(user::Column::MetricsId) - .into_values::<_, QueryAs>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("could not find user"))?; - Ok(metrics_id.to_string()) - }) - .await - } - - pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { - self.transaction(|tx| async move { - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - connected_once: ActiveValue::set(connected_once), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn destroy_user(&self, id: UserId) -> Result<()> { - self.transaction(|tx| async move { - access_token::Entity::delete_many() - .filter(access_token::Column::UserId.eq(id)) - .exec(&*tx) - .await?; - user::Entity::delete_by_id(id).exec(&*tx).await?; - Ok(()) - }) - .await - } - - pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { - self.transaction(|tx| async { - let tx = tx; - let like_string = Self::fuzzy_like_string(name_query); - let query = " - SELECT users.* - FROM users - WHERE github_login ILIKE $1 - ORDER BY github_login <-> $2 - LIMIT $3 - "; - - Ok(user::Entity::find() - .from_raw_sql(Statement::from_sql_and_values( - self.pool.get_database_backend(), - query, - vec![like_string.into(), name_query.into(), limit.into()], - )) - .all(&*tx) - .await?) - }) - .await - } - - pub fn fuzzy_like_string(string: &str) -> String { - let mut result = String::with_capacity(string.len() * 2 + 1); - for c in string.chars() { - if c.is_alphanumeric() { - result.push('%'); - result.push(c); - } - } - result.push('%'); - result - } - - pub async fn create_user_flag(&self, flag: &str) -> Result { - self.transaction(|tx| async move { - let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { - flag: ActiveValue::set(flag.to_string()), - ..Default::default() - }) - .exec(&*tx) - .await? - .last_insert_id; - - Ok(flag) - }) - .await - } - - pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { - self.transaction(|tx| async move { - user_feature::Entity::insert(user_feature::ActiveModel { - user_id: ActiveValue::set(user), - feature_id: ActiveValue::set(flag), - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - pub async fn get_user_flags(&self, user: UserId) -> Result> { - self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - Flag, - } - - let flags = user::Model { - id: user, - ..Default::default() - } - .find_linked(user::UserFlags) - .select_only() - .column(feature_flag::Column::Flag) - .into_values::<_, QueryAs>() - .all(&*tx) - .await?; - - Ok(flags) - }) - .await - } -} diff --git a/crates/collab2/src/db/tables.rs b/crates/collab2/src/db/tables.rs deleted file mode 100644 index 4f28ce4fbd..0000000000 --- a/crates/collab2/src/db/tables.rs +++ /dev/null @@ -1,32 +0,0 @@ -pub mod access_token; -pub mod buffer; -pub mod buffer_operation; -pub mod buffer_snapshot; -pub mod channel; -pub mod channel_buffer_collaborator; -pub mod channel_chat_participant; -pub mod channel_member; -pub mod channel_message; -pub mod channel_message_mention; -pub mod contact; -pub mod feature_flag; -pub mod follower; -pub mod language_server; -pub mod notification; -pub mod notification_kind; -pub mod observed_buffer_edits; -pub mod observed_channel_messages; -pub mod project; -pub mod project_collaborator; -pub mod room; -pub mod room_participant; -pub mod server; -pub mod signup; -pub mod user; -pub mod user_feature; -pub mod worktree; -pub mod worktree_diagnostic_summary; -pub mod worktree_entry; -pub mod worktree_repository; -pub mod worktree_repository_statuses; -pub mod worktree_settings_file; diff --git a/crates/collab2/src/db/tables/access_token.rs b/crates/collab2/src/db/tables/access_token.rs deleted file mode 100644 index da7392b98c..0000000000 --- a/crates/collab2/src/db/tables/access_token.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::db::{AccessTokenId, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "access_tokens")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: AccessTokenId, - pub user_id: UserId, - pub hash: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/buffer.rs b/crates/collab2/src/db/tables/buffer.rs deleted file mode 100644 index ec2ffd4a68..0000000000 --- a/crates/collab2/src/db/tables/buffer.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::db::{BufferId, ChannelId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "buffers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: BufferId, - pub epoch: i32, - pub channel_id: ChannelId, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::buffer_operation::Entity")] - Operations, - #[sea_orm(has_many = "super::buffer_snapshot::Entity")] - Snapshots, - #[sea_orm( - belongs_to = "super::channel::Entity", - from = "Column::ChannelId", - to = "super::channel::Column::Id" - )] - Channel, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Operations.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Snapshots.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Channel.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/buffer_operation.rs b/crates/collab2/src/db/tables/buffer_operation.rs deleted file mode 100644 index 37bd4bedfe..0000000000 --- a/crates/collab2/src/db/tables/buffer_operation.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::db::BufferId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "buffer_operations")] -pub struct Model { - #[sea_orm(primary_key)] - pub buffer_id: BufferId, - #[sea_orm(primary_key)] - pub epoch: i32, - #[sea_orm(primary_key)] - pub lamport_timestamp: i32, - #[sea_orm(primary_key)] - pub replica_id: i32, - pub value: Vec, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::buffer::Entity", - from = "Column::BufferId", - to = "super::buffer::Column::Id" - )] - Buffer, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Buffer.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/buffer_snapshot.rs b/crates/collab2/src/db/tables/buffer_snapshot.rs deleted file mode 100644 index c9de665e43..0000000000 --- a/crates/collab2/src/db/tables/buffer_snapshot.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::db::BufferId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "buffer_snapshots")] -pub struct Model { - #[sea_orm(primary_key)] - pub buffer_id: BufferId, - #[sea_orm(primary_key)] - pub epoch: i32, - pub text: String, - pub operation_serialization_version: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::buffer::Entity", - from = "Column::BufferId", - to = "super::buffer::Column::Id" - )] - Buffer, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Buffer.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/channel.rs b/crates/collab2/src/db/tables/channel.rs deleted file mode 100644 index e30ec9af61..0000000000 --- a/crates/collab2/src/db/tables/channel.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::db::{ChannelId, ChannelVisibility}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "channels")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ChannelId, - pub name: String, - pub visibility: ChannelVisibility, - pub parent_path: String, -} - -impl Model { - pub fn parent_id(&self) -> Option { - self.ancestors().last() - } - - pub fn ancestors(&self) -> impl Iterator + '_ { - self.parent_path - .trim_end_matches('/') - .split('/') - .filter_map(|id| Some(ChannelId::from_proto(id.parse().ok()?))) - } - - pub fn ancestors_including_self(&self) -> impl Iterator + '_ { - self.ancestors().chain(Some(self.id)) - } - - pub fn path(&self) -> String { - format!("{}{}/", self.parent_path, self.id) - } -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_one = "super::room::Entity")] - Room, - #[sea_orm(has_one = "super::buffer::Entity")] - Buffer, - #[sea_orm(has_many = "super::channel_member::Entity")] - Member, - #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] - BufferCollaborators, - #[sea_orm(has_many = "super::channel_chat_participant::Entity")] - ChatParticipants, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Member.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Buffer.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::BufferCollaborators.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::ChatParticipants.def() - } -} diff --git a/crates/collab2/src/db/tables/channel_buffer_collaborator.rs b/crates/collab2/src/db/tables/channel_buffer_collaborator.rs deleted file mode 100644 index ac2637b36e..0000000000 --- a/crates/collab2/src/db/tables/channel_buffer_collaborator.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId}; -use rpc::ConnectionId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "channel_buffer_collaborators")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ChannelBufferCollaboratorId, - pub channel_id: ChannelId, - pub connection_id: i32, - pub connection_server_id: ServerId, - pub connection_lost: bool, - pub user_id: UserId, - pub replica_id: ReplicaId, -} - -impl Model { - pub fn connection(&self) -> ConnectionId { - ConnectionId { - owner_id: self.connection_server_id.0 as u32, - id: self.connection_id as u32, - } - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::channel::Entity", - from = "Column::ChannelId", - to = "super::channel::Column::Id" - )] - Channel, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Channel.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/channel_chat_participant.rs b/crates/collab2/src/db/tables/channel_chat_participant.rs deleted file mode 100644 index f3ef36c289..0000000000 --- a/crates/collab2/src/db/tables/channel_chat_participant.rs +++ /dev/null @@ -1,41 +0,0 @@ -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/collab2/src/db/tables/channel_member.rs b/crates/collab2/src/db/tables/channel_member.rs deleted file mode 100644 index 5498a00856..0000000000 --- a/crates/collab2/src/db/tables/channel_member.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "channel_members")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ChannelMemberId, - pub channel_id: ChannelId, - pub user_id: UserId, - pub accepted: bool, - pub role: ChannelRole, -} - -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::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() - } -} - -#[derive(Debug)] -pub struct UserToChannel; - -impl Linked for UserToChannel { - type FromEntity = super::user::Entity; - - type ToEntity = super::channel::Entity; - - fn link(&self) -> Vec { - vec![ - channel_member::Relation::User.def().rev(), - channel_member::Relation::Channel.def(), - ] - } -} diff --git a/crates/collab2/src/db/tables/channel_message.rs b/crates/collab2/src/db/tables/channel_message.rs deleted file mode 100644 index ff49c63ba7..0000000000 --- a/crates/collab2/src/db/tables/channel_message.rs +++ /dev/null @@ -1,45 +0,0 @@ -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/collab2/src/db/tables/channel_message_mention.rs b/crates/collab2/src/db/tables/channel_message_mention.rs deleted file mode 100644 index 6155b057f0..0000000000 --- a/crates/collab2/src/db/tables/channel_message_mention.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::db::{MessageId, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "channel_message_mentions")] -pub struct Model { - #[sea_orm(primary_key)] - pub message_id: MessageId, - #[sea_orm(primary_key)] - pub start_offset: i32, - pub end_offset: i32, - pub user_id: UserId, -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::channel_message::Entity", - from = "Column::MessageId", - to = "super::channel_message::Column::Id" - )] - Message, - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - MentionedUser, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Message.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::MentionedUser.def() - } -} diff --git a/crates/collab2/src/db/tables/contact.rs b/crates/collab2/src/db/tables/contact.rs deleted file mode 100644 index 38af8b782b..0000000000 --- a/crates/collab2/src/db/tables/contact.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::db::{ContactId, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "contacts")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ContactId, - pub user_id_a: UserId, - pub user_id_b: UserId, - pub a_to_b: bool, - pub should_notify: bool, - pub accepted: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room_participant::Entity", - from = "Column::UserIdA", - to = "super::room_participant::Column::UserId" - )] - UserARoomParticipant, - #[sea_orm( - belongs_to = "super::room_participant::Entity", - from = "Column::UserIdB", - to = "super::room_participant::Column::UserId" - )] - UserBRoomParticipant, -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/feature_flag.rs b/crates/collab2/src/db/tables/feature_flag.rs deleted file mode 100644 index 41c1451c64..0000000000 --- a/crates/collab2/src/db/tables/feature_flag.rs +++ /dev/null @@ -1,40 +0,0 @@ -use sea_orm::entity::prelude::*; - -use crate::db::FlagId; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "feature_flags")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: FlagId, - pub flag: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::user_feature::Entity")] - UserFeature, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UserFeature.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -pub struct FlaggedUsers; - -impl Linked for FlaggedUsers { - type FromEntity = Entity; - - type ToEntity = super::user::Entity; - - fn link(&self) -> Vec { - vec![ - super::user_feature::Relation::Flag.def().rev(), - super::user_feature::Relation::User.def(), - ] - } -} diff --git a/crates/collab2/src/db/tables/follower.rs b/crates/collab2/src/db/tables/follower.rs deleted file mode 100644 index ffd45434e9..0000000000 --- a/crates/collab2/src/db/tables/follower.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::db::{FollowerId, ProjectId, RoomId, ServerId}; -use rpc::ConnectionId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "followers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: FollowerId, - pub room_id: RoomId, - pub project_id: ProjectId, - pub leader_connection_server_id: ServerId, - pub leader_connection_id: i32, - pub follower_connection_server_id: ServerId, - pub follower_connection_id: i32, -} - -impl Model { - pub fn leader_connection(&self) -> ConnectionId { - ConnectionId { - owner_id: self.leader_connection_server_id.0 as u32, - id: self.leader_connection_id as u32, - } - } - - pub fn follower_connection(&self) -> ConnectionId { - ConnectionId { - owner_id: self.follower_connection_server_id.0 as u32, - id: self.follower_connection_id as u32, - } - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::RoomId", - to = "super::room::Column::Id" - )] - Room, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/language_server.rs b/crates/collab2/src/db/tables/language_server.rs deleted file mode 100644 index 9ff8c75fc6..0000000000 --- a/crates/collab2/src/db/tables/language_server.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::db::ProjectId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "language_servers")] -pub struct Model { - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub id: i64, - pub name: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::project::Entity", - from = "Column::ProjectId", - to = "super::project::Column::Id" - )] - Project, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Project.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/notification.rs b/crates/collab2/src/db/tables/notification.rs deleted file mode 100644 index 3105198fa2..0000000000 --- a/crates/collab2/src/db/tables/notification.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::db::{NotificationId, NotificationKindId, UserId}; -use sea_orm::entity::prelude::*; -use time::PrimitiveDateTime; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "notifications")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: NotificationId, - pub created_at: PrimitiveDateTime, - pub recipient_id: UserId, - pub kind: NotificationKindId, - pub entity_id: Option, - pub content: String, - pub is_read: bool, - pub response: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::RecipientId", - to = "super::user::Column::Id" - )] - Recipient, -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/notification_kind.rs b/crates/collab2/src/db/tables/notification_kind.rs deleted file mode 100644 index 865b5da04b..0000000000 --- a/crates/collab2/src/db/tables/notification_kind.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::db::NotificationKindId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "notification_kinds")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: NotificationKindId, - pub name: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/observed_buffer_edits.rs b/crates/collab2/src/db/tables/observed_buffer_edits.rs deleted file mode 100644 index e8e7aafaa2..0000000000 --- a/crates/collab2/src/db/tables/observed_buffer_edits.rs +++ /dev/null @@ -1,43 +0,0 @@ -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/collab2/src/db/tables/observed_channel_messages.rs b/crates/collab2/src/db/tables/observed_channel_messages.rs deleted file mode 100644 index 18259f8442..0000000000 --- a/crates/collab2/src/db/tables/observed_channel_messages.rs +++ /dev/null @@ -1,41 +0,0 @@ -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/collab2/src/db/tables/project.rs b/crates/collab2/src/db/tables/project.rs deleted file mode 100644 index 8c26836046..0000000000 --- a/crates/collab2/src/db/tables/project.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::db::{ProjectId, Result, RoomId, ServerId, UserId}; -use anyhow::anyhow; -use rpc::ConnectionId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "projects")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ProjectId, - pub room_id: RoomId, - pub host_user_id: UserId, - pub host_connection_id: Option, - pub host_connection_server_id: Option, -} - -impl Model { - pub fn host_connection(&self) -> Result { - let host_connection_server_id = self - .host_connection_server_id - .ok_or_else(|| anyhow!("empty host_connection_server_id"))?; - let host_connection_id = self - .host_connection_id - .ok_or_else(|| anyhow!("empty host_connection_id"))?; - Ok(ConnectionId { - owner_id: host_connection_server_id.0 as u32, - id: host_connection_id as u32, - }) - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::HostUserId", - to = "super::user::Column::Id" - )] - HostUser, - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::RoomId", - to = "super::room::Column::Id" - )] - Room, - #[sea_orm(has_many = "super::worktree::Entity")] - Worktrees, - #[sea_orm(has_many = "super::project_collaborator::Entity")] - Collaborators, - #[sea_orm(has_many = "super::language_server::Entity")] - LanguageServers, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::HostUser.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Worktrees.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Collaborators.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::LanguageServers.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/project_collaborator.rs b/crates/collab2/src/db/tables/project_collaborator.rs deleted file mode 100644 index ac57befa63..0000000000 --- a/crates/collab2/src/db/tables/project_collaborator.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::db::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId}; -use rpc::ConnectionId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "project_collaborators")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ProjectCollaboratorId, - pub project_id: ProjectId, - pub connection_id: i32, - pub connection_server_id: ServerId, - pub user_id: UserId, - pub replica_id: ReplicaId, - pub is_host: bool, -} - -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::project::Entity", - from = "Column::ProjectId", - to = "super::project::Column::Id" - )] - Project, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Project.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/room.rs b/crates/collab2/src/db/tables/room.rs deleted file mode 100644 index 4150c741ac..0000000000 --- a/crates/collab2/src/db/tables/room.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::db::{ChannelId, RoomId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "rooms")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: RoomId, - pub live_kit_room: String, - pub channel_id: Option, - pub enviroment: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::room_participant::Entity")] - RoomParticipant, - #[sea_orm(has_many = "super::project::Entity")] - Project, - #[sea_orm(has_many = "super::follower::Entity")] - Follower, - #[sea_orm( - belongs_to = "super::channel::Entity", - from = "Column::ChannelId", - to = "super::channel::Column::Id" - )] - Channel, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::RoomParticipant.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Project.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Follower.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Channel.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/room_participant.rs b/crates/collab2/src/db/tables/room_participant.rs deleted file mode 100644 index 4c5b8cc11c..0000000000 --- a/crates/collab2/src/db/tables/room_participant.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; -use rpc::ConnectionId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "room_participants")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: RoomParticipantId, - pub room_id: RoomId, - pub user_id: UserId, - pub answering_connection_id: Option, - pub answering_connection_server_id: Option, - pub answering_connection_lost: bool, - pub location_kind: Option, - pub location_project_id: Option, - pub initial_project_id: Option, - 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)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::RoomId", - to = "super::room::Column::Id" - )] - Room, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/server.rs b/crates/collab2/src/db/tables/server.rs deleted file mode 100644 index ea847bdf74..0000000000 --- a/crates/collab2/src/db/tables/server.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::db::ServerId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "servers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ServerId, - pub environment: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/signup.rs b/crates/collab2/src/db/tables/signup.rs deleted file mode 100644 index 79d9f0580c..0000000000 --- a/crates/collab2/src/db/tables/signup.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::db::{SignupId, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "signups")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: SignupId, - pub email_address: String, - pub email_confirmation_code: String, - pub email_confirmation_sent: bool, - pub created_at: DateTime, - pub device_id: Option, - pub user_id: Option, - pub inviting_user_id: Option, - pub platform_mac: bool, - pub platform_linux: bool, - pub platform_windows: bool, - pub platform_unknown: bool, - pub editor_features: Option>, - pub programming_languages: Option>, - pub added_to_mailing_list: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/user.rs b/crates/collab2/src/db/tables/user.rs deleted file mode 100644 index 739693527f..0000000000 --- a/crates/collab2/src/db/tables/user.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::db::UserId; -use sea_orm::entity::prelude::*; -use serde::Serialize; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] -#[sea_orm(table_name = "users")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: UserId, - pub github_login: String, - pub github_user_id: Option, - pub email_address: Option, - pub admin: bool, - pub invite_code: Option, - pub invite_count: i32, - pub inviter_id: Option, - pub connected_once: bool, - pub metrics_id: Uuid, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::access_token::Entity")] - AccessToken, - #[sea_orm(has_one = "super::room_participant::Entity")] - RoomParticipant, - #[sea_orm(has_many = "super::project::Entity")] - HostedProjects, - #[sea_orm(has_many = "super::channel_member::Entity")] - ChannelMemberships, - #[sea_orm(has_many = "super::user_feature::Entity")] - UserFeatures, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::AccessToken.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::RoomParticipant.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::HostedProjects.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::ChannelMemberships.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UserFeatures.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -pub struct UserFlags; - -impl Linked for UserFlags { - type FromEntity = Entity; - - type ToEntity = super::feature_flag::Entity; - - fn link(&self) -> Vec { - vec![ - super::user_feature::Relation::User.def().rev(), - super::user_feature::Relation::Flag.def(), - ] - } -} diff --git a/crates/collab2/src/db/tables/user_feature.rs b/crates/collab2/src/db/tables/user_feature.rs deleted file mode 100644 index cc24b5e796..0000000000 --- a/crates/collab2/src/db/tables/user_feature.rs +++ /dev/null @@ -1,42 +0,0 @@ -use sea_orm::entity::prelude::*; - -use crate::db::{FlagId, UserId}; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "user_features")] -pub struct Model { - #[sea_orm(primary_key)] - pub user_id: UserId, - #[sea_orm(primary_key)] - pub feature_id: FlagId, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::feature_flag::Entity", - from = "Column::FeatureId", - to = "super::feature_flag::Column::Id" - )] - Flag, - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Flag.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/worktree.rs b/crates/collab2/src/db/tables/worktree.rs deleted file mode 100644 index 46d9877dff..0000000000 --- a/crates/collab2/src/db/tables/worktree.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::db::ProjectId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "worktrees")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(primary_key)] - pub project_id: ProjectId, - pub abs_path: String, - pub root_name: String, - pub visible: bool, - /// The last scan for which we've observed entries. It may be in progress. - pub scan_id: i64, - /// The last scan that fully completed. - pub completed_scan_id: i64, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::project::Entity", - from = "Column::ProjectId", - to = "super::project::Column::Id" - )] - Project, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Project.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/worktree_diagnostic_summary.rs b/crates/collab2/src/db/tables/worktree_diagnostic_summary.rs deleted file mode 100644 index 5620ed255f..0000000000 --- a/crates/collab2/src/db/tables/worktree_diagnostic_summary.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::db::ProjectId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "worktree_diagnostic_summaries")] -pub struct Model { - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub worktree_id: i64, - #[sea_orm(primary_key)] - pub path: String, - pub language_server_id: i64, - pub error_count: i32, - pub warning_count: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/worktree_entry.rs b/crates/collab2/src/db/tables/worktree_entry.rs deleted file mode 100644 index 81bf6e2d53..0000000000 --- a/crates/collab2/src/db/tables/worktree_entry.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::db::ProjectId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "worktree_entries")] -pub struct Model { - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub worktree_id: i64, - #[sea_orm(primary_key)] - pub id: i64, - pub is_dir: bool, - pub path: String, - pub inode: i64, - pub mtime_seconds: i64, - pub mtime_nanos: i32, - pub git_status: Option, - pub is_symlink: bool, - pub is_ignored: bool, - pub is_external: bool, - pub is_deleted: bool, - pub scan_id: i64, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/worktree_repository.rs b/crates/collab2/src/db/tables/worktree_repository.rs deleted file mode 100644 index 6f86ff0c2d..0000000000 --- a/crates/collab2/src/db/tables/worktree_repository.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::db::ProjectId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "worktree_repositories")] -pub struct Model { - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub worktree_id: i64, - #[sea_orm(primary_key)] - pub work_directory_id: i64, - pub scan_id: i64, - pub branch: Option, - pub is_deleted: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/worktree_repository_statuses.rs b/crates/collab2/src/db/tables/worktree_repository_statuses.rs deleted file mode 100644 index cab016749d..0000000000 --- a/crates/collab2/src/db/tables/worktree_repository_statuses.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::db::ProjectId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "worktree_repository_statuses")] -pub struct Model { - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub worktree_id: i64, - #[sea_orm(primary_key)] - pub work_directory_id: i64, - #[sea_orm(primary_key)] - pub repo_path: String, - pub status: i64, - pub scan_id: i64, - pub is_deleted: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tables/worktree_settings_file.rs b/crates/collab2/src/db/tables/worktree_settings_file.rs deleted file mode 100644 index 92348c1ec9..0000000000 --- a/crates/collab2/src/db/tables/worktree_settings_file.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::db::ProjectId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "worktree_settings_files")] -pub struct Model { - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub worktree_id: i64, - #[sea_orm(primary_key)] - pub path: String, - pub content: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab2/src/db/tests.rs b/crates/collab2/src/db/tests.rs deleted file mode 100644 index 56e37abc1d..0000000000 --- a/crates/collab2/src/db/tests.rs +++ /dev/null @@ -1,187 +0,0 @@ -mod buffer_tests; -mod channel_tests; -mod db_tests; -mod feature_flag_tests; -mod message_tests; - -use super::*; -use gpui::BackgroundExecutor; -use parking_lot::Mutex; -use sea_orm::ConnectionTrait; -use sqlx::migrate::MigrateDatabase; -use std::sync::{ - atomic::{AtomicI32, AtomicU32, Ordering::SeqCst}, - Arc, -}; - -const TEST_RELEASE_CHANNEL: &'static str = "test"; - -pub struct TestDb { - pub db: Option>, - pub connection: Option, -} - -impl TestDb { - pub fn sqlite(background: BackgroundExecutor) -> Self { - let url = format!("sqlite::memory:"); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - let mut db = runtime.block_on(async { - let mut options = ConnectOptions::new(url); - options.max_connections(5); - let mut db = Database::new(options, Executor::Deterministic(background)) - .await - .unwrap(); - let sql = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/migrations.sqlite/20221109000000_test_schema.sql" - )); - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - sql, - )) - .await - .unwrap(); - db.initialize_notification_kinds().await.unwrap(); - db - }); - - db.runtime = Some(runtime); - - Self { - db: Some(Arc::new(db)), - connection: None, - } - } - - pub fn postgres(background: BackgroundExecutor) -> Self { - static LOCK: Mutex<()> = Mutex::new(()); - - let _guard = LOCK.lock(); - let mut rng = StdRng::from_entropy(); - let url = format!( - "postgres://postgres@localhost/zed-test-{}", - rng.gen::() - ); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - let mut db = runtime.block_on(async { - sqlx::Postgres::create_database(&url) - .await - .expect("failed to create test db"); - let mut options = ConnectOptions::new(url); - options - .max_connections(5) - .idle_timeout(Duration::from_secs(0)); - let mut db = Database::new(options, Executor::Deterministic(background)) - .await - .unwrap(); - let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"); - db.migrate(Path::new(migrations_path), false).await.unwrap(); - db.initialize_notification_kinds().await.unwrap(); - db - }); - - db.runtime = Some(runtime); - - Self { - db: Some(Arc::new(db)), - connection: None, - } - } - - pub fn db(&self) -> &Arc { - self.db.as_ref().unwrap() - } -} - -#[macro_export] -macro_rules! test_both_dbs { - ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { - #[gpui::test] - async fn $postgres_test_name(cx: &mut gpui::TestAppContext) { - let test_db = crate::db::TestDb::postgres(cx.executor().clone()); - $test_name(test_db.db()).await; - } - - #[gpui::test] - async fn $sqlite_test_name(cx: &mut gpui::TestAppContext) { - let test_db = crate::db::TestDb::sqlite(cx.executor().clone()); - $test_name(test_db.db()).await; - } - }; -} - -impl Drop for TestDb { - fn drop(&mut self) { - let db = self.db.take().unwrap(); - if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() { - db.runtime.as_ref().unwrap().block_on(async { - use util::ResultExt; - let query = " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE - pg_stat_activity.datname = current_database() AND - pid <> pg_backend_pid(); - "; - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - query, - )) - .await - .log_err(); - sqlx::Postgres::drop_database(db.options.get_url()) - .await - .log_err(); - }) - } - } -} - -fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec { - channels - .iter() - .map(|(id, parent_path, name, role)| Channel { - id: *id, - name: name.to_string(), - visibility: ChannelVisibility::Members, - role: *role, - parent_path: parent_path.to_vec(), - }) - .collect() -} - -static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5); - -async fn new_test_user(db: &Arc, email: &str) -> UserId { - db.create_user( - email, - false, - NewUserParams { - github_login: email[0..email.find("@").unwrap()].to_string(), - github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst), - }, - ) - .await - .unwrap() - .user_id -} - -static TEST_CONNECTION_ID: AtomicU32 = AtomicU32::new(1); -fn new_test_connection(server: ServerId) -> ConnectionId { - ConnectionId { - id: TEST_CONNECTION_ID.fetch_add(1, SeqCst), - owner_id: server.0 as u32, - } -} diff --git a/crates/collab2/src/db/tests/buffer_tests.rs b/crates/collab2/src/db/tests/buffer_tests.rs deleted file mode 100644 index 222514da0b..0000000000 --- a/crates/collab2/src/db/tests/buffer_tests.rs +++ /dev/null @@ -1,506 +0,0 @@ -use super::*; -use crate::test_both_dbs; -use language::proto::{self, serialize_version}; -use text::Buffer; - -test_both_dbs!( - test_channel_buffers, - test_channel_buffers_postgres, - test_channel_buffers_sqlite -); - -async fn test_channel_buffers(db: &Arc) { - let a_id = db - .create_user( - "user_a@example.com", - false, - NewUserParams { - github_login: "user_a".into(), - github_user_id: 101, - }, - ) - .await - .unwrap() - .user_id; - let b_id = db - .create_user( - "user_b@example.com", - false, - NewUserParams { - github_login: "user_b".into(), - github_user_id: 102, - }, - ) - .await - .unwrap() - .user_id; - - // This user will not be a part of the channel - let c_id = db - .create_user( - "user_c@example.com", - false, - NewUserParams { - github_login: "user_c".into(), - github_user_id: 102, - }, - ) - .await - .unwrap() - .user_id; - - let owner_id = db.create_server("production").await.unwrap().0 as u32; - - let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - - db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member) - .await - .unwrap(); - - db.respond_to_channel_invite(zed_id, b_id, true) - .await - .unwrap(); - - let connection_id_a = ConnectionId { owner_id, id: 1 }; - let _ = db - .join_channel_buffer(zed_id, a_id, connection_id_a) - .await - .unwrap(); - - let mut buffer_a = Buffer::new(0, 0, "".to_string()); - let mut operations = Vec::new(); - operations.push(buffer_a.edit([(0..0, "hello world")])); - operations.push(buffer_a.edit([(5..5, ", cruel")])); - operations.push(buffer_a.edit([(0..5, "goodbye")])); - operations.push(buffer_a.undo().unwrap().1); - assert_eq!(buffer_a.text(), "hello, cruel world"); - - let operations = operations - .into_iter() - .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) - .collect::>(); - - db.update_channel_buffer(zed_id, a_id, &operations) - .await - .unwrap(); - - let connection_id_b = ConnectionId { owner_id, id: 2 }; - let buffer_response_b = db - .join_channel_buffer(zed_id, b_id, connection_id_b) - .await - .unwrap(); - - let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text); - buffer_b - .apply_ops(buffer_response_b.operations.into_iter().map(|operation| { - let operation = proto::deserialize_operation(operation).unwrap(); - if let language::Operation::Buffer(operation) = operation { - operation - } else { - unreachable!() - } - })) - .unwrap(); - - assert_eq!(buffer_b.text(), "hello, cruel world"); - - // Ensure that C fails to open the buffer - assert!(db - .join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 }) - .await - .is_err()); - - // Ensure that both collaborators have shown up - assert_eq!( - buffer_response_b.collaborators, - &[ - rpc::proto::Collaborator { - user_id: a_id.to_proto(), - peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), - replica_id: 0, - }, - rpc::proto::Collaborator { - user_id: b_id.to_proto(), - peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), - replica_id: 1, - } - ] - ); - - // Ensure that get_channel_buffer_collaborators works - let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); - assert_eq!(zed_collaborats, &[a_id, b_id]); - - let left_buffer = db - .leave_channel_buffer(zed_id, connection_id_b) - .await - .unwrap(); - - assert_eq!(left_buffer.connections, &[connection_id_a],); - - let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap(); - let _ = db - .join_channel_buffer(cargo_id, a_id, connection_id_a) - .await - .unwrap(); - - db.leave_channel_buffers(connection_id_a).await.unwrap(); - - let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); - let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap(); - assert_eq!(zed_collaborators, &[]); - assert_eq!(cargo_collaborators, &[]); - - // When everyone has left the channel, the operations are collapsed into - // a new base text. - let buffer_response_b = db - .join_channel_buffer(zed_id, b_id, connection_id_b) - .await - .unwrap(); - assert_eq!(buffer_response_b.base_text, "hello, cruel world"); - assert_eq!(buffer_response_b.operations, &[]); -} - -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, - }, - ) - .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, - }, - ) - .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}"), user_id) - .await - .unwrap(); - - db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member) - .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/collab2/src/db/tests/channel_tests.rs b/crates/collab2/src/db/tests/channel_tests.rs deleted file mode 100644 index 8a7a19ed3a..0000000000 --- a/crates/collab2/src/db/tests/channel_tests.rs +++ /dev/null @@ -1,831 +0,0 @@ -use crate::{ - db::{ - tests::{channel_tree, new_test_connection, new_test_user, TEST_RELEASE_CHANNEL}, - Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, - }, - test_both_dbs, -}; -use rpc::{ - proto::{self}, - ConnectionId, -}; -use std::sync::Arc; - -test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); - -async fn test_channels(db: &Arc) { - let a_id = new_test_user(db, "user1@example.com").await; - let b_id = new_test_user(db, "user2@example.com").await; - - let zed_id = db.create_root_channel("zed", 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.is_err()); - - db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member) - .await - .unwrap(); - - db.respond_to_channel_invite(zed_id, b_id, true) - .await - .unwrap(); - - let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap(); - let livestreaming_id = db - .create_sub_channel("livestreaming", zed_id, a_id) - .await - .unwrap(); - let replace_id = db - .create_sub_channel("replace", zed_id, a_id) - .await - .unwrap(); - - let mut members = db - .transaction(|tx| async move { - let channel = db.get_channel_internal(replace_id, &*tx).await?; - Ok(db.get_channel_participants(&channel, &*tx).await?) - }) - .await - .unwrap(); - members.sort(); - assert_eq!(members, &[a_id, b_id]); - - let rust_id = db.create_root_channel("rust", a_id).await.unwrap(); - let cargo_id = db.create_sub_channel("cargo", rust_id, a_id).await.unwrap(); - - let cargo_ra_id = db - .create_sub_channel("cargo-ra", cargo_id, a_id) - .await - .unwrap(); - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_eq!( - result.channels, - channel_tree(&[ - (zed_id, &[], "zed", ChannelRole::Admin), - (crdb_id, &[zed_id], "crdb", ChannelRole::Admin), - ( - livestreaming_id, - &[zed_id], - "livestreaming", - ChannelRole::Admin - ), - (replace_id, &[zed_id], "replace", ChannelRole::Admin), - (rust_id, &[], "rust", ChannelRole::Admin), - (cargo_id, &[rust_id], "cargo", ChannelRole::Admin), - ( - cargo_ra_id, - &[rust_id, cargo_id], - "cargo-ra", - ChannelRole::Admin - ) - ],) - ); - - let result = db.get_channels_for_user(b_id).await.unwrap(); - assert_eq!( - result.channels, - channel_tree(&[ - (zed_id, &[], "zed", ChannelRole::Member), - (crdb_id, &[zed_id], "crdb", ChannelRole::Member), - ( - livestreaming_id, - &[zed_id], - "livestreaming", - ChannelRole::Member - ), - (replace_id, &[zed_id], "replace", ChannelRole::Member) - ],) - ); - - // Update member permissions - let set_subchannel_admin = db - .set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin) - .await; - assert!(set_subchannel_admin.is_err()); - let set_channel_admin = db - .set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin) - .await; - assert!(set_channel_admin.is_ok()); - - let result = db.get_channels_for_user(b_id).await.unwrap(); - assert_eq!( - result.channels, - channel_tree(&[ - (zed_id, &[], "zed", ChannelRole::Admin), - (crdb_id, &[zed_id], "crdb", ChannelRole::Admin), - ( - livestreaming_id, - &[zed_id], - "livestreaming", - ChannelRole::Admin - ), - (replace_id, &[zed_id], "replace", ChannelRole::Admin) - ],) - ); - - // Remove a single channel - db.delete_channel(crdb_id, a_id).await.unwrap(); - assert!(db.get_channel(crdb_id, a_id).await.is_err()); - - // 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.is_err()); - assert!(db.get_channel(cargo_id, a_id).await.is_err()); - assert!(db.get_channel(cargo_ra_id, a_id).await.is_err()); -} - -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 = new_test_user(db, "user1@example.com").await; - let user_2 = new_test_user(db, "user2@example.com").await; - - let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); - - // can join a room with membership to its channel - let (joined_room, _, _) = db - .join_channel( - channel_1, - user_1, - ConnectionId { owner_id, id: 1 }, - TEST_RELEASE_CHANNEL, - ) - .await - .unwrap(); - assert_eq!(joined_room.room.participants.len(), 1); - - let room_id = RoomId::from_proto(joined_room.room.id); - drop(joined_room); - // cannot join a room without membership to its channel - assert!(db - .join_room( - room_id, - user_2, - ConnectionId { owner_id, id: 1 }, - TEST_RELEASE_CHANNEL - ) - .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 = new_test_user(db, "user1@example.com").await; - let user_2 = new_test_user(db, "user2@example.com").await; - let user_3 = new_test_user(db, "user3@example.com").await; - - let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); - - let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap(); - - db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member) - .await - .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member) - .await - .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin) - .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 mut members = db - .get_channel_participant_details(channel_1_1, user_1) - .await - .unwrap(); - - members.sort_by_key(|member| member.user_id); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - role: proto::ChannelRole::Admin.into(), - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - role: proto::ChannelRole::Member.into(), - }, - proto::ChannelMember { - user_id: user_3.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - role: proto::ChannelRole::Admin.into(), - }, - ] - ); - - db.respond_to_channel_invite(channel_1_1, user_2, true) - .await - .unwrap(); - - let channel_1_3 = db - .create_sub_channel("channel_3", channel_1_1, user_1) - .await - .unwrap(); - - let members = db - .get_channel_participant_details(channel_1_3, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Admin.into(), - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Member.into(), - }, - ] - ); -} - -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, - }, - ) - .await - .unwrap() - .user_id; - - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", user_1).await.unwrap(); - - db.rename_channel(zed_id, user_1, "#zed-archive") - .await - .unwrap(); - - let channel = db.get_channel(zed_id, user_1).await.unwrap(); - assert_eq!(channel.name, "zed-archive"); - - let non_permissioned_rename = db.rename_channel(zed_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, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - - let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap(); - - let gpui2_id = db.create_sub_channel("gpui2", zed_id, a_id).await.unwrap(); - - let livestreaming_id = db - .create_sub_channel("livestreaming", crdb_id, a_id) - .await - .unwrap(); - - let livestreaming_dag_id = db - .create_sub_channel("livestreaming_dag", livestreaming_id, 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_channel_tree( - result.channels, - &[ - (zed_id, &[]), - (crdb_id, &[zed_id]), - (livestreaming_id, &[zed_id, crdb_id]), - (livestreaming_dag_id, &[zed_id, crdb_id, livestreaming_id]), - (gpui2_id, &[zed_id]), - ], - ); -} - -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, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", user_id).await.unwrap(); - - let projects_id = db - .create_sub_channel("projects", zed_id, user_id) - .await - .unwrap(); - - let livestreaming_id = db - .create_sub_channel("livestreaming", projects_id, user_id) - .await - .unwrap(); - - // Move to same parent should be a no-op - assert!(db - .move_channel(projects_id, Some(zed_id), user_id) - .await - .unwrap() - .is_none()); - - let result = db.get_channels_for_user(user_id).await.unwrap(); - assert_channel_tree( - result.channels, - &[ - (zed_id, &[]), - (projects_id, &[zed_id]), - (livestreaming_id, &[zed_id, projects_id]), - ], - ); - - // Move the project channel to the root - db.move_channel(projects_id, None, user_id).await.unwrap(); - let result = db.get_channels_for_user(user_id).await.unwrap(); - assert_channel_tree( - result.channels, - &[ - (zed_id, &[]), - (projects_id, &[]), - (livestreaming_id, &[projects_id]), - ], - ); - - // Can't move a channel into its ancestor - db.move_channel(projects_id, Some(livestreaming_id), user_id) - .await - .unwrap_err(); - let result = db.get_channels_for_user(user_id).await.unwrap(); - assert_channel_tree( - result.channels, - &[ - (zed_id, &[]), - (projects_id, &[]), - (livestreaming_id, &[projects_id]), - ], - ); -} - -test_both_dbs!( - test_user_is_channel_participant, - test_user_is_channel_participant_postgres, - test_user_is_channel_participant_sqlite -); - -async fn test_user_is_channel_participant(db: &Arc) { - let admin = new_test_user(db, "admin@example.com").await; - let member = new_test_user(db, "member@example.com").await; - let guest = new_test_user(db, "guest@example.com").await; - - let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); - let active_channel_id = db - .create_sub_channel("active", zed_channel, admin) - .await - .unwrap(); - let vim_channel_id = db - .create_sub_channel("vim", active_channel_id, admin) - .await - .unwrap(); - - db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin) - .await - .unwrap(); - db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member) - .await - .unwrap(); - db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest) - .await - .unwrap(); - - db.respond_to_channel_invite(active_channel_id, member, true) - .await - .unwrap(); - - db.transaction(|tx| async move { - db.check_user_is_channel_participant( - &db.get_channel_internal(vim_channel_id, &*tx).await?, - admin, - &*tx, - ) - .await - }) - .await - .unwrap(); - db.transaction(|tx| async move { - db.check_user_is_channel_participant( - &db.get_channel_internal(vim_channel_id, &*tx).await?, - member, - &*tx, - ) - .await - }) - .await - .unwrap(); - - let mut members = db - .get_channel_participant_details(vim_channel_id, admin) - .await - .unwrap(); - - members.sort_by_key(|member| member.user_id); - - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: admin.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Admin.into(), - }, - proto::ChannelMember { - user_id: member.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Member.into(), - }, - proto::ChannelMember { - user_id: guest.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - role: proto::ChannelRole::Guest.into(), - }, - ] - ); - - db.respond_to_channel_invite(vim_channel_id, guest, true) - .await - .unwrap(); - - db.transaction(|tx| async move { - db.check_user_is_channel_participant( - &db.get_channel_internal(vim_channel_id, &*tx).await?, - guest, - &*tx, - ) - .await - }) - .await - .unwrap(); - - let channels = db.get_channels_for_user(guest).await.unwrap().channels; - assert_channel_tree(channels, &[(vim_channel_id, &[])]); - let channels = db.get_channels_for_user(member).await.unwrap().channels; - assert_channel_tree( - channels, - &[ - (active_channel_id, &[]), - (vim_channel_id, &[active_channel_id]), - ], - ); - - db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned) - .await - .unwrap(); - assert!(db - .transaction(|tx| async move { - db.check_user_is_channel_participant( - &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), - guest, - &*tx, - ) - .await - }) - .await - .is_err()); - - let mut members = db - .get_channel_participant_details(vim_channel_id, admin) - .await - .unwrap(); - - members.sort_by_key(|member| member.user_id); - - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: admin.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Admin.into(), - }, - proto::ChannelMember { - user_id: member.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Member.into(), - }, - proto::ChannelMember { - user_id: guest.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - role: proto::ChannelRole::Banned.into(), - }, - ] - ); - - db.remove_channel_member(vim_channel_id, guest, admin) - .await - .unwrap(); - - db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) - .await - .unwrap(); - - db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest) - .await - .unwrap(); - - // currently people invited to parent channels are not shown here - let mut members = db - .get_channel_participant_details(vim_channel_id, admin) - .await - .unwrap(); - - members.sort_by_key(|member| member.user_id); - - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: admin.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Admin.into(), - }, - proto::ChannelMember { - user_id: member.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Member.into(), - }, - ] - ); - - db.respond_to_channel_invite(zed_channel, guest, true) - .await - .unwrap(); - - db.transaction(|tx| async move { - db.check_user_is_channel_participant( - &db.get_channel_internal(zed_channel, &*tx).await.unwrap(), - guest, - &*tx, - ) - .await - }) - .await - .unwrap(); - assert!(db - .transaction(|tx| async move { - db.check_user_is_channel_participant( - &db.get_channel_internal(active_channel_id, &*tx) - .await - .unwrap(), - guest, - &*tx, - ) - .await - }) - .await - .is_err(),); - - db.transaction(|tx| async move { - db.check_user_is_channel_participant( - &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), - guest, - &*tx, - ) - .await - }) - .await - .unwrap(); - - let mut members = db - .get_channel_participant_details(vim_channel_id, admin) - .await - .unwrap(); - - members.sort_by_key(|member| member.user_id); - - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: admin.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Admin.into(), - }, - proto::ChannelMember { - user_id: member.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Member.into(), - }, - proto::ChannelMember { - user_id: guest.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - role: proto::ChannelRole::Guest.into(), - }, - ] - ); - - let channels = db.get_channels_for_user(guest).await.unwrap().channels; - assert_channel_tree( - channels, - &[(zed_channel, &[]), (vim_channel_id, &[zed_channel])], - ) -} - -test_both_dbs!( - test_user_joins_correct_channel, - test_user_joins_correct_channel_postgres, - test_user_joins_correct_channel_sqlite -); - -async fn test_user_joins_correct_channel(db: &Arc) { - let admin = new_test_user(db, "admin@example.com").await; - - let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); - - let active_channel = db - .create_sub_channel("active", zed_channel, admin) - .await - .unwrap(); - - let vim_channel = db - .create_sub_channel("vim", active_channel, admin) - .await - .unwrap(); - - let vim2_channel = db - .create_sub_channel("vim2", vim_channel, admin) - .await - .unwrap(); - - db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) - .await - .unwrap(); - - db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin) - .await - .unwrap(); - - db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin) - .await - .unwrap(); - - let most_public = db - .transaction(|tx| async move { - Ok(db - .public_ancestors_including_self( - &db.get_channel_internal(vim_channel, &*tx).await.unwrap(), - &tx, - ) - .await? - .first() - .cloned()) - }) - .await - .unwrap() - .unwrap() - .id; - - assert_eq!(most_public, zed_channel) -} - -test_both_dbs!( - test_guest_access, - test_guest_access_postgres, - test_guest_access_sqlite -); - -async fn test_guest_access(db: &Arc) { - let server = db.create_server("test").await.unwrap(); - - let admin = new_test_user(db, "admin@example.com").await; - let guest = new_test_user(db, "guest@example.com").await; - let guest_connection = new_test_connection(server); - - let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); - db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) - .await - .unwrap(); - - assert!(db - .join_channel_chat(zed_channel, guest_connection, guest) - .await - .is_err()); - - db.join_channel(zed_channel, guest, guest_connection, TEST_RELEASE_CHANNEL) - .await - .unwrap(); - - assert!(db - .join_channel_chat(zed_channel, guest_connection, guest) - .await - .is_ok()) -} - -#[track_caller] -fn assert_channel_tree(actual: Vec, expected: &[(ChannelId, &[ChannelId])]) { - let actual = actual - .iter() - .map(|channel| (channel.id, channel.parent_path.as_slice())) - .collect::>(); - pretty_assertions::assert_eq!( - actual, - expected.to_vec(), - "wrong channel ids and parent paths" - ); -} diff --git a/crates/collab2/src/db/tests/db_tests.rs b/crates/collab2/src/db/tests/db_tests.rs deleted file mode 100644 index 1f825efd74..0000000000 --- a/crates/collab2/src/db/tests/db_tests.rs +++ /dev/null @@ -1,633 +0,0 @@ -use super::*; -use crate::test_both_dbs; -use gpui::TestAppContext; -use pretty_assertions::{assert_eq, assert_ne}; -use std::sync::Arc; -use tests::TestDb; - -test_both_dbs!( - test_get_users, - test_get_users_by_ids_postgres, - test_get_users_by_ids_sqlite -); - -async fn test_get_users(db: &Arc) { - let mut user_ids = Vec::new(); - let mut user_metric_ids = Vec::new(); - for i in 1..=4 { - let user = db - .create_user( - &format!("user{i}@example.com"), - false, - NewUserParams { - github_login: format!("user{i}"), - github_user_id: i, - }, - ) - .await - .unwrap(); - user_ids.push(user.user_id); - user_metric_ids.push(user.metrics_id); - } - - assert_eq!( - db.get_users_by_ids(user_ids.clone()).await.unwrap(), - vec![ - User { - id: user_ids[0], - github_login: "user1".to_string(), - github_user_id: Some(1), - email_address: Some("user1@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[0].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[1], - github_login: "user2".to_string(), - github_user_id: Some(2), - email_address: Some("user2@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[1].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[2], - github_login: "user3".to_string(), - github_user_id: Some(3), - email_address: Some("user3@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[2].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[3], - github_login: "user4".to_string(), - github_user_id: Some(4), - email_address: Some("user4@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[3].parse().unwrap(), - ..Default::default() - } - ] - ); -} - -test_both_dbs!( - test_get_or_create_user_by_github_account, - test_get_or_create_user_by_github_account_postgres, - test_get_or_create_user_by_github_account_sqlite -); - -async fn test_get_or_create_user_by_github_account(db: &Arc) { - let user_id1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "login1".into(), - github_user_id: 101, - }, - ) - .await - .unwrap() - .user_id; - let user_id2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "login2".into(), - github_user_id: 102, - }, - ) - .await - .unwrap() - .user_id; - - let user = db - .get_or_create_user_by_github_account("login1", None, None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id1); - assert_eq!(&user.github_login, "login1"); - assert_eq!(user.github_user_id, Some(101)); - - assert!(db - .get_or_create_user_by_github_account("non-existent-login", None, None) - .await - .unwrap() - .is_none()); - - let user = db - .get_or_create_user_by_github_account("the-new-login2", Some(102), None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id2); - assert_eq!(&user.github_login, "the-new-login2"); - assert_eq!(user.github_user_id, Some(102)); - - let user = db - .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) - .await - .unwrap() - .unwrap(); - assert_eq!(&user.github_login, "login3"); - assert_eq!(user.github_user_id, Some(103)); - assert_eq!(user.email_address, Some("user3@example.com".into())); -} - -test_both_dbs!( - test_create_access_tokens, - test_create_access_tokens_postgres, - test_create_access_tokens_sqlite -); - -async fn test_create_access_tokens(db: &Arc) { - let user = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 1, - }, - ) - .await - .unwrap() - .user_id; - - let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); - let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_1).await.unwrap(), - access_token::Model { - id: token_1, - user_id: user, - hash: "h1".into(), - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - - let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - assert!(db.get_access_token(token_1).await.is_err()); - - let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_4).await.unwrap(), - access_token::Model { - id: token_4, - user_id: user, - hash: "h4".into() - } - ); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert!(db.get_access_token(token_2).await.is_err()); - assert!(db.get_access_token(token_1).await.is_err()); -} - -test_both_dbs!( - test_add_contacts, - test_add_contacts_postgres, - test_add_contacts_sqlite -); - -async fn test_add_contacts(db: &Arc) { - let mut user_ids = Vec::new(); - for i in 0..3 { - user_ids.push( - db.create_user( - &format!("user{i}@example.com"), - false, - NewUserParams { - github_login: format!("user{i}"), - github_user_id: i, - }, - ) - .await - .unwrap() - .user_id, - ); - } - - let user_1 = user_ids[0]; - let user_2 = user_ids[1]; - let user_3 = user_ids[2]; - - // User starts with no contacts - assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]); - - // User requests a contact. Both users see the pending request. - db.send_contact_request(user_1, user_2).await.unwrap(); - assert!(!db.has_contact(user_1, user_2).await.unwrap()); - assert!(!db.has_contact(user_2, user_1).await.unwrap()); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Outgoing { user_id: user_2 }], - ); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Incoming { user_id: user_1 }] - ); - - // User 2 dismisses the contact request notification without accepting or rejecting. - // We shouldn't notify them again. - db.dismiss_contact_notification(user_1, user_2) - .await - .unwrap_err(); - db.dismiss_contact_notification(user_2, user_1) - .await - .unwrap(); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Incoming { user_id: user_1 }] - ); - - // User can't accept their own contact request - db.respond_to_contact_request(user_1, user_2, true) - .await - .unwrap_err(); - - // User accepts a contact request. Both users see the contact. - db.respond_to_contact_request(user_2, user_1, true) - .await - .unwrap(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Accepted { - user_id: user_2, - busy: false, - }], - ); - assert!(db.has_contact(user_1, user_2).await.unwrap()); - assert!(db.has_contact(user_2, user_1).await.unwrap()); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - busy: false, - }] - ); - - // Users cannot re-request existing contacts. - db.send_contact_request(user_1, user_2).await.unwrap_err(); - db.send_contact_request(user_2, user_1).await.unwrap_err(); - - // Users can't dismiss notifications of them accepting other users' requests. - db.dismiss_contact_notification(user_2, user_1) - .await - .unwrap_err(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Accepted { - user_id: user_2, - busy: false, - }] - ); - - // Users can dismiss notifications of other users accepting their requests. - db.dismiss_contact_notification(user_1, user_2) - .await - .unwrap(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[Contact::Accepted { - user_id: user_2, - busy: false, - }] - ); - - // Users send each other concurrent contact requests and - // see that they are immediately accepted. - db.send_contact_request(user_1, user_3).await.unwrap(); - db.send_contact_request(user_3, user_1).await.unwrap(); - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_2, - busy: false, - }, - Contact::Accepted { - user_id: user_3, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user_3).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - busy: false, - }], - ); - - // User declines a contact request. Both users see that it is gone. - db.send_contact_request(user_2, user_3).await.unwrap(); - db.respond_to_contact_request(user_3, user_2, false) - .await - .unwrap(); - assert!(!db.has_contact(user_2, user_3).await.unwrap()); - assert!(!db.has_contact(user_3, user_2).await.unwrap()); - assert_eq!( - db.get_contacts(user_2).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - busy: false, - }] - ); - assert_eq!( - db.get_contacts(user_3).await.unwrap(), - &[Contact::Accepted { - user_id: user_1, - busy: false, - }], - ); -} - -test_both_dbs!( - test_metrics_id, - test_metrics_id_postgres, - test_metrics_id_sqlite -); - -async fn test_metrics_id(db: &Arc) { - let NewUserResult { - user_id: user1, - metrics_id: metrics_id1, - .. - } = db - .create_user( - "person1@example.com", - false, - NewUserParams { - github_login: "person1".into(), - github_user_id: 101, - }, - ) - .await - .unwrap(); - let NewUserResult { - user_id: user2, - metrics_id: metrics_id2, - .. - } = db - .create_user( - "person2@example.com", - false, - NewUserParams { - github_login: "person2".into(), - github_user_id: 102, - }, - ) - .await - .unwrap(); - - assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1); - assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2); - assert_eq!(metrics_id1.len(), 36); - assert_eq!(metrics_id2.len(), 36); - assert_ne!(metrics_id1, metrics_id2); -} - -test_both_dbs!( - test_project_count, - test_project_count_postgres, - test_project_count_sqlite -); - -async fn test_project_count(db: &Arc) { - let owner_id = db.create_server("test").await.unwrap().0 as u32; - - let user1 = db - .create_user( - &format!("admin@example.com"), - true, - NewUserParams { - github_login: "admin".into(), - github_user_id: 0, - }, - ) - .await - .unwrap(); - let user2 = db - .create_user( - &format!("user@example.com"), - false, - NewUserParams { - github_login: "user".into(), - github_user_id: 1, - }, - ) - .await - .unwrap(); - - let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev") - .await - .unwrap() - .id, - ); - db.call( - room_id, - user1.user_id, - ConnectionId { owner_id, id: 0 }, - user2.user_id, - None, - ) - .await - .unwrap(); - db.join_room( - room_id, - user2.user_id, - ConnectionId { owner_id, id: 1 }, - "dev", - ) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); - - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); - - // Projects shared by admins aren't counted. - db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); - - db.leave_room(ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); -} - -#[test] -fn test_fuzzy_like_string() { - assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%"); - assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%"); - assert_eq!(Database::fuzzy_like_string(" z "), "%z%"); -} - -#[gpui::test] -async fn test_fuzzy_search_users(cx: &mut TestAppContext) { - let test_db = TestDb::postgres(cx.executor()); - let db = test_db.db(); - for (i, github_login) in [ - "California", - "colorado", - "oregon", - "washington", - "florida", - "delaware", - "rhode-island", - ] - .into_iter() - .enumerate() - { - db.create_user( - &format!("{github_login}@example.com"), - false, - NewUserParams { - github_login: github_login.into(), - github_user_id: i as i32, - }, - ) - .await - .unwrap(); - } - - assert_eq!( - fuzzy_search_user_names(db, "clr").await, - &["colorado", "California"] - ); - assert_eq!( - fuzzy_search_user_names(db, "ro").await, - &["rhode-island", "colorado", "oregon"], - ); - - async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec { - db.fuzzy_search_users(query, 10) - .await - .unwrap() - .into_iter() - .map(|user| user.github_login) - .collect::>() - } -} - -test_both_dbs!( - test_non_matching_release_channels, - test_non_matching_release_channels_postgres, - test_non_matching_release_channels_sqlite -); - -async fn test_non_matching_release_channels(db: &Arc) { - let owner_id = db.create_server("test").await.unwrap().0 as u32; - - let user1 = db - .create_user( - &format!("admin@example.com"), - true, - NewUserParams { - github_login: "admin".into(), - github_user_id: 0, - }, - ) - .await - .unwrap(); - let user2 = db - .create_user( - &format!("user@example.com"), - false, - NewUserParams { - github_login: "user".into(), - github_user_id: 1, - }, - ) - .await - .unwrap(); - - let room = db - .create_room( - user1.user_id, - ConnectionId { owner_id, id: 0 }, - "", - "stable", - ) - .await - .unwrap(); - - db.call( - RoomId::from_proto(room.id), - user1.user_id, - ConnectionId { owner_id, id: 0 }, - user2.user_id, - None, - ) - .await - .unwrap(); - - // User attempts to join from preview - let result = db - .join_room( - RoomId::from_proto(room.id), - user2.user_id, - ConnectionId { owner_id, id: 1 }, - "preview", - ) - .await; - - assert!(result.is_err()); - - // User switches to stable - let result = db - .join_room( - RoomId::from_proto(room.id), - user2.user_id, - ConnectionId { owner_id, id: 1 }, - "stable", - ) - .await; - - assert!(result.is_ok()) -} diff --git a/crates/collab2/src/db/tests/feature_flag_tests.rs b/crates/collab2/src/db/tests/feature_flag_tests.rs deleted file mode 100644 index 0286a6308e..0000000000 --- a/crates/collab2/src/db/tests/feature_flag_tests.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::{ - db::{Database, NewUserParams}, - test_both_dbs, -}; -use std::sync::Arc; - -test_both_dbs!( - test_get_user_flags, - test_get_user_flags_postgres, - test_get_user_flags_sqlite -); - -async fn test_get_user_flags(db: &Arc) { - let user_1 = db - .create_user( - &format!("user1@example.com"), - false, - NewUserParams { - github_login: format!("user1"), - github_user_id: 1, - }, - ) - .await - .unwrap() - .user_id; - - let user_2 = db - .create_user( - &format!("user2@example.com"), - false, - NewUserParams { - github_login: format!("user2"), - github_user_id: 2, - }, - ) - .await - .unwrap() - .user_id; - - const CHANNELS_ALPHA: &'static str = "channels-alpha"; - const NEW_SEARCH: &'static str = "new-search"; - - let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap(); - let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap(); - - db.add_user_flag(user_1, channels_flag).await.unwrap(); - db.add_user_flag(user_1, search_flag).await.unwrap(); - - db.add_user_flag(user_2, channels_flag).await.unwrap(); - - let mut user_1_flags = db.get_user_flags(user_1).await.unwrap(); - user_1_flags.sort(); - assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]); - - let mut user_2_flags = db.get_user_flags(user_2).await.unwrap(); - user_2_flags.sort(); - assert_eq!(user_2_flags, &[CHANNELS_ALPHA]); -} diff --git a/crates/collab2/src/db/tests/message_tests.rs b/crates/collab2/src/db/tests/message_tests.rs deleted file mode 100644 index 10d9778612..0000000000 --- a/crates/collab2/src/db/tests/message_tests.rs +++ /dev/null @@ -1,454 +0,0 @@ -use super::new_test_user; -use crate::{ - db::{ChannelRole, Database, MessageId}, - test_both_dbs, -}; -use channel::mentions_to_proto; -use std::sync::Arc; -use time::OffsetDateTime; - -test_both_dbs!( - test_channel_message_retrieval, - test_channel_message_retrieval_postgres, - test_channel_message_retrieval_sqlite -); - -async fn test_channel_message_retrieval(db: &Arc) { - let user = new_test_user(db, "user@example.com").await; - let result = db.create_channel("channel", None, user).await.unwrap(); - - let owner_id = db.create_server("test").await.unwrap().0 as u32; - db.join_channel_chat( - result.channel.id, - rpc::ConnectionId { owner_id, id: 0 }, - user, - ) - .await - .unwrap(); - - let mut all_messages = Vec::new(); - for i in 0..10 { - all_messages.push( - db.create_channel_message( - result.channel.id, - user, - &i.to_string(), - &[], - OffsetDateTime::now_utc(), - i, - ) - .await - .unwrap() - .message_id - .to_proto(), - ); - } - - let messages = db - .get_channel_messages(result.channel.id, user, 3, None) - .await - .unwrap() - .into_iter() - .map(|message| message.id) - .collect::>(); - assert_eq!(messages, &all_messages[7..10]); - - let messages = db - .get_channel_messages( - result.channel.id, - user, - 4, - Some(MessageId::from_proto(all_messages[6])), - ) - .await - .unwrap() - .into_iter() - .map(|message| message.id) - .collect::>(); - assert_eq!(messages, &all_messages[2..6]); -} - -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_a = new_test_user(db, "user_a@example.com").await; - let user_b = new_test_user(db, "user_b@example.com").await; - let user_c = new_test_user(db, "user_c@example.com").await; - let channel = db.create_root_channel("channel", user_a).await.unwrap(); - db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) - .await - .unwrap(); - db.invite_channel_member(channel, user_c, user_a, ChannelRole::Member) - .await - .unwrap(); - db.respond_to_channel_invite(channel, user_b, true) - .await - .unwrap(); - db.respond_to_channel_invite(channel, user_c, true) - .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_a) - .await - .unwrap(); - db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 1 }, user_b) - .await - .unwrap(); - - // As user A, create messages that re-use the same nonces. The requests - // succeed, but return the same ids. - let id1 = db - .create_channel_message( - channel, - user_a, - "hi @user_b", - &mentions_to_proto(&[(3..10, user_b.to_proto())]), - OffsetDateTime::now_utc(), - 100, - ) - .await - .unwrap() - .message_id; - let id2 = db - .create_channel_message( - channel, - user_a, - "hello, fellow users", - &mentions_to_proto(&[]), - OffsetDateTime::now_utc(), - 200, - ) - .await - .unwrap() - .message_id; - let id3 = db - .create_channel_message( - channel, - user_a, - "bye @user_c (same nonce as first message)", - &mentions_to_proto(&[(4..11, user_c.to_proto())]), - OffsetDateTime::now_utc(), - 100, - ) - .await - .unwrap() - .message_id; - let id4 = db - .create_channel_message( - channel, - user_a, - "omg (same nonce as second message)", - &mentions_to_proto(&[]), - OffsetDateTime::now_utc(), - 200, - ) - .await - .unwrap() - .message_id; - - // As a different user, reuse one of the same nonces. This request succeeds - // and returns a different id. - let id5 = db - .create_channel_message( - channel, - user_b, - "omg @user_a (same nonce as user_a's first message)", - &mentions_to_proto(&[(4..11, user_a.to_proto())]), - OffsetDateTime::now_utc(), - 100, - ) - .await - .unwrap() - .message_id; - - assert_ne!(id1, id2); - assert_eq!(id1, id3); - assert_eq!(id2, id4); - assert_ne!(id5, id1); - - let messages = db - .get_channel_messages(channel, user_a, 5, None) - .await - .unwrap() - .into_iter() - .map(|m| (m.id, m.body, m.mentions)) - .collect::>(); - assert_eq!( - messages, - &[ - ( - id1.to_proto(), - "hi @user_b".into(), - mentions_to_proto(&[(3..10, user_b.to_proto())]), - ), - ( - id2.to_proto(), - "hello, fellow users".into(), - mentions_to_proto(&[]) - ), - ( - id5.to_proto(), - "omg @user_a (same nonce as user_a's first message)".into(), - mentions_to_proto(&[(4..11, user_a.to_proto())]), - ), - ] - ); -} - -test_both_dbs!( - test_unseen_channel_messages, - test_unseen_channel_messages_postgres, - test_unseen_channel_messages_sqlite -); - -async fn test_unseen_channel_messages(db: &Arc) { - let user = new_test_user(db, "user_a@example.com").await; - let observer = new_test_user(db, "user_b@example.com").await; - - let channel_1 = db.create_root_channel("channel", user).await.unwrap(); - let channel_2 = db.create_root_channel("channel-2", user).await.unwrap(); - - db.invite_channel_member(channel_1, observer, user, ChannelRole::Member) - .await - .unwrap(); - db.invite_channel_member(channel_2, observer, user, ChannelRole::Member) - .await - .unwrap(); - - db.respond_to_channel_invite(channel_1, observer, true) - .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() - .message_id; - - let third_message = db - .create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3) - .await - .unwrap() - .message_id; - - 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() - .message_id; - - // 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(), - }] - ); -} - -test_both_dbs!( - test_channel_message_mentions, - test_channel_message_mentions_postgres, - test_channel_message_mentions_sqlite -); - -async fn test_channel_message_mentions(db: &Arc) { - let user_a = new_test_user(db, "user_a@example.com").await; - let user_b = new_test_user(db, "user_b@example.com").await; - let user_c = new_test_user(db, "user_c@example.com").await; - - let channel = db - .create_channel("channel", None, user_a) - .await - .unwrap() - .channel - .id; - db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) - .await - .unwrap(); - db.respond_to_channel_invite(channel, user_b, true) - .await - .unwrap(); - - let owner_id = db.create_server("test").await.unwrap().0 as u32; - let connection_id = rpc::ConnectionId { owner_id, id: 0 }; - db.join_channel_chat(channel, connection_id, user_a) - .await - .unwrap(); - - db.create_channel_message( - channel, - user_a, - "hi @user_b and @user_c", - &mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]), - OffsetDateTime::now_utc(), - 1, - ) - .await - .unwrap(); - db.create_channel_message( - channel, - user_a, - "bye @user_c", - &mentions_to_proto(&[(4..11, user_c.to_proto())]), - OffsetDateTime::now_utc(), - 2, - ) - .await - .unwrap(); - db.create_channel_message( - channel, - user_a, - "umm", - &mentions_to_proto(&[]), - OffsetDateTime::now_utc(), - 3, - ) - .await - .unwrap(); - db.create_channel_message( - channel, - user_a, - "@user_b, stop.", - &mentions_to_proto(&[(0..7, user_b.to_proto())]), - OffsetDateTime::now_utc(), - 4, - ) - .await - .unwrap(); - - let messages = db - .get_channel_messages(channel, user_b, 5, None) - .await - .unwrap() - .into_iter() - .map(|m| (m.body, m.mentions)) - .collect::>(); - assert_eq!( - &messages, - &[ - ( - "hi @user_b and @user_c".into(), - mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]), - ), - ( - "bye @user_c".into(), - mentions_to_proto(&[(4..11, user_c.to_proto())]), - ), - ("umm".into(), mentions_to_proto(&[]),), - ( - "@user_b, stop.".into(), - mentions_to_proto(&[(0..7, user_b.to_proto())]), - ), - ] - ); -} diff --git a/crates/collab2/src/env.rs b/crates/collab2/src/env.rs deleted file mode 100644 index 58c29b0205..0000000000 --- a/crates/collab2/src/env.rs +++ /dev/null @@ -1,20 +0,0 @@ -use anyhow::anyhow; -use std::fs; - -pub fn load_dotenv() -> anyhow::Result<()> { - let env: toml::map::Map = toml::de::from_str( - &fs::read_to_string("./.env.toml").map_err(|_| anyhow!("no .env.toml file found"))?, - )?; - - for (key, value) in env { - let value = match value { - toml::Value::String(value) => value, - toml::Value::Integer(value) => value.to_string(), - toml::Value::Float(value) => value.to_string(), - _ => panic!("unsupported TOML value in .env.toml for key {}", key), - }; - std::env::set_var(key, value); - } - - Ok(()) -} diff --git a/crates/collab2/src/errors.rs b/crates/collab2/src/errors.rs deleted file mode 100644 index 93e46848a1..0000000000 --- a/crates/collab2/src/errors.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Allow tide Results to accept context like other Results do when -// using anyhow. -pub trait TideResultExt { - fn context(self, cx: C) -> Self - where - C: std::fmt::Display + Send + Sync + 'static; - - fn with_context(self, f: F) -> Self - where - C: std::fmt::Display + Send + Sync + 'static, - F: FnOnce() -> C; -} - -impl TideResultExt for tide::Result { - fn context(self, cx: C) -> Self - where - C: std::fmt::Display + Send + Sync + 'static, - { - self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(cx))) - } - - fn with_context(self, f: F) -> Self - where - C: std::fmt::Display + Send + Sync + 'static, - F: FnOnce() -> C, - { - self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(f()))) - } -} diff --git a/crates/collab2/src/executor.rs b/crates/collab2/src/executor.rs deleted file mode 100644 index 81d5e977a6..0000000000 --- a/crates/collab2/src/executor.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::{future::Future, time::Duration}; - -#[cfg(test)] -use gpui::BackgroundExecutor; - -#[derive(Clone)] -pub enum Executor { - Production, - #[cfg(test)] - Deterministic(BackgroundExecutor), -} - -impl Executor { - pub fn spawn_detached(&self, future: F) - where - F: 'static + Send + Future, - { - match self { - Executor::Production => { - tokio::spawn(future); - } - #[cfg(test)] - Executor::Deterministic(background) => { - background.spawn(future).detach(); - } - } - } - - pub fn sleep(&self, duration: Duration) -> impl Future { - let this = self.clone(); - async move { - match this { - Executor::Production => tokio::time::sleep(duration).await, - #[cfg(test)] - Executor::Deterministic(background) => background.timer(duration).await, - } - } - } -} diff --git a/crates/collab2/src/lib.rs b/crates/collab2/src/lib.rs deleted file mode 100644 index 85216525b0..0000000000 --- a/crates/collab2/src/lib.rs +++ /dev/null @@ -1,147 +0,0 @@ -pub mod api; -pub mod auth; -pub mod db; -pub mod env; -pub mod executor; -pub mod rpc; - -#[cfg(test)] -mod tests; - -use axum::{http::StatusCode, response::IntoResponse}; -use db::Database; -use executor::Executor; -use serde::Deserialize; -use std::{path::PathBuf, sync::Arc}; - -pub type Result = std::result::Result; - -pub enum Error { - Http(StatusCode, String), - Database(sea_orm::error::DbErr), - Internal(anyhow::Error), -} - -impl From for Error { - fn from(error: anyhow::Error) -> Self { - Self::Internal(error) - } -} - -impl From for Error { - fn from(error: sea_orm::error::DbErr) -> Self { - Self::Database(error) - } -} - -impl From for Error { - fn from(error: axum::Error) -> Self { - Self::Internal(error.into()) - } -} - -impl From for Error { - fn from(error: hyper::Error) -> Self { - Self::Internal(error.into()) - } -} - -impl From for Error { - fn from(error: serde_json::Error) -> Self { - Self::Internal(error.into()) - } -} - -impl IntoResponse for Error { - fn into_response(self) -> axum::response::Response { - match self { - Error::Http(code, message) => (code, message).into_response(), - Error::Database(error) => { - (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() - } - Error::Internal(error) => { - (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() - } - } - } -} - -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Http(code, message) => (code, message).fmt(f), - Error::Database(error) => error.fmt(f), - Error::Internal(error) => error.fmt(f), - } - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Http(code, message) => write!(f, "{code}: {message}"), - Error::Database(error) => error.fmt(f), - Error::Internal(error) => error.fmt(f), - } - } -} - -impl std::error::Error for Error {} - -#[derive(Default, Deserialize)] -pub struct Config { - pub http_port: u16, - pub database_url: String, - pub database_max_connections: u32, - pub api_token: String, - pub invite_link_prefix: String, - pub live_kit_server: Option, - pub live_kit_key: Option, - pub live_kit_secret: Option, - pub rust_log: Option, - pub log_json: Option, - pub zed_environment: String, -} - -#[derive(Default, Deserialize)] -pub struct MigrateConfig { - pub database_url: String, - pub migrations_path: Option, -} - -pub struct AppState { - pub db: Arc, - pub live_kit_client: Option>, - pub config: Config, -} - -impl AppState { - pub async fn new(config: Config) -> Result> { - let mut db_options = db::ConnectOptions::new(config.database_url.clone()); - db_options.max_connections(config.database_max_connections); - let mut db = Database::new(db_options, Executor::Production).await?; - db.initialize_notification_kinds().await?; - - let live_kit_client = if let Some(((server, key), secret)) = config - .live_kit_server - .as_ref() - .zip(config.live_kit_key.as_ref()) - .zip(config.live_kit_secret.as_ref()) - { - Some(Arc::new(live_kit_server::api::LiveKitClient::new( - server.clone(), - key.clone(), - secret.clone(), - )) as Arc) - } else { - None - }; - - let this = Self { - db: Arc::new(db), - live_kit_client, - config, - }; - Ok(Arc::new(this)) - } -} diff --git a/crates/collab2/src/main.rs b/crates/collab2/src/main.rs deleted file mode 100644 index a7167ef630..0000000000 --- a/crates/collab2/src/main.rs +++ /dev/null @@ -1,139 +0,0 @@ -use anyhow::anyhow; -use axum::{routing::get, Extension, Router}; -use collab2::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result}; -use db::Database; -use std::{ - env::args, - net::{SocketAddr, TcpListener}, - path::Path, - sync::Arc, -}; -use tokio::signal::unix::SignalKind; -use tracing_log::LogTracer; -use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer}; -use util::ResultExt; - -const VERSION: &'static str = env!("CARGO_PKG_VERSION"); - -#[tokio::main] -async fn main() -> Result<()> { - if let Err(error) = env::load_dotenv() { - eprintln!( - "error loading .env.toml (this is expected in production): {}", - error - ); - } - - match args().skip(1).next().as_deref() { - Some("version") => { - println!("collab v{VERSION}"); - } - Some("migrate") => { - let config = envy::from_env::().expect("error loading config"); - let mut db_options = db::ConnectOptions::new(config.database_url.clone()); - db_options.max_connections(5); - let db = Database::new(db_options, Executor::Production).await?; - - let migrations_path = config - .migrations_path - .as_deref() - .unwrap_or_else(|| Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"))); - - let migrations = db.migrate(&migrations_path, false).await?; - for (migration, duration) in migrations { - println!( - "Ran {} {} {:?}", - migration.version, migration.description, duration - ); - } - - return Ok(()); - } - Some("serve") => { - let config = envy::from_env::().expect("error loading config"); - init_tracing(&config); - - let state = AppState::new(config).await?; - - let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port)) - .expect("failed to bind TCP listener"); - - let epoch = state - .db - .create_server(&state.config.zed_environment) - .await?; - let rpc_server = collab2::rpc::Server::new(epoch, state.clone(), Executor::Production); - rpc_server.start().await?; - - let app = collab2::api::routes(rpc_server.clone(), state.clone()) - .merge(collab2::rpc::routes(rpc_server.clone())) - .merge( - Router::new() - .route("/", get(handle_root)) - .route("/healthz", get(handle_liveness_probe)) - .layer(Extension(state.clone())), - ); - - axum::Server::from_tcp(listener)? - .serve(app.into_make_service_with_connect_info::()) - .with_graceful_shutdown(async move { - let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate()) - .expect("failed to listen for interrupt signal"); - let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt()) - .expect("failed to listen for interrupt signal"); - let sigterm = sigterm.recv(); - let sigint = sigint.recv(); - futures::pin_mut!(sigterm, sigint); - futures::future::select(sigterm, sigint).await; - tracing::info!("Received interrupt signal"); - rpc_server.teardown(); - }) - .await?; - } - _ => { - Err(anyhow!("usage: collab "))?; - } - } - Ok(()) -} - -async fn handle_root() -> String { - format!("collab v{VERSION}") -} - -async fn handle_liveness_probe(Extension(state): Extension>) -> Result { - state.db.get_all_users(0, 1).await?; - Ok("ok".to_string()) -} - -pub fn init_tracing(config: &Config) -> Option<()> { - use std::str::FromStr; - use tracing_subscriber::layer::SubscriberExt; - let rust_log = config.rust_log.clone()?; - - LogTracer::init().log_err()?; - - let subscriber = tracing_subscriber::Registry::default() - .with(if config.log_json.unwrap_or(false) { - Box::new( - tracing_subscriber::fmt::layer() - .fmt_fields(JsonFields::default()) - .event_format( - tracing_subscriber::fmt::format() - .json() - .flatten_event(true) - .with_span_list(true), - ), - ) as Box + Send + Sync> - } else { - Box::new( - tracing_subscriber::fmt::layer() - .event_format(tracing_subscriber::fmt::format().pretty()), - ) - }) - .with(EnvFilter::from_str(rust_log.as_str()).log_err()?); - - tracing::subscriber::set_global_default(subscriber).unwrap(); - - None -} diff --git a/crates/collab2/src/rpc.rs b/crates/collab2/src/rpc.rs deleted file mode 100644 index 835b48809d..0000000000 --- a/crates/collab2/src/rpc.rs +++ /dev/null @@ -1,3495 +0,0 @@ -mod connection_pool; - -use crate::{ - auth, - db::{ - self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult, - CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, - MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult, - RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult, - User, UserId, - }, - executor::Executor, - AppState, Result, -}; -use anyhow::anyhow; -use async_tungstenite::tungstenite::{ - protocol::CloseFrame as TungsteniteCloseFrame, Message as TungsteniteMessage, -}; -use axum::{ - body::Body, - extract::{ - ws::{CloseFrame as AxumCloseFrame, Message as AxumMessage}, - ConnectInfo, WebSocketUpgrade, - }, - headers::{Header, HeaderName}, - http::StatusCode, - middleware, - response::IntoResponse, - routing::get, - Extension, Router, TypedHeader, -}; -use collections::{HashMap, HashSet}; -pub use connection_pool::ConnectionPool; -use futures::{ - channel::oneshot, - future::{self, BoxFuture}, - stream::FuturesUnordered, - FutureExt, SinkExt, StreamExt, TryStreamExt, -}; -use lazy_static::lazy_static; -use prometheus::{register_int_gauge, IntGauge}; -use rpc::{ - proto::{ - self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, UpdateChannelBufferCollaborators, - }, - Connection, ConnectionId, Peer, Receipt, TypedEnvelope, -}; -use serde::{Serialize, Serializer}; -use std::{ - any::TypeId, - fmt, - future::Future, - marker::PhantomData, - mem, - net::SocketAddr, - ops::{Deref, DerefMut}, - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, - Arc, - }, - time::{Duration, Instant}, -}; -use time::OffsetDateTime; -use tokio::sync::{watch, Semaphore}; -use tower::ServiceBuilder; -use tracing::{info_span, instrument, Instrument}; -use util::channel::RELEASE_CHANNEL_NAME; - -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; -const NOTIFICATION_COUNT_PER_PAGE: usize = 50; - -lazy_static! { - static ref METRIC_CONNECTIONS: IntGauge = - register_int_gauge!("connections", "number of connections").unwrap(); - static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!( - "shared_projects", - "number of open projects with one or more guests" - ) - .unwrap(); -} - -type MessageHandler = - Box, Session) -> BoxFuture<'static, ()>>; - -struct Response { - peer: Arc, - receipt: Receipt, - responded: Arc, -} - -impl Response { - fn send(self, payload: R::Response) -> Result<()> { - self.responded.store(true, SeqCst); - self.peer.respond(self.receipt, payload)?; - Ok(()) - } -} - -#[derive(Clone)] -struct Session { - user_id: UserId, - connection_id: ConnectionId, - db: Arc>, - peer: Arc, - connection_pool: Arc>, - live_kit_client: Option>, - _executor: Executor, -} - -impl Session { - async fn db(&self) -> tokio::sync::MutexGuard { - #[cfg(test)] - tokio::task::yield_now().await; - let guard = self.db.lock().await; - #[cfg(test)] - tokio::task::yield_now().await; - guard - } - - async fn connection_pool(&self) -> ConnectionPoolGuard<'_> { - #[cfg(test)] - tokio::task::yield_now().await; - let guard = self.connection_pool.lock(); - ConnectionPoolGuard { - guard, - _not_send: PhantomData, - } - } -} - -impl fmt::Debug for Session { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Session") - .field("user_id", &self.user_id) - .field("connection_id", &self.connection_id) - .finish() - } -} - -struct DbHandle(Arc); - -impl Deref for DbHandle { - type Target = Database; - - fn deref(&self) -> &Self::Target { - self.0.as_ref() - } -} - -pub struct Server { - id: parking_lot::Mutex, - peer: Arc, - pub(crate) connection_pool: Arc>, - app_state: Arc, - executor: Executor, - handlers: HashMap, - teardown: watch::Sender<()>, -} - -pub(crate) struct ConnectionPoolGuard<'a> { - guard: parking_lot::MutexGuard<'a, ConnectionPool>, - _not_send: PhantomData>, -} - -#[derive(Serialize)] -pub struct ServerSnapshot<'a> { - peer: &'a Peer, - #[serde(serialize_with = "serialize_deref")] - connection_pool: ConnectionPoolGuard<'a>, -} - -pub fn serialize_deref(value: &T, serializer: S) -> Result -where - S: Serializer, - T: Deref, - U: Serialize, -{ - Serialize::serialize(value.deref(), serializer) -} - -impl Server { - pub fn new(id: ServerId, app_state: Arc, executor: Executor) -> Arc { - let mut server = Self { - id: parking_lot::Mutex::new(id), - peer: Peer::new(id.0 as u32), - app_state, - executor, - connection_pool: Default::default(), - handlers: Default::default(), - teardown: watch::channel(()).0, - }; - - server - .add_request_handler(ping) - .add_request_handler(create_room) - .add_request_handler(join_room) - .add_request_handler(rejoin_room) - .add_request_handler(leave_room) - .add_request_handler(call) - .add_request_handler(cancel_call) - .add_message_handler(decline_call) - .add_request_handler(update_participant_location) - .add_request_handler(share_project) - .add_message_handler(unshare_project) - .add_request_handler(join_project) - .add_message_handler(leave_project) - .add_request_handler(update_project) - .add_request_handler(update_worktree) - .add_message_handler(start_language_server) - .add_message_handler(update_language_server) - .add_message_handler(update_diagnostic_summary) - .add_message_handler(update_worktree_settings) - .add_message_handler(refresh_inlay_hints) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_message_handler(create_buffer_for_peer) - .add_request_handler(update_buffer) - .add_message_handler(update_buffer_file) - .add_message_handler(buffer_reloaded) - .add_message_handler(buffer_saved) - .add_request_handler(forward_project_request::) - .add_request_handler(get_users) - .add_request_handler(fuzzy_search_users) - .add_request_handler(request_contact) - .add_request_handler(remove_contact) - .add_request_handler(respond_to_contact_request) - .add_request_handler(create_channel) - .add_request_handler(delete_channel) - .add_request_handler(invite_channel_member) - .add_request_handler(remove_channel_member) - .add_request_handler(set_channel_member_role) - .add_request_handler(set_channel_visibility) - .add_request_handler(rename_channel) - .add_request_handler(join_channel_buffer) - .add_request_handler(leave_channel_buffer) - .add_message_handler(update_channel_buffer) - .add_request_handler(rejoin_channel_buffers) - .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(get_channel_messages_by_id) - .add_request_handler(get_notifications) - .add_request_handler(mark_notification_as_read) - .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_message_handler(acknowledge_channel_message) - .add_message_handler(acknowledge_buffer_version); - - Arc::new(server) - } - - pub async fn start(&self) -> Result<()> { - let server_id = *self.id.lock(); - let app_state = self.app_state.clone(); - let peer = self.peer.clone(); - let timeout = self.executor.sleep(CLEANUP_TIMEOUT); - let pool = self.connection_pool.clone(); - let live_kit_client = self.app_state.live_kit_client.clone(); - - let span = info_span!("start server"); - self.executor.spawn_detached( - async move { - tracing::info!("waiting for cleanup timeout"); - timeout.await; - tracing::info!("cleanup timeout expired, retrieving stale rooms"); - if let Some((room_ids, channel_ids)) = app_state - .db - .stale_server_resource_ids(&app_state.config.zed_environment, server_id) - .await - .trace_err() - { - tracing::info!(stale_room_count = room_ids.len(), "retrieved stale rooms"); - tracing::info!( - stale_channel_buffer_count = channel_ids.len(), - "retrieved stale channel buffers" - ); - - for channel_id in channel_ids { - if let Some(refreshed_channel_buffer) = app_state - .db - .clear_stale_channel_buffer_collaborators(channel_id, server_id) - .await - .trace_err() - { - for connection_id in refreshed_channel_buffer.connection_ids { - peer.send( - connection_id, - proto::UpdateChannelBufferCollaborators { - channel_id: channel_id.to_proto(), - collaborators: refreshed_channel_buffer - .collaborators - .clone(), - }, - ) - .trace_err(); - } - } - } - - for room_id in room_ids { - let mut contacts_to_update = HashSet::default(); - let mut canceled_calls_to_user_ids = Vec::new(); - let mut live_kit_room = String::new(); - let mut delete_live_kit_room = false; - - if let Some(mut refreshed_room) = app_state - .db - .clear_stale_room_participants(room_id, server_id) - .await - .trace_err() - { - tracing::info!( - room_id = room_id.0, - new_participant_count = refreshed_room.room.participants.len(), - "refreshed room" - ); - room_updated(&refreshed_room.room, &peer); - if let Some(channel_id) = refreshed_room.channel_id { - channel_updated( - channel_id, - &refreshed_room.room, - &refreshed_room.channel_members, - &peer, - &*pool.lock(), - ); - } - contacts_to_update - .extend(refreshed_room.stale_participant_user_ids.iter().copied()); - contacts_to_update - .extend(refreshed_room.canceled_calls_to_user_ids.iter().copied()); - canceled_calls_to_user_ids = - mem::take(&mut refreshed_room.canceled_calls_to_user_ids); - live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room); - delete_live_kit_room = refreshed_room.room.participants.is_empty(); - } - - { - let pool = pool.lock(); - for canceled_user_id in canceled_calls_to_user_ids { - for connection_id in pool.user_connection_ids(canceled_user_id) { - peer.send( - connection_id, - proto::CallCanceled { - room_id: room_id.to_proto(), - }, - ) - .trace_err(); - } - } - } - - for user_id in contacts_to_update { - let busy = app_state.db.is_user_busy(user_id).await.trace_err(); - let contacts = app_state.db.get_contacts(user_id).await.trace_err(); - if let Some((busy, contacts)) = busy.zip(contacts) { - let pool = pool.lock(); - let updated_contact = contact_for_user(user_id, busy, &pool); - for contact in contacts { - if let db::Contact::Accepted { - user_id: contact_user_id, - .. - } = contact - { - for contact_conn_id in - pool.user_connection_ids(contact_user_id) - { - peer.send( - contact_conn_id, - proto::UpdateContacts { - contacts: vec![updated_contact.clone()], - remove_contacts: Default::default(), - incoming_requests: Default::default(), - remove_incoming_requests: Default::default(), - outgoing_requests: Default::default(), - remove_outgoing_requests: Default::default(), - }, - ) - .trace_err(); - } - } - } - } - } - - if let Some(live_kit) = live_kit_client.as_ref() { - if delete_live_kit_room { - live_kit.delete_room(live_kit_room).await.trace_err(); - } - } - } - } - - app_state - .db - .delete_stale_servers(&app_state.config.zed_environment, server_id) - .await - .trace_err(); - } - .instrument(span), - ); - Ok(()) - } - - pub fn teardown(&self) { - self.peer.teardown(); - self.connection_pool.lock().reset(); - let _ = self.teardown.send(()); - } - - #[cfg(test)] - pub fn reset(&self, id: ServerId) { - self.teardown(); - *self.id.lock() = id; - self.peer.reset(id.0 as u32); - } - - #[cfg(test)] - pub fn id(&self) -> ServerId { - *self.id.lock() - } - - fn add_handler(&mut self, handler: F) -> &mut Self - where - F: 'static + Send + Sync + Fn(TypedEnvelope, Session) -> Fut, - Fut: 'static + Send + Future>, - M: EnvelopedMessage, - { - let prev_handler = self.handlers.insert( - TypeId::of::(), - Box::new(move |envelope, session| { - let envelope = envelope.into_any().downcast::>().unwrap(); - let span = info_span!( - "handle message", - payload_type = envelope.payload_type_name() - ); - span.in_scope(|| { - tracing::info!( - payload_type = envelope.payload_type_name(), - "message received" - ); - }); - let start_time = Instant::now(); - let future = (handler)(*envelope, session); - async move { - let result = future.await; - let duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0; - match result { - Err(error) => { - tracing::error!(%error, ?duration_ms, "error handling message") - } - Ok(()) => tracing::info!(?duration_ms, "finished handling message"), - } - } - .instrument(span) - .boxed() - }), - ); - if prev_handler.is_some() { - panic!("registered a handler for the same message twice"); - } - self - } - - fn add_message_handler(&mut self, handler: F) -> &mut Self - where - F: 'static + Send + Sync + Fn(M, Session) -> Fut, - Fut: 'static + Send + Future>, - M: EnvelopedMessage, - { - self.add_handler(move |envelope, session| handler(envelope.payload, session)); - self - } - - fn add_request_handler(&mut self, handler: F) -> &mut Self - where - F: 'static + Send + Sync + Fn(M, Response, Session) -> Fut, - Fut: Send + Future>, - M: RequestMessage, - { - let handler = Arc::new(handler); - self.add_handler(move |envelope, session| { - let receipt = envelope.receipt(); - let handler = handler.clone(); - async move { - let peer = session.peer.clone(); - let responded = Arc::new(AtomicBool::default()); - let response = Response { - peer: peer.clone(), - responded: responded.clone(), - receipt, - }; - match (handler)(envelope.payload, response, session).await { - Ok(()) => { - if responded.load(std::sync::atomic::Ordering::SeqCst) { - Ok(()) - } else { - Err(anyhow!("handler did not send a response"))? - } - } - Err(error) => { - peer.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } - }) - } - - pub fn handle_connection( - self: &Arc, - connection: Connection, - address: String, - user: User, - mut send_connection_id: Option>, - executor: Executor, - ) -> impl Future> { - let this = self.clone(); - let user_id = user.id; - let login = user.github_login; - let span = info_span!("handle connection", %user_id, %login, %address); - let mut teardown = self.teardown.subscribe(); - async move { - let (connection_id, handle_io, mut incoming_rx) = this - .peer - .add_connection(connection, { - let executor = executor.clone(); - move |duration| executor.sleep(duration) - }); - - tracing::info!(%user_id, %login, %connection_id, %address, "connection opened"); - this.peer.send(connection_id, proto::Hello { peer_id: Some(connection_id.into()) })?; - tracing::info!(%user_id, %login, %connection_id, %address, "sent hello message"); - - if let Some(send_connection_id) = send_connection_id.take() { - let _ = send_connection_id.send(connection_id); - } - - if !user.connected_once { - this.peer.send(connection_id, proto::ShowContacts {})?; - this.app_state.db.set_user_connected_once(user_id, true).await?; - } - - let (contacts, channels_for_user, channel_invites) = future::try_join3( - this.app_state.db.get_contacts(user_id), - this.app_state.db.get_channels_for_user(user_id), - this.app_state.db.get_channel_invites_for_user(user_id), - ).await?; - - { - let mut pool = this.connection_pool.lock(); - pool.add_connection(connection_id, user_id, user.admin); - this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; - this.peer.send(connection_id, build_channels_update( - channels_for_user, - channel_invites - ))?; - } - - if let Some(incoming_call) = this.app_state.db.incoming_call_for_user(user_id).await? { - this.peer.send(connection_id, incoming_call)?; - } - - let session = Session { - user_id, - connection_id, - db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))), - peer: this.peer.clone(), - connection_pool: this.connection_pool.clone(), - live_kit_client: this.app_state.live_kit_client.clone(), - _executor: executor.clone() - }; - update_user_contacts(user_id, &session).await?; - - let handle_io = handle_io.fuse(); - futures::pin_mut!(handle_io); - - // Handlers for foreground messages are pushed into the following `FuturesUnordered`. - // This prevents deadlocks when e.g., client A performs a request to client B and - // client B performs a request to client A. If both clients stop processing further - // messages until their respective request completes, they won't have a chance to - // respond to the other client's request and cause a deadlock. - // - // This arrangement ensures we will attempt to process earlier messages first, but fall - // back to processing messages arrived later in the spirit of making progress. - let mut foreground_message_handlers = FuturesUnordered::new(); - let concurrent_handlers = Arc::new(Semaphore::new(256)); - loop { - let next_message = async { - let permit = concurrent_handlers.clone().acquire_owned().await.unwrap(); - let message = incoming_rx.next().await; - (permit, message) - }.fuse(); - futures::pin_mut!(next_message); - futures::select_biased! { - _ = teardown.changed().fuse() => return Ok(()), - result = handle_io => { - if let Err(error) = result { - tracing::error!(?error, %user_id, %login, %connection_id, %address, "error handling I/O"); - } - break; - } - _ = foreground_message_handlers.next() => {} - next_message = next_message => { - let (permit, message) = next_message; - if let Some(message) = message { - let type_name = message.payload_type_name(); - let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name); - let span_enter = span.enter(); - if let Some(handler) = this.handlers.get(&message.payload_type_id()) { - let is_background = message.is_background(); - let handle_message = (handler)(message, session.clone()); - drop(span_enter); - - let handle_message = async move { - handle_message.await; - drop(permit); - }.instrument(span); - if is_background { - executor.spawn_detached(handle_message); - } else { - foreground_message_handlers.push(handle_message); - } - } else { - tracing::error!(%user_id, %login, %connection_id, %address, "no message handler"); - } - } else { - tracing::info!(%user_id, %login, %connection_id, %address, "connection closed"); - break; - } - } - } - } - - drop(foreground_message_handlers); - tracing::info!(%user_id, %login, %connection_id, %address, "signing out"); - if let Err(error) = connection_lost(session, teardown, executor).await { - tracing::error!(%user_id, %login, %connection_id, %address, ?error, "error signing out"); - } - - Ok(()) - }.instrument(span) - } - - pub async fn invite_code_redeemed( - self: &Arc, - inviter_id: UserId, - invitee_id: UserId, - ) -> Result<()> { - if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? { - if let Some(code) = &user.invite_code { - let pool = self.connection_pool.lock(); - let invitee_contact = contact_for_user(invitee_id, false, &pool); - for connection_id in pool.user_connection_ids(inviter_id) { - self.peer.send( - connection_id, - proto::UpdateContacts { - contacts: vec![invitee_contact.clone()], - ..Default::default() - }, - )?; - self.peer.send( - connection_id, - proto::UpdateInviteInfo { - url: format!("{}{}", self.app_state.config.invite_link_prefix, &code), - count: user.invite_count as u32, - }, - )?; - } - } - } - Ok(()) - } - - pub async fn invite_count_updated(self: &Arc, user_id: UserId) -> Result<()> { - if let Some(user) = self.app_state.db.get_user_by_id(user_id).await? { - if let Some(invite_code) = &user.invite_code { - let pool = self.connection_pool.lock(); - for connection_id in pool.user_connection_ids(user_id) { - self.peer.send( - connection_id, - proto::UpdateInviteInfo { - url: format!( - "{}{}", - self.app_state.config.invite_link_prefix, invite_code - ), - count: user.invite_count as u32, - }, - )?; - } - } - } - Ok(()) - } - - pub async fn snapshot<'a>(self: &'a Arc) -> ServerSnapshot<'a> { - ServerSnapshot { - connection_pool: ConnectionPoolGuard { - guard: self.connection_pool.lock(), - _not_send: PhantomData, - }, - peer: &self.peer, - } - } -} - -impl<'a> Deref for ConnectionPoolGuard<'a> { - type Target = ConnectionPool; - - fn deref(&self) -> &Self::Target { - &*self.guard - } -} - -impl<'a> DerefMut for ConnectionPoolGuard<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut *self.guard - } -} - -impl<'a> Drop for ConnectionPoolGuard<'a> { - fn drop(&mut self) { - #[cfg(test)] - self.check_invariants(); - } -} - -fn broadcast( - sender_id: Option, - receiver_ids: impl IntoIterator, - mut f: F, -) where - F: FnMut(ConnectionId) -> anyhow::Result<()>, -{ - for receiver_id in receiver_ids { - if Some(receiver_id) != sender_id { - if let Err(error) = f(receiver_id) { - tracing::error!("failed to send to {:?} {}", receiver_id, error); - } - } - } -} - -lazy_static! { - static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version"); -} - -pub struct ProtocolVersion(u32); - -impl Header for ProtocolVersion { - fn name() -> &'static HeaderName { - &ZED_PROTOCOL_VERSION - } - - fn decode<'i, I>(values: &mut I) -> Result - where - Self: Sized, - I: Iterator, - { - let version = values - .next() - .ok_or_else(axum::headers::Error::invalid)? - .to_str() - .map_err(|_| axum::headers::Error::invalid())? - .parse() - .map_err(|_| axum::headers::Error::invalid())?; - Ok(Self(version)) - } - - fn encode>(&self, values: &mut E) { - values.extend([self.0.to_string().parse().unwrap()]); - } -} - -pub fn routes(server: Arc) -> Router { - Router::new() - .route("/rpc", get(handle_websocket_request)) - .layer( - ServiceBuilder::new() - .layer(Extension(server.app_state.clone())) - .layer(middleware::from_fn(auth::validate_header)), - ) - .route("/metrics", get(handle_metrics)) - .layer(Extension(server)) -} - -pub async fn handle_websocket_request( - TypedHeader(ProtocolVersion(protocol_version)): TypedHeader, - ConnectInfo(socket_address): ConnectInfo, - Extension(server): Extension>, - Extension(user): Extension, - ws: WebSocketUpgrade, -) -> axum::response::Response { - if protocol_version != rpc::PROTOCOL_VERSION { - return ( - StatusCode::UPGRADE_REQUIRED, - "client must be upgraded".to_string(), - ) - .into_response(); - } - let socket_address = socket_address.to_string(); - ws.on_upgrade(move |socket| { - use util::ResultExt; - let socket = socket - .map_ok(to_tungstenite_message) - .err_into() - .with(|message| async move { Ok(to_axum_message(message)) }); - let connection = Connection::new(Box::pin(socket)); - async move { - server - .handle_connection(connection, socket_address, user, None, Executor::Production) - .await - .log_err(); - } - }) -} - -pub async fn handle_metrics(Extension(server): Extension>) -> Result { - let connections = server - .connection_pool - .lock() - .connections() - .filter(|connection| !connection.admin) - .count(); - - METRIC_CONNECTIONS.set(connections as _); - - let shared_projects = server.app_state.db.project_count_excluding_admins().await?; - METRIC_SHARED_PROJECTS.set(shared_projects as _); - - let encoder = prometheus::TextEncoder::new(); - let metric_families = prometheus::gather(); - let encoded_metrics = encoder - .encode_to_string(&metric_families) - .map_err(|err| anyhow!("{}", err))?; - Ok(encoded_metrics) -} - -#[instrument(err, skip(executor))] -async fn connection_lost( - session: Session, - mut teardown: watch::Receiver<()>, - executor: Executor, -) -> Result<()> { - session.peer.disconnect(session.connection_id); - session - .connection_pool() - .await - .remove_connection(session.connection_id)?; - - session - .db() - .await - .connection_lost(session.connection_id) - .await - .trace_err(); - - futures::select_biased! { - _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { - log::info!("connection lost, removing all resources for user:{}, connection:{:?}", session.user_id, session.connection_id); - leave_room_for_session(&session).await.trace_err(); - leave_channel_buffers_for_session(&session) - .await - .trace_err(); - - if !session - .connection_pool() - .await - .is_user_online(session.user_id) - { - let db = session.db().await; - if let Some(room) = db.decline_call(None, session.user_id).await.trace_err().flatten() { - room_updated(&room, &session.peer); - } - } - - update_user_contacts(session.user_id, &session).await?; - } - _ = teardown.changed().fuse() => {} - } - - Ok(()) -} - -async fn ping(_: proto::Ping, response: Response, _session: Session) -> Result<()> { - response.send(proto::Ack {})?; - Ok(()) -} - -async fn create_room( - _request: proto::CreateRoom, - response: Response, - session: Session, -) -> Result<()> { - let live_kit_room = nanoid::nanoid!(30); - - let live_kit_connection_info = { - let live_kit_room = live_kit_room.clone(); - let live_kit = session.live_kit_client.as_ref(); - - util::async_maybe!({ - let live_kit = live_kit?; - - let token = live_kit - .room_token(&live_kit_room, &session.user_id.to_string()) - .trace_err()?; - - Some(proto::LiveKitConnectionInfo { - server_url: live_kit.url().into(), - token, - can_publish: true, - }) - }) - } - .await; - - let room = session - .db() - .await - .create_room( - session.user_id, - session.connection_id, - &live_kit_room, - RELEASE_CHANNEL_NAME.as_str(), - ) - .await?; - - response.send(proto::CreateRoomResponse { - room: Some(room.clone()), - live_kit_connection_info, - })?; - - update_user_contacts(session.user_id, &session).await?; - Ok(()) -} - -async fn join_room( - request: proto::JoinRoom, - response: Response, - session: Session, -) -> Result<()> { - let room_id = RoomId::from_proto(request.id); - - let channel_id = session.db().await.channel_id_for_room(room_id).await?; - - if let Some(channel_id) = channel_id { - return join_channel_internal(channel_id, Box::new(response), session).await; - } - - let joined_room = { - let room = session - .db() - .await - .join_room( - room_id, - session.user_id, - session.connection_id, - RELEASE_CHANNEL_NAME.as_str(), - ) - .await?; - room_updated(&room.room, &session.peer); - room.into_inner() - }; - - for connection_id in session - .connection_pool() - .await - .user_connection_ids(session.user_id) - { - session - .peer - .send( - connection_id, - proto::CallCanceled { - room_id: room_id.to_proto(), - }, - ) - .trace_err(); - } - - let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() { - if let Some(token) = live_kit - .room_token( - &joined_room.room.live_kit_room, - &session.user_id.to_string(), - ) - .trace_err() - { - Some(proto::LiveKitConnectionInfo { - server_url: live_kit.url().into(), - token, - can_publish: true, - }) - } else { - None - } - } else { - None - }; - - response.send(proto::JoinRoomResponse { - room: Some(joined_room.room), - channel_id: None, - live_kit_connection_info, - })?; - - update_user_contacts(session.user_id, &session).await?; - Ok(()) -} - -async fn rejoin_room( - request: proto::RejoinRoom, - response: Response, - session: Session, -) -> Result<()> { - let room; - let channel_id; - let channel_members; - { - let mut rejoined_room = session - .db() - .await - .rejoin_room(request, session.user_id, session.connection_id) - .await?; - - response.send(proto::RejoinRoomResponse { - room: Some(rejoined_room.room.clone()), - reshared_projects: rejoined_room - .reshared_projects - .iter() - .map(|project| proto::ResharedProject { - id: project.id.to_proto(), - collaborators: project - .collaborators - .iter() - .map(|collaborator| collaborator.to_proto()) - .collect(), - }) - .collect(), - rejoined_projects: rejoined_room - .rejoined_projects - .iter() - .map(|rejoined_project| proto::RejoinedProject { - id: rejoined_project.id.to_proto(), - worktrees: rejoined_project - .worktrees - .iter() - .map(|worktree| proto::WorktreeMetadata { - id: worktree.id, - root_name: worktree.root_name.clone(), - visible: worktree.visible, - abs_path: worktree.abs_path.clone(), - }) - .collect(), - collaborators: rejoined_project - .collaborators - .iter() - .map(|collaborator| collaborator.to_proto()) - .collect(), - language_servers: rejoined_project.language_servers.clone(), - }) - .collect(), - })?; - room_updated(&rejoined_room.room, &session.peer); - - for project in &rejoined_room.reshared_projects { - for collaborator in &project.collaborators { - session - .peer - .send( - collaborator.connection_id, - proto::UpdateProjectCollaborator { - project_id: project.id.to_proto(), - old_peer_id: Some(project.old_connection_id.into()), - new_peer_id: Some(session.connection_id.into()), - }, - ) - .trace_err(); - } - - broadcast( - Some(session.connection_id), - project - .collaborators - .iter() - .map(|collaborator| collaborator.connection_id), - |connection_id| { - session.peer.forward_send( - session.connection_id, - connection_id, - proto::UpdateProject { - project_id: project.id.to_proto(), - worktrees: project.worktrees.clone(), - }, - ) - }, - ); - } - - for project in &rejoined_room.rejoined_projects { - for collaborator in &project.collaborators { - session - .peer - .send( - collaborator.connection_id, - proto::UpdateProjectCollaborator { - project_id: project.id.to_proto(), - old_peer_id: Some(project.old_connection_id.into()), - new_peer_id: Some(session.connection_id.into()), - }, - ) - .trace_err(); - } - } - - for project in &mut rejoined_room.rejoined_projects { - for worktree in mem::take(&mut project.worktrees) { - #[cfg(any(test, feature = "test-support"))] - const MAX_CHUNK_SIZE: usize = 2; - #[cfg(not(any(test, feature = "test-support")))] - const MAX_CHUNK_SIZE: usize = 256; - - // Stream this worktree's entries. - let message = proto::UpdateWorktree { - project_id: project.id.to_proto(), - worktree_id: worktree.id, - abs_path: worktree.abs_path.clone(), - root_name: worktree.root_name, - updated_entries: worktree.updated_entries, - removed_entries: worktree.removed_entries, - scan_id: worktree.scan_id, - is_last_update: worktree.completed_scan_id == worktree.scan_id, - updated_repositories: worktree.updated_repositories, - removed_repositories: worktree.removed_repositories, - }; - for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { - session.peer.send(session.connection_id, update.clone())?; - } - - // Stream this worktree's diagnostics. - for summary in worktree.diagnostic_summaries { - session.peer.send( - session.connection_id, - proto::UpdateDiagnosticSummary { - project_id: project.id.to_proto(), - worktree_id: worktree.id, - summary: Some(summary), - }, - )?; - } - - for settings_file in worktree.settings_files { - session.peer.send( - session.connection_id, - proto::UpdateWorktreeSettings { - project_id: project.id.to_proto(), - worktree_id: worktree.id, - path: settings_file.path, - content: Some(settings_file.content), - }, - )?; - } - } - - for language_server in &project.language_servers { - session.peer.send( - session.connection_id, - proto::UpdateLanguageServer { - project_id: project.id.to_proto(), - language_server_id: language_server.id, - variant: Some( - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ), - }, - )?; - } - } - - let rejoined_room = rejoined_room.into_inner(); - - room = rejoined_room.room; - channel_id = rejoined_room.channel_id; - channel_members = rejoined_room.channel_members; - } - - if let Some(channel_id) = channel_id { - channel_updated( - channel_id, - &room, - &channel_members, - &session.peer, - &*session.connection_pool().await, - ); - } - - update_user_contacts(session.user_id, &session).await?; - Ok(()) -} - -async fn leave_room( - _: proto::LeaveRoom, - response: Response, - session: Session, -) -> Result<()> { - leave_room_for_session(&session).await?; - response.send(proto::Ack {})?; - Ok(()) -} - -async fn call( - request: proto::Call, - response: Response, - session: Session, -) -> Result<()> { - let room_id = RoomId::from_proto(request.room_id); - let calling_user_id = session.user_id; - let calling_connection_id = session.connection_id; - let called_user_id = UserId::from_proto(request.called_user_id); - let initial_project_id = request.initial_project_id.map(ProjectId::from_proto); - if !session - .db() - .await - .has_contact(calling_user_id, called_user_id) - .await? - { - return Err(anyhow!("cannot call a user who isn't a contact"))?; - } - - let incoming_call = { - let (room, incoming_call) = &mut *session - .db() - .await - .call( - room_id, - calling_user_id, - calling_connection_id, - called_user_id, - initial_project_id, - ) - .await?; - room_updated(&room, &session.peer); - mem::take(incoming_call) - }; - update_user_contacts(called_user_id, &session).await?; - - let mut calls = session - .connection_pool() - .await - .user_connection_ids(called_user_id) - .map(|connection_id| session.peer.request(connection_id, incoming_call.clone())) - .collect::>(); - - while let Some(call_response) = calls.next().await { - match call_response.as_ref() { - Ok(_) => { - response.send(proto::Ack {})?; - return Ok(()); - } - Err(_) => { - call_response.trace_err(); - } - } - } - - { - let room = session - .db() - .await - .call_failed(room_id, called_user_id) - .await?; - room_updated(&room, &session.peer); - } - update_user_contacts(called_user_id, &session).await?; - - Err(anyhow!("failed to ring user"))? -} - -async fn cancel_call( - request: proto::CancelCall, - response: Response, - session: Session, -) -> Result<()> { - let called_user_id = UserId::from_proto(request.called_user_id); - let room_id = RoomId::from_proto(request.room_id); - { - let room = session - .db() - .await - .cancel_call(room_id, session.connection_id, called_user_id) - .await?; - room_updated(&room, &session.peer); - } - - for connection_id in session - .connection_pool() - .await - .user_connection_ids(called_user_id) - { - session - .peer - .send( - connection_id, - proto::CallCanceled { - room_id: room_id.to_proto(), - }, - ) - .trace_err(); - } - response.send(proto::Ack {})?; - - update_user_contacts(called_user_id, &session).await?; - Ok(()) -} - -async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<()> { - let room_id = RoomId::from_proto(message.room_id); - { - let room = session - .db() - .await - .decline_call(Some(room_id), session.user_id) - .await? - .ok_or_else(|| anyhow!("failed to decline call"))?; - room_updated(&room, &session.peer); - } - - for connection_id in session - .connection_pool() - .await - .user_connection_ids(session.user_id) - { - session - .peer - .send( - connection_id, - proto::CallCanceled { - room_id: room_id.to_proto(), - }, - ) - .trace_err(); - } - update_user_contacts(session.user_id, &session).await?; - Ok(()) -} - -async fn update_participant_location( - request: proto::UpdateParticipantLocation, - response: Response, - session: Session, -) -> Result<()> { - let room_id = RoomId::from_proto(request.room_id); - let location = request - .location - .ok_or_else(|| anyhow!("invalid location"))?; - - let db = session.db().await; - let room = db - .update_room_participant_location(room_id, session.connection_id, location) - .await?; - - room_updated(&room, &session.peer); - response.send(proto::Ack {})?; - Ok(()) -} - -async fn share_project( - request: proto::ShareProject, - response: Response, - session: Session, -) -> Result<()> { - let (project_id, room) = &*session - .db() - .await - .share_project( - RoomId::from_proto(request.room_id), - session.connection_id, - &request.worktrees, - ) - .await?; - response.send(proto::ShareProjectResponse { - project_id: project_id.to_proto(), - })?; - room_updated(&room, &session.peer); - - Ok(()) -} - -async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(message.project_id); - - let (room, guest_connection_ids) = &*session - .db() - .await - .unshare_project(project_id, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |conn_id| session.peer.send(conn_id, message.clone()), - ); - room_updated(&room, &session.peer); - - Ok(()) -} - -async fn join_project( - request: proto::JoinProject, - response: Response, - session: Session, -) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let guest_user_id = session.user_id; - - tracing::info!(%project_id, "join project"); - - let (project, replica_id) = &mut *session - .db() - .await - .join_project(project_id, session.connection_id) - .await?; - - let collaborators = project - .collaborators - .iter() - .filter(|collaborator| collaborator.connection_id != session.connection_id) - .map(|collaborator| collaborator.to_proto()) - .collect::>(); - - let worktrees = project - .worktrees - .iter() - .map(|(id, worktree)| proto::WorktreeMetadata { - id: *id, - root_name: worktree.root_name.clone(), - visible: worktree.visible, - abs_path: worktree.abs_path.clone(), - }) - .collect::>(); - - for collaborator in &collaborators { - session - .peer - .send( - collaborator.peer_id.unwrap().into(), - proto::AddProjectCollaborator { - project_id: project_id.to_proto(), - collaborator: Some(proto::Collaborator { - peer_id: Some(session.connection_id.into()), - replica_id: replica_id.0 as u32, - user_id: guest_user_id.to_proto(), - }), - }, - ) - .trace_err(); - } - - // First, we send the metadata associated with each worktree. - response.send(proto::JoinProjectResponse { - worktrees: worktrees.clone(), - replica_id: replica_id.0 as u32, - collaborators: collaborators.clone(), - language_servers: project.language_servers.clone(), - })?; - - for (worktree_id, worktree) in mem::take(&mut project.worktrees) { - #[cfg(any(test, feature = "test-support"))] - const MAX_CHUNK_SIZE: usize = 2; - #[cfg(not(any(test, feature = "test-support")))] - const MAX_CHUNK_SIZE: usize = 256; - - // Stream this worktree's entries. - let message = proto::UpdateWorktree { - project_id: project_id.to_proto(), - worktree_id, - abs_path: worktree.abs_path.clone(), - root_name: worktree.root_name, - updated_entries: worktree.entries, - removed_entries: Default::default(), - scan_id: worktree.scan_id, - is_last_update: worktree.scan_id == worktree.completed_scan_id, - updated_repositories: worktree.repository_entries.into_values().collect(), - removed_repositories: Default::default(), - }; - for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { - session.peer.send(session.connection_id, update.clone())?; - } - - // Stream this worktree's diagnostics. - for summary in worktree.diagnostic_summaries { - session.peer.send( - session.connection_id, - proto::UpdateDiagnosticSummary { - project_id: project_id.to_proto(), - worktree_id: worktree.id, - summary: Some(summary), - }, - )?; - } - - for settings_file in worktree.settings_files { - session.peer.send( - session.connection_id, - proto::UpdateWorktreeSettings { - project_id: project_id.to_proto(), - worktree_id: worktree.id, - path: settings_file.path, - content: Some(settings_file.content), - }, - )?; - } - } - - for language_server in &project.language_servers { - session.peer.send( - session.connection_id, - proto::UpdateLanguageServer { - project_id: project_id.to_proto(), - language_server_id: language_server.id, - variant: Some( - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ), - }, - )?; - } - - Ok(()) -} - -async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { - let sender_id = session.connection_id; - let project_id = ProjectId::from_proto(request.project_id); - - let (room, project) = &*session - .db() - .await - .leave_project(project_id, sender_id) - .await?; - tracing::info!( - %project_id, - host_user_id = %project.host_user_id, - host_connection_id = %project.host_connection_id, - "leave project" - ); - - project_left(&project, &session); - room_updated(&room, &session.peer); - - Ok(()) -} - -async fn update_project( - request: proto::UpdateProject, - response: Response, - session: Session, -) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let (room, guest_connection_ids) = &*session - .db() - .await - .update_project(project_id, session.connection_id, &request.worktrees) - .await?; - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - room_updated(&room, &session.peer); - response.send(proto::Ack {})?; - - Ok(()) -} - -async fn update_worktree( - request: proto::UpdateWorktree, - response: Response, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .update_worktree(&request, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - response.send(proto::Ack {})?; - Ok(()) -} - -async fn update_diagnostic_summary( - message: proto::UpdateDiagnosticSummary, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .update_diagnostic_summary(&message, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, message.clone()) - }, - ); - - Ok(()) -} - -async fn update_worktree_settings( - message: proto::UpdateWorktreeSettings, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .update_worktree_settings(&message, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, message.clone()) - }, - ); - - Ok(()) -} - -async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> { - broadcast_project_message(request.project_id, request, session).await -} - -async fn start_language_server( - request: proto::StartLanguageServer, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .start_language_server(&request, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn update_language_server( - request: proto::UpdateLanguageServer, - session: Session, -) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn forward_project_request( - request: T, - response: Response, - session: Session, -) -> Result<()> -where - T: EntityMessage + RequestMessage, -{ - let project_id = ProjectId::from_proto(request.remote_entity_id()); - let host_connection_id = { - let collaborators = session - .db() - .await - .project_collaborators(project_id, session.connection_id) - .await?; - collaborators - .iter() - .find(|collaborator| collaborator.is_host) - .ok_or_else(|| anyhow!("host not found"))? - .connection_id - }; - - let payload = session - .peer - .forward_request(session.connection_id, host_connection_id, request) - .await?; - - response.send(payload)?; - Ok(()) -} - -async fn create_buffer_for_peer( - request: proto::CreateBufferForPeer, - session: Session, -) -> Result<()> { - let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?; - session - .peer - .forward_send(session.connection_id, peer_id.into(), request)?; - Ok(()) -} - -async fn update_buffer( - request: proto::UpdateBuffer, - response: Response, - session: Session, -) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let mut guest_connection_ids; - let mut host_connection_id = None; - { - let collaborators = session - .db() - .await - .project_collaborators(project_id, session.connection_id) - .await?; - guest_connection_ids = Vec::with_capacity(collaborators.len() - 1); - for collaborator in collaborators.iter() { - if collaborator.is_host { - host_connection_id = Some(collaborator.connection_id); - } else { - guest_connection_ids.push(collaborator.connection_id); - } - } - } - let host_connection_id = host_connection_id.ok_or_else(|| anyhow!("host not found"))?; - - broadcast( - Some(session.connection_id), - guest_connection_ids, - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - if host_connection_id != session.connection_id { - session - .peer - .forward_request(session.connection_id, host_connection_id, request.clone()) - .await?; - } - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> { - broadcast_project_message(request.project_id, request, session).await -} - -async fn broadcast_project_message( - project_id: u64, - request: T, - session: Session, -) -> Result<()> { - let project_id = ProjectId::from_proto(project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn follow( - request: proto::Follow, - response: Response, - session: Session, -) -> Result<()> { - 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; - - session - .db() - .await - .check_room_participants(room_id, leader_id, session.connection_id) - .await?; - - let response_payload = session - .peer - .forward_request(session.connection_id, leader_id, request) - .await?; - response.send(response_payload)?; - - 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 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; - - session - .db() - .await - .check_room_participants(room_id, leader_id, session.connection_id) - .await?; - - session - .peer - .forward_send(session.connection_id, leader_id, request)?; - - 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 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? - }; - - // 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, - _ => None, - }); - - for follower_peer_id in request.follower_ids.iter().copied() { - let follower_connection_id = follower_peer_id.into(); - if Some(follower_peer_id) != connection_id_to_omit - && connection_ids.contains(&follower_connection_id) - { - session.peer.forward_send( - session.connection_id, - follower_connection_id, - request.clone(), - )?; - } - } - Ok(()) -} - -async fn get_users( - request: proto::GetUsers, - response: Response, - session: Session, -) -> Result<()> { - let user_ids = request - .user_ids - .into_iter() - .map(UserId::from_proto) - .collect(); - let users = session - .db() - .await - .get_users_by_ids(user_ids) - .await? - .into_iter() - .map(|user| proto::User { - id: user.id.to_proto(), - avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), - github_login: user.github_login, - }) - .collect(); - response.send(proto::UsersResponse { users })?; - Ok(()) -} - -async fn fuzzy_search_users( - request: proto::FuzzySearchUsers, - response: Response, - session: Session, -) -> Result<()> { - let query = request.query; - let users = match query.len() { - 0 => vec![], - 1 | 2 => session - .db() - .await - .get_user_by_github_login(&query) - .await? - .into_iter() - .collect(), - _ => session.db().await.fuzzy_search_users(&query, 10).await?, - }; - let users = users - .into_iter() - .filter(|user| user.id != session.user_id) - .map(|user| proto::User { - id: user.id.to_proto(), - avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), - github_login: user.github_login, - }) - .collect(); - response.send(proto::UsersResponse { users })?; - Ok(()) -} - -async fn request_contact( - request: proto::RequestContact, - response: Response, - session: Session, -) -> Result<()> { - let requester_id = session.user_id; - let responder_id = UserId::from_proto(request.responder_id); - if requester_id == responder_id { - return Err(anyhow!("cannot add yourself as a contact"))?; - } - - let notifications = session - .db() - .await - .send_contact_request(requester_id, responder_id) - .await?; - - // Update outgoing contact requests of requester - let mut update = proto::UpdateContacts::default(); - update.outgoing_requests.push(responder_id.to_proto()); - for connection_id in session - .connection_pool() - .await - .user_connection_ids(requester_id) - { - session.peer.send(connection_id, update.clone())?; - } - - // Update incoming contact requests of responder - let mut update = proto::UpdateContacts::default(); - update - .incoming_requests - .push(proto::IncomingContactRequest { - requester_id: requester_id.to_proto(), - }); - let connection_pool = session.connection_pool().await; - for connection_id in connection_pool.user_connection_ids(responder_id) { - session.peer.send(connection_id, update.clone())?; - } - - send_notifications(&*connection_pool, &session.peer, notifications); - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn respond_to_contact_request( - request: proto::RespondToContactRequest, - response: Response, - session: Session, -) -> Result<()> { - let responder_id = session.user_id; - let requester_id = UserId::from_proto(request.requester_id); - let db = session.db().await; - if request.response == proto::ContactRequestResponse::Dismiss as i32 { - db.dismiss_contact_notification(responder_id, requester_id) - .await?; - } else { - let accept = request.response == proto::ContactRequestResponse::Accept as i32; - - let notifications = db - .respond_to_contact_request(responder_id, requester_id, accept) - .await?; - let requester_busy = db.is_user_busy(requester_id).await?; - let responder_busy = db.is_user_busy(responder_id).await?; - - let pool = session.connection_pool().await; - // Update responder with new contact - let mut update = proto::UpdateContacts::default(); - if accept { - update - .contacts - .push(contact_for_user(requester_id, requester_busy, &pool)); - } - update - .remove_incoming_requests - .push(requester_id.to_proto()); - for connection_id in pool.user_connection_ids(responder_id) { - session.peer.send(connection_id, update.clone())?; - } - - // Update requester with new contact - let mut update = proto::UpdateContacts::default(); - if accept { - update - .contacts - .push(contact_for_user(responder_id, responder_busy, &pool)); - } - update - .remove_outgoing_requests - .push(responder_id.to_proto()); - - for connection_id in pool.user_connection_ids(requester_id) { - session.peer.send(connection_id, update.clone())?; - } - - send_notifications(&*pool, &session.peer, notifications); - } - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn remove_contact( - request: proto::RemoveContact, - response: Response, - session: Session, -) -> Result<()> { - let requester_id = session.user_id; - let responder_id = UserId::from_proto(request.user_id); - let db = session.db().await; - let (contact_accepted, deleted_notification_id) = - db.remove_contact(requester_id, responder_id).await?; - - let pool = session.connection_pool().await; - // Update outgoing contact requests of requester - let mut update = proto::UpdateContacts::default(); - if contact_accepted { - update.remove_contacts.push(responder_id.to_proto()); - } else { - update - .remove_outgoing_requests - .push(responder_id.to_proto()); - } - for connection_id in pool.user_connection_ids(requester_id) { - session.peer.send(connection_id, update.clone())?; - } - - // Update incoming contact requests of responder - let mut update = proto::UpdateContacts::default(); - if contact_accepted { - update.remove_contacts.push(requester_id.to_proto()); - } else { - update - .remove_incoming_requests - .push(requester_id.to_proto()); - } - for connection_id in pool.user_connection_ids(responder_id) { - session.peer.send(connection_id, update.clone())?; - if let Some(notification_id) = deleted_notification_id { - session.peer.send( - connection_id, - proto::DeleteNotification { - notification_id: notification_id.to_proto(), - }, - )?; - } - } - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn create_channel( - request: proto::CreateChannel, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - - let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); - let CreateChannelResult { - channel, - participants_to_update, - } = db - .create_channel(&request.name, parent_id, session.user_id) - .await?; - - response.send(proto::CreateChannelResponse { - channel: Some(channel.to_proto()), - parent_id: request.parent_id, - })?; - - let connection_pool = session.connection_pool().await; - for (user_id, channels) in participants_to_update { - let update = build_channels_update(channels, vec![]); - for connection_id in connection_pool.user_connection_ids(user_id) { - if user_id == session.user_id { - continue; - } - session.peer.send(connection_id, update.clone())?; - } - } - - Ok(()) -} - -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 - .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 - .delete_channels - .extend(removed_channels.into_iter().map(|id| id.to_proto())); - - let connection_pool = session.connection_pool().await; - for member_id in member_ids { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } - } - - Ok(()) -} - -async fn invite_channel_member( - request: proto::InviteChannelMember, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let invitee_id = UserId::from_proto(request.user_id); - let InviteMemberResult { - channel, - notifications, - } = db - .invite_channel_member( - channel_id, - invitee_id, - session.user_id, - request.role().into(), - ) - .await?; - - let update = proto::UpdateChannels { - channel_invitations: vec![channel.to_proto()], - ..Default::default() - }; - - let connection_pool = session.connection_pool().await; - for connection_id in connection_pool.user_connection_ids(invitee_id) { - session.peer.send(connection_id, update.clone())?; - } - - send_notifications(&*connection_pool, &session.peer, notifications); - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn remove_channel_member( - request: proto::RemoveChannelMember, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let member_id = UserId::from_proto(request.user_id); - - let RemoveChannelMemberResult { - membership_update, - notification_id, - } = db - .remove_channel_member(channel_id, member_id, session.user_id) - .await?; - - let connection_pool = &session.connection_pool().await; - notify_membership_updated( - &connection_pool, - membership_update, - member_id, - &session.peer, - ); - for connection_id in connection_pool.user_connection_ids(member_id) { - if let Some(notification_id) = notification_id { - session - .peer - .send( - connection_id, - proto::DeleteNotification { - notification_id: notification_id.to_proto(), - }, - ) - .trace_err(); - } - } - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn set_channel_visibility( - request: proto::SetChannelVisibility, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let visibility = request.visibility().into(); - - let SetChannelVisibilityResult { - participants_to_update, - participants_to_remove, - channels_to_remove, - } = db - .set_channel_visibility(channel_id, visibility, session.user_id) - .await?; - - let connection_pool = session.connection_pool().await; - for (user_id, channels) in participants_to_update { - let update = build_channels_update(channels, vec![]); - for connection_id in connection_pool.user_connection_ids(user_id) { - session.peer.send(connection_id, update.clone())?; - } - } - for user_id in participants_to_remove { - let update = proto::UpdateChannels { - delete_channels: channels_to_remove.iter().map(|id| id.to_proto()).collect(), - ..Default::default() - }; - for connection_id in connection_pool.user_connection_ids(user_id) { - session.peer.send(connection_id, update.clone())?; - } - } - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn set_channel_member_role( - request: proto::SetChannelMemberRole, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let member_id = UserId::from_proto(request.user_id); - let result = db - .set_channel_member_role( - channel_id, - session.user_id, - member_id, - request.role().into(), - ) - .await?; - - match result { - db::SetMemberRoleResult::MembershipUpdated(membership_update) => { - let connection_pool = session.connection_pool().await; - notify_membership_updated( - &connection_pool, - membership_update, - member_id, - &session.peer, - ) - } - db::SetMemberRoleResult::InviteUpdated(channel) => { - let update = proto::UpdateChannels { - channel_invitations: vec![channel.to_proto()], - ..Default::default() - }; - - for connection_id in session - .connection_pool() - .await - .user_connection_ids(member_id) - { - session.peer.send(connection_id, update.clone())?; - } - } - } - - response.send(proto::Ack {})?; - Ok(()) -} - -async fn rename_channel( - request: proto::RenameChannel, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let RenameChannelResult { - channel, - participants_to_update, - } = db - .rename_channel(channel_id, session.user_id, &request.name) - .await?; - - response.send(proto::RenameChannelResponse { - channel: Some(channel.to_proto()), - })?; - - let connection_pool = session.connection_pool().await; - for (user_id, channel) in participants_to_update { - for connection_id in connection_pool.user_connection_ids(user_id) { - let update = proto::UpdateChannels { - channels: vec![channel.to_proto()], - ..Default::default() - }; - - session.peer.send(connection_id, update.clone())?; - } - } - - Ok(()) -} - -async fn move_channel( - request: proto::MoveChannel, - response: Response, - session: Session, -) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - let to = request.to.map(ChannelId::from_proto); - - let result = session - .db() - .await - .move_channel(channel_id, to, session.user_id) - .await?; - - notify_channel_moved(result, session).await?; - - response.send(Ack {})?; - Ok(()) -} - -async fn notify_channel_moved(result: Option, session: Session) -> Result<()> { - let Some(MoveChannelResult { - participants_to_remove, - participants_to_update, - moved_channels, - }) = result - else { - return Ok(()); - }; - let moved_channels: Vec = moved_channels.iter().map(|id| id.to_proto()).collect(); - - let connection_pool = session.connection_pool().await; - for (user_id, channels) in participants_to_update { - let mut update = build_channels_update(channels, vec![]); - update.delete_channels = moved_channels.clone(); - for connection_id in connection_pool.user_connection_ids(user_id) { - session.peer.send(connection_id, update.clone())?; - } - } - - for user_id in participants_to_remove { - let update = proto::UpdateChannels { - delete_channels: moved_channels.clone(), - ..Default::default() - }; - for connection_id in connection_pool.user_connection_ids(user_id) { - session.peer.send(connection_id, update.clone())?; - } - } - Ok(()) -} - -async fn get_channel_members( - request: proto::GetChannelMembers, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let members = db - .get_channel_participant_details(channel_id, session.user_id) - .await?; - response.send(proto::GetChannelMembersResponse { members })?; - Ok(()) -} - -async fn respond_to_channel_invite( - request: proto::RespondToChannelInvite, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let RespondToChannelInvite { - membership_update, - notifications, - } = db - .respond_to_channel_invite(channel_id, session.user_id, request.accept) - .await?; - - let connection_pool = session.connection_pool().await; - if let Some(membership_update) = membership_update { - notify_membership_updated( - &connection_pool, - membership_update, - session.user_id, - &session.peer, - ); - } else { - let update = proto::UpdateChannels { - remove_channel_invitations: vec![channel_id.to_proto()], - ..Default::default() - }; - - for connection_id in connection_pool.user_connection_ids(session.user_id) { - session.peer.send(connection_id, update.clone())?; - } - }; - - send_notifications(&*connection_pool, &session.peer, notifications); - - response.send(proto::Ack {})?; - - Ok(()) -} - -async fn join_channel( - request: proto::JoinChannel, - response: Response, - session: Session, -) -> Result<()> { - let channel_id = ChannelId::from_proto(request.channel_id); - join_channel_internal(channel_id, Box::new(response), session).await -} - -trait JoinChannelInternalResponse { - fn send(self, result: proto::JoinRoomResponse) -> Result<()>; -} -impl JoinChannelInternalResponse for Response { - fn send(self, result: proto::JoinRoomResponse) -> Result<()> { - Response::::send(self, result) - } -} -impl JoinChannelInternalResponse for Response { - fn send(self, result: proto::JoinRoomResponse) -> Result<()> { - Response::::send(self, result) - } -} - -async fn join_channel_internal( - channel_id: ChannelId, - response: Box, - session: Session, -) -> Result<()> { - let joined_room = { - leave_room_for_session(&session).await?; - let db = session.db().await; - - let (joined_room, membership_updated, role) = db - .join_channel( - channel_id, - session.user_id, - session.connection_id, - RELEASE_CHANNEL_NAME.as_str(), - ) - .await?; - - let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { - let (can_publish, token) = if role == ChannelRole::Guest { - ( - false, - live_kit - .guest_token( - &joined_room.room.live_kit_room, - &session.user_id.to_string(), - ) - .trace_err()?, - ) - } else { - ( - true, - live_kit - .room_token( - &joined_room.room.live_kit_room, - &session.user_id.to_string(), - ) - .trace_err()?, - ) - }; - - Some(LiveKitConnectionInfo { - server_url: live_kit.url().into(), - token, - can_publish, - }) - }); - - response.send(proto::JoinRoomResponse { - room: Some(joined_room.room.clone()), - channel_id: joined_room.channel_id.map(|id| id.to_proto()), - live_kit_connection_info, - })?; - - let connection_pool = session.connection_pool().await; - if let Some(membership_updated) = membership_updated { - notify_membership_updated( - &connection_pool, - membership_updated, - session.user_id, - &session.peer, - ); - } - - room_updated(&joined_room.room, &session.peer); - - joined_room - }; - - channel_updated( - channel_id, - &joined_room.room, - &joined_room.channel_members, - &session.peer, - &*session.connection_pool().await, - ); - - update_user_contacts(session.user_id, &session).await?; - Ok(()) -} - -async fn join_channel_buffer( - request: proto::JoinChannelBuffer, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - - let open_response = db - .join_channel_buffer(channel_id, session.user_id, session.connection_id) - .await?; - - let collaborators = open_response.collaborators.clone(); - response.send(open_response)?; - - let update = UpdateChannelBufferCollaborators { - channel_id: channel_id.to_proto(), - collaborators: collaborators.clone(), - }; - channel_buffer_updated( - session.connection_id, - collaborators - .iter() - .filter_map(|collaborator| Some(collaborator.peer_id?.into())), - &update, - &session.peer, - ); - - Ok(()) -} - -async fn update_channel_buffer( - request: proto::UpdateChannelBuffer, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - - let (collaborators, non_collaborators, epoch, version) = db - .update_channel_buffer(channel_id, session.user_id, &request.operations) - .await?; - - channel_buffer_updated( - session.connection_id, - collaborators, - &proto::UpdateChannelBuffer { - channel_id: channel_id.to_proto(), - operations: request.operations, - }, - &session.peer, - ); - - 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(()) -} - -async fn rejoin_channel_buffers( - request: proto::RejoinChannelBuffers, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let buffers = db - .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id) - .await?; - - for rejoined_buffer in &buffers { - let collaborators_to_notify = rejoined_buffer - .buffer - .collaborators - .iter() - .filter_map(|c| Some(c.peer_id?.into())); - channel_buffer_updated( - session.connection_id, - collaborators_to_notify, - &proto::UpdateChannelBufferCollaborators { - channel_id: rejoined_buffer.buffer.channel_id, - collaborators: rejoined_buffer.buffer.collaborators.clone(), - }, - &session.peer, - ); - } - - response.send(proto::RejoinChannelBuffersResponse { - buffers: buffers.into_iter().map(|b| b.buffer).collect(), - })?; - - Ok(()) -} - -async fn leave_channel_buffer( - request: proto::LeaveChannelBuffer, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - - let left_buffer = db - .leave_channel_buffer(channel_id, session.connection_id) - .await?; - - response.send(Ack {})?; - - channel_buffer_updated( - session.connection_id, - left_buffer.connections, - &proto::UpdateChannelBufferCollaborators { - channel_id: channel_id.to_proto(), - collaborators: left_buffer.collaborators, - }, - &session.peer, - ); - - Ok(()) -} - -fn channel_buffer_updated( - sender_id: ConnectionId, - collaborators: impl IntoIterator, - message: &T, - peer: &Peer, -) { - broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| { - peer.send(peer_id.into(), message.clone()) - }); -} - -fn send_notifications( - connection_pool: &ConnectionPool, - peer: &Peer, - notifications: db::NotificationBatch, -) { - for (user_id, notification) in notifications { - for connection_id in connection_pool.user_connection_ids(user_id) { - if let Err(error) = peer.send( - connection_id, - proto::AddNotification { - notification: Some(notification.clone()), - }, - ) { - tracing::error!( - "failed to send notification to {:?} {}", - connection_id, - error - ); - } - } - } -} - -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"))?; - } - - // TODO: adjust mentions if body is trimmed - - 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 CreatedChannelMessage { - message_id, - participant_connection_ids, - channel_members, - notifications, - } = session - .db() - .await - .create_channel_message( - channel_id, - session.user_id, - &body, - &request.mentions, - timestamp, - nonce.clone().into(), - ) - .await?; - let message = proto::ChannelMessage { - sender_id: session.user_id.to_proto(), - id: message_id.to_proto(), - body, - mentions: request.mentions, - timestamp: timestamp.unix_timestamp() as u64, - nonce: Some(nonce), - }; - broadcast( - Some(session.connection_id), - participant_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, - channel_members - .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() - }, - ) - }, - ); - send_notifications(pool, &session.peer, notifications); - - 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); - let notifications = session - .db() - .await - .observe_channel_message(channel_id, session.user_id, message_id) - .await?; - send_notifications( - &*session.connection_pool().await, - &session.peer, - notifications, - ); - 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 get_channel_messages_by_id( - request: proto::GetChannelMessagesById, - response: Response, - session: Session, -) -> Result<()> { - let message_ids = request - .message_ids - .iter() - .map(|id| MessageId::from_proto(*id)) - .collect::>(); - let messages = session - .db() - .await - .get_channel_messages_by_id(session.user_id, &message_ids) - .await?; - response.send(proto::GetChannelMessagesResponse { - done: messages.len() < MESSAGE_COUNT_PER_PAGE, - messages, - })?; - Ok(()) -} - -async fn get_notifications( - request: proto::GetNotifications, - response: Response, - session: Session, -) -> Result<()> { - let notifications = session - .db() - .await - .get_notifications( - session.user_id, - NOTIFICATION_COUNT_PER_PAGE, - request - .before_id - .map(|id| db::NotificationId::from_proto(id)), - ) - .await?; - response.send(proto::GetNotificationsResponse { - done: notifications.len() < NOTIFICATION_COUNT_PER_PAGE, - notifications, - })?; - Ok(()) -} - -async fn mark_notification_as_read( - request: proto::MarkNotificationRead, - response: Response, - session: Session, -) -> Result<()> { - let database = &session.db().await; - let notifications = database - .mark_notification_as_read_by_id( - session.user_id, - NotificationId::from_proto(request.notification_id), - ) - .await?; - send_notifications( - &*session.connection_pool().await, - &session.peer, - notifications, - ); - response.send(proto::Ack {})?; - 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 - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn get_private_user_info( - _request: proto::GetPrivateUserInfo, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - - let metrics_id = db.get_user_metrics_id(session.user_id).await?; - let user = db - .get_user_by_id(session.user_id) - .await? - .ok_or_else(|| anyhow!("user not found"))?; - let flags = db.get_user_flags(session.user_id).await?; - - response.send(proto::GetPrivateUserInfoResponse { - metrics_id, - staff: user.admin, - flags, - })?; - Ok(()) -} - -fn to_axum_message(message: TungsteniteMessage) -> AxumMessage { - match message { - TungsteniteMessage::Text(payload) => AxumMessage::Text(payload), - TungsteniteMessage::Binary(payload) => AxumMessage::Binary(payload), - TungsteniteMessage::Ping(payload) => AxumMessage::Ping(payload), - TungsteniteMessage::Pong(payload) => AxumMessage::Pong(payload), - TungsteniteMessage::Close(frame) => AxumMessage::Close(frame.map(|frame| AxumCloseFrame { - code: frame.code.into(), - reason: frame.reason, - })), - } -} - -fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { - match message { - AxumMessage::Text(payload) => TungsteniteMessage::Text(payload), - AxumMessage::Binary(payload) => TungsteniteMessage::Binary(payload), - AxumMessage::Ping(payload) => TungsteniteMessage::Ping(payload), - AxumMessage::Pong(payload) => TungsteniteMessage::Pong(payload), - AxumMessage::Close(frame) => { - TungsteniteMessage::Close(frame.map(|frame| TungsteniteCloseFrame { - code: frame.code.into(), - reason: frame.reason, - })) - } - } -} - -fn notify_membership_updated( - connection_pool: &ConnectionPool, - result: MembershipUpdated, - user_id: UserId, - peer: &Peer, -) { - let mut update = build_channels_update(result.new_channels, vec![]); - update.delete_channels = result - .removed_channels - .into_iter() - .map(|id| id.to_proto()) - .collect(); - update.remove_channel_invitations = vec![result.channel_id.to_proto()]; - - for connection_id in connection_pool.user_connection_ids(user_id) { - peer.send(connection_id, update.clone()).trace_err(); - } -} - -fn build_channels_update( - channels: ChannelsForUser, - channel_invites: Vec, -) -> proto::UpdateChannels { - let mut update = proto::UpdateChannels::default(); - - for channel in channels.channels { - update.channels.push(channel.to_proto()); - } - - update.unseen_channel_buffer_changes = channels.unseen_buffer_changes; - update.unseen_channel_messages = channels.channel_messages; - - for (channel_id, participants) in channels.channel_participants { - update - .channel_participants - .push(proto::ChannelParticipants { - channel_id: channel_id.to_proto(), - participant_user_ids: participants.into_iter().map(|id| id.to_proto()).collect(), - }); - } - - for channel in channel_invites { - update.channel_invitations.push(channel.to_proto()); - } - - update -} - -fn build_initial_contacts_update( - contacts: Vec, - pool: &ConnectionPool, -) -> proto::UpdateContacts { - let mut update = proto::UpdateContacts::default(); - - for contact in contacts { - match contact { - db::Contact::Accepted { user_id, busy } => { - update.contacts.push(contact_for_user(user_id, busy, &pool)); - } - db::Contact::Outgoing { user_id } => update.outgoing_requests.push(user_id.to_proto()), - db::Contact::Incoming { user_id } => { - update - .incoming_requests - .push(proto::IncomingContactRequest { - requester_id: user_id.to_proto(), - }) - } - } - } - - update -} - -fn contact_for_user(user_id: UserId, busy: bool, pool: &ConnectionPool) -> proto::Contact { - proto::Contact { - user_id: user_id.to_proto(), - online: pool.is_user_online(user_id), - busy, - } -} - -fn room_updated(room: &proto::Room, peer: &Peer) { - broadcast( - None, - room.participants - .iter() - .filter_map(|participant| Some(participant.peer_id?.into())), - |peer_id| { - peer.send( - peer_id.into(), - proto::RoomUpdated { - room: Some(room.clone()), - }, - ) - }, - ); -} - -fn channel_updated( - channel_id: ChannelId, - room: &proto::Room, - channel_members: &[UserId], - peer: &Peer, - pool: &ConnectionPool, -) { - let participants = room - .participants - .iter() - .map(|p| p.user_id) - .collect::>(); - - broadcast( - None, - channel_members - .iter() - .flat_map(|user_id| pool.user_connection_ids(*user_id)), - |peer_id| { - peer.send( - peer_id.into(), - proto::UpdateChannels { - channel_participants: vec![proto::ChannelParticipants { - channel_id: channel_id.to_proto(), - participant_user_ids: participants.clone(), - }], - ..Default::default() - }, - ) - }, - ); -} - -async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> { - let db = session.db().await; - - let contacts = db.get_contacts(user_id).await?; - let busy = db.is_user_busy(user_id).await?; - - let pool = session.connection_pool().await; - let updated_contact = contact_for_user(user_id, busy, &pool); - for contact in contacts { - if let db::Contact::Accepted { - user_id: contact_user_id, - .. - } = contact - { - for contact_conn_id in pool.user_connection_ids(contact_user_id) { - session - .peer - .send( - contact_conn_id, - proto::UpdateContacts { - contacts: vec![updated_contact.clone()], - remove_contacts: Default::default(), - incoming_requests: Default::default(), - remove_incoming_requests: Default::default(), - outgoing_requests: Default::default(), - remove_outgoing_requests: Default::default(), - }, - ) - .trace_err(); - } - } - } - Ok(()) -} - -async fn leave_room_for_session(session: &Session) -> Result<()> { - let mut contacts_to_update = HashSet::default(); - - let room_id; - let canceled_calls_to_user_ids; - let live_kit_room; - let delete_live_kit_room; - let room; - let channel_members; - let channel_id; - - if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? { - contacts_to_update.insert(session.user_id); - - for project in left_room.left_projects.values() { - project_left(project, session); - } - - room_id = RoomId::from_proto(left_room.room.id); - canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids); - live_kit_room = mem::take(&mut left_room.room.live_kit_room); - delete_live_kit_room = left_room.deleted; - room = mem::take(&mut left_room.room); - channel_members = mem::take(&mut left_room.channel_members); - channel_id = left_room.channel_id; - - room_updated(&room, &session.peer); - } else { - return Ok(()); - } - - if let Some(channel_id) = channel_id { - channel_updated( - channel_id, - &room, - &channel_members, - &session.peer, - &*session.connection_pool().await, - ); - } - - { - let pool = session.connection_pool().await; - for canceled_user_id in canceled_calls_to_user_ids { - for connection_id in pool.user_connection_ids(canceled_user_id) { - session - .peer - .send( - connection_id, - proto::CallCanceled { - room_id: room_id.to_proto(), - }, - ) - .trace_err(); - } - contacts_to_update.insert(canceled_user_id); - } - } - - for contact_user_id in contacts_to_update { - update_user_contacts(contact_user_id, &session).await?; - } - - if let Some(live_kit) = session.live_kit_client.as_ref() { - live_kit - .remove_participant(live_kit_room.clone(), session.user_id.to_string()) - .await - .trace_err(); - - if delete_live_kit_room { - live_kit.delete_room(live_kit_room).await.trace_err(); - } - } - - Ok(()) -} - -async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { - let left_channel_buffers = session - .db() - .await - .leave_channel_buffers(session.connection_id) - .await?; - - for left_buffer in left_channel_buffers { - channel_buffer_updated( - session.connection_id, - left_buffer.connections, - &proto::UpdateChannelBufferCollaborators { - channel_id: left_buffer.channel_id.to_proto(), - collaborators: left_buffer.collaborators, - }, - &session.peer, - ); - } - - Ok(()) -} - -fn project_left(project: &db::LeftProject, session: &Session) { - for connection_id in &project.connection_ids { - if project.host_user_id == session.user_id { - session - .peer - .send( - *connection_id, - proto::UnshareProject { - project_id: project.id.to_proto(), - }, - ) - .trace_err(); - } else { - session - .peer - .send( - *connection_id, - proto::RemoveProjectCollaborator { - project_id: project.id.to_proto(), - peer_id: Some(session.connection_id.into()), - }, - ) - .trace_err(); - } - } -} - -pub trait ResultExt { - type Ok; - - fn trace_err(self) -> Option; -} - -impl ResultExt for Result -where - E: std::fmt::Debug, -{ - type Ok = T; - - fn trace_err(self) -> Option { - match self { - Ok(value) => Some(value), - Err(error) => { - tracing::error!("{:?}", error); - None - } - } - } -} diff --git a/crates/collab2/src/rpc/connection_pool.rs b/crates/collab2/src/rpc/connection_pool.rs deleted file mode 100644 index 30c4e144ed..0000000000 --- a/crates/collab2/src/rpc/connection_pool.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::db::UserId; -use anyhow::{anyhow, Result}; -use collections::{BTreeMap, HashSet}; -use rpc::ConnectionId; -use serde::Serialize; -use tracing::instrument; - -#[derive(Default, Serialize)] -pub struct ConnectionPool { - connections: BTreeMap, - connected_users: BTreeMap, -} - -#[derive(Default, Serialize)] -struct ConnectedUser { - connection_ids: HashSet, -} - -#[derive(Serialize)] -pub struct Connection { - pub user_id: UserId, - pub admin: bool, -} - -impl ConnectionPool { - pub fn reset(&mut self) { - self.connections.clear(); - self.connected_users.clear(); - } - - #[instrument(skip(self))] - pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) { - self.connections - .insert(connection_id, Connection { user_id, admin }); - let connected_user = self.connected_users.entry(user_id).or_default(); - connected_user.connection_ids.insert(connection_id); - } - - #[instrument(skip(self))] - pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Result<()> { - let connection = self - .connections - .get_mut(&connection_id) - .ok_or_else(|| anyhow!("no such connection"))?; - - let user_id = connection.user_id; - let connected_user = self.connected_users.get_mut(&user_id).unwrap(); - connected_user.connection_ids.remove(&connection_id); - if connected_user.connection_ids.is_empty() { - self.connected_users.remove(&user_id); - } - self.connections.remove(&connection_id).unwrap(); - Ok(()) - } - - pub fn connections(&self) -> impl Iterator { - self.connections.values() - } - - pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator + '_ { - self.connected_users - .get(&user_id) - .into_iter() - .map(|state| &state.connection_ids) - .flatten() - .copied() - } - - pub fn is_user_online(&self, user_id: UserId) -> bool { - !self - .connected_users - .get(&user_id) - .unwrap_or(&Default::default()) - .connection_ids - .is_empty() - } - - #[cfg(test)] - pub fn check_invariants(&self) { - for (connection_id, connection) in &self.connections { - assert!(self - .connected_users - .get(&connection.user_id) - .unwrap() - .connection_ids - .contains(connection_id)); - } - - for (user_id, state) in &self.connected_users { - for connection_id in &state.connection_ids { - assert_eq!( - self.connections.get(connection_id).unwrap().user_id, - *user_id - ); - } - } - } -} diff --git a/crates/collab2/src/tests.rs b/crates/collab2/src/tests.rs deleted file mode 100644 index a669f260db..0000000000 --- a/crates/collab2/src/tests.rs +++ /dev/null @@ -1,48 +0,0 @@ -use call::Room; -use gpui::{Model, TestAppContext}; - -mod channel_buffer_tests; -mod channel_message_tests; -mod channel_tests; -mod editor_tests; -mod following_tests; -mod integration_tests; -mod notification_tests; -mod random_channel_buffer_tests; -mod random_project_collaboration_tests; -mod randomized_test_helpers; -mod test_server; - -pub use crate as collab2; -pub use randomized_test_helpers::{ - run_randomized_test, save_randomized_test_plan, RandomizedTest, TestError, UserTestPlan, -}; -pub use test_server::{TestClient, TestServer}; - -#[derive(Debug, Eq, PartialEq)] -struct RoomParticipants { - remote: Vec, - pending: Vec, -} - -fn room_participants(room: &Model, cx: &mut TestAppContext) -> RoomParticipants { - room.read_with(cx, |room, _| { - let mut remote = room - .remote_participants() - .iter() - .map(|(_, participant)| participant.user.github_login.clone()) - .collect::>(); - let mut pending = room - .pending_participants() - .iter() - .map(|user| user.github_login.clone()) - .collect::>(); - remote.sort(); - pending.sort(); - RoomParticipants { remote, pending } - }) -} - -fn channel_id(room: &Model, cx: &mut TestAppContext) -> Option { - cx.read(|cx| room.read(cx).channel_id()) -} diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs deleted file mode 100644 index b0af360fd5..0000000000 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ /dev/null @@ -1,881 +0,0 @@ -use crate::{ - rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::TestServer, -}; -use call::ActiveCall; -use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL; -use client::{Collaborator, ParticipantIndex, UserId}; -use collab_ui::channel_view::ChannelView; -use collections::HashMap; -use editor::{Anchor, Editor, ToOffset}; -use futures::future; -use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; -use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; -use serde_json::json; -use std::ops::Range; - -#[gpui::test] -async fn test_core_channel_buffers( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) - .await; - - // Client A joins the channel buffer - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - // Client A edits the buffer - let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(0..0, "hello world")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(5..5, ", cruel")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(0..5, "goodbye")], None, cx) - }); - buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); - assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); - executor.run_until_parked(); - - // Client B joins the channel buffer - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators( - buffer.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - }); - - // Client B sees the correct text, and then edits it - let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); - assert_eq!( - buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), - buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) - ); - assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); - buffer_b.update(cx_b, |buffer, cx| { - buffer.edit([(7..12, "beautiful")], None, cx) - }); - - // Both A and B see the new edit - executor.run_until_parked(); - assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); - assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); - - // Client A closes the channel buffer. - cx_a.update(|_| drop(channel_buffer_a)); - executor.run_until_parked(); - - // Client B sees that client A is gone from the channel buffer. - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); - }); - - // Client A rejoins the channel buffer - let _channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - executor.run_until_parked(); - - // Sanity test, make sure we saw A rejoining - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators( - &buffer.collaborators(), - &[client_a.user_id(), client_b.user_id()], - ); - }); - - // Client A loses connection. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - // Client B observes A disconnect - channel_buffer_b.read_with(cx_b, |buffer, _| { - assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); - }); - - // TODO: - // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects - // - Test interaction with channel deletion while buffer is open -} - -#[gpui::test] -async fn test_channel_notes_participant_indices( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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 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; - - 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, mut cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, mut cx_b) = client_b.build_workspace(&project_b, cx_b); - let (workspace_c, cx_c) = client_c.build_workspace(&project_c, 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_view_b = cx_b - .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) - .await - .unwrap(); - let channel_view_c = cx_c - .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) - .await - .unwrap(); - - // 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]); - }); - }); - }); - executor.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]); - }); - }); - }); - executor.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]); - }); - }); - }); - - // Client A sees clients B and C without assigned colors, because they aren't - // in a call together. - executor.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); - }); - }); - - // 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(); - } - - // Clients A and B see each other with two different assigned colors. Client C - // still doesn't have a color. - executor.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.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 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, cx_b) = client_b.build_workspace(&project_b, cx_b); - - // 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 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]); - }); - }); - executor.run_until_parked(); - - // 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); - }); - 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_multiple_handles_to_channel_buffer( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - - let channel_id = server - .make_channel("the-channel", None, (&client_a, cx_a), &mut []) - .await; - - let channel_buffer_1 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - let channel_buffer_2 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - let channel_buffer_3 = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - - // All concurrent tasks for opening a channel buffer return the same model handle. - let (channel_buffer, channel_buffer_2, channel_buffer_3) = - future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) - .await - .unwrap(); - let channel_buffer_model_id = channel_buffer.entity_id(); - assert_eq!(channel_buffer, channel_buffer_2); - assert_eq!(channel_buffer, channel_buffer_3); - - channel_buffer.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "hello")], None, cx); - }) - }); - deterministic.run_until_parked(); - - cx_a.update(|_| { - drop(channel_buffer); - drop(channel_buffer_2); - drop(channel_buffer_3); - }); - deterministic.run_until_parked(); - - // The channel buffer can be reopened after dropping it. - let channel_buffer = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); - channel_buffer.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, _| { - assert_eq!(buffer.text(), "hello"); - }) - }); -} - -#[gpui::test] -async fn test_channel_buffer_disconnect( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).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(); - - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - channel_buffer_a.update(cx_a, |buffer, cx| { - assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); - assert!(!buffer.is_connected()); - }); - - deterministic.run_until_parked(); - - server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - deterministic.run_until_parked(); - - client_a - .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.remove_channel(channel_id) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - - // Channel buffer observed the deletion - channel_buffer_b.update(cx_b, |buffer, cx| { - assert!(buffer.channel(cx).is_none()); - assert!(!buffer.is_connected()); - }); -} - -#[gpui::test] -async fn test_rejoin_channel_buffer( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).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(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - 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(); - - // Client A disconnects. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - - // Both clients make an edit. - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(1..1, "2")], None, cx); - }) - }); - channel_buffer_b.update(cx_b, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "0")], None, cx); - }) - }); - - // Both clients see their own edit. - deterministic.run_until_parked(); - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "12"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "01"); - }); - - // Client A reconnects. Both clients see each other's edits, and see - // the same collaborators. - server.allow_connections(); - deterministic.advance_clock(RECEIVE_TIMEOUT); - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - - channel_buffer_a.read_with(cx_a, |buffer_a, _| { - channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); - }); - }); -} - -#[gpui::test] -async fn test_channel_buffers_and_server_restarts( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).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_buffer_a = client_a - .channel_store() - .update(cx_a, |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)) - .await - .unwrap(); - let _channel_buffer_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - 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(); - - // Client C can't reconnect. - client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - - // Server stops. - server.reset().await; - deterministic.advance_clock(RECEIVE_TIMEOUT); - - // While the server is down, both clients make an edit. - channel_buffer_a.update(cx_a, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(1..1, "2")], None, cx); - }) - }); - channel_buffer_b.update(cx_b, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.edit([(0..0, "0")], None, cx); - }) - }); - - // Server restarts. - server.start().await.unwrap(); - deterministic.advance_clock(CLEANUP_TIMEOUT); - - // Clients reconnects. Clients A and B see each other's edits, and see - // that client C has disconnected. - channel_buffer_a.read_with(cx_a, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - channel_buffer_b.read_with(cx_b, |buffer, cx| { - assert_eq!(buffer.buffer().read(cx).text(), "012"); - }); - - channel_buffer_a.read_with(cx_a, |buffer_a, _| { - channel_buffer_b.read_with(cx_b, |buffer_b, _| { - 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: BackgroundExecutor, - mut cx_a: &mut TestAppContext, - mut cx_b: &mut TestAppContext, - mut cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).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, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, 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).unwrap().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.update(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.update(cx_b, |notes, cx| { - assert_eq!(notes.channel(cx).unwrap().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. - eprintln!("opening -------------------->"); - - let channel_view_2_a = cx_a - .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) - .await - .unwrap(); - channel_view_2_a.update(cx_a, |notes, cx| { - assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); - }); - - // Client B is taken to the notes for channel 2. - deterministic.run_until_parked(); - - eprintln!("opening <--------------------"); - - let channel_view_2_b = workspace_b.update(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.update(cx_b, |notes, cx| { - assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); - }); -} - -#[gpui::test] -async fn test_channel_buffer_changes( - deterministic: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(deterministic.clone()).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.update(|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, cx_b) = client_b.build_workspace(&project_b, 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.update(|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.clone()); - 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: &HashMap, ids: &[Option]) { - let mut user_ids = collaborators - .values() - .map(|collaborator| collaborator.user_id) - .collect::>(); - user_ids.sort(); - assert_eq!( - user_ids, - ids.into_iter().map(|id| id.unwrap()).collect::>() - ); -} - -fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { - channel_buffer.read_with(cx, |buffer, _| buffer.text()) -} diff --git a/crates/collab2/src/tests/channel_message_tests.rs b/crates/collab2/src/tests/channel_message_tests.rs deleted file mode 100644 index f5da0e3ee6..0000000000 --- a/crates/collab2/src/tests/channel_message_tests.rs +++ /dev/null @@ -1,408 +0,0 @@ -use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -use channel::{ChannelChat, ChannelMessageId, MessageParams}; -use gpui::{BackgroundExecutor, Model, TestAppContext}; -use rpc::Notification; - -#[gpui::test] -async fn test_basic_channel_messages( - executor: BackgroundExecutor, - mut cx_a: &mut TestAppContext, - mut cx_b: &mut TestAppContext, - mut cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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(); - - let message_id = channel_chat_a - .update(cx_a, |c, cx| { - c.send_message( - MessageParams { - text: "hi @user_c!".into(), - mentions: vec![(3..10, client_c.id())], - }, - cx, - ) - .unwrap() - }) - .await - .unwrap(); - channel_chat_a - .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - channel_chat_b - .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap()) - .await - .unwrap(); - - executor.run_until_parked(); - - let channel_chat_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx)) - .await - .unwrap(); - - for (chat, cx) in [ - (&channel_chat_a, &mut cx_a), - (&channel_chat_b, &mut cx_b), - (&channel_chat_c, &mut cx_c), - ] { - chat.update(*cx, |c, _| { - assert_eq!( - c.messages() - .iter() - .map(|m| (m.body.as_str(), m.mentions.as_slice())) - .collect::>(), - vec![ - ("hi @user_c!", [(3..10, client_c.id())].as_slice()), - ("two", &[]), - ("three", &[]) - ], - "results for user {}", - c.client().id(), - ); - }); - } - - client_c.notification_store().update(cx_c, |store, _| { - assert_eq!(store.notification_count(), 2); - assert_eq!(store.unread_notification_count(), 1); - assert_eq!( - store.notification_at(0).unwrap().notification, - Notification::ChannelMessageMention { - message_id, - sender_id: client_a.id(), - channel_id, - } - ); - assert_eq!( - store.notification_at(1).unwrap().notification, - Notification::ChannelInvitation { - channel_id, - channel_name: "the-channel".to_string(), - inviter_id: client_a.id() - } - ); - }); -} - -#[gpui::test] -async fn test_rejoin_channel_chat( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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(); - executor.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( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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. - executor.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. - executor.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: &Model, messages: &[&str], cx: &mut TestAppContext) { - // todo!(don't directly borrow here) - assert_eq!( - chat.read_with(cx, |chat, _| { - chat.messages() - .iter() - .map(|m| m.body.clone()) - .collect::>() - }), - messages - ); -} - -//todo!(collab_ui) -// #[gpui::test] -// async fn test_channel_message_changes( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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(); - -// executor.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, None, cx) -// }) -// .await -// .unwrap(); - -// executor.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(); - -// executor.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(); - -// executor.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(); - -// executor.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/collab2/src/tests/channel_tests.rs b/crates/collab2/src/tests/channel_tests.rs deleted file mode 100644 index 49e7060301..0000000000 --- a/crates/collab2/src/tests/channel_tests.rs +++ /dev/null @@ -1,1543 +0,0 @@ -use crate::{ - db::{self, UserId}, - rpc::RECONNECT_TIMEOUT, - tests::{room_participants, RoomParticipants, TestServer}, -}; -use call::ActiveCall; -use channel::{ChannelId, ChannelMembership, ChannelStore}; -use client::User; -use futures::future::try_join_all; -use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext}; -use rpc::{ - proto::{self, ChannelRole}, - RECEIVE_TIMEOUT, -}; -use std::sync::Arc; - -#[gpui::test] -async fn test_core_channels( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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_a_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-a", None, cx) - }) - .await - .unwrap(); - let channel_b_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-b", Some(channel_a_id), cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - assert_channels( - client_a.channel_store(), - cx_a, - &[ - ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - depth: 0, - role: ChannelRole::Admin, - }, - ExpectedChannel { - id: channel_b_id, - name: "channel-b".into(), - depth: 1, - role: ChannelRole::Admin, - }, - ], - ); - - cx_b.read(|cx| { - client_b.channel_store().read_with(cx, |channels, _| { - assert!(channels.ordered_channels().collect::>().is_empty()) - }) - }); - - // Invite client B to channel A as client A. - client_a - .channel_store() - .update(cx_a, |store, cx| { - assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); - - let invite = store.invite_member( - channel_a_id, - client_b.user_id().unwrap(), - proto::ChannelRole::Member, - cx, - ); - - // Make sure we're synchronously storing the pending invite - assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); - invite - }) - .await - .unwrap(); - - // Client A sees that B has been invited. - executor.run_until_parked(); - assert_channel_invitations( - client_b.channel_store(), - cx_b, - &[ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - depth: 0, - role: ChannelRole::Member, - }], - ); - - let members = client_a - .channel_store() - .update(cx_a, |store, cx| { - assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); - store.get_channel_member_details(channel_a_id, cx) - }) - .await - .unwrap(); - assert_members_eq( - &members, - &[ - ( - client_a.user_id().unwrap(), - proto::ChannelRole::Admin, - proto::channel_member::Kind::Member, - ), - ( - client_b.user_id().unwrap(), - proto::ChannelRole::Member, - proto::channel_member::Kind::Invitee, - ), - ], - ); - - // Client B accepts the invitation. - client_b - .channel_store() - .update(cx_b, |channels, cx| { - channels.respond_to_channel_invite(channel_a_id, true, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - - // Client B now sees that they are a member of channel A and its existing subchannels. - assert_channel_invitations(client_b.channel_store(), cx_b, &[]); - assert_channels( - client_b.channel_store(), - cx_b, - &[ - ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - role: ChannelRole::Member, - depth: 0, - }, - ExpectedChannel { - id: channel_b_id, - name: "channel-b".into(), - role: ChannelRole::Member, - depth: 1, - }, - ], - ); - - let channel_c_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-c", Some(channel_b_id), cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - assert_channels( - client_b.channel_store(), - cx_b, - &[ - ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - role: ChannelRole::Member, - depth: 0, - }, - ExpectedChannel { - id: channel_b_id, - name: "channel-b".into(), - role: ChannelRole::Member, - depth: 1, - }, - ExpectedChannel { - id: channel_c_id, - name: "channel-c".into(), - role: ChannelRole::Member, - depth: 2, - }, - ], - ); - - // Update client B's membership to channel A to be an admin. - client_a - .channel_store() - .update(cx_a, |store, cx| { - store.set_member_role( - channel_a_id, - client_b.user_id().unwrap(), - proto::ChannelRole::Admin, - cx, - ) - }) - .await - .unwrap(); - executor.run_until_parked(); - - // Observe that client B is now an admin of channel A, and that - // their admin priveleges extend to subchannels of channel A. - assert_channel_invitations(client_b.channel_store(), cx_b, &[]); - assert_channels( - client_b.channel_store(), - cx_b, - &[ - ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - depth: 0, - role: ChannelRole::Admin, - }, - ExpectedChannel { - id: channel_b_id, - name: "channel-b".into(), - depth: 1, - role: ChannelRole::Admin, - }, - ExpectedChannel { - id: channel_c_id, - name: "channel-c".into(), - depth: 2, - role: ChannelRole::Admin, - }, - ], - ); - - // Client A deletes the channel, deletion also deletes subchannels. - client_a - .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.remove_channel(channel_b_id) - }) - .await - .unwrap(); - - executor.run_until_parked(); - assert_channels( - client_a.channel_store(), - cx_a, - &[ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - depth: 0, - role: ChannelRole::Admin, - }], - ); - assert_channels( - client_b.channel_store(), - cx_b, - &[ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - depth: 0, - role: ChannelRole::Admin, - }], - ); - - // Remove client B - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.remove_member(channel_a_id, client_b.user_id().unwrap(), cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // Client A still has their channel - assert_channels( - client_a.channel_store(), - cx_a, - &[ExpectedChannel { - id: channel_a_id, - name: "channel-a".into(), - depth: 0, - role: ChannelRole::Admin, - }], - ); - - // Client B no longer has access to the channel - assert_channels(client_b.channel_store(), cx_b, &[]); - - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - server - .app_state - .db - .rename_channel( - db::ChannelId::from_proto(channel_a_id), - UserId::from_proto(client_a.id()), - "channel-a-renamed", - ) - .await - .unwrap(); - - server.allow_connections(); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - assert_channels( - client_a.channel_store(), - cx_a, - &[ExpectedChannel { - id: channel_a_id, - name: "channel-a-renamed".into(), - depth: 0, - role: ChannelRole::Admin, - }], - ); -} - -#[track_caller] -fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u64]) { - assert_eq!( - participants.iter().map(|p| p.id).collect::>(), - expected_partitipants - ); -} - -#[track_caller] -fn assert_members_eq( - members: &[ChannelMembership], - expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)], -) { - assert_eq!( - members - .iter() - .map(|member| (member.user.id, member.role, member.kind)) - .collect::>(), - expected_members - ); -} - -#[gpui::test] -async fn test_joining_channel_ancestor_member( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let parent_id = server - .make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) - .await; - - let sub_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("sub_channel", Some(parent_id), cx) - }) - .await - .unwrap(); - - let active_call_b = cx_b.read(ActiveCall::global); - - assert!(active_call_b - .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx)) - .await - .is_ok()); -} - -#[gpui::test] -async fn test_channel_room( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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 zed_id = server - .make_channel( - "zed", - 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); - - active_call_a - .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) - .await - .unwrap(); - - // Give everyone a chance to observe user A joining - executor.run_until_parked(); - let room_a = - cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); - cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); - - cx_a.read(|cx| { - client_a.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); - }) - }); - - assert_channels( - client_b.channel_store(), - cx_b, - &[ExpectedChannel { - id: zed_id, - name: "zed".into(), - depth: 0, - role: ChannelRole::Member, - }], - ); - cx_b.read(|cx| { - client_b.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); - }) - }); - - cx_c.read(|cx| { - client_c.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); - }) - }); - - active_call_b - .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) - .await - .unwrap(); - - executor.run_until_parked(); - - cx_a.read(|cx| { - client_a.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); - }) - }); - - cx_b.read(|cx| { - client_b.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); - }) - }); - - cx_c.read(|cx| { - client_c.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); - }) - }); - - let room_a = - cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); - cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec![] - } - ); - - let room_b = - cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone())); - cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected()))); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: vec![] - } - ); - - // Make sure that leaving and rejoining works - - active_call_a - .update(cx_a, |active_call, cx| active_call.hang_up(cx)) - .await - .unwrap(); - - executor.run_until_parked(); - - cx_a.read(|cx| { - client_a.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_b.user_id().unwrap()], - ); - }) - }); - - cx_b.read(|cx| { - client_b.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_b.user_id().unwrap()], - ); - }) - }); - - cx_c.read(|cx| { - client_c.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_b.user_id().unwrap()], - ); - }) - }); - - active_call_b - .update(cx_b, |active_call, cx| active_call.hang_up(cx)) - .await - .unwrap(); - - executor.run_until_parked(); - - cx_a.read(|cx| { - client_a.channel_store().read_with(cx, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); - }) - }); - - cx_b.read(|cx| { - client_b.channel_store().read_with(cx, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); - }) - }); - - cx_c.read(|cx| { - client_c.channel_store().read_with(cx, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); - }) - }); - - active_call_a - .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) - .await - .unwrap(); - - active_call_b - .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) - .await - .unwrap(); - - executor.run_until_parked(); - - let room_a = - cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); - cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec![] - } - ); - - let room_b = - cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone())); - cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected()))); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: vec![] - } - ); -} - -#[gpui::test] -async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppContext) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - - let zed_id = server - .make_channel("zed", None, (&client_a, cx_a), &mut []) - .await; - let rust_id = server - .make_channel("rust", None, (&client_a, cx_a), &mut []) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - - active_call_a - .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) - .await - .unwrap(); - - // Give everything a chance to observe user A joining - executor.run_until_parked(); - - cx_a.read(|cx| { - client_a.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(zed_id), - &[client_a.user_id().unwrap()], - ); - assert_participants_eq(channels.channel_participants(rust_id), &[]); - }) - }); - - active_call_a - .update(cx_a, |active_call, cx| { - active_call.join_channel(rust_id, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - cx_a.read(|cx| { - client_a.channel_store().read_with(cx, |channels, _| { - assert_participants_eq(channels.channel_participants(zed_id), &[]); - assert_participants_eq( - channels.channel_participants(rust_id), - &[client_a.user_id().unwrap()], - ); - }) - }); -} - -#[gpui::test] -async fn test_permissions_update_while_invited( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let rust_id = server - .make_channel("rust", None, (&client_a, cx_a), &mut []) - .await; - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.invite_member( - rust_id, - client_b.user_id().unwrap(), - proto::ChannelRole::Member, - cx, - ) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - assert_channel_invitations( - client_b.channel_store(), - cx_b, - &[ExpectedChannel { - depth: 0, - id: rust_id, - name: "rust".into(), - role: ChannelRole::Member, - }], - ); - assert_channels(client_b.channel_store(), cx_b, &[]); - - // Update B's invite before they've accepted it - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_member_role( - rust_id, - client_b.user_id().unwrap(), - proto::ChannelRole::Admin, - cx, - ) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - assert_channel_invitations( - client_b.channel_store(), - cx_b, - &[ExpectedChannel { - depth: 0, - id: rust_id, - name: "rust".into(), - role: ChannelRole::Member, - }], - ); - assert_channels(client_b.channel_store(), cx_b, &[]); -} - -#[gpui::test] -async fn test_channel_rename( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let rust_id = server - .make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) - .await; - - // Rename the channel - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.rename(rust_id, "#rust-archive", cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // Client A sees the channel with its new name. - assert_channels( - client_a.channel_store(), - cx_a, - &[ExpectedChannel { - depth: 0, - id: rust_id, - name: "rust-archive".into(), - role: ChannelRole::Admin, - }], - ); - - // Client B sees the channel with its new name. - assert_channels( - client_b.channel_store(), - cx_b, - &[ExpectedChannel { - depth: 0, - id: rust_id, - name: "rust-archive".into(), - role: ChannelRole::Member, - }], - ); -} - -#[gpui::test] -async fn test_call_from_channel( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - - let channel_id = server - .make_channel( - "x", - 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); - - active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) - .await - .unwrap(); - - // Client A calls client B while in the channel. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - - // Client B accepts the call. - executor.run_until_parked(); - active_call_b - .update(cx_b, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - - // Client B sees that they are now in the channel - executor.run_until_parked(); - cx_b.read(|cx| { - active_call_b.read_with(cx, |call, cx| { - assert_eq!(call.channel_id(cx), Some(channel_id)); - }) - }); - cx_b.read(|cx| { - client_b.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(channel_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); - }) - }); - - // Clients A and C also see that client B is in the channel. - cx_a.read(|cx| { - client_a.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(channel_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); - }) - }); - cx_c.read(|cx| { - client_c.channel_store().read_with(cx, |channels, _| { - assert_participants_eq( - channels.channel_participants(channel_id), - &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], - ); - }) - }); -} - -#[gpui::test] -async fn test_lost_channel_creation( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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 channel_id = server - .make_channel("x", None, (&client_a, cx_a), &mut []) - .await; - - // Invite a member - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.invite_member( - channel_id, - client_b.user_id().unwrap(), - proto::ChannelRole::Member, - cx, - ) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // Sanity check, B has the invitation - assert_channel_invitations( - client_b.channel_store(), - cx_b, - &[ExpectedChannel { - depth: 0, - id: channel_id, - name: "x".into(), - role: ChannelRole::Member, - }], - ); - - // A creates a subchannel while the invite is still pending. - let subchannel_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("subchannel", Some(channel_id), cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // Make sure A sees their new channel - assert_channels( - client_a.channel_store(), - cx_a, - &[ - ExpectedChannel { - depth: 0, - id: channel_id, - name: "x".into(), - role: ChannelRole::Admin, - }, - ExpectedChannel { - depth: 1, - id: subchannel_id, - name: "subchannel".into(), - role: ChannelRole::Admin, - }, - ], - ); - - // Client B accepts the invite - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.respond_to_channel_invite(channel_id, true, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // Client B should now see the channel - assert_channels( - client_b.channel_store(), - cx_b, - &[ - ExpectedChannel { - depth: 0, - id: channel_id, - name: "x".into(), - role: ChannelRole::Member, - }, - ExpectedChannel { - depth: 1, - id: subchannel_id, - name: "subchannel".into(), - role: ChannelRole::Member, - }, - ], - ); -} - -#[gpui::test] -async fn test_channel_link_notifications( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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 user_b = client_b.user_id().unwrap(); - let user_c = client_c.user_id().unwrap(); - - let channels = server - .make_channel_tree(&[("zed", None)], (&client_a, cx_a)) - .await; - let zed_channel = channels[0]; - - try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { - [ - channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), - channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Member, cx), - channel_store.invite_member(zed_channel, user_c, proto::ChannelRole::Guest, cx), - ] - })) - .await - .unwrap(); - - executor.run_until_parked(); - - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.respond_to_channel_invite(zed_channel, true, cx) - }) - .await - .unwrap(); - - client_c - .channel_store() - .update(cx_c, |channel_store, cx| { - channel_store.respond_to_channel_invite(zed_channel, true, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // we have an admin (a), member (b) and guest (c) all part of the zed channel. - - // create a new private channel, make it public, and move it under the previous one, and verify it shows for b and not c - let active_channel = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("active", Some(zed_channel), cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // the new channel shows for b and not c - assert_channels_list_shape( - client_a.channel_store(), - cx_a, - &[(zed_channel, 0), (active_channel, 1)], - ); - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[(zed_channel, 0), (active_channel, 1)], - ); - assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); - - let vim_channel = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("vim", None, cx) - }) - .await - .unwrap(); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx) - }) - .await - .unwrap(); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.move_channel(vim_channel, Some(active_channel), cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // the new channel shows for b and c - assert_channels_list_shape( - client_a.channel_store(), - cx_a, - &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], - ); - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], - ); - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[(zed_channel, 0), (vim_channel, 1)], - ); - - let helix_channel = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("helix", None, cx) - }) - .await - .unwrap(); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.move_channel(helix_channel, Some(vim_channel), cx) - }) - .await - .unwrap(); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility( - helix_channel, - proto::ChannelVisibility::Public, - cx, - ) - }) - .await - .unwrap(); - - // the new channel shows for b and c - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - (zed_channel, 0), - (active_channel, 1), - (vim_channel, 2), - (helix_channel, 3), - ], - ); - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)], - ); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // the members-only channel is still shown for c, but hidden for b - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - (zed_channel, 0), - (active_channel, 1), - (vim_channel, 2), - (helix_channel, 3), - ], - ); - cx_b.read(|cx| { - client_b.channel_store().read_with(cx, |channel_store, _| { - assert_eq!( - channel_store - .channel_for_id(vim_channel) - .unwrap() - .visibility, - proto::ChannelVisibility::Members - ) - }) - }); - - assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); -} - -#[gpui::test] -async fn test_channel_membership_notifications( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_c").await; - - let user_b = client_b.user_id().unwrap(); - - let channels = server - .make_channel_tree( - &[ - ("zed", None), - ("active", Some("zed")), - ("vim", Some("active")), - ], - (&client_a, cx_a), - ) - .await; - let zed_channel = channels[0]; - let _active_channel = channels[1]; - let vim_channel = channels[2]; - - try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { - [ - channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), - channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx), - channel_store.invite_member(vim_channel, user_b, proto::ChannelRole::Member, cx), - channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx), - ] - })) - .await - .unwrap(); - - executor.run_until_parked(); - - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.respond_to_channel_invite(zed_channel, true, cx) - }) - .await - .unwrap(); - - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.respond_to_channel_invite(vim_channel, true, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // we have an admin (a), and a guest (b) with access to all of zed, and membership in vim. - assert_channels( - client_b.channel_store(), - cx_b, - &[ - ExpectedChannel { - depth: 0, - id: zed_channel, - name: "zed".into(), - role: ChannelRole::Guest, - }, - ExpectedChannel { - depth: 1, - id: vim_channel, - name: "vim".into(), - role: ChannelRole::Member, - }, - ], - ); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.remove_member(vim_channel, user_b, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - assert_channels( - client_b.channel_store(), - cx_b, - &[ - ExpectedChannel { - depth: 0, - id: zed_channel, - name: "zed".into(), - role: ChannelRole::Guest, - }, - ExpectedChannel { - depth: 1, - id: vim_channel, - name: "vim".into(), - role: ChannelRole::Guest, - }, - ], - ) -} - -#[gpui::test] -async fn test_guest_access( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channels = server - .make_channel_tree( - &[("channel-a", None), ("channel-b", Some("channel-a"))], - (&client_a, cx_a), - ) - .await; - let channel_a = channels[0]; - let channel_b = channels[1]; - - let active_call_b = cx_b.read(ActiveCall::global); - - // Non-members should not be allowed to join - assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) - .await - .is_err()); - - // Make channels A and B public - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Public, cx) - }) - .await - .unwrap(); - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(channel_b, proto::ChannelVisibility::Public, cx) - }) - .await - .unwrap(); - - // Client B joins channel A as a guest - active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) - .await - .unwrap(); - - executor.run_until_parked(); - assert_channels_list_shape( - client_a.channel_store(), - cx_a, - &[(channel_a, 0), (channel_b, 1)], - ); - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[(channel_a, 0), (channel_b, 1)], - ); - - client_a.channel_store().update(cx_a, |channel_store, _| { - let participants = channel_store.channel_participants(channel_a); - assert_eq!(participants.len(), 1); - assert_eq!(participants[0].id, client_b.user_id().unwrap()); - }); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx) - }) - .await - .unwrap(); - - assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); - - active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b, cx)) - .await - .unwrap(); - - executor.run_until_parked(); - assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]); -} - -#[gpui::test] -async fn test_invite_access( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let channels = server - .make_channel_tree( - &[("channel-a", None), ("channel-b", Some("channel-a"))], - (&client_a, cx_a), - ) - .await; - let channel_a_id = channels[0]; - let channel_b_id = channels[0]; - - let active_call_b = cx_b.read(ActiveCall::global); - - // should not be allowed to join - assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) - .await - .is_err()); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.invite_member( - channel_a_id, - client_b.user_id().unwrap(), - ChannelRole::Member, - cx, - ) - }) - .await - .unwrap(); - - active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) - .await - .unwrap(); - - executor.run_until_parked(); - - client_b.channel_store().update(cx_b, |channel_store, _| { - assert!(channel_store.channel_for_id(channel_b_id).is_some()); - assert!(channel_store.channel_for_id(channel_a_id).is_some()); - }); - - client_a.channel_store().update(cx_a, |channel_store, _| { - let participants = channel_store.channel_participants(channel_b_id); - assert_eq!(participants.len(), 1); - assert_eq!(participants[0].id, client_b.user_id().unwrap()); - }) -} - -#[gpui::test] -async fn test_channel_moving( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - _cx_b: &mut TestAppContext, - _cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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, Some(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), - ], - ); -} - -#[derive(Debug, PartialEq)] -struct ExpectedChannel { - depth: usize, - id: ChannelId, - name: SharedString, - role: ChannelRole, -} - -#[track_caller] -fn assert_channel_invitations( - channel_store: &Model, - cx: &TestAppContext, - expected_channels: &[ExpectedChannel], -) { - let actual = cx.read(|cx| { - channel_store.read_with(cx, |store, _| { - store - .channel_invitations() - .iter() - .map(|channel| ExpectedChannel { - depth: 0, - name: channel.name.clone(), - id: channel.id, - role: channel.role, - }) - .collect::>() - }) - }); - assert_eq!(actual, expected_channels); -} - -#[track_caller] -fn assert_channels( - channel_store: &Model, - cx: &TestAppContext, - expected_channels: &[ExpectedChannel], -) { - let actual = cx.read(|cx| { - channel_store.read_with(cx, |store, _| { - store - .ordered_channels() - .map(|(depth, channel)| ExpectedChannel { - depth, - name: channel.name.clone().into(), - id: channel.id, - role: channel.role, - }) - .collect::>() - }) - }); - pretty_assertions::assert_eq!(actual, expected_channels); -} - -#[track_caller] -fn assert_channels_list_shape( - channel_store: &Model, - cx: &TestAppContext, - expected_channels: &[(u64, usize)], -) { - let actual = cx.read(|cx| { - channel_store.read_with(cx, |store, _| { - store - .ordered_channels() - .map(|(depth, channel)| (channel.id, depth)) - .collect::>() - }) - }); - pretty_assertions::assert_eq!(actual, expected_channels); -} diff --git a/crates/collab2/src/tests/editor_tests.rs b/crates/collab2/src/tests/editor_tests.rs deleted file mode 100644 index 07a4269567..0000000000 --- a/crates/collab2/src/tests/editor_tests.rs +++ /dev/null @@ -1,1889 +0,0 @@ -//todo(partially ported) -// use std::{ -// path::Path, -// sync::{ -// atomic::{self, AtomicBool, AtomicUsize}, -// Arc, -// }, -// }; - -// use call::ActiveCall; -// use editor::{ -// test::editor_test_context::{AssertionContextManager, EditorTestContext}, -// Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, -// ToggleCodeActions, Undo, -// }; -// use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext}; -// use indoc::indoc; -// use language::{ -// language_settings::{AllLanguageSettings, InlayHintSettings}, -// tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, -// }; -// use rpc::RECEIVE_TIMEOUT; -// use serde_json::json; -// use settings::SettingsStore; -// use text::Point; -// use workspace::Workspace; - -// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; - -// #[gpui::test(iterations = 10)] -// async fn test_host_disconnect( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor).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; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) -// .await; - -// cx_b.update(editor::init); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// serde_json::json!({ -// "a.txt": "a-contents", -// "b.txt": "b-contents", -// }), -// ) -// .await; - -// let active_call_a = cx_a.read(ActiveCall::global); -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - -// let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees().next().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; -// executor.run_until_parked(); - -// assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - -// let workspace_b = -// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); -// let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); - -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "b.txt"), None, true, cx) -// }) -// .unwrap() -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// //TODO: focus -// assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); -// editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); -// //todo(is_edited) -// // assert!(workspace_b.is_edited(cx_b)); - -// // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// project_a.read_with(cx_a, |project, _| project.collaborators().is_empty()); - -// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - -// project_b.read_with(cx_b, |project, _| project.is_read_only()); - -// assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - -// // Ensure client B's edited state is reset and that the whole window is blurred. - -// workspace_b.update(cx_b, |_, cx| { -// assert_eq!(cx.focused_view_id(), None); -// }); -// // assert!(!workspace_b.is_edited(cx_b)); - -// // Ensure client B is not prompted to save edits when closing window after disconnecting. -// let can_close = workspace_b -// .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) -// .await -// .unwrap(); -// assert!(can_close); - -// // Allow client A to reconnect to the server. -// server.allow_connections(); -// executor.advance_clock(RECEIVE_TIMEOUT); - -// // Client B calls client A again after they reconnected. -// let active_call_b = cx_b.read(ActiveCall::global); -// active_call_b -// .update(cx_b, |call, cx| { -// call.invite(client_a.user_id().unwrap(), None, cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// active_call_a -// .update(cx_a, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); - -// active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Drop client A's connection again. We should still unshare it successfully. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); -// } - -// #[gpui::test] -// async fn test_newline_above_or_below_does_not_move_guest_cursor( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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); - -// client_a -// .fs() -// .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; -// 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; - -// // Open a buffer as client A -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) -// .await -// .unwrap(); -// let window_a = cx_a.add_empty_window(); -// let editor_a = -// window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); -// let mut editor_cx_a = EditorTestContext { -// cx: cx_a, -// window: window_a.into(), -// editor: editor_a, -// assertion_cx: AssertionContextManager::new(), -// }; - -// // Open a buffer as client B -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_empty_window(); -// let editor_b = -// window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); -// let mut editor_cx_b = EditorTestContext { -// cx: cx_b, -// window: window_b.into(), -// editor: editor_b, -// assertion_cx: AssertionContextManager::new(), -// }; - -// // Test newline above -// editor_cx_a.set_selections_state(indoc! {" -// Some textˇ -// "}); -// editor_cx_b.set_selections_state(indoc! {" -// Some textˇ -// "}); -// editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); -// executor.run_until_parked(); -// editor_cx_a.assert_editor_state(indoc! {" -// ˇ -// Some text -// "}); -// editor_cx_b.assert_editor_state(indoc! {" - -// Some textˇ -// "}); - -// // Test newline below -// editor_cx_a.set_selections_state(indoc! {" - -// Some textˇ -// "}); -// editor_cx_b.set_selections_state(indoc! {" - -// Some textˇ -// "}); -// editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); -// executor.run_until_parked(); -// editor_cx_a.assert_editor_state(indoc! {" - -// Some text -// ˇ -// "}); -// editor_cx_b.assert_editor_state(indoc! {" - -// Some textˇ - -// "}); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_completion( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// 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; - -// // Open a file in an editor as the guest. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_empty_window(); -// let editor_b = window_b.build_view(cx_b, |cx| { -// Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) -// }); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// cx_a.foreground().run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert!(!buffer.completion_triggers().is_empty()) -// }); - -// // Type a completion trigger character as the guest. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(".", cx); -// cx.focus(&editor_b); -// }); - -// // Receive a completion request as the host's language server. -// // Return some completions from the host's language server. -// cx_a.foreground().start_waiting(); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// lsp::CompletionItem { -// label: "first_method(…)".into(), -// detail: Some("fn(&mut self, B) -> C".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "first_method($1)".to_string(), -// range: lsp::Range::new( -// lsp::Position::new(0, 14), -// lsp::Position::new(0, 14), -// ), -// })), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "second_method(…)".into(), -// detail: Some("fn(&mut self, C) -> D".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "second_method()".to_string(), -// range: lsp::Range::new( -// lsp::Position::new(0, 14), -// lsp::Position::new(0, 14), -// ), -// })), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }, -// ]))) -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); - -// // Open the buffer on the host. -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// cx_a.foreground().run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a. }") -// }); - -// // Confirm a completion on the guest. - -// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); -// editor_b.update(cx_b, |editor, cx| { -// editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); -// assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); -// }); - -// // Return a resolved completion from the host's language server. -// // The resolved completion has an additional text edit. -// fake_language_server.handle_request::( -// |params, _| async move { -// assert_eq!(params.label, "first_method(…)"); -// Ok(lsp::CompletionItem { -// label: "first_method(…)".into(), -// detail: Some("fn(&mut self, B) -> C".into()), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// new_text: "first_method($1)".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// })), -// additional_text_edits: Some(vec![lsp::TextEdit { -// new_text: "use d::SomeTrait;\n".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), -// }]), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// ..Default::default() -// }) -// }, -// ); - -// // The additional edit is applied. -// cx_a.executor().run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "use d::SomeTrait;\nfn main() { a.first_method() }" -// ); -// }); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!( -// buffer.text(), -// "use d::SomeTrait;\nfn main() { a.first_method() }" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_code_actions( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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_b.update(editor::init); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", -// "other.rs": "pub fn foo() -> usize { 4 }", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); - -// // Join the project as client B. -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let window_b = -// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); -// let workspace_b = window_b.root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let mut fake_language_server = fake_language_servers.next().await.unwrap(); -// let mut requests = fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!(params.range.start, lsp::Position::new(0, 0)); -// assert_eq!(params.range.end, lsp::Position::new(0, 0)); -// Ok(None) -// }); -// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); -// requests.next().await; - -// // Move cursor to a location that contains code actions. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) -// }); -// cx.focus(&editor_b); -// }); - -// let mut requests = fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!(params.range.start, lsp::Position::new(1, 31)); -// assert_eq!(params.range.end, lsp::Position::new(1, 31)); - -// Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( -// lsp::CodeAction { -// title: "Inline into all callers".to_string(), -// edit: Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(1, 22), -// lsp::Position::new(1, 34), -// ), -// "4".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/a/other.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 0), -// lsp::Position::new(0, 27), -// ), -// "".to_string(), -// )], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// }), -// data: Some(json!({ -// "codeActionParams": { -// "range": { -// "start": {"line": 1, "column": 31}, -// "end": {"line": 1, "column": 31}, -// } -// } -// })), -// ..Default::default() -// }, -// )])) -// }); -// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); -// requests.next().await; - -// // Toggle code actions and wait for them to display. -// editor_b.update(cx_b, |editor, cx| { -// editor.toggle_code_actions( -// &ToggleCodeActions { -// deployed_from_indicator: false, -// }, -// cx, -// ); -// }); -// cx_a.foreground().run_until_parked(); - -// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible())); - -// fake_language_server.remove_request_handler::(); - -// // Confirming the code action will trigger a resolve request. -// let confirm_action = workspace_b -// .update(cx_b, |workspace, cx| { -// Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx) -// }) -// .unwrap(); -// fake_language_server.handle_request::( -// |_, _| async move { -// Ok(lsp::CodeAction { -// title: "Inline into all callers".to_string(), -// edit: Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(1, 22), -// lsp::Position::new(1, 34), -// ), -// "4".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/a/other.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 0), -// lsp::Position::new(0, 27), -// ), -// "".to_string(), -// )], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// }), -// ..Default::default() -// }) -// }, -// ); - -// // After the action is confirmed, an editor containing both modified files is opened. -// confirm_action.await.unwrap(); - -// let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// code_action_editor.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); -// editor.undo(&Undo, cx); -// assert_eq!( -// editor.text(cx), -// "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" -// ); -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_collaborating_with_renames( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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_b.update(editor::init); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { -// prepare_provider: Some(true), -// work_done_progress_options: Default::default(), -// })), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/dir", -// json!({ -// "one.rs": "const ONE: usize = 1;", -// "two.rs": "const TWO: usize = one::ONE + one::ONE;" -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; -// 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 window_b = -// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); -// let workspace_b = window_b.root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "one.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let fake_language_server = fake_language_servers.next().await.unwrap(); - -// // Move cursor to a location that can be renamed. -// let prepare_rename = editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([7..7])); -// editor.rename(&Rename, cx).unwrap() -// }); - -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); -// assert_eq!(params.position, lsp::Position::new(0, 7)); -// Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( -// lsp::Position::new(0, 6), -// lsp::Position::new(0, 9), -// )))) -// }) -// .next() -// .await -// .unwrap(); -// prepare_rename.await.unwrap(); -// editor_b.update(cx_b, |editor, cx| { -// use editor::ToOffset; -// let rename = editor.pending_rename().unwrap(); -// let buffer = editor.buffer().read(cx).snapshot(cx); -// assert_eq!( -// rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), -// 6..9 -// ); -// rename.editor.update(cx, |rename_editor, cx| { -// rename_editor.buffer().update(cx, |rename_buffer, cx| { -// rename_buffer.edit([(0..3, "THREE")], None, cx); -// }); -// }); -// }); - -// let confirm_rename = workspace_b.update(cx_b, |workspace, cx| { -// Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() -// }); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri.as_str(), -// "file:///dir/one.rs" -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 6) -// ); -// assert_eq!(params.new_name, "THREE"); -// Ok(Some(lsp::WorkspaceEdit { -// changes: Some( -// [ -// ( -// lsp::Url::from_file_path("/dir/one.rs").unwrap(), -// vec![lsp::TextEdit::new( -// lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), -// "THREE".to_string(), -// )], -// ), -// ( -// lsp::Url::from_file_path("/dir/two.rs").unwrap(), -// vec![ -// lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 24), -// lsp::Position::new(0, 27), -// ), -// "THREE".to_string(), -// ), -// lsp::TextEdit::new( -// lsp::Range::new( -// lsp::Position::new(0, 35), -// lsp::Position::new(0, 38), -// ), -// "THREE".to_string(), -// ), -// ], -// ), -// ] -// .into_iter() -// .collect(), -// ), -// ..Default::default() -// })) -// }) -// .next() -// .await -// .unwrap(); -// confirm_rename.await.unwrap(); - -// let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// rename_editor.update(cx_b, |editor, cx| { -// assert_eq!( -// editor.text(cx), -// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" -// ); -// editor.undo(&Undo, cx); -// assert_eq!( -// editor.text(cx), -// "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" -// ); -// editor.redo(&Redo, cx); -// assert_eq!( -// editor.text(cx), -// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" -// ); -// }); - -// // Ensure temporary rename edits cannot be undone/redone. -// editor_b.update(cx_b, |editor, cx| { -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "const ONE: usize = 1;"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "const ONE: usize = 1;"); -// editor.redo(&Redo, cx); -// assert_eq!(editor.text(cx), "const THREE: usize = 1;"); -// }) -// } - -// #[gpui::test(iterations = 10)] -// async fn test_language_server_statuses( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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_b.update(editor::init); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name: "the-language-server", -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/dir", -// json!({ -// "main.rs": "const ONE: usize = 1;", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - -// let _buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// fake_language_server.start_progress("the-token").await; -// fake_language_server.notify::(lsp::ProgressParams { -// token: lsp::NumberOrString::String("the-token".to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( -// lsp::WorkDoneProgressReport { -// message: Some("the-message".to_string()), -// ..Default::default() -// }, -// )), -// }); -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message" -// ); -// }); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; - -// project_b.read_with(cx_b, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// }); - -// fake_language_server.notify::(lsp::ProgressParams { -// token: lsp::NumberOrString::String("the-token".to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report( -// lsp::WorkDoneProgressReport { -// message: Some("the-message-2".to_string()), -// ..Default::default() -// }, -// )), -// }); -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message-2" -// ); -// }); - -// project_b.read_with(cx_b, |project, _| { -// let status = project.language_server_statuses().next().unwrap(); -// assert_eq!(status.name, "the-language-server"); -// assert_eq!(status.pending_work.len(), 1); -// assert_eq!( -// status.pending_work["the-token"].message.as_ref().unwrap(), -// "the-message-2" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_share_project( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let window_b = cx_b.add_empty_window(); -// let mut server = TestServer::start(executor).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; -// server -// .make_contacts(&mut [(&client_a, cx_a), (&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); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// ".gitignore": "ignored-dir", -// "a.txt": "a-contents", -// "b.txt": "b-contents", -// "ignored-dir": { -// "c.txt": "", -// "d.txt": "", -// } -// }), -// ) -// .await; - -// // Invite client B to collaborate on a project -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| { -// call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) -// }) -// .await -// .unwrap(); - -// // Join that project as client B - -// let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); -// executor.run_until_parked(); -// let call = incoming_call_b.borrow().clone().unwrap(); -// assert_eq!(call.calling_user.github_login, "user_a"); -// let initial_project = call.initial_project.unwrap(); -// active_call_b -// .update(cx_b, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); -// let client_b_peer_id = client_b.peer_id().unwrap(); -// let project_b = client_b -// .build_remote_project(initial_project.id, cx_b) -// .await; - -// let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); - -// executor.run_until_parked(); - -// project_a.read_with(cx_a, |project, _| { -// let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); -// assert_eq!(client_b_collaborator.replica_id, replica_id_b); -// }); - -// project_b.read_with(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap().read(cx); -// assert_eq!( -// worktree.paths().map(AsRef::as_ref).collect::>(), -// [ -// Path::new(".gitignore"), -// Path::new("a.txt"), -// Path::new("b.txt"), -// Path::new("ignored-dir"), -// ] -// ); -// }); - -// project_b -// .update(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap(); -// let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); -// project.expand_entry(worktree_id, entry.id, cx).unwrap() -// }) -// .await -// .unwrap(); - -// project_b.read_with(cx_b, |project, cx| { -// let worktree = project.worktrees().next().unwrap().read(cx); -// assert_eq!( -// worktree.paths().map(AsRef::as_ref).collect::>(), -// [ -// Path::new(".gitignore"), -// Path::new("a.txt"), -// Path::new("b.txt"), -// Path::new("ignored-dir"), -// Path::new("ignored-dir/c.txt"), -// Path::new("ignored-dir/d.txt"), -// ] -// ); -// }); - -// // Open the same file as client B and client A. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) -// .await -// .unwrap(); - -// buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); - -// project_a.read_with(cx_a, |project, cx| { -// assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) -// }); -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) -// .await -// .unwrap(); - -// let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); - -// // Client A sees client B's selection -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// buffer -// .snapshot() -// .remote_selections_in_range(Anchor::MIN..Anchor::MAX) -// .count() -// == 1 -// }); - -// // Edit the buffer as client B and see that edit as client A. -// editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "ok, b-contents") -// }); - -// // Client B can invite client C on a project shared by client A. -// active_call_b -// .update(cx_b, |call, cx| { -// call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) -// }) -// .await -// .unwrap(); - -// let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); -// executor.run_until_parked(); -// let call = incoming_call_c.borrow().clone().unwrap(); -// assert_eq!(call.calling_user.github_login, "user_b"); -// let initial_project = call.initial_project.unwrap(); -// active_call_c -// .update(cx_c, |call, cx| call.accept_incoming(cx)) -// .await -// .unwrap(); -// let _project_c = client_c -// .build_remote_project(initial_project.id, cx_c) -// .await; - -// // Client B closes the editor, and client A sees client B's selections removed. -// cx_b.update(move |_| drop(editor_b)); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// buffer -// .snapshot() -// .remote_selections_in_range(Anchor::MIN..Anchor::MAX) -// .count() -// == 0 -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_host_to_guest( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// 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; - -// // Open a file in an editor as the host. -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_a = cx_a.add_empty_window(); -// let editor_a = window_a -// .update(cx_a, |_, cx| { -// cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) -// }) -// .unwrap(); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); - -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// fake_language_server.handle_request::( -// |params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~<".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }, -// ); - -// // Open the buffer on the guest and see that the formattings worked -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); - -// // Type a on type formatting trigger character as the guest. -// editor_a.update(cx_a, |editor, cx| { -// cx.focus(&editor_a); -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(">", cx); -// }); - -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a>~< }") -// }); - -// // Undo should remove LSP edits first -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a>~< }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// }); -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a> }") -// }); - -// editor_a.update(cx_a, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a> }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); - -// buffer_b.read_with(cx_b, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_on_input_format_from_guest_to_host( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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); - -// // Set up a fake language server. -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { -// first_trigger_character: ":".to_string(), -// more_trigger_character: Some(vec![">".to_string()]), -// }), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// client_a.language_registry().add(Arc::new(language)); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a }", -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// 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; - -// // Open a file in an editor as the guest. -// let buffer_b = project_b -// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// let window_b = cx_b.add_empty_window(); -// let editor_b = window_b.build_view(cx_b, |cx| { -// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) -// }); - -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// executor.run_until_parked(); -// // Type a on type formatting trigger character as the guest. -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); - -// // Receive an OnTypeFormatting request as the host's language server. -// // Return some formattings from the host's language server. -// cx_a.foreground().start_waiting(); -// fake_language_server -// .handle_request::(|params, _| async move { -// assert_eq!( -// params.text_document_position.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// assert_eq!( -// params.text_document_position.position, -// lsp::Position::new(0, 14), -// ); - -// Ok(Some(vec![lsp::TextEdit { -// new_text: "~:".to_string(), -// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)), -// }])) -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); - -// // Open the buffer on the host and see that the formattings worked -// let buffer_a = project_a -// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a:~: }") -// }); - -// // Undo should remove LSP edits first -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a:~: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// }); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a: }") -// }); - -// editor_b.update(cx_b, |editor, cx| { -// assert_eq!(editor.text(cx), "fn main() { a: }"); -// editor.undo(&Undo, cx); -// assert_eq!(editor.text(cx), "fn main() { a }"); -// }); -// executor.run_until_parked(); - -// buffer_a.read_with(cx_a, |buffer, _| { -// assert_eq!(buffer.text(), "fn main() { a }") -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_mutual_editor_inlay_hint_cache_update( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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); - -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: false, -// show_other_hints: true, -// }) -// }); -// }); -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); - -// // Client A opens a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .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(); - -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a); -// cx_a.foreground().start_waiting(); - -// // The host opens a rust file. -// let _buffer_a = project_a -// .update(cx_a, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Set up the language server to return an additional inlay hint on each request. -// let edits_made = Arc::new(AtomicUsize::new(0)); -// let closure_edits_made = Arc::clone(&edits_made); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_edits_made = Arc::clone(&closure_edits_made); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let edits_made = task_edits_made.load(atomic::Ordering::Acquire); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, edits_made as u32), -// label: lsp::InlayHintLabel::String(edits_made.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); - -// executor.run_until_parked(); - -// let initial_edit = edits_made.load(atomic::Ordering::Acquire); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Host should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Host editor update the cache version after every cache/view change", -// ); -// }); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![initial_edit.to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Guest editor update the cache version after every cache/view change" -// ); -// }); - -// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_b.update(cx_b, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); -// editor.handle_input(":", cx); -// cx.focus(&editor_b); -// }); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_client_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 2); -// }); - -// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// editor_a.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("a change to increment both buffers' versions", cx); -// cx.focus(&editor_a); -// }); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_host_edit.to_string()], -// extract_hint_labels(editor), -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!(inlay_cache.version(), 3); -// }); - -// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Host should react to /refresh LSP request" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Host should accepted all edits and bump its cache version every time" -// ); -// }); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec![after_special_edit_for_refresh.to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 4, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_inlay_hint_refresh_is_forwarded( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(&executor).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); - -// cx_a.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: false, -// show_parameter_hints: false, -// show_other_hints: false, -// }) -// }); -// }); -// }); -// cx_b.update(|cx| { -// cx.update_global(|store: &mut SettingsStore, cx| { -// store.update_user_settings::(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// }); -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_language_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// client_a.language_registry().add(Arc::clone(&language)); -// client_b.language_registry().add(language); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .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 workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// cx_a.foreground().start_waiting(); -// cx_b.foreground().start_waiting(); - -// let editor_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let editor_b = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let other_hints = Arc::new(AtomicBool::new(false)); -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// let closure_other_hints = Arc::clone(&other_hints); -// fake_language_server -// .handle_request::(move |params, _| { -// let task_other_hints = Arc::clone(&closure_other_hints); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let other_hints = task_other_hints.load(atomic::Ordering::Acquire); -// let character = if other_hints { 0 } else { 2 }; -// let label = if other_hints { -// "other hint" -// } else { -// "initial hint" -// }; -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, character), -// label: lsp::InlayHintLabel::String(label.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await -// .unwrap(); -// cx_a.foreground().finish_waiting(); -// cx_b.foreground().finish_waiting(); - -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get no hints due to them turned off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates" -// ); -// }); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["initial hint".to_string()], -// extract_hint_labels(editor), -// "Client should get its first hints when opens an editor" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 1, -// "Should update cache verison after first hints" -// ); -// }); - -// other_hints.fetch_or(true, atomic::Ordering::Release); -// fake_language_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// executor.run_until_parked(); -// editor_a.update(cx_a, |editor, _| { -// assert!( -// extract_hint_labels(editor).is_empty(), -// "Host should get nop hints due to them turned off, even after the /refresh" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 0, -// "Turned off hints should not generate version updates, again" -// ); -// }); - -// executor.run_until_parked(); -// editor_b.update(cx_b, |editor, _| { -// assert_eq!( -// vec!["other hint".to_string()], -// extract_hint_labels(editor), -// "Guest should get a /refresh LSP request propagated by host despite host hints are off" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.version(), -// 2, -// "Guest should accepted all edits and bump its cache version every time" -// ); -// }); -// } - -// fn extract_hint_labels(editor: &Editor) -> Vec { -// let mut labels = Vec::new(); -// for hint in editor.inlay_hint_cache().hints() { -// match hint.label { -// project::InlayHintLabel::String(s) => labels.push(s), -// _ => unreachable!(), -// } -// } -// labels -// } diff --git a/crates/collab2/src/tests/following_tests.rs b/crates/collab2/src/tests/following_tests.rs deleted file mode 100644 index 5178df408f..0000000000 --- a/crates/collab2/src/tests/following_tests.rs +++ /dev/null @@ -1,1890 +0,0 @@ -//todo!(workspace) - -// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -// use call::ActiveCall; -// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; -// use editor::{Editor, ExcerptRange, MultiBuffer}; -// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext}; -// use live_kit_client::MacOSDisplay; -// use project::project_settings::ProjectSettings; -// use rpc::proto::PeerId; -// use serde_json::json; -// use settings::SettingsStore; -// use std::borrow::Cow; -// 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( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// cx_d: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).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).unwrap(); -// let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b).unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; -// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c); -// let cx_c = &mut cx_c; -// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d); -// let cx_d = &mut cx_d; - -// // Client A opens some editors. -// let pane_a = workspace_a.update(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.executor().run_until_parked(); -// let editor_b2 = workspace_b.update(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.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![2..1] -// ); -// assert_eq!( -// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![3..2] -// ); - -// cx_c.executor().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).unwrap(); -// 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.executor().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) -// .unwrap(); -// 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.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b, peer_id_c])], -// "followers seen by {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.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b])], -// "followers seen by {name}" -// ); -// } - -// // Client C re-follows client A. -// workspace_c -// .update(cx_c, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// // All clients see that clients B and C are following client A. -// cx_c.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b, peer_id_c])], -// "followers seen by {name}" -// ); -// } - -// // Client D follows client B, then switches to following client C. -// workspace_d -// .update(cx_d, |workspace, cx| { -// workspace.follow(peer_id_b, cx).unwrap() -// }) -// .await -// .unwrap(); -// 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.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[ -// (peer_id_a, vec![peer_id_b, peer_id_c]), -// (peer_id_c, vec![peer_id_d]) -// ], -// "followers seen by {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.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b]),], -// "followers seen by {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) -// }); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// // When client A opens a multibuffer, client B does so as well. -// let multibuffer_a = cx_a.build_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.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); -// workspace.add_item(Box::new(editor.clone()), cx); -// editor -// }); -// executor.run_until_parked(); -// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), -// multibuffer_editor_b.update(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(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b2.item_id() -// ); -// }); - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_forward(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_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])); -// }); -// executor.run_until_parked(); -// editor_b1.update(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)); -// executor.run_until_parked(); -// editor_b1.update(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(point(0., 100.), cx); -// }); -// executor.run_until_parked(); -// editor_b1.update(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) -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// editor_b1.item_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.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), -// Some(peer_id_b) -// ); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// editor_a1.item_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(); -// executor.run_until_parked(); -// let shared_screen = workspace_a.update(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(); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_a1.item_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) -// }); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// multibuffer_editor_a.item_id() -// ) -// }); - -// // Client B activates a panel, and the previously-opened screen-sharing item gets activated. -// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left)); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.add_panel(panel, cx); -// workspace.toggle_panel_focus::(cx); -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// shared_screen.item_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); -// }); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// multibuffer_editor_a.item_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.build_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) -// }) -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// shared_screen.item_id() -// ); - -// // Following interrupts when client B disconnects. -// client_b.disconnect(&cx_b.to_async()); -// executor.advance_clock(RECONNECT_TIMEOUT); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), -// None -// ); -// } - -// #[gpui::test] -// async fn test_following_tab_order( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).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) -// .unwrap(); -// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); - -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); -// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); - -// let client_b_id = project_a.update(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: &View, 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(); -// executor.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"]); -// executor.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( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).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) -// .unwrap(); -// 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) -// .unwrap(); -// 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)); -// executor.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(); -// executor.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)); -// executor.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)); -// executor.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); -// }); -// }); -// executor.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(); -// }); -// }); - -// executor.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); -// }); -// }); -// executor.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); -// }); -// }); -// executor.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); -// }); -// }); -// executor.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( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// // 2 clients connect to a server. -// let mut server = TestServer::start(executor.clone()).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(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; - -// // Client A opens some editors. -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// 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) -// .unwrap(); -// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); -// let leader_id = project_b.update(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.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); -// let editor_b2 = workspace_b.update(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.update(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.update(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.update(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.update(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(point(0., 3.), cx) -// }); -// assert_eq!( -// workspace_b.update(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.update(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.update(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.update(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.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_peers_simultaneously_following_each_other( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).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) -// .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; -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); - -// executor.run_until_parked(); -// let client_a_id = project_b.update(cx_b, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); -// let client_b_id = project_a.update(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.update(cx_a, |workspace, _| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_b_id) -// ); -// }); -// workspace_b.update(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( -// executor: BackgroundExecutor, -// 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 -// let mut server = TestServer::start(executor.clone()).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) -// .unwrap(); -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); - -// 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(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) -// }) -// .await -// .unwrap(); - -// executor.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() -// }); - -// executor.run_until_parked(); -// let workspace_b_project_a = cx_b -// .windows() -// .iter() -// .max_by_key(|window| window.item_id()) -// .unwrap() -// .downcast::() -// .unwrap() -// .root(cx_b) -// .unwrap(); - -// // 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(); - -// executor.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() -// }); - -// executor.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(), "x.rs".into()); -// }); - -// // 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(); - -// executor.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(); - -// executor.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.item_id()) -// .unwrap() -// .downcast::() -// .unwrap() -// .root(cx_a) -// .unwrap(); - -// 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")); -// }); -// } - -// #[gpui::test] -// async fn test_following_into_excluded_file( -// executor: BackgroundExecutor, -// mut cx_a: &mut TestAppContext, -// mut cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// for cx in [&mut cx_a, &mut cx_b] { -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, |project_settings| { -// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); -// }); -// }); -// }); -// } -// 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!({ -// ".git": { -// "COMMIT_EDITMSG": "write your commit message here", -// }, -// "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).unwrap(); -// let peer_id_a = client_a.peer_id().unwrap(); -// let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b).unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; - -// // Client A opens editors for a regular file and an excluded file. -// let editor_for_regular = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor_for_excluded_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Client A updates their selections in those editors -// editor_for_regular.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_for_excluded_a.update(cx_a, |editor, cx| { -// editor.select_all(&Default::default(), cx); -// editor.handle_input("new commit message", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![18..17]); -// }); - -// // When client B starts following client A, currently visible file is replicated -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), -// Some((worktree_id, ".git/COMMIT_EDITMSG").into()) -// ); -// assert_eq!( -// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![18..17] -// ); - -// // Changes from B to the excluded file are replicated in A's editor -// editor_for_excluded_b.update(cx_b, |editor, cx| { -// editor.handle_input("\nCo-Authored-By: B ", cx); -// }); -// executor.run_until_parked(); -// editor_for_excluded_a.update(cx_a, |editor, cx| { -// assert_eq!( -// editor.text(cx), -// "new commit messag\nCo-Authored-By: B " -// ); -// }); -// } - -// fn visible_push_notifications( -// cx: &mut TestAppContext, -// ) -> Vec> { -// let mut ret = Vec::new(); -// for window in cx.windows() { -// window.update(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 followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec)> { -// cx.read(|cx| { -// let active_call = ActiveCall::global(cx).read(cx); -// let peer_id = active_call.client().peer_id(); -// let room = active_call.room().unwrap().read(cx); -// let mut result = room -// .remote_participants() -// .values() -// .map(|participant| participant.peer_id) -// .chain(peer_id) -// .filter_map(|peer_id| { -// let followers = room.followers_for(peer_id, project_id); -// if followers.is_empty() { -// None -// } else { -// Some((peer_id, followers.to_vec())) -// } -// }) -// .collect::>(); -// result.sort_by_key(|e| e.0); -// result -// }) -// } - -// fn pane_summaries(workspace: &View, cx: &mut WindowContext<'_>) -> Vec { -// workspace.update(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/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs deleted file mode 100644 index 201ba07dbb..0000000000 --- a/crates/collab2/src/tests/integration_tests.rs +++ /dev/null @@ -1,5722 +0,0 @@ -use crate::{ - rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer}, -}; -use call::{room, ActiveCall, ParticipantLocation, Room}; -use client::{User, RECEIVE_TIMEOUT}; -use collections::{HashMap, HashSet}; -use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; -use futures::StreamExt as _; -use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext}; -use language::{ - language_settings::{AllLanguageSettings, Formatter}, - tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, - LineEnding, OffsetRangeExt, Point, Rope, -}; -use live_kit_client::MacOSDisplay; -use lsp::LanguageServerId; -use project::{ - search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, -}; -use rand::prelude::*; -use serde_json::json; -use settings::SettingsStore; -use std::{ - cell::{Cell, RefCell}, - env, future, mem, - path::{Path, PathBuf}, - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, - Arc, - }, -}; -use unindent::Unindent as _; - -#[ctor::ctor] -fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } -} - -#[gpui::test(iterations = 10)] -async fn test_basic_calls( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_b2: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .make_contacts(&mut [(&client_a, cx_a), (&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); - - // Call user B 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()); - executor.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 B connects via another client and also receives a ring on the newly-connected client. - let _client_b2 = server.create_client(cx_b2, "user_b").await; - let active_call_b2 = cx_b2.read(ActiveCall::global); - - let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming()); - executor.run_until_parked(); - let call_b2 = incoming_call_b2.next().await.unwrap().unwrap(); - assert_eq!(call_b2.calling_user.github_login, "user_a"); - - // User B joins the room using the first client. - 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()); - - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: Default::default() - } - ); - - // Call user C from client B. - - let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); - active_call_b - .update(cx_b, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec!["user_c".to_string()] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: vec!["user_c".to_string()] - } - ); - - // User C receives the call, but declines it. - let call_c = incoming_call_c.next().await.unwrap().unwrap(); - assert_eq!(call_c.calling_user.github_login, "user_b"); - active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap()); - assert!(incoming_call_c.next().await.unwrap().is_none()); - - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: Default::default() - } - ); - - // Call user C again from user A. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec!["user_c".to_string()] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: vec!["user_c".to_string()] - } - ); - - // User C accepts the call. - let call_c = incoming_call_c.next().await.unwrap().unwrap(); - assert_eq!(call_c.calling_user.github_login, "user_a"); - active_call_c - .update(cx_c, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - assert!(incoming_call_c.next().await.unwrap().is_none()); - - let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); - - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string(), "user_c".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string(), "user_c".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec!["user_a".to_string(), "user_b".to_string()], - pending: Default::default() - } - ); - - // User A shares their screen - let display = MacOSDisplay::new(); - let events_b = active_call_events(cx_b); - let events_c = active_call_events(cx_c); - 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(); - - executor.run_until_parked(); - - // User B observes the remote screen sharing track. - assert_eq!(events_b.borrow().len(), 1); - let event_b = events_b.borrow().first().unwrap().clone(); - if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b { - assert_eq!(participant_id, client_a.peer_id().unwrap()); - - room_b.read_with(cx_b, |room, _| { - assert_eq!( - room.remote_participants()[&client_a.user_id().unwrap()] - .video_tracks - .len(), - 1 - ); - }); - } else { - panic!("unexpected event") - } - - // User C observes the remote screen sharing track. - assert_eq!(events_c.borrow().len(), 1); - let event_c = events_c.borrow().first().unwrap().clone(); - if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c { - assert_eq!(participant_id, client_a.peer_id().unwrap()); - - room_c.read_with(cx_c, |room, _| { - assert_eq!( - room.remote_participants()[&client_a.user_id().unwrap()] - .video_tracks - .len(), - 1 - ); - }); - } else { - panic!("unexpected event") - } - - // User A leaves the room. - active_call_a - .update(cx_a, |call, cx| { - let hang_up = call.hang_up(cx); - assert!(call.room().is_none()); - hang_up - }) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_c".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: Default::default() - } - ); - - // User B gets disconnected from the LiveKit server, which causes them - // to automatically leave the room. User C leaves the room as well because - // nobody else is in there. - server - .test_live_kit_server - .disconnect_client(client_b.user_id().unwrap().to_string()) - .await; - executor.run_until_parked(); - - active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none())); - - active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none())); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); -} - -#[gpui::test(iterations = 10)] -async fn test_calling_multiple_users_simultaneously( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, - cx_d: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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 - .make_contacts(&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); - let active_call_c = cx_c.read(ActiveCall::global); - let active_call_d = cx_d.read(ActiveCall::global); - - // Simultaneously call user B and user C from client A. - let b_invite = active_call_a.update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }); - let c_invite = active_call_a.update(cx_a, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }); - b_invite.await.unwrap(); - c_invite.await.unwrap(); - - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: vec!["user_b".to_string(), "user_c".to_string()] - } - ); - - // Call client D from client A. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_d.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: vec![ - "user_b".to_string(), - "user_c".to_string(), - "user_d".to_string() - ] - } - ); - - // Accept the call on all clients simultaneously. - let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx)); - let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx)); - let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx)); - accept_b.await.unwrap(); - accept_c.await.unwrap(); - accept_d.await.unwrap(); - - executor.run_until_parked(); - - let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); - - let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); - - let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone()); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec![ - "user_b".to_string(), - "user_c".to_string(), - "user_d".to_string(), - ], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec![ - "user_a".to_string(), - "user_c".to_string(), - "user_d".to_string(), - ], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec![ - "user_a".to_string(), - "user_b".to_string(), - "user_d".to_string(), - ], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_d, cx_d), - RoomParticipants { - remote: vec![ - "user_a".to_string(), - "user_b".to_string(), - "user_c".to_string(), - ], - pending: Default::default() - } - ); -} - -#[gpui::test(iterations = 10)] -async fn test_joining_channels_and_calling_multiple_users_simultaneously( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - - let channel_1 = server - .make_channel( - "channel1", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b), (&client_c, cx_c)], - ) - .await; - - let channel_2 = server - .make_channel( - "channel2", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b), (&client_c, cx_c)], - ) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - - // Simultaneously join channel 1 and then channel 2 - active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_1, cx)) - .detach(); - let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx)); - - join_channel_2.await.unwrap(); - - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - executor.run_until_parked(); - - assert_eq!(channel_id(&room_a, cx_a), Some(channel_2)); - - // Leave the room - active_call_a - .update(cx_a, |call, cx| { - let hang_up = call.hang_up(cx); - hang_up - }) - .await - .unwrap(); - - // Initiating invites and then joining a channel should fail gracefully - let b_invite = active_call_a.update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }); - let c_invite = active_call_a.update(cx_a, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }); - - let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); - - b_invite.await.unwrap(); - c_invite.await.unwrap(); - join_channel.await.unwrap(); - - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - executor.run_until_parked(); - - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: vec!["user_b".to_string(), "user_c".to_string()] - } - ); - - assert_eq!(channel_id(&room_a, cx_a), None); - - // Leave the room - active_call_a - .update(cx_a, |call, cx| { - let hang_up = call.hang_up(cx); - hang_up - }) - .await - .unwrap(); - - // Simultaneously join channel 1 and call user B and user C from client A. - let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); - - let b_invite = active_call_a.update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }); - let c_invite = active_call_a.update(cx_a, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }); - - join_channel.await.unwrap(); - b_invite.await.unwrap(); - c_invite.await.unwrap(); - - active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - executor.run_until_parked(); -} - -#[gpui::test(iterations = 10)] -async fn test_room_uniqueness( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_a2: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_b2: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let _client_a2 = server.create_client(cx_a2, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let _client_b2 = server.create_client(cx_b2, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - server - .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_a2 = cx_a2.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let active_call_b2 = cx_b2.read(ActiveCall::global); - let active_call_c = cx_c.read(ActiveCall::global); - - // Call user B from client A. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - - // Ensure a new room can't be created given user A just created one. - active_call_a2 - .update(cx_a2, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }) - .await - .unwrap_err(); - - active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none())); - - // User B receives the call from user A. - - let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); - let call_b1 = incoming_call_b.next().await.unwrap().unwrap(); - assert_eq!(call_b1.calling_user.github_login, "user_a"); - - // Ensure calling users A and B from client C fails. - active_call_c - .update(cx_c, |call, cx| { - call.invite(client_a.user_id().unwrap(), None, cx) - }) - .await - .unwrap_err(); - active_call_c - .update(cx_c, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap_err(); - - // Ensure User B can't create a room while they still have an incoming call. - active_call_b2 - .update(cx_b2, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }) - .await - .unwrap_err(); - - active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none())); - - // User B joins the room and calling them after they've joined still fails. - active_call_b - .update(cx_b, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - active_call_c - .update(cx_c, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap_err(); - - // Ensure User B can't create a room while they belong to another room. - active_call_b2 - .update(cx_b2, |call, cx| { - call.invite(client_c.user_id().unwrap(), None, cx) - }) - .await - .unwrap_err(); - - active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none())); - - // Client C can successfully call client B after client B leaves the room. - active_call_b - .update(cx_b, |call, cx| call.hang_up(cx)) - .await - .unwrap(); - executor.run_until_parked(); - active_call_c - .update(cx_c, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - let call_b2 = incoming_call_b.next().await.unwrap().unwrap(); - assert_eq!(call_b2.calling_user.github_login, "user_c"); -} - -#[gpui::test(iterations = 10)] -async fn test_client_disconnecting_from_room( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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 user B 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()); - - // User B receives the call and joins the room. - - let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); - incoming_call_b.next().await.unwrap().unwrap(); - 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()); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: Default::default() - } - ); - - // User A automatically reconnects to the room upon disconnection. - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: Default::default() - } - ); - - // When user A disconnects, both client A and B clear their room on the active call. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none())); - - active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none())); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); - - // Allow user A to reconnect to the server. - server.allow_connections(); - executor.advance_clock(RECEIVE_TIMEOUT); - - // Call user B again 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()); - - // User B receives the call and joins the room. - - let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); - incoming_call_b.next().await.unwrap().unwrap(); - 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()); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: Default::default() - } - ); - - // User B gets disconnected from the LiveKit server, which causes it - // to automatically leave the room. - server - .test_live_kit_server - .disconnect_client(client_b.user_id().unwrap().to_string()) - .await; - executor.run_until_parked(); - active_call_a.update(cx_a, |call, _| assert!(call.room().is_none())); - active_call_b.update(cx_b, |call, _| assert!(call.room().is_none())); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); -} - -#[gpui::test(iterations = 10)] -async fn test_server_restarts( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, - cx_d: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - client_a - .fs() - .insert_tree("/a", json!({ "a.txt": "a-contents" })) - .await; - - // Invite client B to collaborate on a project - let (project_a, _) = client_a.build_local_project("/a", cx_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 - .make_contacts(&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); - let active_call_c = cx_c.read(ActiveCall::global); - let active_call_d = cx_d.read(ActiveCall::global); - - // User A calls users B, C, and D. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) - }) - .await - .unwrap(); - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx) - }) - .await - .unwrap(); - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx) - }) - .await - .unwrap(); - - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - - // User B receives the call and joins the room. - - let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); - assert!(incoming_call_b.next().await.unwrap().is_some()); - 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()); - - // User C receives the call and joins the room. - - let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); - assert!(incoming_call_c.next().await.unwrap().is_some()); - active_call_c - .update(cx_c, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - - let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone()); - - // User D receives the call but doesn't join the room yet. - - let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming()); - assert!(incoming_call_d.next().await.unwrap().is_some()); - - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string(), "user_c".to_string()], - pending: vec!["user_d".to_string()] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string(), "user_c".to_string()], - pending: vec!["user_d".to_string()] - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec!["user_a".to_string(), "user_b".to_string()], - pending: vec!["user_d".to_string()] - } - ); - - // The server is torn down. - server.reset().await; - - // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room. - client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - executor.advance_clock(RECONNECT_TIMEOUT); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string(), "user_c".to_string()], - pending: vec!["user_d".to_string()] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string(), "user_c".to_string()], - pending: vec!["user_d".to_string()] - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec![], - pending: vec![] - } - ); - - // User D is notified again of the incoming call and accepts it. - assert!(incoming_call_d.next().await.unwrap().is_some()); - active_call_d - .update(cx_d, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - executor.run_until_parked(); - - let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone()); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec![ - "user_b".to_string(), - "user_c".to_string(), - "user_d".to_string(), - ], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec![ - "user_a".to_string(), - "user_c".to_string(), - "user_d".to_string(), - ], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec![], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_d, cx_d), - RoomParticipants { - remote: vec![ - "user_a".to_string(), - "user_b".to_string(), - "user_c".to_string(), - ], - pending: vec![] - } - ); - - // The server finishes restarting, cleaning up stale connections. - server.start().await.unwrap(); - executor.advance_clock(CLEANUP_TIMEOUT); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string(), "user_d".to_string()], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string(), "user_d".to_string()], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec![], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_d, cx_d), - RoomParticipants { - remote: vec!["user_a".to_string(), "user_b".to_string()], - pending: vec![] - } - ); - - // User D hangs up. - active_call_d - .update(cx_d, |call, cx| call.hang_up(cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_c, cx_c), - RoomParticipants { - remote: vec![], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_d, cx_d), - RoomParticipants { - remote: vec![], - pending: vec![] - } - ); - - // User B calls user D again. - active_call_b - .update(cx_b, |call, cx| { - call.invite(client_d.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - - // User D receives the call but doesn't join the room yet. - - let mut incoming_call_d = active_call_d.read_with(cx_d, |call, _| call.incoming()); - assert!(incoming_call_d.next().await.unwrap().is_some()); - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec!["user_d".to_string()] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: vec!["user_d".to_string()] - } - ); - - // The server is torn down. - server.reset().await; - - // Users A and B have troubles reconnecting, so they leave the room. - client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - executor.advance_clock(RECONNECT_TIMEOUT); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec![], - pending: vec![] - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec![], - pending: vec![] - } - ); - - // User D is notified again of the incoming call but doesn't accept it. - assert!(incoming_call_d.next().await.unwrap().is_some()); - - // The server finishes restarting, cleaning up stale connections and canceling the - // call to user D because the room has become empty. - server.start().await.unwrap(); - executor.advance_clock(CLEANUP_TIMEOUT); - assert!(incoming_call_d.next().await.unwrap().is_none()); -} - -#[gpui::test(iterations = 10)] -async fn test_calls_on_multiple_connections( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b1: &mut TestAppContext, - cx_b2: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b1 = server.create_client(cx_b1, "user_b").await; - let client_b2 = server.create_client(cx_b2, "user_b").await; - server - .make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)]) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b1 = cx_b1.read(ActiveCall::global); - let active_call_b2 = cx_b2.read(ActiveCall::global); - - let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming()); - - let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming()); - assert!(incoming_call_b1.next().await.unwrap().is_none()); - assert!(incoming_call_b2.next().await.unwrap().is_none()); - - // Call user B from client A, ensuring both clients for user B ring. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b1.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_some()); - assert!(incoming_call_b2.next().await.unwrap().is_some()); - - // User B declines the call on one of the two connections, causing both connections - // to stop ringing. - active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap()); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_none()); - assert!(incoming_call_b2.next().await.unwrap().is_none()); - - // Call user B again from client A. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b1.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_some()); - assert!(incoming_call_b2.next().await.unwrap().is_some()); - - // User B accepts the call on one of the two connections, causing both connections - // to stop ringing. - active_call_b2 - .update(cx_b2, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_none()); - assert!(incoming_call_b2.next().await.unwrap().is_none()); - - // User B disconnects the client that is not on the call. Everything should be fine. - client_b1.disconnect(&cx_b1.to_async()); - executor.advance_clock(RECEIVE_TIMEOUT); - client_b1 - .authenticate_and_connect(false, &cx_b1.to_async()) - .await - .unwrap(); - - // User B hangs up, and user A calls them again. - active_call_b2 - .update(cx_b2, |call, cx| call.hang_up(cx)) - .await - .unwrap(); - executor.run_until_parked(); - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b1.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_some()); - assert!(incoming_call_b2.next().await.unwrap().is_some()); - - // User A cancels the call, causing both connections to stop ringing. - active_call_a - .update(cx_a, |call, cx| { - call.cancel_invite(client_b1.user_id().unwrap(), cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_none()); - assert!(incoming_call_b2.next().await.unwrap().is_none()); - - // User A calls user B again. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b1.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_some()); - assert!(incoming_call_b2.next().await.unwrap().is_some()); - - // User A hangs up, causing both connections to stop ringing. - active_call_a - .update(cx_a, |call, cx| call.hang_up(cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_none()); - assert!(incoming_call_b2.next().await.unwrap().is_none()); - - // User A calls user B again. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b1.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_some()); - assert!(incoming_call_b2.next().await.unwrap().is_some()); - - // User A disconnects, causing both connections to stop ringing. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - assert!(incoming_call_b1.next().await.unwrap().is_none()); - assert!(incoming_call_b2.next().await.unwrap().is_none()); - - // User A reconnects automatically, then calls user B again. - server.allow_connections(); - executor.advance_clock(RECEIVE_TIMEOUT); - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b1.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert!(incoming_call_b1.next().await.unwrap().is_some()); - assert!(incoming_call_b2.next().await.unwrap().is_some()); - - // User B disconnects all clients, causing user A to no longer see a pending call for them. - server.forbid_connections(); - server.disconnect_client(client_b1.peer_id().unwrap()); - server.disconnect_client(client_b2.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - - active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none())); -} - -#[gpui::test(iterations = 10)] -async fn test_unshare_project( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .create_room(&mut [(&client_a, cx_a), (&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); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - executor.run_until_parked(); - - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - - project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - // When client B leaves the room, the project becomes read-only. - active_call_b - .update(cx_b, |call, cx| call.hang_up(cx)) - .await - .unwrap(); - executor.run_until_parked(); - - assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); - - // Client C opens the project. - let project_c = client_c.build_remote_project(project_id, cx_c).await; - - // When client A unshares the project, client C's project becomes read-only. - project_a - .update(cx_a, |project, cx| project.unshare(cx)) - .unwrap(); - executor.run_until_parked(); - - assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - - assert!(project_c.read_with(cx_c, |project, _| project.is_read_only())); - - // Client C can open the project again after client A re-shares. - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_c2 = client_c.build_remote_project(project_id, cx_c).await; - executor.run_until_parked(); - - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - project_c2 - .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - // When client A (the host) leaves the room, the project gets unshared and guests are notified. - active_call_a - .update(cx_a, |call, cx| call.hang_up(cx)) - .await - .unwrap(); - executor.run_until_parked(); - - project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - - project_c2.read_with(cx_c, |project, _| { - assert!(project.is_read_only()); - assert!(project.collaborators().is_empty()); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_project_reconnect( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/root-1", - json!({ - "dir1": { - "a.txt": "a", - "b.txt": "b", - "subdir1": { - "c.txt": "c", - "d.txt": "d", - "e.txt": "e", - } - }, - "dir2": { - "v.txt": "v", - }, - "dir3": { - "w.txt": "w", - "x.txt": "x", - "y.txt": "y", - }, - "dir4": { - "z.txt": "z", - }, - }), - ) - .await; - client_a - .fs() - .insert_tree( - "/root-2", - json!({ - "2.txt": "2", - }), - ) - .await; - client_a - .fs() - .insert_tree( - "/root-3", - json!({ - "3.txt": "3", - }), - ) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - let (project_a1, _) = client_a.build_local_project("/root-1/dir1", cx_a).await; - let (project_a2, _) = client_a.build_local_project("/root-2", cx_a).await; - let (project_a3, _) = client_a.build_local_project("/root-3", cx_a).await; - let worktree_a1 = project_a1.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); - let project1_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a1.clone(), cx)) - .await - .unwrap(); - let project2_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a2.clone(), cx)) - .await - .unwrap(); - let project3_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a3.clone(), cx)) - .await - .unwrap(); - - let project_b1 = client_b.build_remote_project(project1_id, cx_b).await; - let project_b2 = client_b.build_remote_project(project2_id, cx_b).await; - let project_b3 = client_b.build_remote_project(project3_id, cx_b).await; - executor.run_until_parked(); - - let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| { - assert!(worktree.as_local().unwrap().is_shared()); - worktree.id() - }); - let (worktree_a2, _) = project_a1 - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/root-1/dir2", true, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - - let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| { - assert!(tree.as_local().unwrap().is_shared()); - tree.id() - }); - executor.run_until_parked(); - - project_b1.read_with(cx_b, |project, cx| { - assert!(project.worktree_for_id(worktree2_id, cx).is_some()) - }); - - let buffer_a1 = project_a1 - .update(cx_a, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx)) - .await - .unwrap(); - let buffer_b1 = project_b1 - .update(cx_b, |p, cx| p.open_buffer((worktree1_id, "a.txt"), cx)) - .await - .unwrap(); - - // Drop client A's connection. - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT); - - project_a1.read_with(cx_a, |project, _| { - assert!(project.is_shared()); - assert_eq!(project.collaborators().len(), 1); - }); - - project_b1.read_with(cx_b, |project, _| { - assert!(!project.is_read_only()); - assert_eq!(project.collaborators().len(), 1); - }); - - worktree_a1.read_with(cx_a, |tree, _| { - assert!(tree.as_local().unwrap().is_shared()) - }); - - // While client A is disconnected, add and remove files from client A's project. - client_a - .fs() - .insert_tree( - "/root-1/dir1/subdir2", - json!({ - "f.txt": "f-contents", - "g.txt": "g-contents", - "h.txt": "h-contents", - "i.txt": "i-contents", - }), - ) - .await; - client_a - .fs() - .remove_dir( - "/root-1/dir1/subdir1".as_ref(), - RemoveOptions { - recursive: true, - ..Default::default() - }, - ) - .await - .unwrap(); - - // While client A is disconnected, add and remove worktrees from client A's project. - project_a1.update(cx_a, |project, cx| { - project.remove_worktree(worktree2_id, cx) - }); - let (worktree_a3, _) = project_a1 - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/root-1/dir3", true, cx) - }) - .await - .unwrap(); - worktree_a3 - .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - - let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| { - assert!(!tree.as_local().unwrap().is_shared()); - tree.id() - }); - executor.run_until_parked(); - - // While client A is disconnected, close project 2 - cx_a.update(|_| drop(project_a2)); - - // While client A is disconnected, mutate a buffer on both the host and the guest. - buffer_a1.update(cx_a, |buf, cx| buf.edit([(0..0, "W")], None, cx)); - buffer_b1.update(cx_b, |buf, cx| buf.edit([(1..1, "Z")], None, cx)); - executor.run_until_parked(); - - // Client A reconnects. Their project is re-shared, and client B re-joins it. - server.allow_connections(); - client_a - .authenticate_and_connect(false, &cx_a.to_async()) - .await - .unwrap(); - executor.run_until_parked(); - - project_a1.read_with(cx_a, |project, cx| { - assert!(project.is_shared()); - assert!(worktree_a1.read(cx).as_local().unwrap().is_shared()); - assert_eq!( - worktree_a1 - .read(cx) - .snapshot() - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec![ - "a.txt", - "b.txt", - "subdir2", - "subdir2/f.txt", - "subdir2/g.txt", - "subdir2/h.txt", - "subdir2/i.txt" - ] - ); - assert!(worktree_a3.read(cx).as_local().unwrap().is_shared()); - assert_eq!( - worktree_a3 - .read(cx) - .snapshot() - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec!["w.txt", "x.txt", "y.txt"] - ); - }); - - project_b1.read_with(cx_b, |project, cx| { - assert!(!project.is_read_only()); - assert_eq!( - project - .worktree_for_id(worktree1_id, cx) - .unwrap() - .read(cx) - .snapshot() - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec![ - "a.txt", - "b.txt", - "subdir2", - "subdir2/f.txt", - "subdir2/g.txt", - "subdir2/h.txt", - "subdir2/i.txt" - ] - ); - assert!(project.worktree_for_id(worktree2_id, cx).is_none()); - assert_eq!( - project - .worktree_for_id(worktree3_id, cx) - .unwrap() - .read(cx) - .snapshot() - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec!["w.txt", "x.txt", "y.txt"] - ); - }); - - project_b2.read_with(cx_b, |project, _| assert!(project.is_read_only())); - - project_b3.read_with(cx_b, |project, _| assert!(!project.is_read_only())); - - buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ")); - - buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WaZ")); - - // Drop client B's connection. - server.forbid_connections(); - server.disconnect_client(client_b.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT); - - // While client B is disconnected, add and remove files from client A's project - client_a - .fs() - .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into()) - .await; - client_a - .fs() - .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default()) - .await - .unwrap(); - - // While client B is disconnected, add and remove worktrees from client A's project. - let (worktree_a4, _) = project_a1 - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/root-1/dir4", true, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - - let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| { - assert!(tree.as_local().unwrap().is_shared()); - tree.id() - }); - project_a1.update(cx_a, |project, cx| { - project.remove_worktree(worktree3_id, cx) - }); - executor.run_until_parked(); - - // While client B is disconnected, mutate a buffer on both the host and the guest. - buffer_a1.update(cx_a, |buf, cx| buf.edit([(1..1, "X")], None, cx)); - buffer_b1.update(cx_b, |buf, cx| buf.edit([(2..2, "Y")], None, cx)); - executor.run_until_parked(); - - // While disconnected, close project 3 - cx_a.update(|_| drop(project_a3)); - - // Client B reconnects. They re-join the room and the remaining shared project. - server.allow_connections(); - client_b - .authenticate_and_connect(false, &cx_b.to_async()) - .await - .unwrap(); - executor.run_until_parked(); - - project_b1.read_with(cx_b, |project, cx| { - assert!(!project.is_read_only()); - assert_eq!( - project - .worktree_for_id(worktree1_id, cx) - .unwrap() - .read(cx) - .snapshot() - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec![ - "a.txt", - "b.txt", - "subdir2", - "subdir2/f.txt", - "subdir2/g.txt", - "subdir2/h.txt", - "subdir2/j.txt" - ] - ); - assert!(project.worktree_for_id(worktree2_id, cx).is_none()); - assert_eq!( - project - .worktree_for_id(worktree4_id, cx) - .unwrap() - .read(cx) - .snapshot() - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - vec!["z.txt"] - ); - }); - - project_b3.read_with(cx_b, |project, _| assert!(project.is_read_only())); - - buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ")); - - buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "WXaYZ")); -} - -#[gpui::test(iterations = 10)] -async fn test_active_call_events( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - client_a.fs().insert_tree("/a", json!({})).await; - client_b.fs().insert_tree("/b", 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; - - 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 events_a = active_call_events(cx_a); - let events_b = active_call_events(cx_b); - - let project_a_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]); - assert_eq!( - mem::take(&mut *events_b.borrow_mut()), - vec![room::Event::RemoteProjectShared { - owner: Arc::new(User { - id: client_a.user_id().unwrap(), - github_login: "user_a".to_string(), - avatar_uri: "avatar_a".into(), - }), - project_id: project_a_id, - worktree_root_names: vec!["a".to_string()], - }] - ); - - let project_b_id = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - mem::take(&mut *events_a.borrow_mut()), - vec![room::Event::RemoteProjectShared { - owner: Arc::new(User { - id: client_b.user_id().unwrap(), - github_login: "user_b".to_string(), - avatar_uri: "avatar_b".into(), - }), - project_id: project_b_id, - worktree_root_names: vec!["b".to_string()] - }] - ); - assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); - - // Sharing a project twice is idempotent. - let project_b_id_2 = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) - .await - .unwrap(); - assert_eq!(project_b_id_2, project_b_id); - executor.run_until_parked(); - assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]); - assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); -} - -fn active_call_events(cx: &mut TestAppContext) -> Rc>> { - let events = Rc::new(RefCell::new(Vec::new())); - let active_call = cx.read(ActiveCall::global); - cx.update({ - let events = events.clone(); - |cx| { - cx.subscribe(&active_call, move |_, event, _| { - events.borrow_mut().push(event.clone()) - }) - .detach() - } - }); - events -} - -#[gpui::test(iterations = 10)] -async fn test_room_location( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - client_a.fs().insert_tree("/a", json!({})).await; - client_b.fs().insert_tree("/b", json!({})).await; - - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - let a_notified = Rc::new(Cell::new(false)); - cx_a.update({ - let notified = a_notified.clone(); - |cx| { - cx.observe(&active_call_a, move |_, _| notified.set(true)) - .detach() - } - }); - - let b_notified = Rc::new(Cell::new(false)); - cx_b.update({ - let b_notified = b_notified.clone(); - |cx| { - cx.observe(&active_call_b, move |_, _| b_notified.set(true)) - .detach() - } - }); - - let (project_a, _) = 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_b, _) = client_b.build_local_project("/b", cx_b).await; - - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - - let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); - executor.run_until_parked(); - assert!(a_notified.take()); - assert_eq!( - participant_locations(&room_a, cx_a), - vec![("user_b".to_string(), ParticipantLocation::External)] - ); - assert!(b_notified.take()); - assert_eq!( - participant_locations(&room_b, cx_b), - vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)] - ); - - let project_a_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert!(a_notified.take()); - assert_eq!( - participant_locations(&room_a, cx_a), - vec![("user_b".to_string(), ParticipantLocation::External)] - ); - assert!(b_notified.take()); - assert_eq!( - participant_locations(&room_b, cx_b), - vec![( - "user_a".to_string(), - ParticipantLocation::SharedProject { - project_id: project_a_id - } - )] - ); - - let project_b_id = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert!(a_notified.take()); - assert_eq!( - participant_locations(&room_a, cx_a), - vec![("user_b".to_string(), ParticipantLocation::External)] - ); - assert!(b_notified.take()); - assert_eq!( - participant_locations(&room_b, cx_b), - vec![( - "user_a".to_string(), - ParticipantLocation::SharedProject { - project_id: project_a_id - } - )] - ); - - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert!(a_notified.take()); - assert_eq!( - participant_locations(&room_a, cx_a), - vec![( - "user_b".to_string(), - ParticipantLocation::SharedProject { - project_id: project_b_id - } - )] - ); - assert!(b_notified.take()); - assert_eq!( - participant_locations(&room_b, cx_b), - vec![( - "user_a".to_string(), - ParticipantLocation::SharedProject { - project_id: project_a_id - } - )] - ); - - active_call_b - .update(cx_b, |call, cx| call.set_location(None, cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert!(a_notified.take()); - assert_eq!( - participant_locations(&room_a, cx_a), - vec![("user_b".to_string(), ParticipantLocation::External)] - ); - assert!(b_notified.take()); - assert_eq!( - participant_locations(&room_b, cx_b), - vec![( - "user_a".to_string(), - ParticipantLocation::SharedProject { - project_id: project_a_id - } - )] - ); - - fn participant_locations( - room: &Model, - cx: &TestAppContext, - ) -> Vec<(String, ParticipantLocation)> { - room.read_with(cx, |room, _| { - room.remote_participants() - .values() - .map(|participant| { - ( - participant.user.github_login.to_string(), - participant.location, - ) - }) - .collect() - }) - } -} - -#[gpui::test(iterations = 10)] -async fn test_propagate_saves_and_fs_changes( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - let rust = Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - let javascript = Arc::new(Language::new( - LanguageConfig { - name: "JavaScript".into(), - path_suffixes: vec!["js".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - for client in [&client_a, &client_b, &client_c] { - client.language_registry().add(rust.clone()); - client.language_registry().add(javascript.clone()); - } - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "file1.rs": "", - "file2": "" - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - - let worktree_a = project_a.read_with(cx_a, |p, _| p.worktrees().next().unwrap()); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Join that worktree as clients B and C. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - let project_c = client_c.build_remote_project(project_id, cx_c).await; - - let worktree_b = project_b.read_with(cx_b, |p, _| p.worktrees().next().unwrap()); - - let worktree_c = project_c.read_with(cx_c, |p, _| p.worktrees().next().unwrap()); - - // Open and edit a buffer as both guests B and C. - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx)) - .await - .unwrap(); - let buffer_c = project_c - .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx)) - .await - .unwrap(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(&*buffer.language().unwrap().name(), "Rust"); - }); - - buffer_c.read_with(cx_c, |buffer, _| { - assert_eq!(&*buffer.language().unwrap().name(), "Rust"); - }); - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx)); - buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx)); - - // Open and edit that buffer as the host. - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx)) - .await - .unwrap(); - - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buf, _| assert_eq!(buf.text(), "i-am-c, i-am-b, ")); - buffer_a.update(cx_a, |buf, cx| { - buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx) - }); - - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buf, _| { - assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a"); - }); - - buffer_b.read_with(cx_b, |buf, _| { - assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a"); - }); - - buffer_c.read_with(cx_c, |buf, _| { - assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a"); - }); - - // Edit the buffer as the host and concurrently save as guest B. - let save_b = project_b.update(cx_b, |project, cx| { - project.save_buffer(buffer_b.clone(), cx) - }); - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); - save_b.await.unwrap(); - assert_eq!( - client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(), - "hi-a, i-am-c, i-am-b, i-am-a" - ); - - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty())); - - buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty())); - - buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty())); - - // Make changes on host's file system, see those changes on guest worktrees. - client_a - .fs() - .rename( - "/a/file1.rs".as_ref(), - "/a/file1.js".as_ref(), - Default::default(), - ) - .await - .unwrap(); - client_a - .fs() - .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) - .await - .unwrap(); - client_a.fs().insert_file("/a/file4", "4".into()).await; - executor.run_until_parked(); - - worktree_a.read_with(cx_a, |tree, _| { - assert_eq!( - tree.paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["file1.js", "file3", "file4"] - ) - }); - - worktree_b.read_with(cx_b, |tree, _| { - assert_eq!( - tree.paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["file1.js", "file3", "file4"] - ) - }); - - worktree_c.read_with(cx_c, |tree, _| { - assert_eq!( - tree.paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["file1.js", "file3", "file4"] - ) - }); - - // Ensure buffer files are updated as well. - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js")); - assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); - }); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js")); - assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); - }); - - buffer_c.read_with(cx_c, |buffer, _| { - assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js")); - assert_eq!(&*buffer.language().unwrap().name(), "JavaScript"); - }); - - let new_buffer_a = project_a - .update(cx_a, |p, cx| p.create_buffer("", None, cx)) - .unwrap(); - - let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()); - let new_buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx)) - .await - .unwrap(); - - new_buffer_b.read_with(cx_b, |buffer, _| { - assert!(buffer.file().is_none()); - }); - - new_buffer_a.update(cx_a, |buffer, cx| { - buffer.edit([(0..0, "ok")], None, cx); - }); - project_a - .update(cx_a, |project, cx| { - project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - new_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_eq!( - buffer_b.file().unwrap().path().as_ref(), - Path::new("file3.rs") - ); - - new_buffer_a.read_with(cx_a, |buffer_a, _| { - assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime()); - assert_eq!(buffer_b.saved_version(), buffer_a.saved_version()); - }); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_git_diff_base_change( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - ".git": {}, - "sub": { - ".git": {}, - "b.txt": " - one - two - three - ".unindent(), - }, - "a.txt": " - one - two - three - ".unindent(), - }), - ) - .await; - - let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| { - call.share_project(project_local.clone(), cx) - }) - .await - .unwrap(); - - let project_remote = client_b.build_remote_project(project_id, cx_b).await; - - let diff_base = " - one - three - " - .unindent(); - - let new_diff_base = " - one - two - " - .unindent(); - - client_a.fs().set_index_for_repo( - Path::new("/dir/.git"), - &[(Path::new("a.txt"), diff_base.clone())], - ); - - // Create the buffer - let buffer_local_a = project_local - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - // Wait for it to catch up to the new diff - executor.run_until_parked(); - - // Smoke test diffing - - buffer_local_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(1..2, "", "two\n")], - ); - }); - - // Create remote buffer - let buffer_remote_a = project_remote - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - // Wait remote buffer to catch up to the new diff - executor.run_until_parked(); - - // Smoke test diffing - - buffer_remote_a.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(1..2, "", "two\n")], - ); - }); - - client_a.fs().set_index_for_repo( - Path::new("/dir/.git"), - &[(Path::new("a.txt"), new_diff_base.clone())], - ); - - // Wait for buffer_local_a to receive it - executor.run_until_parked(); - - // Smoke test new diffing - - buffer_local_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); - - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(2..3, "", "three\n")], - ); - }); - - // Smoke test B - - buffer_remote_a.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(2..3, "", "three\n")], - ); - }); - - //Nested git dir - - let diff_base = " - one - three - " - .unindent(); - - let new_diff_base = " - one - two - " - .unindent(); - - client_a.fs().set_index_for_repo( - Path::new("/dir/sub/.git"), - &[(Path::new("b.txt"), diff_base.clone())], - ); - - // Create the buffer - let buffer_local_b = project_local - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx)) - .await - .unwrap(); - - // Wait for it to catch up to the new diff - executor.run_until_parked(); - - // Smoke test diffing - - buffer_local_b.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(1..2, "", "two\n")], - ); - }); - - // Create remote buffer - let buffer_remote_b = project_remote - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx)) - .await - .unwrap(); - - // Wait remote buffer to catch up to the new diff - executor.run_until_parked(); - - // Smoke test diffing - - buffer_remote_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(1..2, "", "two\n")], - ); - }); - - client_a.fs().set_index_for_repo( - Path::new("/dir/sub/.git"), - &[(Path::new("b.txt"), new_diff_base.clone())], - ); - - // Wait for buffer_local_b to receive it - executor.run_until_parked(); - - // Smoke test new diffing - - buffer_local_b.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); - println!("{:?}", buffer.as_rope().to_string()); - println!("{:?}", buffer.diff_base()); - println!( - "{:?}", - buffer - .snapshot() - .git_diff_hunks_in_row_range(0..4) - .collect::>() - ); - - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(2..3, "", "three\n")], - ); - }); - - // Smoke test B - - buffer_remote_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), - &buffer, - &diff_base, - &[(2..3, "", "three\n")], - ); - }); -} - -#[gpui::test] -async fn test_git_branch_name( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - ".git": {}, - }), - ) - .await; - - let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| { - call.share_project(project_local.clone(), cx) - }) - .await - .unwrap(); - - let project_remote = client_b.build_remote_project(project_id, cx_b).await; - client_a - .fs() - .set_branch_name(Path::new("/dir/.git"), Some("branch-1")); - - // Wait for it to catch up to the new branch - executor.run_until_parked(); - - #[track_caller] - fn assert_branch(branch_name: Option>, project: &Project, cx: &AppContext) { - let branch_name = branch_name.map(Into::into); - let worktrees = project.visible_worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - let worktree = worktrees[0].clone(); - let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap(); - assert_eq!(root_entry.branch(), branch_name.map(Into::into)); - } - - // Smoke test branch reading - - project_local.read_with(cx_a, |project, cx| { - assert_branch(Some("branch-1"), project, cx) - }); - - project_remote.read_with(cx_b, |project, cx| { - assert_branch(Some("branch-1"), project, cx) - }); - - client_a - .fs() - .set_branch_name(Path::new("/dir/.git"), Some("branch-2")); - - // Wait for buffer_local_a to receive it - executor.run_until_parked(); - - // Smoke test branch reading - - project_local.read_with(cx_a, |project, cx| { - assert_branch(Some("branch-2"), project, cx) - }); - - project_remote.read_with(cx_b, |project, cx| { - assert_branch(Some("branch-2"), project, cx) - }); - - let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; - executor.run_until_parked(); - - project_remote_c.read_with(cx_c, |project, cx| { - assert_branch(Some("branch-2"), project, cx) - }); -} - -#[gpui::test] -async fn test_git_status_sync( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - ".git": {}, - "a.txt": "a", - "b.txt": "b", - }), - ) - .await; - - const A_TXT: &'static str = "a.txt"; - const B_TXT: &'static str = "b.txt"; - - client_a.fs().set_status_for_repo_via_git_operation( - Path::new("/dir/.git"), - &[ - (&Path::new(A_TXT), GitFileStatus::Added), - (&Path::new(B_TXT), GitFileStatus::Added), - ], - ); - - let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| { - call.share_project(project_local.clone(), cx) - }) - .await - .unwrap(); - - let project_remote = client_b.build_remote_project(project_id, cx_b).await; - - // Wait for it to catch up to the new status - executor.run_until_parked(); - - #[track_caller] - fn assert_status( - file: &impl AsRef, - status: Option, - project: &Project, - cx: &AppContext, - ) { - let file = file.as_ref(); - let worktrees = project.visible_worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - let worktree = worktrees[0].clone(); - let snapshot = worktree.read(cx).snapshot(); - assert_eq!(snapshot.status_for_file(file), status); - } - - // Smoke test status reading - - project_local.read_with(cx_a, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); - }); - - project_remote.read_with(cx_b, |project, cx| { - assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); - assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); - }); - - client_a.fs().set_status_for_repo_via_working_copy_change( - Path::new("/dir/.git"), - &[ - (&Path::new(A_TXT), GitFileStatus::Modified), - (&Path::new(B_TXT), GitFileStatus::Modified), - ], - ); - - // Wait for buffer_local_a to receive it - executor.run_until_parked(); - - // Smoke test status reading - - project_local.read_with(cx_a, |project, cx| { - assert_status( - &Path::new(A_TXT), - Some(GitFileStatus::Modified), - project, - cx, - ); - assert_status( - &Path::new(B_TXT), - Some(GitFileStatus::Modified), - project, - cx, - ); - }); - - project_remote.read_with(cx_b, |project, cx| { - assert_status( - &Path::new(A_TXT), - Some(GitFileStatus::Modified), - project, - cx, - ); - assert_status( - &Path::new(B_TXT), - Some(GitFileStatus::Modified), - project, - cx, - ); - }); - - // And synchronization while joining - let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; - executor.run_until_parked(); - - project_remote_c.read_with(cx_c, |project, cx| { - assert_status( - &Path::new(A_TXT), - Some(GitFileStatus::Modified), - project, - cx, - ); - assert_status( - &Path::new(B_TXT), - Some(GitFileStatus::Modified), - project, - cx, - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_fs_operations( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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 worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); - - let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap()); - - let entry = project_b - .update(cx_b, |project, cx| { - project.create_entry((worktree_id, "c.txt"), false, cx) - }) - .await - .unwrap() - .unwrap(); - - worktree_a.read_with(cx_a, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "c.txt"] - ); - }); - - worktree_b.read_with(cx_b, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "c.txt"] - ); - }); - - project_b - .update(cx_b, |project, cx| { - project.rename_entry(entry.id, Path::new("d.txt"), cx) - }) - .await - .unwrap() - .unwrap(); - - worktree_a.read_with(cx_a, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "d.txt"] - ); - }); - - worktree_b.read_with(cx_b, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "d.txt"] - ); - }); - - let dir_entry = project_b - .update(cx_b, |project, cx| { - project.create_entry((worktree_id, "DIR"), true, cx) - }) - .await - .unwrap() - .unwrap(); - - worktree_a.read_with(cx_a, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["DIR", "a.txt", "b.txt", "d.txt"] - ); - }); - - worktree_b.read_with(cx_b, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["DIR", "a.txt", "b.txt", "d.txt"] - ); - }); - - project_b - .update(cx_b, |project, cx| { - project.create_entry((worktree_id, "DIR/e.txt"), false, cx) - }) - .await - .unwrap() - .unwrap(); - project_b - .update(cx_b, |project, cx| { - project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx) - }) - .await - .unwrap() - .unwrap(); - project_b - .update(cx_b, |project, cx| { - project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) - }) - .await - .unwrap() - .unwrap(); - - worktree_a.read_with(cx_a, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - [ - "DIR", - "DIR/SUBDIR", - "DIR/SUBDIR/f.txt", - "DIR/e.txt", - "a.txt", - "b.txt", - "d.txt" - ] - ); - }); - - worktree_b.read_with(cx_b, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - [ - "DIR", - "DIR/SUBDIR", - "DIR/SUBDIR/f.txt", - "DIR/e.txt", - "a.txt", - "b.txt", - "d.txt" - ] - ); - }); - - project_b - .update(cx_b, |project, cx| { - project.copy_entry(entry.id, Path::new("f.txt"), cx) - }) - .await - .unwrap() - .unwrap(); - - worktree_a.read_with(cx_a, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - [ - "DIR", - "DIR/SUBDIR", - "DIR/SUBDIR/f.txt", - "DIR/e.txt", - "a.txt", - "b.txt", - "d.txt", - "f.txt" - ] - ); - }); - - worktree_b.read_with(cx_b, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - [ - "DIR", - "DIR/SUBDIR", - "DIR/SUBDIR/f.txt", - "DIR/e.txt", - "a.txt", - "b.txt", - "d.txt", - "f.txt" - ] - ); - }); - - project_b - .update(cx_b, |project, cx| { - project.delete_entry(dir_entry.id, cx).unwrap() - }) - .await - .unwrap(); - executor.run_until_parked(); - - worktree_a.read_with(cx_a, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "d.txt", "f.txt"] - ); - }); - - worktree_b.read_with(cx_b, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "d.txt", "f.txt"] - ); - }); - - project_b - .update(cx_b, |project, cx| { - project.delete_entry(entry.id, cx).unwrap() - }) - .await - .unwrap(); - - worktree_a.read_with(cx_a, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "f.txt"] - ); - }); - - worktree_b.read_with(cx_b, |worktree, _| { - assert_eq!( - worktree - .paths() - .map(|p| p.to_string_lossy()) - .collect::>(), - ["a.txt", "b.txt", "f.txt"] - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_local_settings( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - // As client A, open a project that contains some local settings files - client_a - .fs() - .insert_tree( - "/dir", - json!({ - ".zed": { - "settings.json": r#"{ "tab_size": 2 }"# - }, - "a": { - ".zed": { - "settings.json": r#"{ "tab_size": 8 }"# - }, - "a.txt": "a-contents", - }, - "b": { - "b.txt": "b-contents", - } - }), - ) - .await; - let (project_a, _) = client_a.build_local_project("/dir", cx_a).await; - executor.run_until_parked(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // As client B, join that project and observe the local settings. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - - let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap()); - executor.run_until_parked(); - cx_b.read(|cx| { - let store = cx.global::(); - assert_eq!( - store - .local_settings(worktree_b.read(cx).id().to_usize()) - .collect::>(), - &[ - (Path::new("").into(), r#"{"tab_size":2}"#.to_string()), - (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), - ] - ) - }); - - // As client A, update a settings file. As Client B, see the changed settings. - client_a - .fs() - .insert_file("/dir/.zed/settings.json", r#"{}"#.into()) - .await; - executor.run_until_parked(); - cx_b.read(|cx| { - let store = cx.global::(); - assert_eq!( - store - .local_settings(worktree_b.read(cx).id().to_usize()) - .collect::>(), - &[ - (Path::new("").into(), r#"{}"#.to_string()), - (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), - ] - ) - }); - - // As client A, create and remove some settings files. As client B, see the changed settings. - client_a - .fs() - .remove_file("/dir/.zed/settings.json".as_ref(), Default::default()) - .await - .unwrap(); - client_a - .fs() - .create_dir("/dir/b/.zed".as_ref()) - .await - .unwrap(); - client_a - .fs() - .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) - .await; - executor.run_until_parked(); - cx_b.read(|cx| { - let store = cx.global::(); - assert_eq!( - store - .local_settings(worktree_b.read(cx).id().to_usize()) - .collect::>(), - &[ - (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), - (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()), - ] - ) - }); - - // As client B, disconnect. - server.forbid_connections(); - server.disconnect_client(client_b.peer_id().unwrap()); - - // As client A, change and remove settings files while client B is disconnected. - client_a - .fs() - .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into()) - .await; - client_a - .fs() - .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) - .await - .unwrap(); - executor.run_until_parked(); - - // As client B, reconnect and see the changed settings. - server.allow_connections(); - executor.advance_clock(RECEIVE_TIMEOUT); - cx_b.read(|cx| { - let store = cx.global::(); - assert_eq!( - store - .local_settings(worktree_b.read(cx).id().to_usize()) - .collect::>(), - &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),] - ) - }); -} - -#[gpui::test(iterations = 10)] -async fn test_buffer_conflict_after_save( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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; - - // Open a buffer as client B - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx)); - - buffer_b.read_with(cx_b, |buf, _| { - assert!(buf.is_dirty()); - assert!(!buf.has_conflict()); - }); - - project_b - .update(cx_b, |project, cx| { - project.save_buffer(buffer_b.clone(), cx) - }) - .await - .unwrap(); - - buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty())); - - buffer_b.read_with(cx_b, |buf, _| { - assert!(!buf.has_conflict()); - }); - - buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx)); - - buffer_b.read_with(cx_b, |buf, _| { - assert!(buf.is_dirty()); - assert!(!buf.has_conflict()); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_buffer_reloading( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - "a.txt": "a\nb\nc", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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; - - // Open a buffer as client B - let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - buffer_b.read_with(cx_b, |buf, _| { - assert!(!buf.is_dirty()); - assert!(!buf.has_conflict()); - assert_eq!(buf.line_ending(), LineEnding::Unix); - }); - - let new_contents = Rope::from("d\ne\nf"); - client_a - .fs() - .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows) - .await - .unwrap(); - - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buf, _| { - assert_eq!(buf.text(), new_contents.to_string()); - assert!(!buf.is_dirty()); - assert!(!buf.has_conflict()); - assert_eq!(buf.line_ending(), LineEnding::Windows); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_editing_while_guest_opens_buffer( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree("/dir", json!({ "a.txt": "a-contents" })) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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; - - // Open a buffer as client A - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - // Start opening the same buffer as client B - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer); - - // Edit the buffer as client A while client B is still opening it. - cx_b.executor().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx)); - cx_b.executor().simulate_random_delay().await; - buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx)); - - let text = buffer_a.read_with(cx_a, |buf, _| buf.text()); - let buffer_b = buffer_b.await.unwrap(); - executor.run_until_parked(); - - buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text)); -} - -#[gpui::test(iterations = 10)] -async fn test_leaving_worktree_while_opening_buffer( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree("/dir", json!({ "a.txt": "a-contents" })) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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; - - // See that a guest has joined as client A. - executor.run_until_parked(); - - project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1)); - - // Begin opening a buffer as client B, but leave the project before the open completes. - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer); - cx_b.update(|_| drop(project_b)); - drop(buffer_b); - - // See that the guest has left. - executor.run_until_parked(); - - project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty())); -} - -#[gpui::test(iterations = 10)] -async fn test_canceling_buffer_opening( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/dir", - json!({ - "a.txt": "abc", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - 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 buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); - - // Open a buffer as client B but cancel after a random amount of time. - let buffer_b = project_b.update(cx_b, |p, cx| { - p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx) - }); - executor.simulate_random_delay().await; - drop(buffer_b); - - // Try opening the same buffer again as client B, and ensure we can - // still do it despite the cancellation above. - let buffer_b = project_b - .update(cx_b, |p, cx| { - p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx) - }) - .await - .unwrap(); - - buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc")); -} - -#[gpui::test(iterations = 10)] -async fn test_leaving_project( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b1 = client_b.build_remote_project(project_id, cx_b).await; - let project_c = client_c.build_remote_project(project_id, cx_c).await; - - // Client A sees that a guest has joined. - executor.run_until_parked(); - - project_a.read_with(cx_a, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - - project_b1.read_with(cx_b, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - - project_c.read_with(cx_c, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - - // Client B opens a buffer. - let buffer_b1 = project_b1 - .update(cx_b, |project, cx| { - let worktree_id = project.worktrees().next().unwrap().read(cx).id(); - project.open_buffer((worktree_id, "a.txt"), cx) - }) - .await - .unwrap(); - - buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); - - // Drop client B's project and ensure client A and client C observe client B leaving. - cx_b.update(|_| drop(project_b1)); - executor.run_until_parked(); - - project_a.read_with(cx_a, |project, _| { - assert_eq!(project.collaborators().len(), 1); - }); - - project_c.read_with(cx_c, |project, _| { - assert_eq!(project.collaborators().len(), 1); - }); - - // Client B re-joins the project and can open buffers as before. - let project_b2 = client_b.build_remote_project(project_id, cx_b).await; - executor.run_until_parked(); - - project_a.read_with(cx_a, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - - project_b2.read_with(cx_b, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - - project_c.read_with(cx_c, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - - let buffer_b2 = project_b2 - .update(cx_b, |project, cx| { - let worktree_id = project.worktrees().next().unwrap().read(cx).id(); - project.open_buffer((worktree_id, "a.txt"), cx) - }) - .await - .unwrap(); - - buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); - - // Drop client B's connection and ensure client A and client C observe client B leaving. - client_b.disconnect(&cx_b.to_async()); - executor.advance_clock(RECONNECT_TIMEOUT); - - project_a.read_with(cx_a, |project, _| { - assert_eq!(project.collaborators().len(), 1); - }); - - project_b2.read_with(cx_b, |project, _| { - assert!(project.is_read_only()); - }); - - project_c.read_with(cx_c, |project, _| { - assert_eq!(project.collaborators().len(), 1); - }); - - // Client B can't join the project, unless they re-join the room. - cx_b.spawn(|cx| { - Project::remote( - project_id, - client_b.app_state.client.clone(), - client_b.user_store().clone(), - client_b.language_registry().clone(), - FakeFs::new(cx.background_executor().clone()), - cx, - ) - }) - .await - .unwrap_err(); - - // Simulate connection loss for client C and ensure client A observes client C leaving the project. - client_c.wait_for_current_user(cx_c).await; - server.forbid_connections(); - server.disconnect_client(client_c.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - executor.run_until_parked(); - - project_a.read_with(cx_a, |project, _| { - assert_eq!(project.collaborators().len(), 0); - }); - - project_b2.read_with(cx_b, |project, _| { - assert!(project.is_read_only()); - }); - - project_c.read_with(cx_c, |project, _| { - assert!(project.is_read_only()); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_collaborating_with_diagnostics( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - // Share a project as client A - client_a - .fs() - .insert_tree( - "/a", - json!({ - "a.rs": "let one = two", - "other.rs": "", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - - // Cause the language server to start. - let _buffer = project_a - .update(cx_a, |project, cx| { - project.open_buffer( - ProjectPath { - worktree_id, - path: Path::new("other.rs").into(), - }, - cx, - ) - }) - .await - .unwrap(); - - // Simulate a language server reporting errors for a file. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server - .receive_notification::() - .await; - fake_language_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - severity: Some(lsp::DiagnosticSeverity::WARNING), - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)), - message: "message 0".to_string(), - ..Default::default() - }], - }, - ); - - // Client A shares the project and, simultaneously, the language server - // publishes a diagnostic. This is done to ensure that the server always - // observes the latest diagnostics for a worktree. - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - fake_language_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - severity: Some(lsp::DiagnosticSeverity::ERROR), - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)), - message: "message 1".to_string(), - ..Default::default() - }], - }, - ); - - // Join the worktree as client B. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - - // Wait for server to see the diagnostics update. - executor.run_until_parked(); - - // Ensure client B observes the new diagnostics. - - project_b.read_with(cx_b, |project, cx| { - assert_eq!( - project.diagnostic_summaries(false, cx).collect::>(), - &[( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")), - }, - LanguageServerId(0), - DiagnosticSummary { - error_count: 1, - warning_count: 0, - ..Default::default() - }, - )] - ) - }); - - // Join project as client C and observe the diagnostics. - let project_c = client_c.build_remote_project(project_id, cx_c).await; - let project_c_diagnostic_summaries = - Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| { - project.diagnostic_summaries(false, cx).collect::>() - }))); - project_c.update(cx_c, |_, cx| { - let summaries = project_c_diagnostic_summaries.clone(); - cx.subscribe(&project_c, { - move |p, _, event, cx| { - if let project::Event::DiskBasedDiagnosticsFinished { .. } = event { - *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect(); - } - } - }) - .detach(); - }); - - executor.run_until_parked(); - assert_eq!( - project_c_diagnostic_summaries.borrow().as_slice(), - &[( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")), - }, - LanguageServerId(0), - DiagnosticSummary { - error_count: 1, - warning_count: 0, - ..Default::default() - }, - )] - ); - - // Simulate a language server reporting more errors for a file. - fake_language_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), - version: None, - diagnostics: vec![ - lsp::Diagnostic { - severity: Some(lsp::DiagnosticSeverity::ERROR), - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)), - message: "message 1".to_string(), - ..Default::default() - }, - lsp::Diagnostic { - severity: Some(lsp::DiagnosticSeverity::WARNING), - range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)), - message: "message 2".to_string(), - ..Default::default() - }, - ], - }, - ); - - // Clients B and C get the updated summaries - executor.run_until_parked(); - - project_b.read_with(cx_b, |project, cx| { - assert_eq!( - project.diagnostic_summaries(false, cx).collect::>(), - [( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")), - }, - LanguageServerId(0), - DiagnosticSummary { - error_count: 1, - warning_count: 1, - }, - )] - ); - }); - - project_c.read_with(cx_c, |project, cx| { - assert_eq!( - project.diagnostic_summaries(false, cx).collect::>(), - [( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")), - }, - LanguageServerId(0), - DiagnosticSummary { - error_count: 1, - warning_count: 1, - }, - )] - ); - }); - - // Open the file with the errors on client B. They should be present. - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(0..buffer.len(), false) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(0, 4)..Point::new(0, 7), - diagnostic: Diagnostic { - group_id: 2, - message: "message 1".to_string(), - severity: lsp::DiagnosticSeverity::ERROR, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(0, 10)..Point::new(0, 13), - diagnostic: Diagnostic { - group_id: 3, - severity: lsp::DiagnosticSeverity::WARNING, - message: "message 2".to_string(), - is_primary: true, - ..Default::default() - } - } - ] - ); - }); - - // Simulate a language server reporting no errors for a file. - fake_language_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), - version: None, - diagnostics: vec![], - }, - ); - executor.run_until_parked(); - - project_a.read_with(cx_a, |project, cx| { - assert_eq!( - project.diagnostic_summaries(false, cx).collect::>(), - [] - ) - }); - - project_b.read_with(cx_b, |project, cx| { - assert_eq!( - project.diagnostic_summaries(false, cx).collect::>(), - [] - ) - }); - - project_c.read_with(cx_c, |project, cx| { - assert_eq!( - project.diagnostic_summaries(false, cx).collect::>(), - [] - ) - }); -} - -#[gpui::test(iterations = 10)] -async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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; - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()), - disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()], - ..Default::default() - })) - .await; - client_a.language_registry().add(Arc::new(language)); - - let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"]; - client_a - .fs() - .insert_tree( - "/test", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = 2;", - "three.rs": "const THREE: usize = 3;", - "four.rs": "const FOUR: usize = 3;", - "five.rs": "const FIVE: usize = 3;", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await; - - // Share a project as client A - let active_call_a = cx_a.read(ActiveCall::global); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Join the project as client B and open all three files. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| { - project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx)) - })) - .await - .unwrap(); - - // Simulate a language server reporting errors for a file. - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server - .request::(lsp::WorkDoneProgressCreateParams { - token: lsp::NumberOrString::String("the-disk-based-token".to_string()), - }) - .await - .unwrap(); - fake_language_server.notify::(lsp::ProgressParams { - token: lsp::NumberOrString::String("the-disk-based-token".to_string()), - value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( - lsp::WorkDoneProgressBegin { - title: "Progress Began".into(), - ..Default::default() - }, - )), - }); - for file_name in file_names { - fake_language_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - severity: Some(lsp::DiagnosticSeverity::WARNING), - source: Some("the-disk-based-diagnostics-source".into()), - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), - message: "message one".to_string(), - ..Default::default() - }], - }, - ); - } - fake_language_server.notify::(lsp::ProgressParams { - token: lsp::NumberOrString::String("the-disk-based-token".to_string()), - value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( - lsp::WorkDoneProgressEnd { message: None }, - )), - }); - - // When the "disk base diagnostics finished" message is received, the buffers' - // diagnostics are expected to be present. - let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false)); - project_b.update(cx_b, { - let project_b = project_b.clone(); - let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone(); - move |_, cx| { - cx.subscribe(&project_b, move |_, _, event, cx| { - if let project::Event::DiskBasedDiagnosticsFinished { .. } = event { - disk_based_diagnostics_finished.store(true, SeqCst); - for buffer in &guest_buffers { - assert_eq!( - buffer - .read(cx) - .snapshot() - .diagnostics_in_range::<_, usize>(0..5, false) - .count(), - 1, - "expected a diagnostic for buffer {:?}", - buffer.read(cx).file().unwrap().path(), - ); - } - } - }) - .detach(); - } - }); - - executor.run_until_parked(); - assert!(disk_based_diagnostics_finished.load(SeqCst)); -} - -#[gpui::test(iterations = 10)] -async fn test_reloading_buffer_manually( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree("/a", json!({ "a.rs": "let one = 1;" })) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), 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; - - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - buffer_b.update(cx_b, |buffer, cx| { - buffer.edit([(4..7, "six")], None, cx); - buffer.edit([(10..11, "6")], None, cx); - assert_eq!(buffer.text(), "let six = 6;"); - assert!(buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;")); - - client_a - .fs() - .save( - "/a/a.rs".as_ref(), - &Rope::from("let seven = 7;"), - LineEnding::Unix, - ) - .await - .unwrap(); - executor.run_until_parked(); - - buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict())); - - buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict())); - - project_b - .update(cx_b, |project, cx| { - project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx) - }) - .await - .unwrap(); - - buffer_a.read_with(cx_a, |buffer, _| { - assert_eq!(buffer.text(), "let seven = 7;"); - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - - buffer_b.read_with(cx_b, |buffer, _| { - assert_eq!(buffer.text(), "let seven = 7;"); - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - - buffer_a.update(cx_a, |buffer, cx| { - // Undoing on the host is a no-op when the reload was initiated by the guest. - buffer.undo(cx); - assert_eq!(buffer.text(), "let seven = 7;"); - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - buffer_b.update(cx_b, |buffer, cx| { - // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared. - buffer.undo(cx); - assert_eq!(buffer.text(), "let six = 6;"); - assert!(buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_formatting_buffer( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - executor.allow_parking(); - let mut server = TestServer::start(executor.clone()).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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - // Here we insert a fake tree with a directory that exists on disk. This is needed - // because later we'll invoke a command, which requires passing a working directory - // that points to a valid location on disk. - let directory = env::current_dir().unwrap(); - client_a - .fs() - .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" })) - .await; - let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; - 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 open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| async move { - Ok(Some(vec![ - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), - new_text: "h".to_string(), - }, - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)), - new_text: "y".to_string(), - }, - ])) - }); - - project_b - .update(cx_b, |project, cx| { - project.format( - HashSet::from_iter([buffer_b.clone()]), - true, - FormatTrigger::Save, - cx, - ) - }) - .await - .unwrap(); - - // The edits from the LSP are applied, and a final newline is added. - assert_eq!( - buffer_b.read_with(cx_b, |buffer, _| buffer.text()), - "let honey = \"two\"\n" - ); - - // Ensure buffer can be formatted using an external command. Notice how the - // host's configuration is honored as opposed to using the guest's settings. - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(Formatter::External { - command: "awk".into(), - arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(), - }); - }); - }); - }); - project_b - .update(cx_b, |project, cx| { - project.format( - HashSet::from_iter([buffer_b.clone()]), - true, - FormatTrigger::Save, - cx, - ) - }) - .await - .unwrap(); - assert_eq!( - buffer_b.read_with(cx_b, |buffer, _| buffer.text()), - format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap()) - ); -} - -#[gpui::test(iterations = 10)] -async fn test_prettier_formatting_buffer( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - prettier_parser_name: Some("test_parser".to_string()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let test_plugin = "test_plugin"; - let mut fake_language_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - prettier_plugins: vec![test_plugin], - ..Default::default() - })) - .await; - let language = Arc::new(language); - client_a.language_registry().add(Arc::clone(&language)); - - // Here we insert a fake tree with a directory that exists on disk. This is needed - // because later we'll invoke a command, which requires passing a working directory - // that points to a valid location on disk. - let directory = env::current_dir().unwrap(); - let buffer_text = "let one = \"two\""; - client_a - .fs() - .insert_tree(&directory, json!({ "a.rs": buffer_text })) - .await; - let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; - let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; - let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); - let buffer_a = cx_a.executor().spawn(open_buffer).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; - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - - cx_a.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(Formatter::Auto); - }); - }); - }); - cx_b.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(Formatter::LanguageServer); - }); - }); - }); - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| async move { - panic!( - "Unexpected: prettier should be preferred since it's enabled and language supports it" - ) - }); - - project_b - .update(cx_b, |project, cx| { - project.format( - HashSet::from_iter([buffer_b.clone()]), - true, - FormatTrigger::Save, - cx, - ) - }) - .await - .unwrap(); - - executor.run_until_parked(); - assert_eq!( - buffer_b.read_with(cx_b, |buffer, _| buffer.text()), - buffer_text.to_string() + "\n" + prettier_format_suffix, - "Prettier formatting was not applied to client buffer after client's request" - ); - - project_a - .update(cx_a, |project, cx| { - project.format( - HashSet::from_iter([buffer_a.clone()]), - true, - FormatTrigger::Manual, - cx, - ) - }) - .await - .unwrap(); - - executor.run_until_parked(); - assert_eq!( - buffer_b.read_with(cx_b, |buffer, _| buffer.text()), - buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix, - "Prettier formatting was not applied to client buffer after host's request" - ); -} - -#[gpui::test(iterations = 10)] -async fn test_definition( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/root", - json!({ - "dir-1": { - "a.rs": "const ONE: usize = b::TWO + b::THREE;", - }, - "dir-2": { - "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;", - "c.rs": "type T2 = usize;", - } - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; - 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; - - // Open the file on client B. - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - - // Request the definition of a symbol as the guest. - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Scalar( - lsp::Location::new( - lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ), - ))) - }); - - let definitions_1 = project_b - .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx)) - .await - .unwrap(); - cx_b.read(|cx| { - assert_eq!(definitions_1.len(), 1); - assert_eq!(project_b.read(cx).worktrees().count(), 2); - let target_buffer = definitions_1[0].target.buffer.read(cx); - assert_eq!( - target_buffer.text(), - "const TWO: c::T2 = 2;\nconst THREE: usize = 3;" - ); - assert_eq!( - definitions_1[0].target.range.to_point(target_buffer), - Point::new(0, 6)..Point::new(0, 9) - ); - }); - - // Try getting more definitions for the same buffer, ensuring the buffer gets reused from - // the previous call to `definition`. - fake_language_server.handle_request::(|_, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Scalar( - lsp::Location::new( - lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), - ), - ))) - }); - - let definitions_2 = project_b - .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx)) - .await - .unwrap(); - cx_b.read(|cx| { - assert_eq!(definitions_2.len(), 1); - assert_eq!(project_b.read(cx).worktrees().count(), 2); - let target_buffer = definitions_2[0].target.buffer.read(cx); - assert_eq!( - target_buffer.text(), - "const TWO: c::T2 = 2;\nconst THREE: usize = 3;" - ); - assert_eq!( - definitions_2[0].target.range.to_point(target_buffer), - Point::new(1, 6)..Point::new(1, 11) - ); - }); - assert_eq!( - definitions_1[0].target.buffer, - definitions_2[0].target.buffer - ); - - fake_language_server.handle_request::( - |req, _| async move { - assert_eq!( - req.text_document_position_params.position, - lsp::Position::new(0, 7) - ); - Ok(Some(lsp::GotoDefinitionResponse::Scalar( - lsp::Location::new( - lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)), - ), - ))) - }, - ); - - let type_definitions = project_b - .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx)) - .await - .unwrap(); - cx_b.read(|cx| { - assert_eq!(type_definitions.len(), 1); - let target_buffer = type_definitions[0].target.buffer.read(cx); - assert_eq!(target_buffer.text(), "type T2 = usize;"); - assert_eq!( - type_definitions[0].target.range.to_point(target_buffer), - Point::new(0, 5)..Point::new(0, 7) - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_references( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/root", - json!({ - "dir-1": { - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - }, - "dir-2": { - "three.rs": "const THREE: usize = two::TWO + one::ONE;", - } - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; - 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; - - // Open the file on client B. - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - - // Request references to a symbol as the guest. - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri.as_str(), - "file:///root/dir-1/one.rs" - ); - Ok(Some(vec![ - lsp::Location { - uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)), - }, - lsp::Location { - uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)), - }, - lsp::Location { - uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)), - }, - ])) - }); - - let references = project_b - .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)) - .await - .unwrap(); - cx_b.read(|cx| { - assert_eq!(references.len(), 3); - assert_eq!(project_b.read(cx).worktrees().count(), 2); - - let two_buffer = references[0].buffer.read(cx); - let three_buffer = references[2].buffer.read(cx); - assert_eq!( - two_buffer.file().unwrap().path().as_ref(), - Path::new("two.rs") - ); - assert_eq!(references[1].buffer, references[0].buffer); - assert_eq!( - three_buffer.file().unwrap().full_path(cx), - Path::new("/root/dir-2/three.rs") - ); - - assert_eq!(references[0].range.to_offset(two_buffer), 24..27); - assert_eq!(references[1].range.to_offset(two_buffer), 35..38); - assert_eq!(references[2].range.to_offset(three_buffer), 37..40); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_project_search( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/root", - json!({ - "dir-1": { - "a": "hello world", - "b": "goodnight moon", - "c": "a world of goo", - "d": "world champion of clown world", - }, - "dir-2": { - "e": "disney world is fun", - } - }), - ) - .await; - let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await; - let (worktree_2, _) = project_a - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/root/dir-2", true, cx) - }) - .await - .unwrap(); - worktree_2 - .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - 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; - - // Perform a search as the guest. - let mut results = HashMap::default(); - let mut search_rx = project_b.update(cx_b, |project, cx| { - project.search( - SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(), - cx, - ) - }); - while let Some((buffer, ranges)) = search_rx.next().await { - results.entry(buffer).or_insert(ranges); - } - - let mut ranges_by_path = results - .into_iter() - .map(|(buffer, ranges)| { - buffer.read_with(cx_b, |buffer, cx| { - let path = buffer.file().unwrap().full_path(cx); - let offset_ranges = ranges - .into_iter() - .map(|range| range.to_offset(buffer)) - .collect::>(); - (path, offset_ranges) - }) - }) - .collect::>(); - ranges_by_path.sort_by_key(|(path, _)| path.clone()); - - assert_eq!( - ranges_by_path, - &[ - (PathBuf::from("dir-1/a"), vec![6..11]), - (PathBuf::from("dir-1/c"), vec![2..7]), - (PathBuf::from("dir-1/d"), vec![0..5, 24..29]), - (PathBuf::from("dir-2/e"), vec![7..12]), - ] - ); -} - -#[gpui::test(iterations = 10)] -async fn test_document_highlights( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/root-1", - json!({ - "main.rs": "fn double(number: i32) -> i32 { number + number }", - }), - ) - .await; - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; - 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; - - // Open the file on client B. - let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_b).await.unwrap(); - - // Request document highlights as the guest. - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::( - |params, _| async move { - assert_eq!( - params - .text_document_position_params - .text_document - .uri - .as_str(), - "file:///root-1/main.rs" - ); - assert_eq!( - params.text_document_position_params.position, - lsp::Position::new(0, 34) - ); - Ok(Some(vec![ - lsp::DocumentHighlight { - kind: Some(lsp::DocumentHighlightKind::WRITE), - range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)), - }, - lsp::DocumentHighlight { - kind: Some(lsp::DocumentHighlightKind::READ), - range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)), - }, - lsp::DocumentHighlight { - kind: Some(lsp::DocumentHighlightKind::READ), - range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)), - }, - ])) - }, - ); - - let highlights = project_b - .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)) - .await - .unwrap(); - - buffer_b.read_with(cx_b, |buffer, _| { - let snapshot = buffer.snapshot(); - - let highlights = highlights - .into_iter() - .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot))) - .collect::>(); - assert_eq!( - highlights, - &[ - (lsp::DocumentHighlightKind::WRITE, 10..16), - (lsp::DocumentHighlightKind::READ, 32..38), - (lsp::DocumentHighlightKind::READ, 41..47) - ] - ) - }); -} - -#[gpui::test(iterations = 10)] -async fn test_lsp_hover( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - client_a - .fs() - .insert_tree( - "/root-1", - json!({ - "main.rs": "use std::collections::HashMap;", - }), - ) - .await; - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; - 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; - - // Open the file as the guest - let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)); - let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); - - // Request hover information as the guest. - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::( - |params, _| async move { - assert_eq!( - params - .text_document_position_params - .text_document - .uri - .as_str(), - "file:///root-1/main.rs" - ); - assert_eq!( - params.text_document_position_params.position, - lsp::Position::new(0, 22) - ); - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Array(vec![ - lsp::MarkedString::String("Test hover content.".to_string()), - lsp::MarkedString::LanguageString(lsp::LanguageString { - language: "Rust".to_string(), - value: "let foo = 42;".to_string(), - }), - ]), - range: Some(lsp::Range::new( - lsp::Position::new(0, 22), - lsp::Position::new(0, 29), - )), - })) - }, - ); - - let hover_info = project_b - .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx)) - .await - .unwrap() - .unwrap(); - - buffer_b.read_with(cx_b, |buffer, _| { - let snapshot = buffer.snapshot(); - assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29); - assert_eq!( - hover_info.contents, - vec![ - project::HoverBlock { - text: "Test hover content.".to_string(), - kind: HoverBlockKind::Markdown, - }, - project::HoverBlock { - text: "let foo = 42;".to_string(), - kind: HoverBlockKind::Code { - language: "Rust".to_string() - }, - } - ] - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_project_symbols( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/code", - json!({ - "crate-1": { - "one.rs": "const ONE: usize = 1;", - }, - "crate-2": { - "two.rs": "const TWO: usize = 2; const THREE: usize = 3;", - }, - "private": { - "passwords.txt": "the-password", - } - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await; - 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; - - // Cause the language server to start. - let open_buffer_task = - project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)); - let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| async move { - Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ - #[allow(deprecated)] - lsp::SymbolInformation { - name: "TWO".into(), - location: lsp::Location { - uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - }, - kind: lsp::SymbolKind::CONSTANT, - tags: None, - container_name: None, - deprecated: None, - }, - ]))) - }); - - // Request the definition of a symbol as the guest. - let symbols = project_b - .update(cx_b, |p, cx| p.symbols("two", cx)) - .await - .unwrap(); - assert_eq!(symbols.len(), 1); - assert_eq!(symbols[0].name, "TWO"); - - // Open one of the returned symbols. - let buffer_b_2 = project_b - .update(cx_b, |project, cx| { - project.open_buffer_for_symbol(&symbols[0], cx) - }) - .await - .unwrap(); - - buffer_b_2.read_with(cx_b, |buffer, _| { - assert_eq!( - buffer.file().unwrap().path().as_ref(), - Path::new("../crate-2/two.rs") - ); - }); - - // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file. - let mut fake_symbol = symbols[0].clone(); - fake_symbol.path.path = Path::new("/code/secrets").into(); - let error = project_b - .update(cx_b, |project, cx| { - project.open_buffer_for_symbol(&fake_symbol, cx) - }) - .await - .unwrap_err(); - assert!(error.to_string().contains("invalid symbol signature")); -} - -#[gpui::test(iterations = 10)] -async fn test_open_buffer_while_getting_definition_pointing_to_it( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - mut rng: StdRng, -) { - let mut server = TestServer::start(executor.clone()).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); - - // Set up a fake language server. - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry().add(Arc::new(language)); - - client_a - .fs() - .insert_tree( - "/root", - json!({ - "a.rs": "const ONE: usize = b::TWO;", - "b.rs": "const TWO: usize = 2", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await; - 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 open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); - let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Scalar( - lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ), - ))) - }); - - let definitions; - let buffer_b2; - if rng.gen() { - definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); - buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx)); - } else { - buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx)); - definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); - } - - let buffer_b2 = buffer_b2.await.unwrap(); - let definitions = definitions.await.unwrap(); - assert_eq!(definitions.len(), 1); - assert_eq!(definitions[0].target.buffer, buffer_b2); -} - -#[gpui::test(iterations = 10)] -async fn test_contacts( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, - cx_d: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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 - .make_contacts(&mut [(&client_a, cx_a), (&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); - let _active_call_d = cx_d.read(ActiveCall::global); - - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "free"), - ("user_b".to_string(), "online", "free") - ] - ); - assert_eq!(contacts(&client_d, cx_d), []); - - server.disconnect_client(client_c.peer_id().unwrap()); - server.forbid_connections(); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "free"), - ("user_c".to_string(), "offline", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "free"), - ("user_c".to_string(), "offline", "free") - ] - ); - assert_eq!(contacts(&client_c, cx_c), []); - assert_eq!(contacts(&client_d, cx_d), []); - - server.allow_connections(); - client_c - .authenticate_and_connect(false, &cx_c.to_async()) - .await - .unwrap(); - - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "free"), - ("user_b".to_string(), "online", "free") - ] - ); - assert_eq!(contacts(&client_d, cx_d), []); - - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "busy"), - ("user_b".to_string(), "online", "busy") - ] - ); - assert_eq!(contacts(&client_d, cx_d), []); - - // Client B and client D become contacts while client B is being called. - server - .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)]) - .await; - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "free"), - ("user_d".to_string(), "online", "free"), - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "busy"), - ("user_b".to_string(), "online", "busy") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "busy")] - ); - - active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap()); - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "free"), - ("user_b".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "free")] - ); - - active_call_c - .update(cx_c, |call, cx| { - call.invite(client_a.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "free"), - ("user_c".to_string(), "online", "busy") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "busy"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "busy"), - ("user_b".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "free")] - ); - - active_call_a - .update(cx_a, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "free"), - ("user_c".to_string(), "online", "busy") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "busy"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "busy"), - ("user_b".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "free")] - ); - - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "busy") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "busy"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "busy"), - ("user_b".to_string(), "online", "busy") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "busy")] - ); - - active_call_a - .update(cx_a, |call, cx| call.hang_up(cx)) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "free"), - ("user_c".to_string(), "online", "free"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "free"), - ("user_b".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "free")] - ); - - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [ - ("user_b".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "online", "busy"), - ("user_c".to_string(), "online", "free"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "online", "busy"), - ("user_b".to_string(), "online", "busy") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "busy")] - ); - - server.forbid_connections(); - server.disconnect_client(client_a.peer_id().unwrap()); - executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - assert_eq!(contacts(&client_a, cx_a), []); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "offline", "free"), - ("user_c".to_string(), "online", "free"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [ - ("user_a".to_string(), "offline", "free"), - ("user_b".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_d, cx_d), - [("user_b".to_string(), "online", "free")] - ); - - // Test removing a contact - client_b - .user_store() - .update(cx_b, |store, cx| { - store.remove_contact(client_c.user_id().unwrap(), cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - assert_eq!( - contacts(&client_b, cx_b), - [ - ("user_a".to_string(), "offline", "free"), - ("user_d".to_string(), "online", "free") - ] - ); - assert_eq!( - contacts(&client_c, cx_c), - [("user_a".to_string(), "offline", "free"),] - ); - - fn contacts( - client: &TestClient, - cx: &TestAppContext, - ) -> Vec<(String, &'static str, &'static str)> { - client.user_store().read_with(cx, |store, _| { - store - .contacts() - .iter() - .map(|contact| { - ( - contact.user.github_login.clone(), - if contact.online { "online" } else { "offline" }, - if contact.busy { "busy" } else { "free" }, - ) - }) - .collect() - }) - } -} - -#[gpui::test(iterations = 10)] -async fn test_contact_requests( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_a2: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_b2: &mut TestAppContext, - cx_c: &mut TestAppContext, - cx_c2: &mut TestAppContext, -) { - // Connect to a server as 3 clients. - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_a2 = server.create_client(cx_a2, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_b2 = server.create_client(cx_b2, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - let client_c2 = server.create_client(cx_c2, "user_c").await; - - assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap()); - assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap()); - assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap()); - - // User A and User C request that user B become their contact. - client_a - .user_store() - .update(cx_a, |store, cx| { - store.request_contact(client_b.user_id().unwrap(), cx) - }) - .await - .unwrap(); - client_c - .user_store() - .update(cx_c, |store, cx| { - store.request_contact(client_b.user_id().unwrap(), cx) - }) - .await - .unwrap(); - executor.run_until_parked(); - - // All users see the pending request appear in all their clients. - assert_eq!( - client_a.summarize_contacts(cx_a).outgoing_requests, - &["user_b"] - ); - assert_eq!( - client_a2.summarize_contacts(cx_a2).outgoing_requests, - &["user_b"] - ); - assert_eq!( - client_b.summarize_contacts(cx_b).incoming_requests, - &["user_a", "user_c"] - ); - assert_eq!( - client_b2.summarize_contacts(cx_b2).incoming_requests, - &["user_a", "user_c"] - ); - assert_eq!( - client_c.summarize_contacts(cx_c).outgoing_requests, - &["user_b"] - ); - assert_eq!( - client_c2.summarize_contacts(cx_c2).outgoing_requests, - &["user_b"] - ); - - // Contact requests are present upon connecting (tested here via disconnect/reconnect) - disconnect_and_reconnect(&client_a, cx_a).await; - disconnect_and_reconnect(&client_b, cx_b).await; - disconnect_and_reconnect(&client_c, cx_c).await; - executor.run_until_parked(); - assert_eq!( - client_a.summarize_contacts(cx_a).outgoing_requests, - &["user_b"] - ); - assert_eq!( - client_b.summarize_contacts(cx_b).incoming_requests, - &["user_a", "user_c"] - ); - assert_eq!( - client_c.summarize_contacts(cx_c).outgoing_requests, - &["user_b"] - ); - - // User B accepts the request from user A. - client_b - .user_store() - .update(cx_b, |store, cx| { - store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // User B sees user A as their contact now in all client, and the incoming request from them is removed. - let contacts_b = client_b.summarize_contacts(cx_b); - assert_eq!(contacts_b.current, &["user_a"]); - assert_eq!(contacts_b.incoming_requests, &["user_c"]); - let contacts_b2 = client_b2.summarize_contacts(cx_b2); - assert_eq!(contacts_b2.current, &["user_a"]); - assert_eq!(contacts_b2.incoming_requests, &["user_c"]); - - // User A sees user B as their contact now in all clients, and the outgoing request to them is removed. - let contacts_a = client_a.summarize_contacts(cx_a); - assert_eq!(contacts_a.current, &["user_b"]); - assert!(contacts_a.outgoing_requests.is_empty()); - let contacts_a2 = client_a2.summarize_contacts(cx_a2); - assert_eq!(contacts_a2.current, &["user_b"]); - assert!(contacts_a2.outgoing_requests.is_empty()); - - // Contacts are present upon connecting (tested here via disconnect/reconnect) - disconnect_and_reconnect(&client_a, cx_a).await; - disconnect_and_reconnect(&client_b, cx_b).await; - disconnect_and_reconnect(&client_c, cx_c).await; - executor.run_until_parked(); - assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]); - assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]); - assert_eq!( - client_b.summarize_contacts(cx_b).incoming_requests, - &["user_c"] - ); - assert!(client_c.summarize_contacts(cx_c).current.is_empty()); - assert_eq!( - client_c.summarize_contacts(cx_c).outgoing_requests, - &["user_b"] - ); - - // User B rejects the request from user C. - client_b - .user_store() - .update(cx_b, |store, cx| { - store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx) - }) - .await - .unwrap(); - - executor.run_until_parked(); - - // User B doesn't see user C as their contact, and the incoming request from them is removed. - let contacts_b = client_b.summarize_contacts(cx_b); - assert_eq!(contacts_b.current, &["user_a"]); - assert!(contacts_b.incoming_requests.is_empty()); - let contacts_b2 = client_b2.summarize_contacts(cx_b2); - assert_eq!(contacts_b2.current, &["user_a"]); - assert!(contacts_b2.incoming_requests.is_empty()); - - // User C doesn't see user B as their contact, and the outgoing request to them is removed. - let contacts_c = client_c.summarize_contacts(cx_c); - assert!(contacts_c.current.is_empty()); - assert!(contacts_c.outgoing_requests.is_empty()); - let contacts_c2 = client_c2.summarize_contacts(cx_c2); - assert!(contacts_c2.current.is_empty()); - assert!(contacts_c2.outgoing_requests.is_empty()); - - // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect) - disconnect_and_reconnect(&client_a, cx_a).await; - disconnect_and_reconnect(&client_b, cx_b).await; - disconnect_and_reconnect(&client_c, cx_c).await; - executor.run_until_parked(); - assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]); - assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]); - assert!(client_b - .summarize_contacts(cx_b) - .incoming_requests - .is_empty()); - assert!(client_c.summarize_contacts(cx_c).current.is_empty()); - assert!(client_c - .summarize_contacts(cx_c) - .outgoing_requests - .is_empty()); - - async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) { - client.disconnect(&cx.to_async()); - client.clear_contacts(cx).await; - client - .authenticate_and_connect(false, &cx.to_async()) - .await - .unwrap(); - } -} - -#[gpui::test(iterations = 10)] -async fn test_join_call_after_screen_was_shared( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).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()); - executor.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()); - - executor.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec![], - } - ); - assert_eq!( - room_participants(&room_b, cx_b), - RoomParticipants { - remote: vec!["user_a".to_string()], - pending: vec![], - } - ); - - // Ensure User B sees User A's screenshare. - - room_b.read_with(cx_b, |room, _| { - assert_eq!( - room.remote_participants() - .get(&client_a.user_id().unwrap()) - .unwrap() - .video_tracks - .len(), - 1 - ); - }); -} diff --git a/crates/collab2/src/tests/notification_tests.rs b/crates/collab2/src/tests/notification_tests.rs deleted file mode 100644 index f6066e6409..0000000000 --- a/crates/collab2/src/tests/notification_tests.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::sync::Arc; - -use gpui::{BackgroundExecutor, TestAppContext}; -use notifications::NotificationEvent; -use parking_lot::Mutex; -use rpc::{proto, Notification}; - -use crate::tests::TestServer; - -#[gpui::test] -async fn test_notifications( - executor: BackgroundExecutor, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - let notification_events_a = Arc::new(Mutex::new(Vec::new())); - let notification_events_b = Arc::new(Mutex::new(Vec::new())); - client_a.notification_store().update(cx_a, |_, cx| { - let events = notification_events_a.clone(); - cx.subscribe(&cx.handle(), move |_, _, event, _| { - events.lock().push(event.clone()); - }) - .detach() - }); - client_b.notification_store().update(cx_b, |_, cx| { - let events = notification_events_b.clone(); - cx.subscribe(&cx.handle(), move |_, _, event, _| { - events.lock().push(event.clone()); - }) - .detach() - }); - - // Client A sends a contact request to client B. - client_a - .user_store() - .update(cx_a, |store, cx| store.request_contact(client_b.id(), cx)) - .await - .unwrap(); - - // Client B receives a contact request notification and responds to the - // request, accepting it. - executor.run_until_parked(); - client_b.notification_store().update(cx_b, |store, cx| { - assert_eq!(store.notification_count(), 1); - assert_eq!(store.unread_notification_count(), 1); - - let entry = store.notification_at(0).unwrap(); - assert_eq!( - entry.notification, - Notification::ContactRequest { - sender_id: client_a.id() - } - ); - assert!(!entry.is_read); - assert_eq!( - ¬ification_events_b.lock()[0..], - &[ - NotificationEvent::NewNotification { - entry: entry.clone(), - }, - NotificationEvent::NotificationsUpdated { - old_range: 0..0, - new_count: 1 - } - ] - ); - - store.respond_to_notification(entry.notification.clone(), true, cx); - }); - - // Client B sees the notification is now read, and that they responded. - executor.run_until_parked(); - client_b.notification_store().read_with(cx_b, |store, _| { - assert_eq!(store.notification_count(), 1); - assert_eq!(store.unread_notification_count(), 0); - - let entry = store.notification_at(0).unwrap(); - assert!(entry.is_read); - assert_eq!(entry.response, Some(true)); - assert_eq!( - ¬ification_events_b.lock()[2..], - &[ - NotificationEvent::NotificationRead { - entry: entry.clone(), - }, - NotificationEvent::NotificationsUpdated { - old_range: 0..1, - new_count: 1 - } - ] - ); - }); - - // Client A receives a notification that client B accepted their request. - client_a.notification_store().read_with(cx_a, |store, _| { - assert_eq!(store.notification_count(), 1); - assert_eq!(store.unread_notification_count(), 1); - - let entry = store.notification_at(0).unwrap(); - assert_eq!( - entry.notification, - Notification::ContactRequestAccepted { - responder_id: client_b.id() - } - ); - assert!(!entry.is_read); - }); - - // Client A creates a channel and invites client B to be a member. - let channel_id = client_a - .channel_store() - .update(cx_a, |store, cx| { - store.create_channel("the-channel", None, cx) - }) - .await - .unwrap(); - client_a - .channel_store() - .update(cx_a, |store, cx| { - store.invite_member(channel_id, client_b.id(), proto::ChannelRole::Member, cx) - }) - .await - .unwrap(); - - // Client B receives a channel invitation notification and responds to the - // invitation, accepting it. - executor.run_until_parked(); - client_b.notification_store().update(cx_b, |store, cx| { - assert_eq!(store.notification_count(), 2); - assert_eq!(store.unread_notification_count(), 1); - - let entry = store.notification_at(0).unwrap(); - assert_eq!( - entry.notification, - Notification::ChannelInvitation { - channel_id, - channel_name: "the-channel".to_string(), - inviter_id: client_a.id() - } - ); - assert!(!entry.is_read); - - store.respond_to_notification(entry.notification.clone(), true, cx); - }); - - // Client B sees the notification is now read, and that they responded. - executor.run_until_parked(); - client_b.notification_store().read_with(cx_b, |store, _| { - assert_eq!(store.notification_count(), 2); - assert_eq!(store.unread_notification_count(), 0); - - let entry = store.notification_at(0).unwrap(); - assert!(entry.is_read); - assert_eq!(entry.response, Some(true)); - }); -} diff --git a/crates/collab2/src/tests/random_channel_buffer_tests.rs b/crates/collab2/src/tests/random_channel_buffer_tests.rs deleted file mode 100644 index f980f7d908..0000000000 --- a/crates/collab2/src/tests/random_channel_buffer_tests.rs +++ /dev/null @@ -1,288 +0,0 @@ -use crate::db::ChannelRole; - -use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; -use anyhow::Result; -use async_trait::async_trait; -use gpui::{BackgroundExecutor, SharedString, TestAppContext}; -use rand::prelude::*; -use serde_derive::{Deserialize, Serialize}; -use std::{ - ops::{Deref, DerefMut, Range}, - rc::Rc, - sync::Arc, -}; -use text::Bias; - -#[gpui::test( - iterations = 100, - on_failure = "crate::tests::save_randomized_test_plan" -)] -async fn test_random_channel_buffers( - cx: &mut TestAppContext, - executor: BackgroundExecutor, - rng: StdRng, -) { - run_randomized_test::(cx, executor, rng).await; -} - -struct RandomChannelBufferTest; - -#[derive(Clone, Serialize, Deserialize)] -enum ChannelBufferOperation { - JoinChannelNotes { - channel_name: SharedString, - }, - LeaveChannelNotes { - channel_name: SharedString, - }, - EditChannelNotes { - channel_name: SharedString, - edits: Vec<(Range, Arc)>, - }, - Noop, -} - -const CHANNEL_COUNT: usize = 3; - -#[async_trait(?Send)] -impl RandomizedTest for RandomChannelBufferTest { - type Operation = ChannelBufferOperation; - - async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) { - let db = &server.app_state.db; - for ix in 0..CHANNEL_COUNT { - let id = db - .create_root_channel(&format!("channel-{ix}"), users[0].user_id) - .await - .unwrap(); - for user in &users[1..] { - db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member) - .await - .unwrap(); - db.respond_to_channel_invite(id, user.user_id, true) - .await - .unwrap(); - } - } - } - - fn generate_operation( - client: &TestClient, - rng: &mut StdRng, - _: &mut UserTestPlan, - cx: &TestAppContext, - ) -> ChannelBufferOperation { - let channel_store = client.channel_store().clone(); - let mut channel_buffers = client.channel_buffers(); - - // When signed out, we can't do anything unless a channel buffer is - // already open. - if channel_buffers.deref_mut().is_empty() - && channel_store.read_with(cx, |store, _| store.channel_count() == 0) - { - return ChannelBufferOperation::Noop; - } - - loop { - match rng.gen_range(0..100_u32) { - 0..=29 => { - let channel_name = client.channel_store().read_with(cx, |store, cx| { - store.ordered_channels().find_map(|(_, channel)| { - if store.has_open_channel_buffer(channel.id, cx) { - None - } else { - Some(channel.name.clone()) - } - }) - }); - if let Some(channel_name) = channel_name { - break ChannelBufferOperation::JoinChannelNotes { channel_name }; - } - } - - 30..=40 => { - if let Some(buffer) = channel_buffers.deref().iter().choose(rng) { - let channel_name = - buffer.read_with(cx, |b, cx| b.channel(cx).unwrap().name.clone()); - break ChannelBufferOperation::LeaveChannelNotes { channel_name }; - } - } - - _ => { - if let Some(buffer) = channel_buffers.deref().iter().choose(rng) { - break buffer.read_with(cx, |b, cx| { - let channel_name = b.channel(cx).unwrap().name.clone(); - let edits = b - .buffer() - .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); - ChannelBufferOperation::EditChannelNotes { - channel_name, - edits, - } - }); - } - } - } - } - } - - async fn apply_operation( - client: &TestClient, - operation: ChannelBufferOperation, - cx: &mut TestAppContext, - ) -> Result<(), TestError> { - match operation { - ChannelBufferOperation::JoinChannelNotes { channel_name } => { - let buffer = client.channel_store().update(cx, |store, cx| { - let channel_id = store - .ordered_channels() - .find(|(_, c)| c.name == channel_name) - .unwrap() - .1 - .id; - if store.has_open_channel_buffer(channel_id, cx) { - Err(TestError::Inapplicable) - } else { - Ok(store.open_channel_buffer(channel_id, cx)) - } - })?; - - log::info!( - "{}: opening notes for channel {channel_name}", - client.username - ); - client.channel_buffers().deref_mut().insert(buffer.await?); - } - - ChannelBufferOperation::LeaveChannelNotes { channel_name } => { - let buffer = cx.update(|cx| { - let mut left_buffer = Err(TestError::Inapplicable); - client.channel_buffers().deref_mut().retain(|buffer| { - if buffer.read(cx).channel(cx).unwrap().name == channel_name { - left_buffer = Ok(buffer.clone()); - false - } else { - true - } - }); - left_buffer - })?; - - log::info!( - "{}: closing notes for channel {channel_name}", - client.username - ); - cx.update(|_| drop(buffer)); - } - - ChannelBufferOperation::EditChannelNotes { - channel_name, - edits, - } => { - let channel_buffer = cx - .read(|cx| { - client - .channel_buffers() - .deref() - .iter() - .find(|buffer| { - buffer.read(cx).channel(cx).unwrap().name == channel_name - }) - .cloned() - }) - .ok_or_else(|| TestError::Inapplicable)?; - - log::info!( - "{}: editing notes for channel {channel_name} with {:?}", - client.username, - edits - ); - - channel_buffer.update(cx, |buffer, cx| { - let buffer = buffer.buffer(); - buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(); - buffer.edit( - edits.into_iter().map(|(range, text)| { - let start = snapshot.clip_offset(range.start, Bias::Left); - let end = snapshot.clip_offset(range.end, Bias::Right); - (start..end, text) - }), - None, - cx, - ); - }); - }); - } - - ChannelBufferOperation::Noop => Err(TestError::Inapplicable)?, - } - Ok(()) - } - - async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { - let channels = server.app_state.db.all_channels().await.unwrap(); - - for (client, client_cx) in clients.iter_mut() { - client_cx.update(|cx| { - client - .channel_buffers() - .deref_mut() - .retain(|b| b.read(cx).is_connected()); - }); - } - - for (channel_id, channel_name) in channels { - let mut prev_text: Option<(u64, String)> = None; - - let mut collaborator_user_ids = server - .app_state - .db - .get_channel_buffer_collaborators(channel_id) - .await - .unwrap() - .into_iter() - .map(|id| id.to_proto()) - .collect::>(); - collaborator_user_ids.sort(); - - for (client, client_cx) in clients.iter() { - let user_id = client.user_id().unwrap(); - client_cx.read(|cx| { - if let Some(channel_buffer) = client - .channel_buffers() - .deref() - .iter() - .find(|b| b.read(cx).channel_id == channel_id.to_proto()) - { - let channel_buffer = channel_buffer.read(cx); - - // Assert that channel buffer's text matches other clients' copies. - let text = channel_buffer.buffer().read(cx).text(); - if let Some((prev_user_id, prev_text)) = &prev_text { - assert_eq!( - &text, - prev_text, - "client {user_id} has different text than client {prev_user_id} for channel {channel_name}", - ); - } else { - prev_text = Some((user_id, text.clone())); - } - - // Assert that all clients and the server agree about who is present in the - // channel buffer. - let collaborators = channel_buffer.collaborators(); - let mut user_ids = - collaborators.values().map(|c| c.user_id).collect::>(); - user_ids.sort(); - assert_eq!( - user_ids, - collaborator_user_ids, - "client {user_id} has different user ids for channel {channel_name} than the server", - ); - } - }); - } - } - } -} diff --git a/crates/collab2/src/tests/random_project_collaboration_tests.rs b/crates/collab2/src/tests/random_project_collaboration_tests.rs deleted file mode 100644 index f4194b98e8..0000000000 --- a/crates/collab2/src/tests/random_project_collaboration_tests.rs +++ /dev/null @@ -1,1587 +0,0 @@ -use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; -use crate::{db::UserId, tests::run_randomized_test}; -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use call::ActiveCall; -use collections::{BTreeMap, HashMap}; -use editor::Bias; -use fs::{repository::GitFileStatus, FakeFs, Fs as _}; -use futures::StreamExt; -use gpui::{BackgroundExecutor, Model, TestAppContext}; -use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; -use lsp::FakeLanguageServer; -use pretty_assertions::assert_eq; -use project::{search::SearchQuery, Project, ProjectPath}; -use rand::{ - distributions::{Alphanumeric, DistString}, - prelude::*, -}; -use serde::{Deserialize, Serialize}; -use std::{ - ops::{Deref, Range}, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, -}; -use util::ResultExt; - -#[gpui::test( - iterations = 100, - on_failure = "crate::tests::save_randomized_test_plan" -)] -async fn test_random_project_collaboration( - cx: &mut TestAppContext, - executor: BackgroundExecutor, - rng: StdRng, -) { - run_randomized_test::(cx, executor, rng).await; -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum ClientOperation { - AcceptIncomingCall, - RejectIncomingCall, - LeaveCall, - InviteContactToCall { - user_id: UserId, - }, - OpenLocalProject { - first_root_name: String, - }, - OpenRemoteProject { - host_id: UserId, - first_root_name: String, - }, - AddWorktreeToProject { - project_root_name: String, - new_root_path: PathBuf, - }, - CloseRemoteProject { - project_root_name: String, - }, - OpenBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - }, - SearchProject { - project_root_name: String, - is_local: bool, - query: String, - detach: bool, - }, - EditBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - edits: Vec<(Range, Arc)>, - }, - CloseBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - }, - SaveBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - detach: bool, - }, - RequestLspDataInBuffer { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - offset: usize, - kind: LspRequestKind, - detach: bool, - }, - CreateWorktreeEntry { - project_root_name: String, - is_local: bool, - full_path: PathBuf, - is_dir: bool, - }, - WriteFsEntry { - path: PathBuf, - is_dir: bool, - content: String, - }, - GitOperation { - operation: GitOperation, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum GitOperation { - WriteGitIndex { - repo_path: PathBuf, - contents: Vec<(PathBuf, String)>, - }, - WriteGitBranch { - repo_path: PathBuf, - new_branch: Option, - }, - WriteGitStatuses { - repo_path: PathBuf, - statuses: Vec<(PathBuf, GitFileStatus)>, - git_operation: bool, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum LspRequestKind { - Rename, - Completion, - CodeAction, - Definition, - Highlights, -} - -struct ProjectCollaborationTest; - -#[async_trait(?Send)] -impl RandomizedTest for ProjectCollaborationTest { - type Operation = ClientOperation; - - async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) { - let db = &server.app_state.db; - for (ix, user_a) in users.iter().enumerate() { - for user_b in &users[ix + 1..] { - db.send_contact_request(user_a.user_id, user_b.user_id) - .await - .unwrap(); - db.respond_to_contact_request(user_b.user_id, user_a.user_id, true) - .await - .unwrap(); - } - } - } - - fn generate_operation( - client: &TestClient, - rng: &mut StdRng, - plan: &mut UserTestPlan, - cx: &TestAppContext, - ) -> ClientOperation { - let call = cx.read(ActiveCall::global); - loop { - match rng.gen_range(0..100_u32) { - // Mutate the call - 0..=29 => { - // Respond to an incoming call - if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) { - break if rng.gen_bool(0.7) { - ClientOperation::AcceptIncomingCall - } else { - ClientOperation::RejectIncomingCall - }; - } - - match rng.gen_range(0..100_u32) { - // Invite a contact to the current call - 0..=70 => { - let available_contacts = - client.user_store().read_with(cx, |user_store, _| { - user_store - .contacts() - .iter() - .filter(|contact| contact.online && !contact.busy) - .cloned() - .collect::>() - }); - if !available_contacts.is_empty() { - let contact = available_contacts.choose(rng).unwrap(); - break ClientOperation::InviteContactToCall { - user_id: UserId(contact.user.id as i32), - }; - } - } - - // Leave the current call - 71.. => { - if plan.allow_client_disconnection - && call.read_with(cx, |call, _| call.room().is_some()) - { - break ClientOperation::LeaveCall; - } - } - } - } - - // Mutate projects - 30..=59 => match rng.gen_range(0..100_u32) { - // Open a new project - 0..=70 => { - // Open a remote project - if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) { - let existing_remote_project_ids = cx.read(|cx| { - client - .remote_projects() - .iter() - .map(|p| p.read(cx).remote_id().unwrap()) - .collect::>() - }); - let new_remote_projects = room.read_with(cx, |room, _| { - room.remote_participants() - .values() - .flat_map(|participant| { - participant.projects.iter().filter_map(|project| { - if existing_remote_project_ids.contains(&project.id) { - None - } else { - Some(( - UserId::from_proto(participant.user.id), - project.worktree_root_names[0].clone(), - )) - } - }) - }) - .collect::>() - }); - if !new_remote_projects.is_empty() { - let (host_id, first_root_name) = - new_remote_projects.choose(rng).unwrap().clone(); - break ClientOperation::OpenRemoteProject { - host_id, - first_root_name, - }; - } - } - // Open a local project - else { - let first_root_name = plan.next_root_dir_name(); - break ClientOperation::OpenLocalProject { first_root_name }; - } - } - - // Close a remote project - 71..=80 => { - if !client.remote_projects().is_empty() { - let project = client.remote_projects().choose(rng).unwrap().clone(); - let first_root_name = root_name_for_project(&project, cx); - break ClientOperation::CloseRemoteProject { - project_root_name: first_root_name, - }; - } - } - - // Mutate project worktrees - 81.. => match rng.gen_range(0..100_u32) { - // Add a worktree to a local project - 0..=50 => { - let Some(project) = client.local_projects().choose(rng).cloned() else { - continue; - }; - let project_root_name = root_name_for_project(&project, cx); - let mut paths = client.fs().paths(false); - paths.remove(0); - let new_root_path = if paths.is_empty() || rng.gen() { - Path::new("/").join(&plan.next_root_dir_name()) - } else { - paths.choose(rng).unwrap().clone() - }; - break ClientOperation::AddWorktreeToProject { - project_root_name, - new_root_path, - }; - } - - // Add an entry to a worktree - _ => { - let Some(project) = choose_random_project(client, rng) else { - continue; - }; - let project_root_name = root_name_for_project(&project, cx); - let is_local = project.read_with(cx, |project, _| project.is_local()); - let worktree = project.read_with(cx, |project, cx| { - project - .worktrees() - .filter(|worktree| { - let worktree = worktree.read(cx); - worktree.is_visible() - && worktree.entries(false).any(|e| e.is_file()) - && worktree.root_entry().map_or(false, |e| e.is_dir()) - }) - .choose(rng) - }); - let Some(worktree) = worktree else { continue }; - let is_dir = rng.gen::(); - let mut full_path = - worktree.read_with(cx, |w, _| PathBuf::from(w.root_name())); - full_path.push(gen_file_name(rng)); - if !is_dir { - full_path.set_extension("rs"); - } - break ClientOperation::CreateWorktreeEntry { - project_root_name, - is_local, - full_path, - is_dir, - }; - } - }, - }, - - // Query and mutate buffers - 60..=90 => { - let Some(project) = choose_random_project(client, rng) else { - continue; - }; - let project_root_name = root_name_for_project(&project, cx); - let is_local = project.read_with(cx, |project, _| project.is_local()); - - match rng.gen_range(0..100_u32) { - // Manipulate an existing buffer - 0..=70 => { - let Some(buffer) = client - .buffers_for_project(&project) - .iter() - .choose(rng) - .cloned() - else { - continue; - }; - - let full_path = buffer - .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); - - match rng.gen_range(0..100_u32) { - // Close the buffer - 0..=15 => { - break ClientOperation::CloseBuffer { - project_root_name, - is_local, - full_path, - }; - } - // Save the buffer - 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => { - let detach = rng.gen_bool(0.3); - break ClientOperation::SaveBuffer { - project_root_name, - is_local, - full_path, - detach, - }; - } - // Edit the buffer - 30..=69 => { - let edits = buffer - .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); - break ClientOperation::EditBuffer { - project_root_name, - is_local, - full_path, - edits, - }; - } - // Make an LSP request - _ => { - let offset = buffer.read_with(cx, |buffer, _| { - buffer.clip_offset( - rng.gen_range(0..=buffer.len()), - language::Bias::Left, - ) - }); - let detach = rng.gen(); - break ClientOperation::RequestLspDataInBuffer { - project_root_name, - full_path, - offset, - is_local, - kind: match rng.gen_range(0..5_u32) { - 0 => LspRequestKind::Rename, - 1 => LspRequestKind::Highlights, - 2 => LspRequestKind::Definition, - 3 => LspRequestKind::CodeAction, - 4.. => LspRequestKind::Completion, - }, - detach, - }; - } - } - } - - 71..=80 => { - let query = rng.gen_range('a'..='z').to_string(); - let detach = rng.gen_bool(0.3); - break ClientOperation::SearchProject { - project_root_name, - is_local, - query, - detach, - }; - } - - // Open a buffer - 81.. => { - let worktree = project.read_with(cx, |project, cx| { - project - .worktrees() - .filter(|worktree| { - let worktree = worktree.read(cx); - worktree.is_visible() - && worktree.entries(false).any(|e| e.is_file()) - }) - .choose(rng) - }); - let Some(worktree) = worktree else { continue }; - let full_path = worktree.read_with(cx, |worktree, _| { - let entry = worktree - .entries(false) - .filter(|e| e.is_file()) - .choose(rng) - .unwrap(); - if entry.path.as_ref() == Path::new("") { - Path::new(worktree.root_name()).into() - } else { - Path::new(worktree.root_name()).join(&entry.path) - } - }); - break ClientOperation::OpenBuffer { - project_root_name, - is_local, - full_path, - }; - } - } - } - - // Update a git related action - 91..=95 => { - break ClientOperation::GitOperation { - operation: generate_git_operation(rng, client), - }; - } - - // Create or update a file or directory - 96.. => { - let is_dir = rng.gen::(); - let content; - let mut path; - let dir_paths = client.fs().directories(false); - - if is_dir { - content = String::new(); - path = dir_paths.choose(rng).unwrap().clone(); - path.push(gen_file_name(rng)); - } else { - content = Alphanumeric.sample_string(rng, 16); - - // Create a new file or overwrite an existing file - let file_paths = client.fs().files(); - if file_paths.is_empty() || rng.gen_bool(0.5) { - path = dir_paths.choose(rng).unwrap().clone(); - path.push(gen_file_name(rng)); - path.set_extension("rs"); - } else { - path = file_paths.choose(rng).unwrap().clone() - }; - } - break ClientOperation::WriteFsEntry { - path, - is_dir, - content, - }; - } - } - } - } - - async fn apply_operation( - client: &TestClient, - operation: ClientOperation, - cx: &mut TestAppContext, - ) -> Result<(), TestError> { - match operation { - ClientOperation::AcceptIncomingCall => { - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) { - Err(TestError::Inapplicable)?; - } - - log::info!("{}: accepting incoming call", client.username); - active_call - .update(cx, |call, cx| call.accept_incoming(cx)) - .await?; - } - - ClientOperation::RejectIncomingCall => { - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) { - Err(TestError::Inapplicable)?; - } - - log::info!("{}: declining incoming call", client.username); - active_call.update(cx, |call, cx| call.decline_incoming(cx))?; - } - - ClientOperation::LeaveCall => { - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.room().is_none()) { - Err(TestError::Inapplicable)?; - } - - log::info!("{}: hanging up", client.username); - active_call.update(cx, |call, cx| call.hang_up(cx)).await?; - } - - ClientOperation::InviteContactToCall { user_id } => { - let active_call = cx.read(ActiveCall::global); - - log::info!("{}: inviting {}", client.username, user_id,); - active_call - .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx)) - .await - .log_err(); - } - - ClientOperation::OpenLocalProject { first_root_name } => { - log::info!( - "{}: opening local project at {:?}", - client.username, - first_root_name - ); - - let root_path = Path::new("/").join(&first_root_name); - client.fs().create_dir(&root_path).await.unwrap(); - client - .fs() - .create_file(&root_path.join("main.rs"), Default::default()) - .await - .unwrap(); - let project = client.build_local_project(root_path, cx).await.0; - ensure_project_shared(&project, client, cx).await; - client.local_projects_mut().push(project.clone()); - } - - ClientOperation::AddWorktreeToProject { - project_root_name, - new_root_path, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: finding/creating local worktree at {:?} to project with root path {}", - client.username, - new_root_path, - project_root_name - ); - - ensure_project_shared(&project, client, cx).await; - if !client.fs().paths(false).contains(&new_root_path) { - client.fs().create_dir(&new_root_path).await.unwrap(); - } - project - .update(cx, |project, cx| { - project.find_or_create_local_worktree(&new_root_path, true, cx) - }) - .await - .unwrap(); - } - - ClientOperation::CloseRemoteProject { project_root_name } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: closing remote project with root path {}", - client.username, - project_root_name, - ); - - let ix = client - .remote_projects() - .iter() - .position(|p| p == &project) - .unwrap(); - cx.update(|_| { - client.remote_projects_mut().remove(ix); - client.buffers().retain(|p, _| *p != project); - drop(project); - }); - } - - ClientOperation::OpenRemoteProject { - host_id, - first_root_name, - } => { - let active_call = cx.read(ActiveCall::global); - let project = active_call - .update(cx, |call, cx| { - let room = call.room().cloned()?; - let participant = room - .read(cx) - .remote_participants() - .get(&host_id.to_proto())?; - let project_id = participant - .projects - .iter() - .find(|project| project.worktree_root_names[0] == first_root_name)? - .id; - Some(room.update(cx, |room, cx| { - room.join_project( - project_id, - client.language_registry().clone(), - FakeFs::new(cx.background_executor().clone()), - cx, - ) - })) - }) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: joining remote project of user {}, root name {}", - client.username, - host_id, - first_root_name, - ); - - let project = project.await?; - client.remote_projects_mut().push(project.clone()); - } - - ClientOperation::CreateWorktreeEntry { - project_root_name, - is_local, - full_path, - is_dir, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let project_path = project_path_for_full_path(&project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: creating {} at path {:?} in {} project {}", - client.username, - if is_dir { "dir" } else { "file" }, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - ); - - ensure_project_shared(&project, client, cx).await; - project - .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) - .await?; - } - - ClientOperation::OpenBuffer { - project_root_name, - is_local, - full_path, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let project_path = project_path_for_full_path(&project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: opening buffer {:?} in {} project {}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - ); - - ensure_project_shared(&project, client, cx).await; - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx)) - .await?; - client.buffers_for_project(&project).insert(buffer); - } - - ClientOperation::EditBuffer { - project_root_name, - is_local, - full_path, - edits, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: editing buffer {:?} in {} project {} with {:?}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - edits - ); - - ensure_project_shared(&project, client, cx).await; - buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(); - buffer.edit( - edits.into_iter().map(|(range, text)| { - let start = snapshot.clip_offset(range.start, Bias::Left); - let end = snapshot.clip_offset(range.end, Bias::Right); - (start..end, text) - }), - None, - cx, - ); - }); - } - - ClientOperation::CloseBuffer { - project_root_name, - is_local, - full_path, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: closing buffer {:?} in {} project {}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name - ); - - ensure_project_shared(&project, client, cx).await; - cx.update(|_| { - client.buffers_for_project(&project).remove(&buffer); - drop(buffer); - }); - } - - ClientOperation::SaveBuffer { - project_root_name, - is_local, - full_path, - detach, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: saving buffer {:?} in {} project {}, {}", - client.username, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - if detach { "detaching" } else { "awaiting" } - ); - - ensure_project_shared(&project, client, cx).await; - let requested_version = buffer.read_with(cx, |buffer, _| buffer.version()); - let save = - project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); - let save = cx.spawn(|cx| async move { - save.await - .map_err(|err| anyhow!("save request failed: {:?}", err))?; - assert!(buffer - .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() }) - .expect("App should not be dropped") - .observed_all(&requested_version)); - anyhow::Ok(()) - }); - if detach { - cx.update(|cx| save.detach_and_log_err(cx)); - } else { - save.await?; - } - } - - ClientOperation::RequestLspDataInBuffer { - project_root_name, - is_local, - full_path, - offset, - kind, - detach, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - let buffer = buffer_for_full_path(client, &project, &full_path, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: request LSP {:?} for buffer {:?} in {} project {}, {}", - client.username, - kind, - full_path, - if is_local { "local" } else { "remote" }, - project_root_name, - if detach { "detaching" } else { "awaiting" } - ); - - use futures::{FutureExt as _, TryFutureExt as _}; - let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left)); - - let process_lsp_request = project.update(cx, |project, cx| match kind { - LspRequestKind::Rename => project - .prepare_rename(buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Completion => project - .completions(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::CodeAction => project - .code_actions(&buffer, offset..offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Definition => project - .definition(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - LspRequestKind::Highlights => project - .document_highlights(&buffer, offset, cx) - .map_ok(|_| ()) - .boxed(), - }); - let request = cx.foreground_executor().spawn(process_lsp_request); - if detach { - request.detach(); - } else { - request.await?; - } - } - - ClientOperation::SearchProject { - project_root_name, - is_local, - query, - detach, - } => { - let project = project_for_root_name(client, &project_root_name, cx) - .ok_or(TestError::Inapplicable)?; - - log::info!( - "{}: search {} project {} for {:?}, {}", - client.username, - if is_local { "local" } else { "remote" }, - project_root_name, - query, - if detach { "detaching" } else { "awaiting" } - ); - - let mut search = project.update(cx, |project, cx| { - project.search( - SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()) - .unwrap(), - cx, - ) - }); - drop(project); - let search = cx.executor().spawn(async move { - let mut results = HashMap::default(); - while let Some((buffer, ranges)) = search.next().await { - results.entry(buffer).or_insert(ranges); - } - results - }); - search.await; - } - - ClientOperation::WriteFsEntry { - path, - is_dir, - content, - } => { - if !client - .fs() - .directories(false) - .contains(&path.parent().unwrap().to_owned()) - { - return Err(TestError::Inapplicable); - } - - if is_dir { - log::info!("{}: creating dir at {:?}", client.username, path); - client.fs().create_dir(&path).await.unwrap(); - } else { - let exists = client.fs().metadata(&path).await?.is_some(); - let verb = if exists { "updating" } else { "creating" }; - log::info!("{}: {} file at {:?}", verb, client.username, path); - - client - .fs() - .save(&path, &content.as_str().into(), text::LineEnding::Unix) - .await - .unwrap(); - } - } - - ClientOperation::GitOperation { operation } => match operation { - GitOperation::WriteGitIndex { - repo_path, - contents, - } => { - if !client.fs().directories(false).contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - for (path, _) in contents.iter() { - if !client.fs().files().contains(&repo_path.join(path)) { - return Err(TestError::Inapplicable); - } - } - - log::info!( - "{}: writing git index for repo {:?}: {:?}", - client.username, - repo_path, - contents - ); - - let dot_git_dir = repo_path.join(".git"); - let contents = contents - .iter() - .map(|(path, contents)| (path.as_path(), contents.clone())) - .collect::>(); - if client.fs().metadata(&dot_git_dir).await?.is_none() { - client.fs().create_dir(&dot_git_dir).await?; - } - client.fs().set_index_for_repo(&dot_git_dir, &contents); - } - GitOperation::WriteGitBranch { - repo_path, - new_branch, - } => { - if !client.fs().directories(false).contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - log::info!( - "{}: writing git branch for repo {:?}: {:?}", - client.username, - repo_path, - new_branch - ); - - let dot_git_dir = repo_path.join(".git"); - if client.fs().metadata(&dot_git_dir).await?.is_none() { - client.fs().create_dir(&dot_git_dir).await?; - } - client - .fs() - .set_branch_name(&dot_git_dir, new_branch.clone()); - } - GitOperation::WriteGitStatuses { - repo_path, - statuses, - git_operation, - } => { - if !client.fs().directories(false).contains(&repo_path) { - return Err(TestError::Inapplicable); - } - for (path, _) in statuses.iter() { - if !client.fs().files().contains(&repo_path.join(path)) { - return Err(TestError::Inapplicable); - } - } - - log::info!( - "{}: writing git statuses for repo {:?}: {:?}", - client.username, - repo_path, - statuses - ); - - let dot_git_dir = repo_path.join(".git"); - - let statuses = statuses - .iter() - .map(|(path, val)| (path.as_path(), val.clone())) - .collect::>(); - - if client.fs().metadata(&dot_git_dir).await?.is_none() { - client.fs().create_dir(&dot_git_dir).await?; - } - - if git_operation { - client.fs().set_status_for_repo_via_git_operation( - &dot_git_dir, - statuses.as_slice(), - ); - } else { - client.fs().set_status_for_repo_via_working_copy_change( - &dot_git_dir, - statuses.as_slice(), - ); - } - } - }, - } - Ok(()) - } - - async fn on_client_added(client: &Rc, _: &mut TestAppContext) { - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - None, - ); - language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-fake-language-server", - capabilities: lsp::LanguageServer::full_capabilities(), - initializer: Some(Box::new({ - let fs = client.app_state.fs.clone(); - move |fake_server: &mut FakeLanguageServer| { - fake_server.handle_request::( - |_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit( - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "the-new-text".to_string(), - }, - )), - ..Default::default() - }, - ]))) - }, - ); - - fake_server.handle_request::( - |_, _| async move { - Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), - ..Default::default() - }, - )])) - }, - ); - - fake_server.handle_request::( - |params, _| async move { - Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( - params.position, - params.position, - )))) - }, - ); - - fake_server.handle_request::({ - let fs = fs.clone(); - move |_, cx| { - let background = cx.background_executor(); - let mut rng = background.rng(); - let count = rng.gen_range::(1..3); - let files = fs.as_fake().files(); - let files = (0..count) - .map(|_| files.choose(&mut rng).unwrap().clone()) - .collect::>(); - async move { - log::info!("LSP: Returning definitions in files {:?}", &files); - Ok(Some(lsp::GotoDefinitionResponse::Array( - files - .into_iter() - .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), - range: Default::default(), - }) - .collect(), - ))) - } - } - }); - - fake_server.handle_request::( - move |_, cx| { - let mut highlights = Vec::new(); - let background = cx.background_executor(); - let mut rng = background.rng(); - - let highlight_count = rng.gen_range(1..=5); - for _ in 0..highlight_count { - let start_row = rng.gen_range(0..100); - let start_column = rng.gen_range(0..100); - let end_row = rng.gen_range(0..100); - let end_column = rng.gen_range(0..100); - let start = PointUtf16::new(start_row, start_column); - let end = PointUtf16::new(end_row, end_column); - let range = if start > end { end..start } else { start..end }; - highlights.push(lsp::DocumentHighlight { - range: range_to_lsp(range.clone()), - kind: Some(lsp::DocumentHighlightKind::READ), - }); - } - highlights.sort_unstable_by_key(|highlight| { - (highlight.range.start, highlight.range.end) - }); - async move { Ok(Some(highlights)) } - }, - ); - } - })), - ..Default::default() - })) - .await; - client.app_state.languages.add(Arc::new(language)); - } - - async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { - for (client, client_cx) in clients.iter() { - for guest_project in client.remote_projects().iter() { - guest_project.read_with(client_cx, |guest_project, cx| { - let host_project = clients.iter().find_map(|(client, cx)| { - let project = client - .local_projects() - .iter() - .find(|host_project| { - host_project.read_with(cx, |host_project, _| { - host_project.remote_id() == guest_project.remote_id() - }) - })? - .clone(); - Some((project, cx)) - }); - - if !guest_project.is_read_only() { - if let Some((host_project, host_cx)) = host_project { - let host_worktree_snapshots = - host_project.read_with(host_cx, |host_project, cx| { - host_project - .worktrees() - .map(|worktree| { - let worktree = worktree.read(cx); - (worktree.id(), worktree.snapshot()) - }) - .collect::>() - }); - let guest_worktree_snapshots = guest_project - .worktrees() - .map(|worktree| { - let worktree = worktree.read(cx); - (worktree.id(), worktree.snapshot()) - }) - .collect::>(); - - assert_eq!( - guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::>(), - host_worktree_snapshots.values().map(|w| w.abs_path()).collect::>(), - "{} has different worktrees than the host for project {:?}", - client.username, guest_project.remote_id(), - ); - - for (id, host_snapshot) in &host_worktree_snapshots { - let guest_snapshot = &guest_worktree_snapshots[id]; - assert_eq!( - guest_snapshot.root_name(), - host_snapshot.root_name(), - "{} has different root name than the host for worktree {}, project {:?}", - client.username, - id, - guest_project.remote_id(), - ); - assert_eq!( - guest_snapshot.abs_path(), - host_snapshot.abs_path(), - "{} has different abs path than the host for worktree {}, project: {:?}", - client.username, - id, - guest_project.remote_id(), - ); - assert_eq!( - guest_snapshot.entries(false).collect::>(), - host_snapshot.entries(false).collect::>(), - "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}", - client.username, - host_snapshot.abs_path(), - id, - guest_project.remote_id(), - ); - assert_eq!(guest_snapshot.repositories().collect::>(), host_snapshot.repositories().collect::>(), - "{} has different repositories than the host for worktree {:?} and project {:?}", - client.username, - host_snapshot.abs_path(), - guest_project.remote_id(), - ); - assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(), - "{} has different scan id than the host for worktree {:?} and project {:?}", - client.username, - host_snapshot.abs_path(), - guest_project.remote_id(), - ); - } - } - } - - for buffer in guest_project.opened_buffers() { - let buffer = buffer.read(cx); - assert_eq!( - buffer.deferred_ops_len(), - 0, - "{} has deferred operations for buffer {:?} in project {:?}", - client.username, - buffer.file().unwrap().full_path(cx), - guest_project.remote_id(), - ); - } - }); - } - - let buffers = client.buffers().clone(); - for (guest_project, guest_buffers) in &buffers { - let project_id = if guest_project.read_with(client_cx, |project, _| { - project.is_local() || project.is_read_only() - }) { - continue; - } else { - guest_project - .read_with(client_cx, |project, _| project.remote_id()) - .unwrap() - }; - let guest_user_id = client.user_id().unwrap(); - - let host_project = clients.iter().find_map(|(client, cx)| { - let project = client - .local_projects() - .iter() - .find(|host_project| { - host_project.read_with(cx, |host_project, _| { - host_project.remote_id() == Some(project_id) - }) - })? - .clone(); - Some((client.user_id().unwrap(), project, cx)) - }); - - let (host_user_id, host_project, host_cx) = - if let Some((host_user_id, host_project, host_cx)) = host_project { - (host_user_id, host_project, host_cx) - } else { - continue; - }; - - for guest_buffer in guest_buffers { - let buffer_id = - guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id()); - let host_buffer = host_project.read_with(host_cx, |project, _| { - project.buffer_for_id(buffer_id).unwrap_or_else(|| { - panic!( - "host does not have buffer for guest:{}, peer:{:?}, id:{}", - client.username, - client.peer_id(), - buffer_id - ) - }) - }); - let path = host_buffer - .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); - - assert_eq!( - guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()), - 0, - "{}, buffer {}, path {:?} has deferred operations", - client.username, - buffer_id, - path, - ); - assert_eq!( - guest_buffer.read_with(client_cx, |buffer, _| buffer.text()), - host_buffer.read_with(host_cx, |buffer, _| buffer.text()), - "{}, buffer {}, path {:?}, differs from the host's buffer", - client.username, - buffer_id, - path - ); - - let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned()); - let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned()); - match (host_file, guest_file) { - (Some(host_file), Some(guest_file)) => { - assert_eq!(guest_file.path(), host_file.path()); - assert_eq!(guest_file.is_deleted(), host_file.is_deleted()); - assert_eq!( - guest_file.mtime(), - host_file.mtime(), - "guest {} mtime does not match host {} for path {:?} in project {}", - guest_user_id, - host_user_id, - guest_file.path(), - project_id, - ); - } - (None, None) => {} - (None, _) => panic!("host's file is None, guest's isn't"), - (_, None) => panic!("guest's file is None, hosts's isn't"), - } - - let host_diff_base = host_buffer - .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string)); - let guest_diff_base = guest_buffer - .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string)); - assert_eq!( - guest_diff_base, host_diff_base, - "guest {} diff base does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_saved_version = - host_buffer.read_with(host_cx, |b, _| b.saved_version().clone()); - let guest_saved_version = - guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone()); - assert_eq!( - guest_saved_version, host_saved_version, - "guest {} saved version does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_saved_version_fingerprint = - host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint()); - let guest_saved_version_fingerprint = - guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint()); - assert_eq!( - guest_saved_version_fingerprint, host_saved_version_fingerprint, - "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime()); - let guest_saved_mtime = - guest_buffer.read_with(client_cx, |b, _| b.saved_mtime()); - assert_eq!( - guest_saved_mtime, host_saved_mtime, - "guest {} saved mtime does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty()); - let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty()); - assert_eq!(guest_is_dirty, host_is_dirty, - "guest {} dirty status does not match host's for path {path:?} in project {project_id}", - client.username - ); - - let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict()); - let guest_has_conflict = - guest_buffer.read_with(client_cx, |b, _| b.has_conflict()); - assert_eq!(guest_has_conflict, host_has_conflict, - "guest {} conflict status does not match host's for path {path:?} in project {project_id}", - client.username - ); - } - } - } - } -} - -fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation { - fn generate_file_paths( - repo_path: &Path, - rng: &mut StdRng, - client: &TestClient, - ) -> Vec { - let mut paths = client - .fs() - .files() - .into_iter() - .filter(|path| path.starts_with(repo_path)) - .collect::>(); - - let count = rng.gen_range(0..=paths.len()); - paths.shuffle(rng); - paths.truncate(count); - - paths - .iter() - .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf()) - .collect::>() - } - - let repo_path = client.fs().directories(false).choose(rng).unwrap().clone(); - - match rng.gen_range(0..100_u32) { - 0..=25 => { - let file_paths = generate_file_paths(&repo_path, rng, client); - - let contents = file_paths - .into_iter() - .map(|path| (path, Alphanumeric.sample_string(rng, 16))) - .collect(); - - GitOperation::WriteGitIndex { - repo_path, - contents, - } - } - 26..=63 => { - let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8)); - - GitOperation::WriteGitBranch { - repo_path, - new_branch, - } - } - 64..=100 => { - let file_paths = generate_file_paths(&repo_path, rng, client); - - let statuses = file_paths - .into_iter() - .map(|paths| { - ( - paths, - match rng.gen_range(0..3_u32) { - 0 => GitFileStatus::Added, - 1 => GitFileStatus::Modified, - 2 => GitFileStatus::Conflict, - _ => unreachable!(), - }, - ) - }) - .collect::>(); - - let git_operation = rng.gen::(); - - GitOperation::WriteGitStatuses { - repo_path, - statuses, - git_operation, - } - } - _ => unreachable!(), - } -} - -fn buffer_for_full_path( - client: &TestClient, - project: &Model, - full_path: &PathBuf, - cx: &TestAppContext, -) -> Option> { - client - .buffers_for_project(project) - .iter() - .find(|buffer| { - buffer.read_with(cx, |buffer, cx| { - buffer.file().unwrap().full_path(cx) == *full_path - }) - }) - .cloned() -} - -fn project_for_root_name( - client: &TestClient, - root_name: &str, - cx: &TestAppContext, -) -> Option> { - if let Some(ix) = project_ix_for_root_name(&*client.local_projects().deref(), root_name, cx) { - return Some(client.local_projects()[ix].clone()); - } - if let Some(ix) = project_ix_for_root_name(&*client.remote_projects().deref(), root_name, cx) { - return Some(client.remote_projects()[ix].clone()); - } - None -} - -fn project_ix_for_root_name( - projects: &[Model], - root_name: &str, - cx: &TestAppContext, -) -> Option { - projects.iter().position(|project| { - project.read_with(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next().unwrap(); - worktree.read(cx).root_name() == root_name - }) - }) -} - -fn root_name_for_project(project: &Model, cx: &TestAppContext) -> String { - project.read_with(cx, |project, cx| { - project - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .root_name() - .to_string() - }) -} - -fn project_path_for_full_path( - project: &Model, - full_path: &Path, - cx: &TestAppContext, -) -> Option { - let mut components = full_path.components(); - let root_name = components.next().unwrap().as_os_str().to_str().unwrap(); - let path = components.as_path().into(); - let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees().find_map(|worktree| { - let worktree = worktree.read(cx); - if worktree.root_name() == root_name { - Some(worktree.id()) - } else { - None - } - }) - })?; - Some(ProjectPath { worktree_id, path }) -} - -async fn ensure_project_shared( - project: &Model, - client: &TestClient, - cx: &mut TestAppContext, -) { - let first_root_name = root_name_for_project(project, cx); - let active_call = cx.read(ActiveCall::global); - if active_call.read_with(cx, |call, _| call.room().is_some()) - && project.read_with(cx, |project, _| project.is_local() && !project.is_shared()) - { - match active_call - .update(cx, |call, cx| call.share_project(project.clone(), cx)) - .await - { - Ok(project_id) => { - log::info!( - "{}: shared project {} with id {}", - client.username, - first_root_name, - project_id - ); - } - Err(error) => { - log::error!( - "{}: error sharing project {}: {:?}", - client.username, - first_root_name, - error - ); - } - } - } -} - -fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option> { - client - .local_projects() - .deref() - .iter() - .chain(client.remote_projects().iter()) - .choose(rng) - .cloned() -} - -fn gen_file_name(rng: &mut StdRng) -> String { - let mut name = String::new(); - for _ in 0..10 { - let letter = rng.gen_range('a'..='z'); - name.push(letter); - } - name -} diff --git a/crates/collab2/src/tests/randomized_test_helpers.rs b/crates/collab2/src/tests/randomized_test_helpers.rs deleted file mode 100644 index 91bd9cf6f6..0000000000 --- a/crates/collab2/src/tests/randomized_test_helpers.rs +++ /dev/null @@ -1,677 +0,0 @@ -use crate::{ - db::{self, NewUserParams, UserId}, - rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::{TestClient, TestServer}, -}; -use async_trait::async_trait; -use futures::StreamExt; -use gpui::{BackgroundExecutor, Task, TestAppContext}; -use parking_lot::Mutex; -use rand::prelude::*; -use rpc::RECEIVE_TIMEOUT; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use settings::SettingsStore; -use std::{ - env, - path::PathBuf, - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, - Arc, - }, -}; - -lazy_static::lazy_static! { - static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); - static ref PLAN_SAVE_PATH: Option = path_env_var("SAVE_PLAN"); - static ref MAX_PEERS: usize = env::var("MAX_PEERS") - .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) - .unwrap_or(3); - static ref MAX_OPERATIONS: usize = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - -} - -static LOADED_PLAN_JSON: Mutex>> = Mutex::new(None); -static LAST_PLAN: Mutex Vec>>> = Mutex::new(None); - -struct TestPlan { - rng: StdRng, - replay: bool, - stored_operations: Vec<(StoredOperation, Arc)>, - max_operations: usize, - operation_ix: usize, - users: Vec, - next_batch_id: usize, - allow_server_restarts: bool, - allow_client_reconnection: bool, - allow_client_disconnection: bool, -} - -pub struct UserTestPlan { - pub user_id: UserId, - pub username: String, - pub allow_client_reconnection: bool, - pub allow_client_disconnection: bool, - next_root_id: usize, - operation_ix: usize, - online: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(untagged)] -enum StoredOperation { - Server(ServerOperation), - Client { - user_id: UserId, - batch_id: usize, - operation: T, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum ServerOperation { - AddConnection { - user_id: UserId, - }, - RemoveConnection { - user_id: UserId, - }, - BounceConnection { - user_id: UserId, - }, - RestartServer, - MutateClients { - batch_id: usize, - #[serde(skip_serializing)] - #[serde(skip_deserializing)] - user_ids: Vec, - quiesce: bool, - }, -} - -pub enum TestError { - Inapplicable, - Other(anyhow::Error), -} - -#[async_trait(?Send)] -pub trait RandomizedTest: 'static + Sized { - type Operation: Send + Clone + Serialize + DeserializeOwned; - - fn generate_operation( - client: &TestClient, - rng: &mut StdRng, - plan: &mut UserTestPlan, - cx: &TestAppContext, - ) -> Self::Operation; - - async fn apply_operation( - client: &TestClient, - operation: Self::Operation, - cx: &mut TestAppContext, - ) -> Result<(), TestError>; - - async fn initialize(server: &mut TestServer, users: &[UserTestPlan]); - - async fn on_client_added(_client: &Rc, _cx: &mut TestAppContext) {} - - async fn on_quiesce(server: &mut TestServer, client: &mut [(Rc, TestAppContext)]); -} - -pub async fn run_randomized_test( - cx: &mut TestAppContext, - executor: BackgroundExecutor, - rng: StdRng, -) { - let mut server = TestServer::start(executor.clone()).await; - let plan = TestPlan::::new(&mut server, rng).await; - - LAST_PLAN.lock().replace({ - let plan = plan.clone(); - Box::new(move || plan.lock().serialize()) - }); - - let mut clients = Vec::new(); - let mut client_tasks = Vec::new(); - let mut operation_channels = Vec::new(); - loop { - let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { - break; - }; - applied.store(true, SeqCst); - let did_apply = TestPlan::apply_server_operation( - plan.clone(), - executor.clone(), - &mut server, - &mut clients, - &mut client_tasks, - &mut operation_channels, - next_operation, - cx, - ) - .await; - if !did_apply { - applied.store(false, SeqCst); - } - } - - drop(operation_channels); - executor.start_waiting(); - futures::future::join_all(client_tasks).await; - executor.finish_waiting(); - - executor.run_until_parked(); - T::on_quiesce(&mut server, &mut clients).await; - - for (client, cx) in clients { - cx.update(|cx| { - let store = cx.remove_global::(); - cx.clear_globals(); - cx.set_global(store); - drop(client); - }); - } - executor.run_until_parked(); - - if let Some(path) = &*PLAN_SAVE_PATH { - eprintln!("saved test plan to path {:?}", path); - std::fs::write(path, plan.lock().serialize()).unwrap(); - } -} - -pub fn save_randomized_test_plan() { - if let Some(serialize_plan) = LAST_PLAN.lock().take() { - if let Some(path) = &*PLAN_SAVE_PATH { - eprintln!("saved test plan to path {:?}", path); - std::fs::write(path, serialize_plan()).unwrap(); - } - } -} - -impl TestPlan { - pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc> { - let allow_server_restarts = rng.gen_bool(0.7); - let allow_client_reconnection = rng.gen_bool(0.7); - let allow_client_disconnection = rng.gen_bool(0.1); - - let mut users = Vec::new(); - for ix in 0..*MAX_PEERS { - let username = format!("user-{}", ix + 1); - let user_id = server - .app_state - .db - .create_user( - &format!("{username}@example.com"), - false, - NewUserParams { - github_login: username.clone(), - github_user_id: ix as i32, - }, - ) - .await - .unwrap() - .user_id; - users.push(UserTestPlan { - user_id, - username, - online: false, - next_root_id: 0, - operation_ix: 0, - allow_client_disconnection, - allow_client_reconnection, - }); - } - - T::initialize(server, &users).await; - - let plan = Arc::new(Mutex::new(Self { - replay: false, - allow_server_restarts, - allow_client_reconnection, - allow_client_disconnection, - stored_operations: Vec::new(), - operation_ix: 0, - next_batch_id: 0, - max_operations: *MAX_OPERATIONS, - users, - rng, - })); - - if let Some(path) = &*PLAN_LOAD_PATH { - let json = LOADED_PLAN_JSON - .lock() - .get_or_insert_with(|| { - eprintln!("loaded test plan from path {:?}", path); - std::fs::read(path).unwrap() - }) - .clone(); - plan.lock().deserialize(json); - } - - plan - } - - fn deserialize(&mut self, json: Vec) { - let stored_operations: Vec> = - serde_json::from_slice(&json).unwrap(); - self.replay = true; - self.stored_operations = stored_operations - .iter() - .cloned() - .enumerate() - .map(|(i, mut operation)| { - let did_apply = Arc::new(AtomicBool::new(false)); - if let StoredOperation::Server(ServerOperation::MutateClients { - batch_id: current_batch_id, - user_ids, - .. - }) = &mut operation - { - assert!(user_ids.is_empty()); - user_ids.extend(stored_operations[i + 1..].iter().filter_map(|operation| { - if let StoredOperation::Client { - user_id, batch_id, .. - } = operation - { - if batch_id == current_batch_id { - return Some(user_id); - } - } - None - })); - user_ids.sort_unstable(); - } - (operation, did_apply) - }) - .collect() - } - - fn serialize(&mut self) -> Vec { - // Format each operation as one line - let mut json = Vec::new(); - json.push(b'['); - for (operation, applied) in &self.stored_operations { - if !applied.load(SeqCst) { - continue; - } - if json.len() > 1 { - json.push(b','); - } - json.extend_from_slice(b"\n "); - serde_json::to_writer(&mut json, operation).unwrap(); - } - json.extend_from_slice(b"\n]\n"); - json - } - - fn next_server_operation( - &mut self, - clients: &[(Rc, TestAppContext)], - ) -> Option<(ServerOperation, Arc)> { - if self.replay { - while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) { - self.operation_ix += 1; - if let (StoredOperation::Server(operation), applied) = stored_operation { - return Some((operation.clone(), applied.clone())); - } - } - None - } else { - let operation = self.generate_server_operation(clients)?; - let applied = Arc::new(AtomicBool::new(false)); - self.stored_operations - .push((StoredOperation::Server(operation.clone()), applied.clone())); - Some((operation, applied)) - } - } - - fn next_client_operation( - &mut self, - client: &TestClient, - current_batch_id: usize, - cx: &TestAppContext, - ) -> Option<(T::Operation, Arc)> { - let current_user_id = client.current_user_id(cx); - let user_ix = self - .users - .iter() - .position(|user| user.user_id == current_user_id) - .unwrap(); - let user_plan = &mut self.users[user_ix]; - - if self.replay { - while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) { - user_plan.operation_ix += 1; - if let ( - StoredOperation::Client { - user_id, operation, .. - }, - applied, - ) = stored_operation - { - if user_id == ¤t_user_id { - return Some((operation.clone(), applied.clone())); - } - } - } - None - } else { - if self.operation_ix == self.max_operations { - return None; - } - self.operation_ix += 1; - let operation = T::generate_operation( - client, - &mut self.rng, - self.users - .iter_mut() - .find(|user| user.user_id == current_user_id) - .unwrap(), - cx, - ); - let applied = Arc::new(AtomicBool::new(false)); - self.stored_operations.push(( - StoredOperation::Client { - user_id: current_user_id, - batch_id: current_batch_id, - operation: operation.clone(), - }, - applied.clone(), - )); - Some((operation, applied)) - } - } - - fn generate_server_operation( - &mut self, - clients: &[(Rc, TestAppContext)], - ) -> Option { - if self.operation_ix == self.max_operations { - return None; - } - - Some(loop { - break match self.rng.gen_range(0..100) { - 0..=29 if clients.len() < self.users.len() => { - let user = self - .users - .iter() - .filter(|u| !u.online) - .choose(&mut self.rng) - .unwrap(); - self.operation_ix += 1; - ServerOperation::AddConnection { - user_id: user.user_id, - } - } - 30..=34 if clients.len() > 1 && self.allow_client_disconnection => { - let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; - let user_id = client.current_user_id(cx); - self.operation_ix += 1; - ServerOperation::RemoveConnection { user_id } - } - 35..=39 if clients.len() > 1 && self.allow_client_reconnection => { - let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; - let user_id = client.current_user_id(cx); - self.operation_ix += 1; - ServerOperation::BounceConnection { user_id } - } - 40..=44 if self.allow_server_restarts && clients.len() > 1 => { - self.operation_ix += 1; - ServerOperation::RestartServer - } - _ if !clients.is_empty() => { - let count = self - .rng - .gen_range(1..10) - .min(self.max_operations - self.operation_ix); - let batch_id = util::post_inc(&mut self.next_batch_id); - let mut user_ids = (0..count) - .map(|_| { - let ix = self.rng.gen_range(0..clients.len()); - let (client, cx) = &clients[ix]; - client.current_user_id(cx) - }) - .collect::>(); - user_ids.sort_unstable(); - ServerOperation::MutateClients { - user_ids, - batch_id, - quiesce: self.rng.gen_bool(0.7), - } - } - _ => continue, - }; - }) - } - - async fn apply_server_operation( - plan: Arc>, - deterministic: BackgroundExecutor, - server: &mut TestServer, - clients: &mut Vec<(Rc, TestAppContext)>, - client_tasks: &mut Vec>, - operation_channels: &mut Vec>, - operation: ServerOperation, - cx: &mut TestAppContext, - ) -> bool { - match operation { - ServerOperation::AddConnection { user_id } => { - let username; - { - let mut plan = plan.lock(); - let user = plan.user(user_id); - if user.online { - return false; - } - user.online = true; - username = user.username.clone(); - }; - log::info!("adding new connection for {}", username); - - let mut client_cx = cx.new_app(); - - let (operation_tx, operation_rx) = futures::channel::mpsc::unbounded(); - let client = Rc::new(server.create_client(&mut client_cx, &username).await); - operation_channels.push(operation_tx); - clients.push((client.clone(), client_cx.clone())); - - let foreground_executor = client_cx.foreground_executor().clone(); - let simulate_client = - Self::simulate_client(plan.clone(), client, operation_rx, client_cx); - client_tasks.push(foreground_executor.spawn(simulate_client)); - - log::info!("added connection for {}", username); - } - - ServerOperation::RemoveConnection { - user_id: removed_user_id, - } => { - log::info!("simulating full disconnection of user {}", removed_user_id); - let client_ix = clients - .iter() - .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); - let Some(client_ix) = client_ix else { - return false; - }; - let user_connection_ids = server - .connection_pool - .lock() - .user_connection_ids(removed_user_id) - .collect::>(); - assert_eq!(user_connection_ids.len(), 1); - let removed_peer_id = user_connection_ids[0].into(); - let (client, client_cx) = clients.remove(client_ix); - let client_task = client_tasks.remove(client_ix); - operation_channels.remove(client_ix); - server.forbid_connections(); - server.disconnect_client(removed_peer_id); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - deterministic.start_waiting(); - log::info!("waiting for user {} to exit...", removed_user_id); - client_task.await; - deterministic.finish_waiting(); - server.allow_connections(); - - for project in client.remote_projects().iter() { - project.read_with(&client_cx, |project, _| { - assert!( - project.is_read_only(), - "project {:?} should be read only", - project.remote_id() - ) - }); - } - - for (client, cx) in clients { - let contacts = server - .app_state - .db - .get_contacts(client.current_user_id(cx)) - .await - .unwrap(); - let pool = server.connection_pool.lock(); - for contact in contacts { - if let db::Contact::Accepted { user_id, busy, .. } = contact { - if user_id == removed_user_id { - assert!(!pool.is_user_online(user_id)); - assert!(!busy); - } - } - } - } - - log::info!("{} removed", client.username); - plan.lock().user(removed_user_id).online = false; - client_cx.update(|cx| { - cx.clear_globals(); - drop(client); - }); - } - - ServerOperation::BounceConnection { user_id } => { - log::info!("simulating temporary disconnection of user {}", user_id); - let user_connection_ids = server - .connection_pool - .lock() - .user_connection_ids(user_id) - .collect::>(); - if user_connection_ids.is_empty() { - return false; - } - assert_eq!(user_connection_ids.len(), 1); - let peer_id = user_connection_ids[0].into(); - server.disconnect_client(peer_id); - deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - } - - ServerOperation::RestartServer => { - log::info!("simulating server restart"); - server.reset().await; - deterministic.advance_clock(RECEIVE_TIMEOUT); - server.start().await.unwrap(); - deterministic.advance_clock(CLEANUP_TIMEOUT); - let environment = &server.app_state.config.zed_environment; - let (stale_room_ids, _) = server - .app_state - .db - .stale_server_resource_ids(environment, server.id()) - .await - .unwrap(); - assert_eq!(stale_room_ids, vec![]); - } - - ServerOperation::MutateClients { - user_ids, - batch_id, - quiesce, - } => { - let mut applied = false; - for user_id in user_ids { - let client_ix = clients - .iter() - .position(|(client, cx)| client.current_user_id(cx) == user_id); - let Some(client_ix) = client_ix else { continue }; - applied = true; - if let Err(err) = operation_channels[client_ix].unbounded_send(batch_id) { - log::error!("error signaling user {user_id}: {err}"); - } - } - - if quiesce && applied { - deterministic.run_until_parked(); - T::on_quiesce(server, clients).await; - } - - return applied; - } - } - true - } - - async fn simulate_client( - plan: Arc>, - client: Rc, - mut operation_rx: futures::channel::mpsc::UnboundedReceiver, - mut cx: TestAppContext, - ) { - T::on_client_added(&client, &mut cx).await; - - while let Some(batch_id) = operation_rx.next().await { - let Some((operation, applied)) = - plan.lock().next_client_operation(&client, batch_id, &cx) - else { - break; - }; - applied.store(true, SeqCst); - match T::apply_operation(&client, operation, &mut cx).await { - Ok(()) => {} - Err(TestError::Inapplicable) => { - applied.store(false, SeqCst); - log::info!("skipped operation"); - } - Err(TestError::Other(error)) => { - log::error!("{} error: {}", client.username, error); - } - } - cx.executor().simulate_random_delay().await; - } - log::info!("{}: done", client.username); - } - - fn user(&mut self, user_id: UserId) -> &mut UserTestPlan { - self.users - .iter_mut() - .find(|user| user.user_id == user_id) - .unwrap() - } -} - -impl UserTestPlan { - pub fn next_root_dir_name(&mut self) -> String { - let user_id = self.user_id; - let root_id = util::post_inc(&mut self.next_root_id); - format!("dir-{user_id}-{root_id}") - } -} - -impl From for TestError { - fn from(value: anyhow::Error) -> Self { - Self::Other(value) - } -} - -fn path_env_var(name: &str) -> Option { - let value = env::var(name).ok()?; - let mut path = PathBuf::from(value); - if path.is_relative() { - let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - abs_path.pop(); - abs_path.pop(); - abs_path.push(path); - path = abs_path - } - Some(path) -} diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs deleted file mode 100644 index cfecb4880d..0000000000 --- a/crates/collab2/src/tests/test_server.rs +++ /dev/null @@ -1,618 +0,0 @@ -use crate::{ - db::{tests::TestDb, NewUserParams, UserId}, - executor::Executor, - rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - AppState, -}; -use anyhow::anyhow; -use call::ActiveCall; -use channel::{ChannelBuffer, ChannelStore}; -use client::{ - self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, -}; -use collections::{HashMap, HashSet}; -use fs::FakeFs; -use futures::{channel::oneshot, StreamExt as _}; -use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext}; -use language::LanguageRegistry; -use node_runtime::FakeNodeRuntime; - -use notifications::NotificationStore; -use parking_lot::Mutex; -use project::{Project, WorktreeId}; -use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT}; -use settings::SettingsStore; -use std::{ - cell::{Ref, RefCell, RefMut}, - env, - ops::{Deref, DerefMut}, - path::Path, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - Arc, - }, -}; -use util::http::FakeHttpClient; -use workspace::{Workspace, WorkspaceStore}; - -pub struct TestServer { - pub app_state: Arc, - pub test_live_kit_server: Arc, - server: Arc, - connection_killers: Arc>>>, - forbid_connections: Arc, - _test_db: TestDb, -} - -pub struct TestClient { - pub username: String, - pub app_state: Arc, - channel_store: Model, - notification_store: Model, - state: RefCell, -} - -#[derive(Default)] -struct TestClientState { - local_projects: Vec>, - remote_projects: Vec>, - buffers: HashMap, HashSet>>, - channel_buffers: HashSet>, -} - -pub struct ContactsSummary { - pub current: Vec, - pub outgoing_requests: Vec, - pub incoming_requests: Vec, -} - -impl TestServer { - pub async fn start(deterministic: BackgroundExecutor) -> Self { - static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0); - - let use_postgres = env::var("USE_POSTGRES").ok(); - let use_postgres = use_postgres.as_deref(); - let test_db = if use_postgres == Some("true") || use_postgres == Some("1") { - TestDb::postgres(deterministic.clone()) - } else { - TestDb::sqlite(deterministic.clone()) - }; - let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst); - let live_kit_server = live_kit_client::TestServer::create( - format!("http://livekit.{}.test", live_kit_server_id), - format!("devkey-{}", live_kit_server_id), - format!("secret-{}", live_kit_server_id), - deterministic.clone(), - ) - .unwrap(); - let app_state = Self::build_app_state(&test_db, &live_kit_server).await; - let epoch = app_state - .db - .create_server(&app_state.config.zed_environment) - .await - .unwrap(); - let server = Server::new( - epoch, - app_state.clone(), - Executor::Deterministic(deterministic.clone()), - ); - server.start().await.unwrap(); - // Advance clock to ensure the server's cleanup task is finished. - deterministic.advance_clock(CLEANUP_TIMEOUT); - Self { - app_state, - server, - connection_killers: Default::default(), - forbid_connections: Default::default(), - _test_db: test_db, - test_live_kit_server: live_kit_server, - } - } - - pub async fn reset(&self) { - self.app_state.db.reset(); - let epoch = self - .app_state - .db - .create_server(&self.app_state.config.zed_environment) - .await - .unwrap(); - self.server.reset(epoch); - } - - pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { - cx.update(|cx| { - if cx.has_global::() { - panic!("Same cx used to create two test clients") - } - let settings = SettingsStore::test(cx); - cx.set_global(settings); - }); - - let http = FakeHttpClient::with_404_response(); - let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await - { - user.id - } else { - self.app_state - .db - .create_user( - &format!("{name}@example.com"), - false, - NewUserParams { - github_login: name.into(), - github_user_id: 0, - }, - ) - .await - .expect("creating user failed") - .user_id - }; - let client_name = name.to_string(); - let mut client = cx.update(|cx| Client::new(http.clone(), cx)); - let server = self.server.clone(); - let db = self.app_state.db.clone(); - let connection_killers = self.connection_killers.clone(); - let forbid_connections = self.forbid_connections.clone(); - - Arc::get_mut(&mut client) - .unwrap() - .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.to_proto(), - access_token, - }) - }) - }) - .override_establish_connection(move |credentials, cx| { - assert_eq!(credentials.user_id, user_id.0 as u64); - assert_eq!(credentials.access_token, "the-token"); - - let server = server.clone(); - let db = db.clone(); - let connection_killers = connection_killers.clone(); - let forbid_connections = forbid_connections.clone(); - let client_name = client_name.clone(); - cx.spawn(move |cx| async move { - if forbid_connections.load(SeqCst) { - Err(EstablishConnectionError::other(anyhow!( - "server is forbidding connections" - ))) - } else { - let (client_conn, server_conn, killed) = - Connection::in_memory(cx.background_executor().clone()); - let (connection_id_tx, connection_id_rx) = oneshot::channel(); - let user = db - .get_user_by_id(user_id) - .await - .expect("retrieving user failed") - .unwrap(); - cx.background_executor() - .spawn(server.handle_connection( - server_conn, - client_name, - user, - Some(connection_id_tx), - Executor::Deterministic(cx.background_executor().clone()), - )) - .detach(); - let connection_id = connection_id_rx.await.unwrap(); - connection_killers - .lock() - .insert(connection_id.into(), killed); - Ok(client_conn) - } - }) - }); - - let fs = FakeFs::new(cx.executor()); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); - let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); - let mut language_registry = LanguageRegistry::test(); - language_registry.set_executor(cx.executor()); - let app_state = Arc::new(workspace::AppState { - client: client.clone(), - user_store: user_store.clone(), - workspace_store, - languages: Arc::new(language_registry), - fs: fs.clone(), - build_window_options: |_, _, _| Default::default(), - node_runtime: FakeNodeRuntime::new(), - }); - - cx.update(|cx| { - theme::init(theme::LoadThemes::JustBase, cx); - Project::init(&client, cx); - client::init(&client, cx); - language::init(cx); - editor::init_settings(cx); - workspace::init(app_state.clone(), cx); - audio::init((), cx); - call::init(client.clone(), user_store.clone(), cx); - channel::init(&client, user_store.clone(), cx); - notifications::init(client.clone(), user_store, cx); - }); - - client - .authenticate_and_connect(false, &cx.to_async()) - .await - .unwrap(); - - let client = TestClient { - app_state, - username: name.to_string(), - channel_store: cx.read(ChannelStore::global).clone(), - notification_store: cx.read(NotificationStore::global).clone(), - state: Default::default(), - }; - client.wait_for_current_user(cx).await; - client - } - - pub fn disconnect_client(&self, peer_id: PeerId) { - self.connection_killers - .lock() - .remove(&peer_id) - .unwrap() - .store(true, SeqCst); - } - - //todo!(workspace) - #[allow(dead_code)] - pub fn simulate_long_connection_interruption( - &self, - peer_id: PeerId, - deterministic: BackgroundExecutor, - ) { - 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); - } - - pub fn allow_connections(&self) { - self.forbid_connections.store(false, SeqCst); - } - - pub async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { - for ix in 1..clients.len() { - let (left, right) = clients.split_at_mut(ix); - let (client_a, cx_a) = left.last_mut().unwrap(); - for (client_b, cx_b) in right { - client_a - .app_state - .user_store - .update(*cx_a, |store, cx| { - store.request_contact(client_b.user_id().unwrap(), cx) - }) - .await - .unwrap(); - cx_a.executor().run_until_parked(); - client_b - .app_state - .user_store - .update(*cx_b, |store, cx| { - store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) - }) - .await - .unwrap(); - } - } - } - - pub async fn make_channel( - &self, - channel: &str, - parent: Option, - admin: (&TestClient, &mut TestAppContext), - members: &mut [(&TestClient, &mut TestAppContext)], - ) -> u64 { - let (_, admin_cx) = admin; - let channel_id = admin_cx - .read(ChannelStore::global) - .update(admin_cx, |channel_store, cx| { - channel_store.create_channel(channel, parent, cx) - }) - .await - .unwrap(); - - for (member_client, member_cx) in members { - admin_cx - .read(ChannelStore::global) - .update(admin_cx, |channel_store, cx| { - channel_store.invite_member( - channel_id, - member_client.user_id().unwrap(), - ChannelRole::Member, - cx, - ) - }) - .await - .unwrap(); - - admin_cx.executor().run_until_parked(); - - member_cx - .read(ChannelStore::global) - .update(*member_cx, |channels, cx| { - channels.respond_to_channel_invite(channel_id, true, cx) - }) - .await - .unwrap(); - } - - 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; - - let (left, right) = clients.split_at_mut(1); - let (_client_a, cx_a) = &mut left[0]; - let active_call_a = cx_a.read(ActiveCall::global); - - for (client_b, cx_b) in right { - let user_id_b = client_b.current_user_id(*cx_b).to_proto(); - active_call_a - .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx)) - .await - .unwrap(); - - cx_b.executor().run_until_parked(); - let active_call_b = cx_b.read(ActiveCall::global); - active_call_b - .update(*cx_b, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - } - } - - pub async fn build_app_state( - test_db: &TestDb, - fake_server: &live_kit_client::TestServer, - ) -> Arc { - Arc::new(AppState { - db: test_db.db().clone(), - live_kit_client: Some(Arc::new(fake_server.create_api_client())), - config: Default::default(), - }) - } -} - -impl Deref for TestServer { - type Target = Server; - - fn deref(&self) -> &Self::Target { - &self.server - } -} - -impl Drop for TestServer { - fn drop(&mut self) { - self.server.teardown(); - self.test_live_kit_server.teardown().unwrap(); - } -} - -impl Deref for TestClient { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.app_state.client - } -} - -impl TestClient { - pub fn fs(&self) -> &FakeFs { - self.app_state.fs.as_fake() - } - - pub fn channel_store(&self) -> &Model { - &self.channel_store - } - - pub fn notification_store(&self) -> &Model { - &self.notification_store - } - - pub fn user_store(&self) -> &Model { - &self.app_state.user_store - } - - pub fn language_registry(&self) -> &Arc { - &self.app_state.languages - } - - pub fn client(&self) -> &Arc { - &self.app_state.client - } - - pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { - UserId::from_proto( - self.app_state - .user_store - .read_with(cx, |user_store, _| user_store.current_user().unwrap().id), - ) - } - - pub async fn wait_for_current_user(&self, cx: &TestAppContext) { - let mut authed_user = self - .app_state - .user_store - .read_with(cx, |user_store, _| user_store.watch_current_user()); - while authed_user.next().await.unwrap().is_none() {} - } - - pub async fn clear_contacts(&self, cx: &mut TestAppContext) { - self.app_state - .user_store - .update(cx, |store, _| store.clear_contacts()) - .await; - } - - pub fn local_projects<'a>(&'a self) -> impl Deref>> + 'a { - Ref::map(self.state.borrow(), |state| &state.local_projects) - } - - pub fn remote_projects<'a>(&'a self) -> impl Deref>> + 'a { - Ref::map(self.state.borrow(), |state| &state.remote_projects) - } - - pub fn local_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { - RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects) - } - - pub fn remote_projects_mut<'a>(&'a self) -> impl DerefMut>> + 'a { - RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects) - } - - pub fn buffers_for_project<'a>( - &'a self, - project: &Model, - ) -> impl DerefMut>> + 'a { - RefMut::map(self.state.borrow_mut(), |state| { - state.buffers.entry(project.clone()).or_default() - }) - } - - pub fn buffers<'a>( - &'a self, - ) -> impl DerefMut, HashSet>>> + 'a - { - RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers) - } - - pub fn channel_buffers<'a>( - &'a self, - ) -> impl DerefMut>> + 'a { - RefMut::map(self.state.borrow_mut(), |state| &mut state.channel_buffers) - } - - pub fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { - self.app_state - .user_store - .read_with(cx, |store, _| ContactsSummary { - current: store - .contacts() - .iter() - .map(|contact| contact.user.github_login.clone()) - .collect(), - outgoing_requests: store - .outgoing_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - incoming_requests: store - .incoming_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - }) - } - - pub async fn build_local_project( - &self, - root_path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Model, WorktreeId) { - 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) - }) - .await - .unwrap(); - worktree - .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) - .await; - (project, worktree.read_with(cx, |tree, _| tree.id())) - } - - pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model { - cx.update(|cx| { - Project::local( - self.client().clone(), - self.app_state.node_runtime.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, - guest_cx: &mut TestAppContext, - ) -> Model { - let active_call = guest_cx.read(ActiveCall::global); - let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone()); - room.update(guest_cx, |room, cx| { - room.join_project( - host_project_id, - self.app_state.languages.clone(), - self.app_state.fs.clone(), - cx, - ) - }) - .await - .unwrap() - } - - pub fn build_workspace<'a>( - &'a self, - project: &Model, - cx: &'a mut TestAppContext, - ) -> (View, &'a mut VisualTestContext) { - cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) - } -} - -impl Drop for TestClient { - fn drop(&mut self) { - self.app_state.client.teardown(); - } -} diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 791c6b2fa7..84c1810bc8 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -9,6 +9,8 @@ path = "src/collab_ui.rs" doctest = false [features] +default = [] +stories = ["dep:story"] test-support = [ "call/test-support", "client/test-support", @@ -29,14 +31,14 @@ client = { path = "../client" } channel = { path = "../channel" } clock = { path = "../clock" } collections = { path = "../collections" } -context_menu = { path = "../context_menu" } -drag_and_drop = { path = "../drag_and_drop" } +# context_menu = { path = "../context_menu" } +# drag_and_drop = { path = "../drag_and_drop" } editor = { path = "../editor" } feedback = { path = "../feedback" } -fuzzy = { path = "../fuzzy" } +fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } -menu = { path = "../menu" } +menu = { path = "../menu" } notifications = { path = "../notifications" } rich_text = { path = "../rich_text" } picker = { path = "../picker" } @@ -44,13 +46,15 @@ project = { path = "../project" } recent_projects = { path = "../recent_projects" } rpc = { path = "../rpc" } settings = { path = "../settings" } -feature_flags = {path = "../feature_flags"} +story = { path = "../story", optional = true } +feature_flags = { path = "../feature_flags"} theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } vcs_menu = { path = "../vcs_menu" } +ui = { path = "../ui" } util = { path = "../util" } workspace = { path = "../workspace" } -zed-actions = {path = "../zed-actions"} +zed_actions = { path = "../zed_actions"} anyhow.workspace = true futures.workspace = true @@ -60,6 +64,7 @@ schemars.workspace = true postage.workspace = true serde.workspace = true serde_derive.workspace = true +serde_json.workspace = true time.workspace = true smallvec.workspace = true diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index fe46f3bb3e..ce68acfbd8 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use call::report_call_event_for_channel; use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; use client::{ @@ -6,20 +6,18 @@ use client::{ Collaborator, ParticipantIndex, }; use collections::HashMap; -use editor::{CollaborationHub, Editor}; +use editor::{CollaborationHub, Editor, EditorEvent}; use gpui::{ - actions, - elements::{ChildView, Label}, - geometry::vector::Vector2F, - AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, - ViewContext, ViewHandle, + actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView, + IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext, + VisualContext as _, WindowContext, }; use project::Project; -use smallvec::SmallVec; use std::{ any::{Any, TypeId}, sync::Arc, }; +use ui::{prelude::*, Label}; use util::ResultExt; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle}, @@ -28,17 +26,17 @@ use workspace::{ ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, }; -actions!(channel_view, [Deploy]); +actions!(collab, [Deploy]); pub fn init(cx: &mut AppContext) { register_followable_item::(cx) } pub struct ChannelView { - pub editor: ViewHandle, - project: ModelHandle, - channel_store: ModelHandle, - channel_buffer: ModelHandle, + pub editor: View, + project: Model, + channel_store: Model, + channel_buffer: Model, remote_id: Option, _editor_event_subscription: Subscription, } @@ -46,9 +44,9 @@ pub struct ChannelView { impl ChannelView { pub fn open( channel_id: ChannelId, - workspace: ViewHandle, - cx: &mut AppContext, - ) -> Task>> { + workspace: View, + cx: &mut WindowContext, + ) -> 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 { @@ -61,17 +59,17 @@ impl ChannelView { 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, - cx: &mut AppContext, - ) -> Task>> { + pane: View, + workspace: View, + cx: &mut WindowContext, + ) -> Task>> { let workspace = workspace.read(cx); let project = workspace.project().to_owned(); let channel_store = ChannelStore::global(cx); @@ -91,7 +89,7 @@ impl ChannelView { buffer.set_language(Some(markdown), cx); } }) - }); + })?; pane.update(&mut cx, |pane, cx| { let buffer_id = channel_buffer.read(cx).remote_id(cx); @@ -107,7 +105,7 @@ impl ChannelView { } } - let view = cx.add_view(|cx| { + let view = cx.new_view(|cx| { let mut this = Self::new(project, channel_store, channel_buffer, cx); this.acknowledge_buffer_version(cx); this @@ -117,7 +115,7 @@ impl ChannelView { // replace that. if let Some(existing_item) = existing_view { if let Some(ix) = pane.index_for_item(&existing_item) { - pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx) + pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx) .detach(); pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); } @@ -125,31 +123,25 @@ impl ChannelView { view }) - .ok_or_else(|| anyhow!("pane was dropped")) }) } pub fn new( - project: ModelHandle, - channel_store: ModelHandle, - channel_buffer: ModelHandle, + project: Model, + channel_store: Model, + channel_buffer: Model, cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - let editor = cx.add_view(|cx| { + let editor = cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, None, cx); editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( channel_buffer.clone(), ))); - editor.set_read_only( - !channel_buffer - .read(cx) - .channel(cx) - .is_some_and(|c| c.can_edit_notes()), - ); editor }); - let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + let _editor_event_subscription = + cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone())); cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) .detach(); @@ -170,7 +162,7 @@ impl ChannelView { fn handle_channel_buffer_event( &mut self, - _: ModelHandle, + _: Model, event: &ChannelBufferEvent, cx: &mut ViewContext, ) { @@ -180,14 +172,13 @@ impl ChannelView { cx.notify(); }), ChannelBufferEvent::ChannelChanged => { - self.editor.update(cx, |editor, cx| { - editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); - cx.emit(editor::Event::TitleChanged); + self.editor.update(cx, |_, cx| { + cx.emit(editor::EditorEvent::TitleChanged); cx.notify() }); } ChannelBufferEvent::BufferEdited => { - if cx.is_self_focused() || self.editor.is_focused(cx) { + if self.editor.read(cx).is_focused(cx) { self.acknowledge_buffer_version(cx); } else { self.channel_store.update(cx, |store, cx| { @@ -205,7 +196,7 @@ impl ChannelView { } } - fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext) { self.channel_store.update(cx, |store, cx| { let channel_buffer = self.channel_buffer.read(cx); store.acknowledge_notes_version( @@ -221,71 +212,69 @@ impl ChannelView { } } -impl Entity for ChannelView { - type Event = editor::Event; +impl EventEmitter for ChannelView {} + +impl Render for ChannelView { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + self.editor.clone() + } } -impl View for ChannelView { - fn ui_name() -> &'static str { - "ChannelView" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(self.editor.as_any(), cx).into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - self.acknowledge_buffer_version(cx); - cx.focus(self.editor.as_any()) - } +impl FocusableView for ChannelView { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.editor.read(cx).focus_handle(cx) } } impl Item for ChannelView { + type Event = EditorEvent; + fn act_as_type<'a>( &'a self, type_id: TypeId, - self_handle: &'a ViewHandle, + self_handle: &'a View, _: &'a AppContext, - ) -> Option<&'a AnyViewHandle> { + ) -> Option { if type_id == TypeId::of::() { - Some(self_handle) + Some(self_handle.to_any()) } else if type_id == TypeId::of::() { - Some(&self.editor) + Some(self.editor.to_any()) } else { None } } - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - cx: &gpui::AppContext, - ) -> AnyElement { + fn tab_content(&self, _: Option, selected: bool, cx: &WindowContext) -> AnyElement { let label = if let Some(channel) = self.channel(cx) { match ( - channel.can_edit_notes(), + self.channel_buffer.read(cx).buffer().read(cx).read_only(), self.channel_buffer.read(cx).is_connected(), ) { - (true, true) => format!("#{}", channel.name), - (false, true) => format!("#{} (read-only)", channel.name), + (false, true) => format!("#{}", channel.name), + (true, true) => format!("#{} (read-only)", channel.name), (_, false) => format!("#{} (disconnected)", channel.name), } } else { format!("channel notes (disconnected)") }; - Label::new(label, style.label.to_owned()).into_any() + Label::new(label) + .color(if selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } - 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, - )) + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option> { + Some(cx.new_view(|cx| { + Self::new( + self.project.clone(), + self.channel_store.clone(), + self.channel_buffer.clone(), + cx, + ) + })) } fn is_singleton(&self, _cx: &AppContext) -> bool { @@ -307,7 +296,7 @@ impl Item for ChannelView { .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) } - fn as_searchable(&self, _: &ViewHandle) -> Option> { + fn as_searchable(&self, _: &View) -> Option> { Some(Box::new(self.editor.clone())) } @@ -315,12 +304,12 @@ impl Item for ChannelView { true } - fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { self.editor.read(cx).pixel_position_of_cursor(cx) } - fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { - editor::Editor::to_item_events(event) + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) } } @@ -329,7 +318,7 @@ impl FollowableItem for ChannelView { self.remote_id } - fn to_state_proto(&self, cx: &AppContext) -> Option { + fn to_state_proto(&self, cx: &WindowContext) -> Option { let channel_buffer = self.channel_buffer.read(cx); if !channel_buffer.is_connected() { return None; @@ -350,12 +339,12 @@ impl FollowableItem for ChannelView { } fn from_state_proto( - pane: ViewHandle, - workspace: ViewHandle, + pane: View, + workspace: View, remote_id: workspace::ViewId, state: &mut Option, - cx: &mut AppContext, - ) -> Option>>> { + cx: &mut WindowContext, + ) -> Option>>> { let Some(proto::view::Variant::ChannelView(_)) = state else { return None; }; @@ -368,30 +357,28 @@ impl FollowableItem for ChannelView { Some(cx.spawn(|mut cx| async move { let this = open.await?; - let task = this - .update(&mut cx, |this, cx| { - this.remote_id = Some(remote_id); + let task = this.update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); - if let Some(state) = state.editor { - Some(this.editor.update(cx, |editor, cx| { - editor.apply_update_proto( - &this.project, - proto::update_view::Variant::Editor(proto::update_view::Editor { - selections: state.selections, - pending_selection: state.pending_selection, - scroll_top_anchor: state.scroll_top_anchor, - scroll_x: state.scroll_x, - scroll_y: state.scroll_y, - ..Default::default() - }), - cx, - ) - })) - } else { - None - } - }) - .ok_or_else(|| anyhow!("window was closed"))?; + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + })?; if let Some(task) = task { task.await?; @@ -403,9 +390,9 @@ impl FollowableItem for ChannelView { fn add_event_to_update_proto( &self, - event: &Self::Event, + event: &EditorEvent, update: &mut Option, - cx: &AppContext, + cx: &WindowContext, ) -> bool { self.editor .read(cx) @@ -414,7 +401,7 @@ impl FollowableItem for ChannelView { fn apply_update_proto( &mut self, - project: &ModelHandle, + project: &Model, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> gpui::Task> { @@ -429,16 +416,16 @@ impl FollowableItem for ChannelView { }) } - fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { - Editor::should_unfollow_on_event(event, cx) + fn is_project_item(&self, _cx: &WindowContext) -> bool { + false } - fn is_project_item(&self, _cx: &AppContext) -> bool { - false + fn to_follow_event(event: &Self::Event) -> Option { + Editor::to_follow_event(event) } } -struct ChannelBufferCollaborationHub(ModelHandle); +struct ChannelBufferCollaborationHub(Model); impl CollaborationHub for ChannelBufferCollaborationHub { fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 5a4dafb6d4..5786ab10d4 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1,6 +1,4 @@ -use crate::{ - channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings, -}; +use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings}; use anyhow::Result; use call::ActiveCall; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; @@ -9,13 +7,9 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ - actions, - elements::*, - platform::{CursorStyle, MouseButton}, - serde_json, - views::{ItemType, Select, SelectStyle}, - AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + actions, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent, + ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render, + Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::LanguageRegistry; use menu::Confirm; @@ -23,13 +17,13 @@ use message_editor::MessageEditor; use project::Fs; use rich_text::RichText; use serde::{Deserialize, Serialize}; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use std::sync::Arc; -use theme::{IconButton, Theme}; use time::{OffsetDateTime, UtcOffset}; +use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip}; use util::{ResultExt, TryFutureExt}; use workspace::{ - dock::{DockPosition, Panel}, + dock::{DockPosition, Panel, PanelEvent}, Workspace, }; @@ -38,29 +32,36 @@ mod message_editor; const MESSAGE_LOADING_THRESHOLD: usize = 50; const CHAT_PANEL_KEY: &'static str = "ChatPanel"; +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }) + .detach(); +} + pub struct ChatPanel { client: Arc, - channel_store: ModelHandle, + channel_store: Model, languages: Arc, - active_chat: Option<(ModelHandle, Subscription)>, - message_list: ListState, - input_editor: ViewHandle, - channel_select: ViewHandle, - ) -> AnyElement