diff --git a/Cargo.lock b/Cargo.lock index c9d548d8bd9ead089d69426ba8a7009ed3f16dbe..57dcade5b9a1a63c3d85c9535e0671c2fbd70d57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,7 @@ dependencies = [ "async-tungstenite", "axum", "base64 0.13.0", + "clap 3.1.12", "client", "collections", "ctor", @@ -859,6 +860,7 @@ dependencies = [ "parking_lot", "project", "rand 0.8.3", + "reqwest", "rpc", "scrypt", "serde", @@ -930,10 +932,17 @@ name = "contacts_panel" version = "0.1.0" dependencies = [ "client", + "editor", + "futures", + "fuzzy", "gpui", + "log", + "picker", "postage", + "serde", "settings", "theme", + "util", "workspace", ] @@ -2133,6 +2142,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.2.3" @@ -2237,6 +2259,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "isahc" version = "1.7.0" @@ -2720,6 +2748,24 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nb-connect" version = "1.0.3" @@ -2903,6 +2949,32 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.4" @@ -2911,9 +2983,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.65" +version = "0.9.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" dependencies = [ "autocfg 1.0.1", "cc", @@ -3677,6 +3749,42 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "reqwest" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +dependencies = [ + "base64 0.13.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.9", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "resvg" version = "0.14.0" @@ -4957,6 +5065,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.22.0" @@ -5632,6 +5750,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.74" @@ -5777,6 +5907,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "wio" version = "0.2.2" diff --git a/assets/icons/accept.svg b/assets/icons/accept.svg new file mode 100644 index 0000000000000000000000000000000000000000..6a8a7d67a08a3215966942430fe0d528374eee82 --- /dev/null +++ b/assets/icons/accept.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/add-contact.svg b/assets/icons/add-contact.svg new file mode 100644 index 0000000000000000000000000000000000000000..4fc7790b9dc0d204997cff308d93d2bf4497c678 --- /dev/null +++ b/assets/icons/add-contact.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/reject.svg b/assets/icons/reject.svg new file mode 100644 index 0000000000000000000000000000000000000000..e78f49a22894055f22cf5af1fede318e50af8963 --- /dev/null +++ b/assets/icons/reject.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 3b19c2e63eef855981632972fe933324dd677902..cb1208a1db764bbb540dc73540eb723bfded2d91 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#19171c", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#7e7887", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "border": { + "color": "#26232a", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#e2dfe7", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#8b8792", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#655f6d", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#e2dfe7", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#26232a", + "color": "#e2dfe7", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5852603d" + } + }, + "disabled_contact_button": { + "background": "#26232a", + "color": "#8b8792", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#26232a", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#8b8792", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#576ddb", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#5852605c", + "text": { + "family": "Zed Sans", + "color": "#e2dfe7", + "size": 14 + } + }, + "hover": { + "background": "#5852603d" + } + }, + "border": { + "color": "#19171c", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#7e7887", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#19171c", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#7e7887", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + }, + "border": { + "color": "#26232a", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#26232a", + "color": "#e2dfe7", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5852603d" + } + }, + "disabled_contact_button": { + "background": "#26232a", + "color": "#8b8792", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#955ae77a", "tab_icon_spacing": 8, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 2e33fb774fe95b06c4da7d8d7d6209c20673b592..f0b3f5bd438a53178d9f56f88e8fbf1dbd41472d 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#efecf4", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#655f6d", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "border": { + "color": "#e2dfe7", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#26232a", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#585260", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#7e7887", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#26232a", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#e2dfe7", + "color": "#26232a", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#8b87921f" + } + }, + "disabled_contact_button": { + "background": "#e2dfe7", + "color": "#585260", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#e2dfe7", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#585260", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#576ddb", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#8b87922e", + "text": { + "family": "Zed Sans", + "color": "#26232a", + "size": 14 + } + }, + "hover": { + "background": "#8b87921f" + } + }, + "border": { + "color": "#efecf4", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#655f6d", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#efecf4", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#655f6d", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + }, + "border": { + "color": "#e2dfe7", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#e2dfe7", + "color": "#26232a", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#8b87921f" + } + }, + "disabled_contact_button": { + "background": "#e2dfe7", + "color": "#585260", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#955ae73d", "tab_icon_spacing": 8, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index ba9b7189d35fada8abcc2b42bace6c9f5a81fd19..9cc3badc8104dbee88d2862ad7feeedba22383e8 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#000000", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#c6c6c6", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#404040", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#f1f1f1", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#2b2b2b", + "color": "#c6c6c6", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#323232" + } + }, + "disabled_contact_button": { + "background": "#2b2b2b", + "color": "#555555", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#1c1c1c", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#4f8ff7", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#2b2b2b", + "text": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14 + } + }, + "hover": { + "background": "#232323" + } + }, + "border": { + "color": "#070707", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#000000", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#00000052", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#2b2b2b", + "color": "#c6c6c6", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#323232" + } + }, + "disabled_contact_button": { + "background": "#2b2b2b", + "color": "#555555", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#3f15a380", "tab_icon_spacing": 8, diff --git a/assets/themes/light.json b/assets/themes/light.json index 7cbd315c8a41ff3572cb414727618607ae3e8875..e2563fadad64d74d0b7add301cbfb2fc0969be6d 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#ffffff", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#393939", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#e3e3e3", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#2b2b2b", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#eaeaea", + "color": "#393939", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#e3e3e3" + } + }, + "disabled_contact_button": { + "background": "#eaeaea", + "color": "#9c9c9c", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#f8f8f8", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#484bed", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#e3e3e3", + "text": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14 + } + }, + "hover": { + "background": "#eaeaea" + } + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#ffffff", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#eaeaea", + "color": "#393939", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#e3e3e3" + } + }, + "disabled_contact_button": { + "background": "#eaeaea", + "color": "#9c9c9c", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#fce9b7", "tab_icon_spacing": 8, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 8672518b4cf3ef8a93cc9ad4793a6ba0106c2faa..6e8c405b6c212bf77fc99c99ea3c2a6dcf5a2f07 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#002b36", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#839496", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "border": { + "color": "#073642", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#eee8d5", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#93a1a1", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#657b83", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#eee8d5", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#073642", + "color": "#eee8d5", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#586e753d" + } + }, + "disabled_contact_button": { + "background": "#073642", + "color": "#93a1a1", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#073642", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#93a1a1", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#268bd2", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#586e755c", + "text": { + "family": "Zed Sans", + "color": "#eee8d5", + "size": 14 + } + }, + "hover": { + "background": "#586e753d" + } + }, + "border": { + "color": "#002b36", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#839496", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#002b36", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#839496", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + }, + "border": { + "color": "#073642", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#073642", + "color": "#eee8d5", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#586e753d" + } + }, + "disabled_contact_button": { + "background": "#073642", + "color": "#93a1a1", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#6c71c47a", "tab_icon_spacing": 8, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 66b43e613dc725687d3f34c3ab427d7a1311ae61..3f5b26ee56a24a883fc21c45a9de60bec20b9a5d 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#fdf6e3", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#657b83", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "border": { + "color": "#eee8d5", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#073642", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#586e75", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#839496", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#073642", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#eee8d5", + "color": "#073642", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#93a1a11f" + } + }, + "disabled_contact_button": { + "background": "#eee8d5", + "color": "#586e75", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#eee8d5", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#586e75", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#268bd2", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#93a1a12e", + "text": { + "family": "Zed Sans", + "color": "#073642", + "size": 14 + } + }, + "hover": { + "background": "#93a1a11f" + } + }, + "border": { + "color": "#fdf6e3", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#657b83", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#fdf6e3", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#657b83", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + }, + "border": { + "color": "#eee8d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#eee8d5", + "color": "#073642", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#93a1a11f" + } + }, + "disabled_contact_button": { + "background": "#eee8d5", + "color": "#586e75", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#6c71c43d", "tab_icon_spacing": 8, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 66f5182172e01e14902e28566600bbd7084de1de..0f2a868f24d028be4c960db27b39b36b540a3e5d 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#202746", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#898ea4", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "border": { + "color": "#293256", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#dfe2f1", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#979db4", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#6b7394", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#dfe2f1", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#293256", + "color": "#dfe2f1", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5e66873d" + } + }, + "disabled_contact_button": { + "background": "#293256", + "color": "#979db4", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#293256", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#979db4", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#3d8fd1", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#5e66875c", + "text": { + "family": "Zed Sans", + "color": "#dfe2f1", + "size": 14 + } + }, + "hover": { + "background": "#5e66873d" + } + }, + "border": { + "color": "#202746", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#898ea4", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#202746", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#898ea4", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + }, + "border": { + "color": "#293256", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#293256", + "color": "#dfe2f1", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5e66873d" + } + }, + "disabled_contact_button": { + "background": "#293256", + "color": "#979db4", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#6679cc7a", "tab_icon_spacing": 8, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 34a33897288143e9e5c8492d5c753232ac1422b9..b9106c62f3d273a537616f0cbd38090b8c854411 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1212,14 +1212,64 @@ "bottom": 12, "right": 12 }, - "host_row_height": 28, + "user_query_editor": { + "background": "#f5f7ff", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#6b7394", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "border": { + "color": "#dfe2f1", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "user_query_editor_height": 32, + "add_contact_button": { + "margin": { + "left": 6 + }, + "color": "#293256", + "button_width": 8, + "icon_width": 8 + }, + "row": { + "padding": { + "left": 8 + } + }, + "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#5e6687", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#898ea4", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#293256", "size": 14, @@ -1227,6 +1277,23 @@ "left": 8 } }, + "contact_button": { + "background": "#dfe2f1", + "color": "#293256", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#979db41f" + } + }, + "disabled_contact_button": { + "background": "#dfe2f1", + "color": "#5e6687", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + }, "project": { "guest_avatar_spacing": 4, "height": 24, @@ -1328,6 +1395,122 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#dfe2f1", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#5e6687", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#3d8fd1", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#979db42e", + "text": { + "family": "Zed Sans", + "color": "#293256", + "size": 14 + } + }, + "hover": { + "background": "#979db41f" + } + }, + "border": { + "color": "#f5f7ff", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#6b7394", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#f5f7ff", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#6b7394", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + }, + "border": { + "color": "#dfe2f1", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "padding": { + "left": 8 + } + }, + "contact_button": { + "background": "#dfe2f1", + "color": "#293256", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#979db41f" + } + }, + "disabled_contact_button": { + "background": "#dfe2f1", + "color": "#5e6687", + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 + } + }, "search": { "match_background": "#6679cc3d", "tab_icon_spacing": 8, diff --git a/crates/client/src/channel.rs b/crates/client/src/channel.rs index 51d4007ff75dc546008dbc8e1ca8ac33e9d2ff5c..0d44c6719139d3b4ed1344295841480f6df2f1f9 100644 --- a/crates/client/src/channel.rs +++ b/crates/client/src/channel.rs @@ -500,7 +500,7 @@ async fn messages_from_proto( .collect(); user_store .update(cx, |user_store, cx| { - user_store.load_users(unique_user_ids, cx) + user_store.get_users(unique_user_ids, cx) }) .await?; @@ -639,7 +639,7 @@ mod tests { server .respond( get_users.receipt(), - proto::GetUsersResponse { + proto::UsersResponse { users: vec![proto::User { id: 5, github_login: "nathansobo".into(), @@ -690,7 +690,7 @@ mod tests { server .respond( get_users.receipt(), - proto::GetUsersResponse { + proto::UsersResponse { users: vec![proto::User { id: 6, github_login: "maxbrunsfeld".into(), @@ -738,7 +738,7 @@ mod tests { server .respond( get_users.receipt(), - proto::GetUsersResponse { + proto::UsersResponse { users: vec![proto::User { id: 7, github_login: "as-cii".into(), diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e6fc1bf19ad5fe0a886f1e077fdb335af19e5f40..75d5b459e134d97b61d01e949e164ab80baf9d89 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -117,7 +117,7 @@ impl EstablishConnectionError { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Status { SignedOut, UpgradeRequired, @@ -293,6 +293,7 @@ impl Client { } 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; @@ -629,10 +630,13 @@ impl Client { async fn set_connection(self: &Arc, conn: Connection, cx: &AsyncAppContext) { let executor = cx.background(); + log::info!("add connection to peer"); let (connection_id, handle_io, mut incoming) = self .peer .add_connection(conn, move |duration| executor.timer(duration)) .await; + log::info!("set status to connected {}", connection_id); + self.set_status(Status::Connected { connection_id }, cx); cx.foreground() .spawn({ let cx = cx.clone(); @@ -730,15 +734,17 @@ impl Client { }) .detach(); - self.set_status(Status::Connected { connection_id }, cx); - let handle_io = cx.background().spawn(handle_io); let this = self.clone(); let cx = cx.clone(); cx.foreground() .spawn(async move { match handle_io.await { - Ok(()) => this.set_status(Status::SignedOut, &cx), + Ok(()) => { + if *this.status().borrow() == (Status::Connected { connection_id }) { + this.set_status(Status::SignedOut, &cx); + } + } Err(err) => { log::error!("connection error: {:?}", err); this.set_status(Status::ConnectionLost, &cx); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index c8432d92d1ff38c66550ee69b997e0cc2748d8af..1874822774a36ef2c7c777bd5e7d91f2d5e72b71 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,10 +1,11 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; -use anyhow::{anyhow, Result}; -use futures::{future, AsyncReadExt}; +use anyhow::{anyhow, Context, Result}; +use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; +use rpc::proto::{RequestMessage, UsersResponse}; use std::{ - collections::{HashMap, HashSet}, + collections::{hash_map::Entry, HashMap, HashSet}, sync::{Arc, Weak}, }; use util::TryFutureExt as _; @@ -19,6 +20,7 @@ pub struct User { #[derive(Debug)] pub struct Contact { pub user: Arc, + pub online: bool, pub projects: Vec, } @@ -30,11 +32,22 @@ pub struct ProjectMetadata { pub guests: Vec>, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContactRequestStatus { + None, + RequestSent, + RequestReceived, + RequestAccepted, +} + pub struct UserStore { users: HashMap>, - update_contacts_tx: watch::Sender>, + update_contacts_tx: mpsc::UnboundedSender, current_user: watch::Receiver>>, - contacts: Arc<[Contact]>, + contacts: Vec>, + incoming_contact_requests: Vec>, + outgoing_contact_requests: Vec>, + pending_contact_requests: HashMap, client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -47,6 +60,11 @@ impl Entity for UserStore { type Event = Event; } +enum UpdateContacts { + Update(proto::UpdateContacts), + Clear(postage::barrier::Sender), +} + impl UserStore { pub fn new( client: Arc, @@ -54,21 +72,22 @@ impl UserStore { cx: &mut ModelContext, ) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); - let (update_contacts_tx, mut update_contacts_rx) = - watch::channel::>(); + let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_contacts); Self { users: Default::default(), current_user: current_user_rx, - contacts: Arc::from([]), + contacts: Default::default(), + incoming_contact_requests: Default::default(), + outgoing_contact_requests: Default::default(), client: Arc::downgrade(&client), update_contacts_tx, http, _maintain_contacts: cx.spawn_weak(|this, mut cx| async move { let _subscription = rpc_subscription; - while let Some(message) = update_contacts_rx.recv().await { - if let Some((message, this)) = message.zip(this.upgrade(&cx)) { + while let Some(message) = update_contacts_rx.next().await { + if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| this.update_contacts(message, cx)) .log_err() .await; @@ -90,11 +109,20 @@ impl UserStore { } Status::SignedOut => { current_user_tx.send(None).await.ok(); + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| this.clear_contacts()).await; + } + } + Status::ConnectionLost => { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| this.clear_contacts()).await; + } } _ => {} } } }), + pending_contact_requests: Default::default(), } } @@ -105,76 +133,278 @@ impl UserStore { mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, _| { - *this.update_contacts_tx.borrow_mut() = Some(msg.payload); + this.update_contacts_tx + .unbounded_send(UpdateContacts::Update(msg.payload)) + .unwrap(); }); Ok(()) } fn update_contacts( &mut self, - message: proto::UpdateContacts, + message: UpdateContacts, cx: &mut ModelContext, ) -> Task> { - let mut user_ids = HashSet::new(); - for contact in &message.contacts { - user_ids.insert(contact.user_id); - user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); - } + match message { + UpdateContacts::Clear(barrier) => { + self.contacts.clear(); + self.incoming_contact_requests.clear(); + self.outgoing_contact_requests.clear(); + drop(barrier); + Task::ready(Ok(())) + } + UpdateContacts::Update(message) => { + log::info!( + "update contacts on client {}: {:?}", + self.client.upgrade().unwrap().id, + message + ); + let mut user_ids = HashSet::new(); + for contact in &message.contacts { + user_ids.insert(contact.user_id); + user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); + } + user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id)); + user_ids.extend(message.outgoing_requests.iter()); - let load_users = self.load_users(user_ids.into_iter().collect(), cx); - cx.spawn(|this, mut cx| async move { - load_users.await?; + 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(); + 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.fetch_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.fetch_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| !removed_incoming_requests.contains(&user.id)); + // Update existing incoming requests and insert new ones + for request in incoming_requests { + match this + .incoming_contact_requests + .binary_search_by_key(&&request.github_login, |contact| { + &contact.github_login + }) { + Ok(ix) => this.incoming_contact_requests[ix] = request, + Err(ix) => this.incoming_contact_requests.insert(ix, request), + } + } + + // 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), + } + } - let mut contacts = Vec::new(); - for contact in message.contacts { - contacts.push(Contact::from_proto(contact, &this, &mut cx).await?); + 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 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::Reject + } as i32, + }, + cx, + ) + } + + 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(|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| { - contacts.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); - this.contacts = contacts.into(); + 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 contacts(&self) -> &Arc<[Contact]> { - &self.contacts + 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.recv().await; + } } - pub fn load_users( + pub fn get_users( &mut self, mut user_ids: Vec, cx: &mut ModelContext, ) -> Task> { - let rpc = self.client.clone(); - let http = self.http.clone(); user_ids.retain(|id| !self.users.contains_key(id)); - cx.spawn_weak(|this, mut cx| async move { - if let Some(rpc) = rpc.upgrade() { - if !user_ids.is_empty() { - let response = rpc.request(proto::GetUsers { user_ids }).await?; - let new_users = future::join_all( - response - .users - .into_iter() - .map(|user| User::new(user, http.as_ref())), - ) - .await; + if user_ids.is_empty() { + Task::ready(Ok(())) + } else { + let load = self.load_users(proto::GetUsers { user_ids }, cx); + cx.foreground().spawn(async move { + load.await?; + Ok(()) + }) + } + } - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - for user in new_users { - this.users.insert(user.id, Arc::new(user)); - } - }); - } - } - } - Ok(()) - }) + pub fn fuzzy_search_users( + &mut self, + query: String, + cx: &mut ModelContext, + ) -> Task>>> { + self.load_users(proto::FuzzySearchUsers { query }, cx) } pub fn fetch_user( @@ -186,7 +416,7 @@ impl UserStore { return cx.foreground().spawn(async move { Ok(user) }); } - let load_users = self.load_users(vec![user_id], cx); + let load_users = self.get_users(vec![user_id], cx); cx.spawn(|this, mut cx| async move { load_users.await?; this.update(&mut cx, |this, _| { @@ -205,15 +435,47 @@ impl UserStore { 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(); + let http = self.http.clone(); + cx.spawn_weak(|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; + + 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()) + } + }) + } } impl User { - async fn new(message: proto::User, http: &dyn HttpClient) -> Self { - User { + async fn new(message: proto::User, http: &dyn HttpClient) -> Arc { + Arc::new(User { id: message.id, github_login: message.github_login, avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await, - } + }) } } @@ -247,7 +509,17 @@ impl Contact { guests, }); } - Ok(Self { user, projects }) + Ok(Self { + user, + online: contact.online, + projects, + }) + } + + pub fn non_empty_projects(&self) -> impl Iterator { + self.projects + .iter() + .filter(|project| !project.worktree_root_names.is_empty()) } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index feeed1ba6c7d65573322a1441e2116060a70caae..a5541990d302919956a67bf5cf5f815f794a58de 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -22,6 +22,7 @@ async-trait = "0.1.50" async-tungstenite = "0.16" axum = { version = "0.5", features = ["json", "headers", "ws"] } base64 = "0.13" +clap = { version = "3.1", features = ["derive"], optional = true } envy = "0.4.2" env_logger = "0.8" futures = "0.3" @@ -32,6 +33,7 @@ opentelemetry = { version = "0.17", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.10", features = ["tls-roots"] } parking_lot = "0.11.1" rand = "0.8" +reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -69,4 +71,4 @@ lazy_static = "1.4" serde_json = { version = "1.0.64", features = ["preserve_order"] } [features] -seed-support = ["lipsum"] +seed-support = ["clap", "lipsum", "reqwest"] diff --git a/crates/collab/migrations/20220505144506_add_trigram_index_to_users.sql b/crates/collab/migrations/20220505144506_add_trigram_index_to_users.sql new file mode 100644 index 0000000000000000000000000000000000000000..3d6fd3179a236bf8407464f69f1e67469eb31d27 --- /dev/null +++ b/crates/collab/migrations/20220505144506_add_trigram_index_to_users.sql @@ -0,0 +1,2 @@ +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/collab/migrations/20220506130724_create_contacts.sql b/crates/collab/migrations/20220506130724_create_contacts.sql new file mode 100644 index 0000000000000000000000000000000000000000..56beb70fd06ce8a3b7bb00d2f0ada2e465906c69 --- /dev/null +++ b/crates/collab/migrations/20220506130724_create_contacts.sql @@ -0,0 +1,11 @@ +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/collab/src/bin/seed.rs b/crates/collab/src/bin/seed.rs index 7201fd5113a1bf1679862324242b1d0d74cc5b82..ee202ee4a84b128e7fcc91b03a589bb309641a86 100644 --- a/crates/collab/src/bin/seed.rs +++ b/crates/collab/src/bin/seed.rs @@ -1,31 +1,87 @@ +use clap::Parser; use db::{Db, PostgresDb, UserId}; use rand::prelude::*; +use serde::Deserialize; +use std::fmt::Write; use time::{Duration, OffsetDateTime}; #[allow(unused)] #[path = "../db.rs"] mod db; +#[derive(Parser)] +struct Args { + /// Seed users from GitHub. + #[clap(short, long)] + github_users: bool, +} + +#[derive(Debug, Deserialize)] +struct GitHubUser { + id: usize, + login: String, +} + #[tokio::main] async fn main() { + let args = Args::parse(); let mut rng = StdRng::from_entropy(); let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); let db = PostgresDb::new(&database_url, 5) .await .expect("failed to connect to postgres database"); - let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"]; + let mut zed_users = vec![ + "nathansobo".to_string(), + "maxbrunsfeld".to_string(), + "as-cii".to_string(), + "iamnbutler".to_string(), + "gibusu".to_string(), + "Kethku".to_string(), + ]; + + if args.github_users { + let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var"); + let client = reqwest::Client::new(); + let mut last_user_id = None; + for page in 0..20 { + println!("Downloading users from GitHub, page {}", page); + 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 response = client + .get(uri) + .bearer_auth(&github_token) + .header("user-agent", "zed") + .send() + .await + .expect("failed to fetch github users"); + let users = response + .json::>() + .await + .expect("failed to deserialize github user"); + zed_users.extend(users.iter().map(|user| user.login.clone())); + + if let Some(last_user) = users.last() { + last_user_id = Some(last_user.id); + } else { + break; + } + } + } + let mut zed_user_ids = Vec::::new(); for zed_user in zed_users { if let Some(user) = db - .get_user_by_github_login(zed_user) + .get_user_by_github_login(&zed_user) .await .expect("failed to fetch user") { zed_user_ids.push(user.id); } else { zed_user_ids.push( - db.create_user(zed_user, true) + db.create_user(&zed_user, true) .await .expect("failed to insert user"), ); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 28375b6685f62cec951b0d34860ed5a920a2fed2..4bb61c34046df885acb4a97960f0392da275f093 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,6 +1,6 @@ -use anyhow::Context; -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; +use futures::StreamExt; use serde::Serialize; pub use sqlx::postgres::PgPoolOptions as DbOptions; use sqlx::{types::Uuid, FromRow}; @@ -10,11 +10,28 @@ use time::OffsetDateTime; pub trait Db: Send + Sync { async fn create_user(&self, github_login: &str, admin: bool) -> Result; async fn get_all_users(&self) -> Result>; + async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result>; async fn get_user_by_id(&self, id: UserId) -> Result>; async fn get_users_by_ids(&self, ids: Vec) -> Result>; async fn get_user_by_github_login(&self, github_login: &str) -> Result>; async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()>; async fn destroy_user(&self, id: UserId) -> Result<()>; + + async fn get_contacts(&self, id: UserId) -> Result; + async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; + async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; + async fn dismiss_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + ) -> Result<()>; + async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()>; + async fn create_access_token_hash( &self, user_id: UserId, @@ -23,6 +40,7 @@ pub trait Db: Send + Sync { ) -> Result<()>; async fn get_access_token_hashes(&self, user_id: UserId) -> Result>; #[cfg(any(test, feature = "seed-support"))] + async fn find_org_by_slug(&self, slug: &str) -> Result>; #[cfg(any(test, feature = "seed-support"))] async fn create_org(&self, name: &str, slug: &str) -> Result; @@ -31,6 +49,7 @@ pub trait Db: Send + Sync { #[cfg(any(test, feature = "seed-support"))] async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result; #[cfg(any(test, feature = "seed-support"))] + async fn get_org_channels(&self, org_id: OrgId) -> Result>; async fn get_accessible_channels(&self, user_id: UserId) -> Result>; async fn can_user_access_channel(&self, user_id: UserId, channel_id: ChannelId) @@ -58,6 +77,8 @@ pub trait Db: Send + Sync { ) -> Result>; #[cfg(test)] async fn teardown(&self, url: &str); + #[cfg(test)] + fn as_fake<'a>(&'a self) -> Option<&'a tests::FakeDb>; } pub struct PostgresDb { @@ -99,6 +120,23 @@ impl Db for PostgresDb { Ok(sqlx::query_as(query).fetch_all(&self.pool).await?) } + async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { + let like_string = fuzzy_like_string(name_query); + let query = " + SELECT users.* + FROM users + WHERE github_login ILIKE $1 + ORDER BY github_login <-> $2 + LIMIT $3 + "; + Ok(sqlx::query_as(query) + .bind(like_string) + .bind(name_query) + .bind(limit) + .fetch_all(&self.pool) + .await?) + } + async fn get_user_by_id(&self, id: UserId) -> Result> { let users = self.get_users_by_ids(vec![id]).await?; Ok(users.into_iter().next()) @@ -150,6 +188,188 @@ impl Db for PostgresDb { .map(drop)?) } + // contacts + + async fn get_contacts(&self, user_id: UserId) -> Result { + let query = " + SELECT user_id_a, user_id_b, a_to_b, accepted, should_notify + FROM contacts + WHERE user_id_a = $1 OR user_id_b = $1; + "; + + let mut rows = sqlx::query_as::<_, (UserId, UserId, bool, bool, bool)>(query) + .bind(user_id) + .fetch(&self.pool); + + let mut current = vec![user_id]; + let mut outgoing_requests = Vec::new(); + let mut incoming_requests = Vec::new(); + while let Some(row) = rows.next().await { + let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?; + + if user_id_a == user_id { + if accepted { + current.push(user_id_b); + } else if a_to_b { + outgoing_requests.push(user_id_b); + } else { + incoming_requests.push(IncomingContactRequest { + requester_id: user_id_b, + should_notify, + }); + } + } else { + if accepted { + current.push(user_id_a); + } else if a_to_b { + incoming_requests.push(IncomingContactRequest { + requester_id: user_id_a, + should_notify, + }); + } else { + outgoing_requests.push(user_id_a); + } + } + } + + current.sort_unstable(); + outgoing_requests.sort_unstable(); + incoming_requests.sort_unstable(); + + Ok(Contacts { + current, + outgoing_requests, + incoming_requests, + }) + } + + async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> { + 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 query = " + INSERT into contacts (user_id_a, user_id_b, a_to_b, accepted, should_notify) + VALUES ($1, $2, $3, 'f', 't') + ON CONFLICT (user_id_a, user_id_b) DO UPDATE + SET + accepted = 't' + WHERE + NOT contacts.accepted AND + ((contacts.a_to_b = excluded.a_to_b AND contacts.user_id_a = excluded.user_id_b) OR + (contacts.a_to_b != excluded.a_to_b AND contacts.user_id_a = excluded.user_id_a)); + "; + let result = sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 1 { + Ok(()) + } else { + Err(anyhow!("contact already requested")) + } + } + + async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { + let (id_a, id_b) = if responder_id < requester_id { + (responder_id, requester_id) + } else { + (requester_id, responder_id) + }; + let query = " + DELETE FROM contacts + WHERE user_id_a = $1 AND user_id_b = $2; + "; + let result = sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 1 { + Ok(()) + } else { + Err(anyhow!("no such contact")) + } + } + + async fn dismiss_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + ) -> Result<()> { + 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 query = " + UPDATE contacts + SET should_notify = 'f' + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + "; + + let result = sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 0 { + Err(anyhow!("no such contact request"))?; + } + + Ok(()) + } + + async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()> { + 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 result = if accept { + let query = " + UPDATE contacts + SET accepted = 't', should_notify = 'f' + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + "; + sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await? + } else { + let query = " + DELETE FROM contacts + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3 AND NOT accepted; + "; + sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await? + }; + if result.rows_affected() == 1 { + Ok(()) + } else { + Err(anyhow!("no such contact request")) + } + } + // access tokens async fn create_access_token_hash( @@ -406,12 +626,17 @@ impl Db for PostgresDb { .await .log_err(); } + + #[cfg(test)] + fn as_fake(&self) -> Option<&tests::FakeDb> { + None + } } macro_rules! id_type { ($name:ident) => { #[derive( - Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize, + Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize, )] #[sqlx(transparent)] #[serde(transparent)] @@ -476,6 +701,31 @@ pub struct ChannelMessage { pub nonce: Uuid, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Contacts { + pub current: Vec, + pub incoming_requests: Vec, + pub outgoing_requests: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct IncomingContactRequest { + pub requester_id: UserId, + pub should_notify: bool, +} + +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 +} + #[cfg(test)] pub mod tests { use super::*; @@ -640,6 +890,185 @@ pub mod tests { ); } + #[test] + fn test_fuzzy_like_string() { + assert_eq!(fuzzy_like_string("abcd"), "%a%b%c%d%"); + assert_eq!(fuzzy_like_string("x y"), "%x%y%"); + assert_eq!(fuzzy_like_string(" z "), "%z%"); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_fuzzy_search_users() { + let test_db = TestDb::postgres().await; + let db = test_db.db(); + for github_login in [ + "California", + "colorado", + "oregon", + "washington", + "florida", + "delaware", + "rhode-island", + ] { + db.create_user(github_login, false).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: &Arc, query: &str) -> Vec { + db.fuzzy_search_users(query, 10) + .await + .unwrap() + .into_iter() + .map(|user| user.github_login) + .collect::>() + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_add_contacts() { + for test_db in [ + TestDb::postgres().await, + TestDb::fake(Arc::new(gpui::executor::Background::new())), + ] { + let db = test_db.db(); + + let user_1 = db.create_user("user1", false).await.unwrap(); + let user_2 = db.create_user("user2", false).await.unwrap(); + let user_3 = db.create_user("user3", false).await.unwrap(); + + // User starts with no contacts + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + Contacts { + current: vec![user_1], + outgoing_requests: vec![], + incoming_requests: vec![], + }, + ); + + // User requests a contact. Both users see the pending request. + db.send_contact_request(user_1, user_2).await.unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + Contacts { + current: vec![user_1], + outgoing_requests: vec![user_2], + incoming_requests: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![user_2], + outgoing_requests: vec![], + incoming_requests: vec![IncomingContactRequest { + requester_id: user_1, + should_notify: true + }], + }, + ); + + // User 2 dismisses the contact request notification without accepting or rejecting. + // We shouldn't notify them again. + db.dismiss_contact_request(user_1, user_2) + .await + .unwrap_err(); + db.dismiss_contact_request(user_2, user_1).await.unwrap(); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![user_2], + outgoing_requests: vec![], + incoming_requests: vec![IncomingContactRequest { + requester_id: user_1, + should_notify: false + }], + }, + ); + + // User can't accept their own contact request + db.respond_to_contact_request(user_1, user_2, true) + .await + .unwrap_err(); + + // User accepts a contact request. Both users see the contact. + db.respond_to_contact_request(user_2, user_1, true) + .await + .unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + Contacts { + current: vec![user_1, user_2], + outgoing_requests: vec![], + incoming_requests: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![user_1, user_2], + outgoing_requests: vec![], + incoming_requests: vec![], + }, + ); + + // 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 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(), + Contacts { + current: vec![user_1, user_2, user_3], + outgoing_requests: vec![], + incoming_requests: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_3).await.unwrap(), + Contacts { + current: vec![user_1, user_3], + outgoing_requests: vec![], + incoming_requests: vec![], + }, + ); + + // 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_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![user_1, user_2], + outgoing_requests: vec![], + incoming_requests: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_3).await.unwrap(), + Contacts { + current: vec![user_1, user_3], + outgoing_requests: vec![], + incoming_requests: vec![], + }, + ); + } + } + pub struct TestDb { pub db: Option>, pub url: String, @@ -690,16 +1119,25 @@ pub mod tests { pub struct FakeDb { background: Arc, - users: Mutex>, + pub users: Mutex>, + pub orgs: Mutex>, + pub org_memberships: Mutex>, + pub channels: Mutex>, + pub channel_memberships: Mutex>, + pub channel_messages: Mutex>, + pub contacts: Mutex>, + next_channel_message_id: Mutex, next_user_id: Mutex, - orgs: Mutex>, next_org_id: Mutex, - org_memberships: Mutex>, - channels: Mutex>, next_channel_id: Mutex, - channel_memberships: Mutex>, - channel_messages: Mutex>, - next_channel_message_id: Mutex, + } + + #[derive(Debug)] + pub struct FakeContact { + pub requester_id: UserId, + pub responder_id: UserId, + pub accepted: bool, + pub should_notify: bool, } impl FakeDb { @@ -716,6 +1154,7 @@ pub mod tests { channel_memberships: Default::default(), channel_messages: Default::default(), next_channel_message_id: Mutex::new(1), + contacts: Default::default(), } } } @@ -749,6 +1188,10 @@ pub mod tests { unimplemented!() } + async fn fuzzy_search_users(&self, _: &str, _: u32) -> Result> { + unimplemented!() + } + async fn get_user_by_id(&self, id: UserId) -> Result> { Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next()) } @@ -759,8 +1202,13 @@ pub mod tests { Ok(ids.iter().filter_map(|id| users.get(id).cloned()).collect()) } - async fn get_user_by_github_login(&self, _github_login: &str) -> Result> { - unimplemented!() + async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + Ok(self + .users + .lock() + .values() + .find(|user| user.github_login == github_login) + .cloned()) } async fn set_user_is_admin(&self, _id: UserId, _is_admin: bool) -> Result<()> { @@ -771,6 +1219,122 @@ pub mod tests { unimplemented!() } + async fn get_contacts(&self, id: UserId) -> Result { + self.background.simulate_random_delay().await; + let mut current = vec![id]; + let mut outgoing_requests = Vec::new(); + let mut incoming_requests = Vec::new(); + + for contact in self.contacts.lock().iter() { + if contact.requester_id == id { + if contact.accepted { + current.push(contact.responder_id); + } else { + outgoing_requests.push(contact.responder_id); + } + } else if contact.responder_id == id { + if contact.accepted { + current.push(contact.requester_id); + } else { + incoming_requests.push(IncomingContactRequest { + requester_id: contact.requester_id, + should_notify: contact.should_notify, + }); + } + } + } + + current.sort_unstable(); + outgoing_requests.sort_unstable(); + incoming_requests.sort_unstable(); + + Ok(Contacts { + current, + outgoing_requests, + incoming_requests, + }) + } + + async fn send_contact_request( + &self, + requester_id: UserId, + responder_id: UserId, + ) -> Result<()> { + let mut contacts = self.contacts.lock(); + for contact in contacts.iter_mut() { + if contact.requester_id == requester_id && contact.responder_id == responder_id { + if contact.accepted { + Err(anyhow!("contact already exists"))?; + } else { + Err(anyhow!("contact already requested"))?; + } + } + if contact.responder_id == requester_id && contact.requester_id == responder_id { + if contact.accepted { + Err(anyhow!("contact already exists"))?; + } else { + contact.accepted = true; + return Ok(()); + } + } + } + contacts.push(FakeContact { + requester_id, + responder_id, + accepted: false, + should_notify: true, + }); + Ok(()) + } + + async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { + self.contacts.lock().retain(|contact| { + !(contact.requester_id == requester_id && contact.responder_id == responder_id) + }); + Ok(()) + } + + async fn dismiss_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + ) -> Result<()> { + let mut contacts = self.contacts.lock(); + for contact in contacts.iter_mut() { + if contact.requester_id == requester_id && contact.responder_id == responder_id { + if contact.accepted { + return Err(anyhow!("contact already confirmed")); + } + contact.should_notify = false; + return Ok(()); + } + } + Err(anyhow!("no such contact request")) + } + + async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()> { + let mut contacts = self.contacts.lock(); + for (ix, contact) in contacts.iter_mut().enumerate() { + if contact.requester_id == requester_id && contact.responder_id == responder_id { + if contact.accepted { + return Err(anyhow!("contact already confirmed")); + } + if accept { + contact.accepted = true; + } else { + contacts.remove(ix); + } + return Ok(()); + } + } + Err(anyhow!("no such contact request")) + } + async fn create_access_token_hash( &self, _user_id: UserId, @@ -965,5 +1529,10 @@ pub mod tests { } async fn teardown(&self, _: &str) {} + + #[cfg(test)] + fn as_fake(&self) -> Option<&FakeDb> { + Some(self) + } } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 827ac564f827746700ea6456f35bd54289e64fe2..33d1d526775f841e98e24e154d1ae29dcf54292e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -18,16 +18,16 @@ use axum::{ headers::{Header, HeaderName}, http::StatusCode, middleware, - response::{IntoResponse, Response}, + response::IntoResponse, routing::get, Extension, Router, TypedHeader, }; -use collections::{HashMap, HashSet}; +use collections::HashMap; use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt}; use lazy_static::lazy_static; use rpc::{ proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, - Connection, ConnectionId, Peer, TypedEnvelope, + Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; use std::{ any::TypeId, @@ -36,7 +36,10 @@ use std::{ net::SocketAddr, ops::{Deref, DerefMut}, rc::Rc, - sync::Arc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, time::Duration, }; use store::{Store, Worktree}; @@ -46,11 +49,25 @@ use tokio::{ time::Sleep, }; use tower::ServiceBuilder; -use tracing::{info_span, instrument, Instrument}; +use tracing::{info_span, Instrument}; type MessageHandler = Box, Box) -> BoxFuture<'static, ()>>; +struct Response { + server: Arc, + receipt: Receipt, + responded: Arc, +} + +impl Response { + fn send(self, payload: R::Response) -> Result<()> { + self.responded.store(true, SeqCst); + self.server.peer.respond(self.receipt, payload)?; + Ok(()) + } +} + pub struct Server { peer: Arc, store: RwLock, @@ -100,7 +117,7 @@ impl Server { .add_message_handler(Server::unregister_project) .add_request_handler(Server::share_project) .add_message_handler(Server::unshare_project) - .add_sync_request_handler(Server::join_project) + .add_request_handler(Server::join_project) .add_message_handler(Server::leave_project) .add_request_handler(Server::register_worktree) .add_message_handler(Server::unregister_worktree) @@ -136,6 +153,10 @@ impl Server { .add_request_handler(Server::save_buffer) .add_request_handler(Server::get_channels) .add_request_handler(Server::get_users) + .add_request_handler(Server::fuzzy_search_users) + .add_request_handler(Server::request_contact) + .add_request_handler(Server::remove_contact) + .add_request_handler(Server::respond_to_contact_request) .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) .add_request_handler(Server::send_channel_message) @@ -178,43 +199,12 @@ impl Server { self } - fn add_request_handler(&mut self, handler: F) -> &mut Self - where - F: 'static + Send + Sync + Fn(Arc, TypedEnvelope) -> Fut, - Fut: 'static + Send + Future>, - M: RequestMessage, - { - self.add_message_handler(move |server, envelope| { - let receipt = envelope.receipt(); - let response = (handler)(server.clone(), envelope); - async move { - match response.await { - Ok(response) => { - server.peer.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - server.peer.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } - }) - } - /// Handle a request while holding a lock to the store. This is useful when we're registering /// a connection but we want to respond on the connection before anybody else can send on it. - fn add_sync_request_handler(&mut self, handler: F) -> &mut Self + fn add_request_handler(&mut self, handler: F) -> &mut Self where - F: 'static - + Send - + Sync - + Fn(Arc, &mut Store, TypedEnvelope) -> Result, + F: 'static + Send + Sync + Fn(Arc, TypedEnvelope, Response) -> Fut, + Fut: Send + Future>, M: RequestMessage, { let handler = Arc::new(handler); @@ -222,12 +212,19 @@ impl Server { let receipt = envelope.receipt(); let handler = handler.clone(); async move { - let mut store = server.state_mut().await; - let response = (handler)(server.clone(), &mut *store, envelope); - match response { - Ok(response) => { - server.peer.respond(receipt, response)?; - Ok(()) + let responded = Arc::new(AtomicBool::default()); + let response = Response { + server: server.clone(), + responded: responded.clone(), + receipt: envelope.receipt(), + }; + match (handler)(server.clone(), envelope, response).await { + Ok(()) => { + if responded.load(std::sync::atomic::Ordering::SeqCst) { + Ok(()) + } else { + Err(anyhow!("handler did not send a response"))? + } } Err(error) => { server.peer.respond_with_error( @@ -250,7 +247,7 @@ impl Server { user_id: UserId, mut send_connection_id: Option>, executor: E, - ) -> impl Future { + ) -> impl Future> { let mut this = self.clone(); let span = info_span!("handle connection", %user_id, %address); async move { @@ -273,11 +270,14 @@ impl Server { let _ = send_connection_id.send(connection_id).await; } + let contacts = this.app_state.db.get_contacts(user_id).await?; + { - let mut state = this.state_mut().await; - state.add_connection(connection_id, user_id); - this.update_contacts_for_users(&*state, &[user_id]); + let mut store = this.store_mut().await; + store.add_connection(connection_id, user_id); + this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; } + this.update_user_contacts(user_id).await?; let handle_io = handle_io.fuse(); futures::pin_mut!(handle_io); @@ -326,24 +326,21 @@ impl Server { if let Err(error) = this.sign_out(connection_id).await { tracing::error!(%error, "error signing out"); } + + Ok(()) }.instrument(span) } async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> Result<()> { self.peer.disconnect(connection_id); - let mut state = self.state_mut().await; - let removed_connection = state.remove_connection(connection_id)?; + let removed_connection = self.store_mut().await.remove_connection(connection_id)?; for (project_id, project) in removed_connection.hosted_projects { if let Some(share) = project.share { - broadcast( - connection_id, - share.guests.keys().copied().collect(), - |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }, - ); + broadcast(connection_id, share.guests.keys().copied(), |conn_id| { + self.peer + .send(conn_id, proto::UnshareProject { project_id }) + }); } } @@ -359,44 +356,89 @@ impl Server { }); } - self.update_contacts_for_users(&*state, removed_connection.contact_ids.iter()); + self.update_user_contacts(removed_connection.user_id) + .await?; + Ok(()) } - async fn ping(self: Arc, _: TypedEnvelope) -> Result { - Ok(proto::Ack {}) + async fn ping( + self: Arc, + _: TypedEnvelope, + response: Response, + ) -> Result<()> { + response.send(proto::Ack {})?; + Ok(()) } async fn register_project( self: Arc, request: TypedEnvelope, - ) -> Result { - let project_id = { - let mut state = self.state_mut().await; - let user_id = state.user_id_for_connection(request.sender_id)?; - state.register_project(request.sender_id, user_id) + response: Response, + ) -> Result<()> { + let user_id; + let project_id; + { + let mut state = self.store_mut().await; + user_id = state.user_id_for_connection(request.sender_id)?; + project_id = state.register_project(request.sender_id, user_id); }; - Ok(proto::RegisterProjectResponse { project_id }) + self.update_user_contacts(user_id).await?; + response.send(proto::RegisterProjectResponse { project_id })?; + Ok(()) } async fn unregister_project( self: Arc, request: TypedEnvelope, ) -> Result<()> { - let mut state = self.state_mut().await; - let project = state.unregister_project(request.payload.project_id, request.sender_id)?; - self.update_contacts_for_users(&*state, &project.authorized_user_ids()); + let user_id = { + let mut state = self.store_mut().await; + state.unregister_project(request.payload.project_id, request.sender_id)?; + state.user_id_for_connection(request.sender_id)? + }; + + self.update_user_contacts(user_id).await?; Ok(()) } async fn share_project( self: Arc, request: TypedEnvelope, - ) -> Result { - let mut state = self.state_mut().await; - let project = state.share_project(request.payload.project_id, request.sender_id)?; - self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); - Ok(proto::Ack {}) + response: Response, + ) -> Result<()> { + let user_id = { + let mut state = self.store_mut().await; + state.share_project(request.payload.project_id, request.sender_id)?; + state.user_id_for_connection(request.sender_id)? + }; + self.update_user_contacts(user_id).await?; + response.send(proto::Ack {})?; + Ok(()) + } + + async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { + let contacts = self.app_state.db.get_contacts(user_id).await?; + let store = self.store().await; + let updated_contact = store.contact_for_user(user_id); + for contact_user_id in contacts.current { + for contact_conn_id in store.connection_ids_for_user(contact_user_id) { + self.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 unshare_project( @@ -404,89 +446,103 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let project_id = request.payload.project_id; - let mut state = self.state_mut().await; - let project = state.unshare_project(project_id, request.sender_id)?; - broadcast(request.sender_id, project.connection_ids, |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }); - self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); + let project; + { + let mut state = self.store_mut().await; + project = state.unshare_project(project_id, request.sender_id)?; + broadcast(request.sender_id, project.connection_ids, |conn_id| { + self.peer + .send(conn_id, proto::UnshareProject { project_id }) + }); + } + self.update_user_contacts(project.host_user_id).await?; Ok(()) } - fn join_project( + async fn join_project( self: Arc, - state: &mut Store, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let project_id = request.payload.project_id; + let host_user_id; + let guest_user_id; + { + let state = self.store().await; + host_user_id = state.project(project_id)?.host_user_id; + guest_user_id = state.user_id_for_connection(request.sender_id)?; + }; - let user_id = state.user_id_for_connection(request.sender_id)?; - let (response, connection_ids, contact_user_ids) = state - .join_project(request.sender_id, user_id, project_id) - .and_then(|joined| { - let share = joined.project.share()?; - let peer_count = share.guests.len(); - let mut collaborators = Vec::with_capacity(peer_count); - collaborators.push(proto::Collaborator { - peer_id: joined.project.host_connection_id.0, - replica_id: 0, - user_id: joined.project.host_user_id.to_proto(), - }); - let worktrees = share - .worktrees - .iter() - .filter_map(|(id, shared_worktree)| { - let worktree = joined.project.worktrees.get(&id)?; - Some(proto::Worktree { - id: *id, - root_name: worktree.root_name.clone(), - entries: shared_worktree.entries.values().cloned().collect(), - diagnostic_summaries: shared_worktree - .diagnostic_summaries - .values() - .cloned() - .collect(), - visible: worktree.visible, - scan_id: shared_worktree.scan_id, - }) + let guest_contacts = self.app_state.db.get_contacts(guest_user_id).await?; + if !guest_contacts.current.contains(&host_user_id) { + return Err(anyhow!("no such project"))?; + } + + { + let state = &mut *self.store_mut().await; + let joined = state.join_project(request.sender_id, guest_user_id, project_id)?; + let share = joined.project.share()?; + let peer_count = share.guests.len(); + let mut collaborators = Vec::with_capacity(peer_count); + collaborators.push(proto::Collaborator { + peer_id: joined.project.host_connection_id.0, + replica_id: 0, + user_id: joined.project.host_user_id.to_proto(), + }); + let worktrees = share + .worktrees + .iter() + .filter_map(|(id, shared_worktree)| { + let worktree = joined.project.worktrees.get(&id)?; + Some(proto::Worktree { + id: *id, + root_name: worktree.root_name.clone(), + entries: shared_worktree.entries.values().cloned().collect(), + diagnostic_summaries: shared_worktree + .diagnostic_summaries + .values() + .cloned() + .collect(), + visible: worktree.visible, + scan_id: shared_worktree.scan_id, }) - .collect(); - for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { - if *peer_conn_id != request.sender_id { - collaborators.push(proto::Collaborator { - peer_id: peer_conn_id.0, - replica_id: *peer_replica_id as u32, - user_id: peer_user_id.to_proto(), - }); - } + }) + .collect(); + for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { + if *peer_conn_id != request.sender_id { + collaborators.push(proto::Collaborator { + peer_id: peer_conn_id.0, + replica_id: *peer_replica_id as u32, + user_id: peer_user_id.to_proto(), + }); } - let response = proto::JoinProjectResponse { - worktrees, - replica_id: joined.replica_id as u32, - collaborators, - language_servers: joined.project.language_servers.clone(), - }; - let connection_ids = joined.project.connection_ids(); - let contact_user_ids = joined.project.authorized_user_ids(); - Ok((response, connection_ids, contact_user_ids)) - })?; - - broadcast(request.sender_id, connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::AddProjectCollaborator { - project_id, - collaborator: Some(proto::Collaborator { - peer_id: request.sender_id.0, - replica_id: response.replica_id, - user_id: user_id.to_proto(), - }), + } + broadcast( + request.sender_id, + joined.project.connection_ids(), + |conn_id| { + self.peer.send( + conn_id, + proto::AddProjectCollaborator { + project_id, + collaborator: Some(proto::Collaborator { + peer_id: request.sender_id.0, + replica_id: joined.replica_id as u32, + user_id: guest_user_id.to_proto(), + }), + }, + ) }, - ) - }); - self.update_contacts_for_users(state, &contact_user_ids); - Ok(response) + ); + response.send(proto::JoinProjectResponse { + worktrees, + replica_id: joined.replica_id as u32, + collaborators, + language_servers: joined.project.language_servers.clone(), + })?; + } + self.update_user_contacts(host_user_id).await?; + Ok(()) } async fn leave_project( @@ -495,85 +551,89 @@ impl Server { ) -> Result<()> { let sender_id = request.sender_id; let project_id = request.payload.project_id; - let mut state = self.state_mut().await; - let worktree = state.leave_project(sender_id, project_id)?; - broadcast(sender_id, worktree.connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { - project_id, - peer_id: sender_id.0, - }, - ) - }); - self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); + let project; + { + let mut state = self.store_mut().await; + project = state.leave_project(sender_id, project_id)?; + broadcast(sender_id, project.connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id, + peer_id: sender_id.0, + }, + ) + }); + } + self.update_user_contacts(project.host_user_id).await?; Ok(()) } async fn register_worktree( self: Arc, request: TypedEnvelope, - ) -> Result { - let mut contact_user_ids = HashSet::default(); - for github_login in &request.payload.authorized_logins { - let contact_user_id = self.app_state.db.create_user(github_login, false).await?; - contact_user_ids.insert(contact_user_id); - } - - let mut state = self.state_mut().await; - let host_user_id = state.user_id_for_connection(request.sender_id)?; - contact_user_ids.insert(host_user_id); - - let contact_user_ids = contact_user_ids.into_iter().collect::>(); - let guest_connection_ids = state - .read_project(request.payload.project_id, request.sender_id)? - .guest_connection_ids(); - state.register_worktree( - request.payload.project_id, - request.payload.worktree_id, - request.sender_id, - Worktree { - authorized_user_ids: contact_user_ids.clone(), - root_name: request.payload.root_name.clone(), - visible: request.payload.visible, - }, - )?; + response: Response, + ) -> Result<()> { + let host_user_id; + { + let mut state = self.store_mut().await; + host_user_id = state.user_id_for_connection(request.sender_id)?; + + let guest_connection_ids = state + .read_project(request.payload.project_id, request.sender_id)? + .guest_connection_ids(); + state.register_worktree( + request.payload.project_id, + request.payload.worktree_id, + request.sender_id, + Worktree { + root_name: request.payload.root_name.clone(), + visible: request.payload.visible, + }, + )?; - broadcast(request.sender_id, guest_connection_ids, |connection_id| { - self.peer - .forward_send(request.sender_id, connection_id, request.payload.clone()) - }); - self.update_contacts_for_users(&*state, &contact_user_ids); - Ok(proto::Ack {}) + broadcast(request.sender_id, guest_connection_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }); + } + self.update_user_contacts(host_user_id).await?; + response.send(proto::Ack {})?; + Ok(()) } async fn unregister_worktree( self: Arc, request: TypedEnvelope, ) -> Result<()> { + let host_user_id; let project_id = request.payload.project_id; let worktree_id = request.payload.worktree_id; - let mut state = self.state_mut().await; - let (worktree, guest_connection_ids) = - state.unregister_worktree(project_id, worktree_id, request.sender_id)?; - broadcast(request.sender_id, guest_connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::UnregisterWorktree { - project_id, - worktree_id, - }, - ) - }); - self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); + { + let mut state = self.store_mut().await; + let (_, guest_connection_ids) = + state.unregister_worktree(project_id, worktree_id, request.sender_id)?; + host_user_id = state.user_id_for_connection(request.sender_id)?; + broadcast(request.sender_id, guest_connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::UnregisterWorktree { + project_id, + worktree_id, + }, + ) + }); + } + self.update_user_contacts(host_user_id).await?; Ok(()) } async fn update_worktree( self: Arc, request: TypedEnvelope, - ) -> Result { - let connection_ids = self.state_mut().await.update_worktree( + response: Response, + ) -> Result<()> { + let connection_ids = self.store_mut().await.update_worktree( request.sender_id, request.payload.project_id, request.payload.worktree_id, @@ -586,8 +646,8 @@ impl Server { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); - - Ok(proto::Ack {}) + response.send(proto::Ack {})?; + Ok(()) } async fn update_diagnostic_summary( @@ -599,7 +659,7 @@ impl Server { .summary .clone() .ok_or_else(|| anyhow!("invalid summary"))?; - let receiver_ids = self.state_mut().await.update_diagnostic_summary( + let receiver_ids = self.store_mut().await.update_diagnostic_summary( request.payload.project_id, request.payload.worktree_id, request.sender_id, @@ -617,7 +677,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let receiver_ids = self.state_mut().await.start_language_server( + let receiver_ids = self.store_mut().await.start_language_server( request.payload.project_id, request.sender_id, request @@ -638,7 +698,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -651,61 +711,69 @@ impl Server { async fn forward_project_request( self: Arc, request: TypedEnvelope, - ) -> Result + response: Response, + ) -> Result<()> where T: EntityMessage + RequestMessage, { let host_connection_id = self - .state() + .store() .await .read_project(request.payload.remote_entity_id(), request.sender_id)? .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) + + response.send( + self.peer + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?, + )?; + Ok(()) } async fn save_buffer( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let host = self - .state() + .store() .await .read_project(request.payload.project_id, request.sender_id)? .host_connection_id; - let response = self + let response_payload = self .peer .forward_request(request.sender_id, host, request.payload.clone()) .await?; let mut guests = self - .state() + .store() .await .read_project(request.payload.project_id, request.sender_id)? .connection_ids(); guests.retain(|guest_connection_id| *guest_connection_id != request.sender_id); broadcast(host, guests, |conn_id| { - self.peer.forward_send(host, conn_id, response.clone()) + self.peer + .forward_send(host, conn_id, response_payload.clone()) }); - - Ok(response) + response.send(response_payload)?; + Ok(()) } async fn update_buffer( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); - Ok(proto::Ack {}) + response.send(proto::Ack {})?; + Ok(()) } async fn update_buffer_file( @@ -713,7 +781,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -728,7 +796,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -743,7 +811,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -756,31 +824,33 @@ impl Server { async fn follow( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let leader_id = ConnectionId(request.payload.leader_id); let follower_id = request.sender_id; if !self - .state() + .store() .await .project_connection_ids(request.payload.project_id, follower_id)? .contains(&leader_id) { Err(anyhow!("no such peer"))?; } - let mut response = self + let mut response_payload = self .peer .forward_request(request.sender_id, leader_id, request.payload) .await?; - response + response_payload .views .retain(|view| view.leader_id != Some(follower_id.0)); - Ok(response) + response.send(response_payload)?; + Ok(()) } async fn unfollow(self: Arc, request: TypedEnvelope) -> Result<()> { let leader_id = ConnectionId(request.payload.leader_id); if !self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)? .contains(&leader_id) @@ -797,7 +867,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let connection_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; let leader_id = request @@ -822,13 +892,14 @@ impl Server { async fn get_channels( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channels = self.app_state.db.get_accessible_channels(user_id).await?; - Ok(proto::GetChannelsResponse { + response.send(proto::GetChannelsResponse { channels: channels .into_iter() .map(|chan| proto::Channel { @@ -836,13 +907,15 @@ impl Server { name: chan.name, }) .collect(), - }) + })?; + Ok(()) } async fn get_users( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_ids = request .payload .user_ids @@ -861,36 +934,197 @@ impl Server { github_login: user.github_login, }) .collect(); - Ok(proto::GetUsersResponse { users }) + response.send(proto::UsersResponse { users })?; + Ok(()) } - #[instrument(skip(self, state, user_ids))] - fn update_contacts_for_users<'a>( - self: &Arc, - state: &Store, - user_ids: impl IntoIterator, - ) { - for user_id in user_ids { - let contacts = state.contacts_for_user(*user_id); - for connection_id in state.connection_ids_for_user(*user_id) { - self.peer - .send( - connection_id, - proto::UpdateContacts { - contacts: contacts.clone(), - }, - ) - .trace_err(); - } + async fn fuzzy_search_users( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let user_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; + let query = request.payload.query; + let db = &self.app_state.db; + let users = match query.len() { + 0 => vec![], + 1 | 2 => db + .get_user_by_github_login(&query) + .await? + .into_iter() + .collect(), + _ => db.fuzzy_search_users(&query, 10).await?, + }; + let users = users + .into_iter() + .filter(|user| user.id != 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( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let requester_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; + let responder_id = UserId::from_proto(request.payload.responder_id); + if requester_id == responder_id { + return Err(anyhow!("cannot add yourself as a contact"))?; + } + + self.app_state + .db + .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 self.store().await.connection_ids_for_user(requester_id) { + self.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(), + should_notify: true, + }); + for connection_id in self.store().await.connection_ids_for_user(responder_id) { + self.peer.send(connection_id, update.clone())?; + } + + response.send(proto::Ack {})?; + Ok(()) } + async fn respond_to_contact_request( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let responder_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; + let requester_id = UserId::from_proto(request.payload.requester_id); + let accept = request.payload.response == proto::ContactRequestResponse::Accept as i32; + self.app_state + .db + .respond_to_contact_request(responder_id, requester_id, accept) + .await?; + + let store = self.store().await; + // Update responder with new contact + let mut update = proto::UpdateContacts::default(); + if accept { + update.contacts.push(store.contact_for_user(requester_id)); + } + update + .remove_incoming_requests + .push(requester_id.to_proto()); + for connection_id in store.connection_ids_for_user(responder_id) { + self.peer.send(connection_id, update.clone())?; + } + + // Update requester with new contact + let mut update = proto::UpdateContacts::default(); + if accept { + update.contacts.push(store.contact_for_user(responder_id)); + } + update + .remove_outgoing_requests + .push(responder_id.to_proto()); + for connection_id in store.connection_ids_for_user(requester_id) { + self.peer.send(connection_id, update.clone())?; + } + + response.send(proto::Ack {})?; + Ok(()) + } + + async fn remove_contact( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let requester_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; + let responder_id = UserId::from_proto(request.payload.user_id); + self.app_state + .db + .remove_contact(requester_id, responder_id) + .await?; + + // Update outgoing contact requests of requester + let mut update = proto::UpdateContacts::default(); + update + .remove_outgoing_requests + .push(responder_id.to_proto()); + for connection_id in self.store().await.connection_ids_for_user(requester_id) { + self.peer.send(connection_id, update.clone())?; + } + + // Update incoming contact requests of responder + let mut update = proto::UpdateContacts::default(); + update + .remove_incoming_requests + .push(requester_id.to_proto()); + for connection_id in self.store().await.connection_ids_for_user(responder_id) { + self.peer.send(connection_id, update.clone())?; + } + + response.send(proto::Ack {})?; + Ok(()) + } + + // #[instrument(skip(self, state, user_ids))] + // fn update_contacts_for_users<'a>( + // self: &Arc, + // state: &Store, + // user_ids: impl IntoIterator, + // ) { + // for user_id in user_ids { + // let contacts = state.contacts_for_user(*user_id); + // for connection_id in state.connection_ids_for_user(*user_id) { + // self.peer + // .send( + // connection_id, + // proto::UpdateContacts { + // contacts: contacts.clone(), + // pending_requests_from_user_ids: Default::default(), + // pending_requests_to_user_ids: Default::default(), + // }, + // ) + // .trace_err(); + // } + // } + // } + async fn join_channel( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -903,7 +1137,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.state_mut() + self.store_mut() .await .join_channel(request.sender_id, channel_id); let messages = self @@ -920,10 +1154,11 @@ impl Server { nonce: Some(msg.nonce.as_u128().into()), }) .collect::>(); - Ok(proto::JoinChannelResponse { + response.send(proto::JoinChannelResponse { done: messages.len() < MESSAGE_COUNT_PER_PAGE, messages, - }) + })?; + Ok(()) } async fn leave_channel( @@ -931,7 +1166,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -944,7 +1179,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.state_mut() + self.store_mut() .await .leave_channel(request.sender_id, channel_id); @@ -954,12 +1189,13 @@ impl Server { async fn send_channel_message( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let channel_id = ChannelId::from_proto(request.payload.channel_id); let user_id; let connection_ids; { - let state = self.state().await; + let state = self.store().await; user_id = state.user_id_for_connection(request.sender_id)?; connection_ids = state.channel_connection_ids(channel_id)?; } @@ -1001,17 +1237,19 @@ impl Server { }, ) }); - Ok(proto::SendChannelMessageResponse { + response.send(proto::SendChannelMessageResponse { message: Some(message), - }) + })?; + Ok(()) } async fn get_channel_messages( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -1042,14 +1280,14 @@ impl Server { nonce: Some(msg.nonce.as_u128().into()), }) .collect::>(); - - Ok(proto::GetChannelMessagesResponse { + response.send(proto::GetChannelMessagesResponse { done: messages.len() < MESSAGE_COUNT_PER_PAGE, messages, - }) + })?; + Ok(()) } - async fn state<'a>(self: &'a Arc) -> StoreReadGuard<'a> { + async fn store<'a>(self: &'a Arc) -> StoreReadGuard<'a> { #[cfg(test)] tokio::task::yield_now().await; let guard = self.store.read().await; @@ -1061,7 +1299,7 @@ impl Server { } } - async fn state_mut<'a>(self: &'a Arc) -> StoreWriteGuard<'a> { + async fn store_mut<'a>(self: &'a Arc) -> StoreWriteGuard<'a> { #[cfg(test)] tokio::task::yield_now().await; let guard = self.store.write().await; @@ -1123,9 +1361,11 @@ impl Executor for RealExecutor { } } -#[instrument(skip(f))] -fn broadcast(sender_id: ConnectionId, receiver_ids: Vec, mut f: F) -where +fn broadcast( + sender_id: ConnectionId, + receiver_ids: impl IntoIterator, + mut f: F, +) where F: FnMut(ConnectionId) -> anyhow::Result<()>, { for receiver_id in receiver_ids { @@ -1184,7 +1424,7 @@ pub async fn handle_websocket_request( Extension(server): Extension>, Extension(user_id): Extension, ws: WebSocketUpgrade, -) -> Response { +) -> axum::response::Response { if protocol_version != rpc::PROTOCOL_VERSION { return ( StatusCode::UPGRADE_REQUIRED, @@ -1194,12 +1434,18 @@ pub async fn handle_websocket_request( } 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)); - server.handle_connection(connection, socket_address, user_id, None, RealExecutor) + async move { + server + .handle_connection(connection, socket_address, user_id, None, RealExecutor) + .await + .log_err(); + } }) } @@ -1266,7 +1512,7 @@ mod tests { self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT, }; - use collections::BTreeMap; + use collections::{BTreeMap, HashSet}; use editor::{ self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, ToOffset, ToggleCodeActions, Undo, @@ -1326,12 +1572,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -1448,12 +1696,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -1541,12 +1791,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -1647,12 +1899,18 @@ mod tests { 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(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) + .await; // Share a worktree as client A. fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, "file1": "", "file2": "" }), @@ -1773,7 +2031,7 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>() - == [".zed.toml", "file1-renamed", "file3", "file4"] + == ["file1-renamed", "file3", "file4"] }) .await; worktree_b @@ -1781,7 +2039,7 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>() - == [".zed.toml", "file1-renamed", "file3", "file4"] + == ["file1-renamed", "file3", "file4"] }) .await; worktree_c @@ -1789,7 +2047,7 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>() - == [".zed.toml", "file1-renamed", "file3", "file4"] + == ["file1-renamed", "file3", "file4"] }) .await; @@ -1824,12 +2082,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -1864,7 +2124,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "c.txt"] + ["a.txt", "b.txt", "c.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -1873,7 +2133,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "c.txt"] + ["a.txt", "b.txt", "c.txt"] ); }); @@ -1890,7 +2150,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -1899,7 +2159,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); @@ -1917,7 +2177,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"] + ["DIR", "a.txt", "b.txt", "d.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -1926,7 +2186,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"] + ["DIR", "a.txt", "b.txt", "d.txt"] ); }); @@ -1942,7 +2202,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -1951,7 +2211,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); @@ -1967,7 +2227,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt"] + ["a.txt", "b.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -1976,7 +2236,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt"] + ["a.txt", "b.txt"] ); }); } @@ -1991,12 +2251,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, "a.txt": "a-contents", }), ) @@ -2073,12 +2335,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, "a.txt": "a-contents", }), ) @@ -2155,12 +2419,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", }), ) @@ -2234,12 +2500,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", }), ) @@ -2306,12 +2574,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -2386,7 +2656,7 @@ mod tests { // Simulate connection loss for client B and ensure client A observes client B leaving the project. client_b.wait_for_current_user(cx_b).await; server.disconnect_client(client_b.current_user_id(cx_b)); - cx_a.foreground().advance_clock(Duration::from_secs(3)); + cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); project_a .condition(cx_a, |p, _| p.collaborators().len() == 0) .await; @@ -2417,12 +2687,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "let one = two", "other.rs": "", }), @@ -2665,12 +2937,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "main.rs": "fn main() { a }", "other.rs": "", }), @@ -2846,12 +3120,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "let one = 1;", }), ) @@ -2975,12 +3251,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "let one = two", }), ) @@ -3059,7 +3337,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "const ONE: usize = b::TWO + b::THREE;", }), ) @@ -3088,6 +3365,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A let project_a = cx_a.update(|cx| { @@ -3203,7 +3483,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "one.rs": "const ONE: usize = 1;", "two.rs": "const TWO: usize = one::ONE + one::ONE;", }), @@ -3233,6 +3512,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A let project_a = cx_a.update(|cx| { @@ -3344,7 +3626,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a": "hello world", "b": "goodnight moon", "c": "a world of goo", @@ -3364,6 +3645,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A let project_a = cx_a.update(|cx| { @@ -3451,7 +3735,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "main.rs": "fn double(number: i32) -> i32 { number + number }", }), ) @@ -3473,6 +3756,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A let project_a = cx_a.update(|cx| { @@ -3589,7 +3875,6 @@ mod tests { "/code", json!({ "crate-1": { - ".zed.toml": r#"collaborators = ["user_b"]"#, "one.rs": "const ONE: usize = 1;", }, "crate-2": { @@ -3618,6 +3903,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A let project_a = cx_a.update(|cx| { @@ -3725,7 +4013,6 @@ mod tests { fs.insert_tree( "/root", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "const ONE: usize = b::TWO;", "b.rs": "const TWO: usize = 2", }), @@ -3748,6 +4035,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A let project_a = cx_a.update(|cx| { @@ -3845,12 +4135,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", "other.rs": "pub fn foo() -> usize { 4 }", }), @@ -4093,12 +4385,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; // Share a project as client A fs.insert_tree( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "one.rs": "const ONE: usize = 1;", "two.rs": "const TWO: usize = one::ONE + one::ONE;" }), @@ -4573,7 +4867,7 @@ mod tests { // Disconnect client B, ensuring we can still access its cached channel data. server.forbid_connections(); server.disconnect_client(client_b.current_user_id(&cx_b)); - cx_b.foreground().advance_clock(Duration::from_secs(3)); + cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); while !matches!( status_b.next().await, Some(client::Status::ReconnectionError { .. }) @@ -4695,6 +4989,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_contacts( + deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, @@ -4708,15 +5003,30 @@ mod tests { 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(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) + .await; + + deterministic.run_until_parked(); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } // Share a worktree as client A. - fs.insert_tree( - "/a", - json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, - }), - ) - .await; + fs.create_dir(Path::new("/a")).await.unwrap(); let project_a = cx_a.update(|cx| { Project::local( @@ -4737,24 +5047,19 @@ mod tests { .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - client_a - .user_store - .condition(&cx_a, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])] - }) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])] - }) - .await; - client_c - .user_store - .condition(&cx_c, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])] - }) - .await; + deterministic.run_until_parked(); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![("a", false, vec![])]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } let project_id = project_a .update(cx_a, |project, _| project.next_remote_id()) @@ -4763,24 +5068,20 @@ mod tests { .update(cx_a, |project, cx| project.share(cx)) .await .unwrap(); - client_a - .user_store - .condition(&cx_a, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])] - }) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])] - }) - .await; - client_c - .user_store - .condition(&cx_c, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])] - }) - .await; + + deterministic.run_until_parked(); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![("a", true, vec![])]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } let _project_b = Project::remote( project_id, @@ -4793,24 +5094,19 @@ mod tests { .await .unwrap(); - client_a - .user_store - .condition(&cx_a, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])] - }) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])] - }) - .await; - client_c - .user_store - .condition(&cx_c, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])] - }) - .await; + deterministic.run_until_parked(); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![("a", true, vec!["user_b"])]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } project_a .condition(&cx_a, |project, _| { @@ -4819,20 +5115,60 @@ mod tests { .await; cx_a.update(move |_| drop(project_a)); - client_a - .user_store - .condition(&cx_a, |user_store, _| contacts(user_store) == vec![]) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| contacts(user_store) == vec![]) - .await; + deterministic.run_until_parked(); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } + + server.disconnect_client(client_c.current_user_id(cx_c)); + server.forbid_connections(); + deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", false, vec![]) + ] + ) + }); + } client_c .user_store - .condition(&cx_c, |user_store, _| contacts(user_store) == vec![]) - .await; + .read_with(cx_c, |store, _| assert_eq!(contacts(store), [])); - fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, bool, Vec<&str>)>)> { + server.allow_connections(); + client_c + .authenticate_and_connect(false, &cx_c.to_async()) + .await + .unwrap(); + + deterministic.run_until_parked(); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } + + fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, bool, Vec<&str>)>)> { user_store .contacts() .iter() @@ -4848,12 +5184,212 @@ mod tests { ) }) .collect(); - (contact.user.github_login.as_str(), worktrees) + ( + contact.user.github_login.as_str(), + contact.online, + worktrees, + ) }) .collect() } } + #[gpui::test(iterations = 10)] + async fn test_contact_requests( + executor: Arc, + cx_a: &mut TestAppContext, + cx_a2: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_b2: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_c2: &mut TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + + // Connect to a server as 3 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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", "user_b"]); + assert_eq!(contacts_b.incoming_requests, &["user_c"]); + let contacts_b2 = client_b2.summarize_contacts(&cx_b2); + assert_eq!(contacts_b2.current, &["user_a", "user_b"]); + 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_a", "user_b"]); + assert!(contacts_a.outgoing_requests.is_empty()); + let contacts_a2 = client_a2.summarize_contacts(&cx_a2); + assert_eq!(contacts_a2.current, &["user_a", "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_a", "user_b"] + ); + assert_eq!( + client_b.summarize_contacts(&cx_b).current, + &["user_a", "user_b"] + ); + assert_eq!( + client_b.summarize_contacts(&cx_b).incoming_requests, + &["user_c"] + ); + assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]); + 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", "user_b"]); + assert!(contacts_b.incoming_requests.is_empty()); + let contacts_b2 = client_b2.summarize_contacts(&cx_b2); + assert_eq!(contacts_b2.current, &["user_a", "user_b"]); + 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_eq!(contacts_c.current, &["user_c"]); + assert!(contacts_c.outgoing_requests.is_empty()); + let contacts_c2 = client_c2.summarize_contacts(&cx_c2); + assert_eq!(contacts_c2.current, &["user_c"]); + 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_a", "user_b"] + ); + assert_eq!( + client_b.summarize_contacts(&cx_b).current, + &["user_a", "user_b"] + ); + assert!(client_b + .summarize_contacts(&cx_b) + .incoming_requests + .is_empty()); + assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]); + 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()).unwrap(); + client.clear_contacts(cx).await; + client + .authenticate_and_connect(false, &cx.to_async()) + .await + .unwrap(); + } + } + #[gpui::test(iterations = 10)] async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); @@ -4863,6 +5399,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4870,7 +5409,6 @@ mod tests { fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "1.txt": "one", "2.txt": "two", "3.txt": "three", @@ -5074,6 +5612,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -5081,7 +5622,6 @@ mod tests { fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "1.txt": "one", "2.txt": "two", "3.txt": "three", @@ -5220,6 +5760,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -5227,7 +5770,6 @@ mod tests { fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "1.txt": "one", "2.txt": "two", "3.txt": "three", @@ -5399,19 +5941,30 @@ mod tests { let host_language_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/_collab", - json!({ - ".zed.toml": r#"collaborators = ["guest-1", "guest-2", "guest-3", "guest-4"]"# - }), - ) - .await; + fs.insert_tree("/_collab", json!({"init": ""})).await; let mut server = TestServer::start(cx.foreground(), cx.background()).await; + let db = server.app_state.db.clone(); + let host_user_id = db.create_user("host", false).await.unwrap(); + for username in ["guest-1", "guest-2", "guest-3", "guest-4"] { + let guest_user_id = db.create_user(username, false).await.unwrap(); + server + .app_state + .db + .send_contact_request(guest_user_id, host_user_id) + .await + .unwrap(); + server + .app_state + .db + .respond_to_contact_request(host_user_id, guest_user_id, true) + .await + .unwrap(); + } + let mut clients = Vec::new(); let mut user_ids = Vec::new(); let mut op_start_signals = Vec::new(); - let files = Arc::new(Mutex::new(Vec::new())); let mut next_entity_id = 100000; let mut host_cx = TestAppContext::new( @@ -5465,7 +6018,7 @@ mod tests { capabilities: lsp::LanguageServer::full_capabilities(), initializer: Some(Box::new({ let rng = rng.clone(); - let files = files.clone(); + let fs = fs.clone(); let project = host_project.downgrade(); move |fake_server: &mut FakeLanguageServer| { fake_server.handle_request::( @@ -5506,13 +6059,13 @@ mod tests { ); fake_server.handle_request::({ - let files = files.clone(); + let fs = fs.clone(); let rng = rng.clone(); move |_, _| { - let files = files.clone(); + let fs = fs.clone(); let rng = rng.clone(); async move { - let files = files.lock(); + let files = fs.files().await; let mut rng = rng.lock(); let count = rng.gen_range::(1..3); let files = (0..count) @@ -5583,7 +6136,6 @@ mod tests { op_start_signals.push(op_start_signal.0); clients.push(host_cx.foreground().spawn(host.simulate_host( host_project, - files, op_start_signal.1, rng.clone(), host_cx, @@ -5621,15 +6173,16 @@ mod tests { if let Some(guest_err) = guest_err { log::error!("{} error - {}", guest.username, guest_err); } - let contacts = server - .store - .read() - .await - .contacts_for_user(guest.current_user_id(&guest_cx)); - assert!(!contacts - .iter() - .flat_map(|contact| &contact.projects) - .any(|project| project.id == host_project_id)); + // TODO + // let contacts = server + // .store + // .read() + // .await + // .contacts_for_user(guest.current_user_id(&guest_cx)); + // assert!(!contacts + // .iter() + // .flat_map(|contact| &contact.projects) + // .any(|project| project.id == host_project_id)); guest .project .as_ref() @@ -5684,8 +6237,8 @@ mod tests { operations += 1; } 20..=29 if clients.len() > 1 => { - log::info!("Removing guest"); let guest_ix = rng.lock().gen_range(1..clients.len()); + log::info!("Removing guest {}", user_ids[guest_ix]); let removed_guest_id = user_ids.remove(guest_ix); let guest = clients.remove(guest_ix); op_start_signals.remove(guest_ix); @@ -5700,22 +6253,23 @@ mod tests { .as_ref() .unwrap() .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); - for user_id in &user_ids { - for contact in server.store.read().await.contacts_for_user(*user_id) { - assert_ne!( - contact.user_id, removed_guest_id.0 as u64, - "removed guest is still a contact of another peer" - ); - for project in contact.projects { - for project_guest_id in project.guests { - assert_ne!( - project_guest_id, removed_guest_id.0 as u64, - "removed guest appears as still participating on a project" - ); - } - } - } - } + // TODO + // for user_id in &user_ids { + // for contact in server.store.read().await.contacts_for_user(*user_id) { + // assert_ne!( + // contact.user_id, removed_guest_id.0 as u64, + // "removed guest is still a contact of another peer" + // ); + // for project in contact.projects { + // for project_guest_id in project.guests { + // assert_ne!( + // project_guest_id, removed_guest_id.0 as u64, + // "removed guest appears as still participating on a project" + // ); + // } + // } + // } + // } log::info!("{} removed", guest.username); available_guests.push(guest.username.clone()); @@ -5890,7 +6444,12 @@ mod tests { }); let http = FakeHttpClient::with_404_response(); - let user_id = self.app_state.db.create_user(name, false).await.unwrap(); + 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(name, false).await.unwrap() + }; let client_name = name.to_string(); let mut client = Client::new(http.clone()); let server = self.server.clone(); @@ -5984,6 +6543,28 @@ mod tests { self.forbid_connections.store(false, SeqCst); } + async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) { + while let Some((client_a, cx_a)) = clients.pop() { + for (client_b, cx_b) in &mut clients { + client_a + .user_store + .update(cx_a, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + client_b + .user_store + .update(*cx_b, |store, cx| { + store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) + }) + .await + .unwrap(); + } + } + } + async fn build_app_state(test_db: &TestDb) -> Arc { Arc::new(AppState { db: test_db.db().clone(), @@ -6043,6 +6624,12 @@ mod tests { } } + struct ContactsSummary { + pub current: Vec, + pub outgoing_requests: Vec, + pub incoming_requests: Vec, + } + impl TestClient { pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { UserId::from_proto( @@ -6058,6 +6645,32 @@ mod tests { while authed_user.next().await.unwrap().is_none() {} } + async fn clear_contacts(&self, cx: &mut TestAppContext) { + self.user_store + .update(cx, |store, _| store.clear_contacts()) + .await; + } + + fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { + self.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(), + }) + } + async fn build_local_project( &mut self, fs: Arc, @@ -6136,7 +6749,6 @@ mod tests { async fn simulate_host( mut self, project: ModelHandle, - files: Arc>>, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, @@ -6144,7 +6756,6 @@ mod tests { async fn simulate_host_internal( client: &mut TestClient, project: ModelHandle, - files: Arc>>, mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, cx: &mut TestAppContext, @@ -6153,9 +6764,10 @@ mod tests { while op_start_signal.next().await.is_some() { let distribution = rng.lock().gen_range::(0..100); + let files = fs.as_fake().files().await; match distribution { - 0..=20 if !files.lock().is_empty() => { - let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + 0..=20 if !files.is_empty() => { + let path = files.choose(&mut *rng.lock()).unwrap(); let mut path = path.as_path(); while let Some(parent_path) = path.parent() { path = parent_path; @@ -6174,9 +6786,9 @@ mod tests { find_or_create_worktree.await?; } } - 10..=80 if !files.lock().is_empty() => { + 10..=80 if !files.is_empty() => { let buffer = if client.buffers.is_empty() || rng.lock().gen() { - let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let file = files.choose(&mut *rng.lock()).unwrap(); let (worktree, path) = project .update(cx, |project, cx| { project.find_or_create_local_worktree( @@ -6250,7 +6862,6 @@ mod tests { if fs.create_dir(&parent_path).await.is_ok() && fs.create_file(&path, Default::default()).await.is_ok() { - files.lock().push(path); break; } else { log::info!("Host: cannot create file"); @@ -6264,15 +6875,9 @@ mod tests { Ok(()) } - let result = simulate_host_internal( - &mut self, - project.clone(), - files, - op_start_signal, - rng, - &mut cx, - ) - .await; + let result = + simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx) + .await; log::info!("Host done"); self.project = Some(project); (self, cx, result.err()) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 4737dd2c804ded463841948413d404d09d0858c0..8ca270622832b4fc23151508d3df4a4fa9f013c8 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1,4 +1,4 @@ -use crate::db::{ChannelId, UserId}; +use crate::db::{self, ChannelId, UserId}; use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, HashSet}; use rpc::{proto, ConnectionId}; @@ -10,7 +10,6 @@ pub struct Store { connections: HashMap, connections_by_user_id: HashMap>, projects: HashMap, - visible_projects_by_user_id: HashMap>, channels: HashMap, next_project_id: u64, } @@ -30,7 +29,6 @@ pub struct Project { } pub struct Worktree { - pub authorized_user_ids: Vec, pub root_name: String, pub visible: bool, } @@ -58,6 +56,7 @@ pub type ReplicaId = u16; #[derive(Default)] pub struct RemovedConnectionState { + pub user_id: UserId, pub hosted_projects: HashMap, pub guest_project_ids: HashMap>, pub contact_ids: HashSet, @@ -68,18 +67,16 @@ pub struct JoinedProject<'a> { pub project: &'a Project, } -pub struct SharedProject { - pub authorized_user_ids: Vec, -} +pub struct SharedProject {} pub struct UnsharedProject { pub connection_ids: Vec, - pub authorized_user_ids: Vec, + pub host_user_id: UserId, } pub struct LeftProject { pub connection_ids: Vec, - pub authorized_user_ids: Vec, + pub host_user_id: UserId, } #[derive(Copy, Clone)] @@ -151,15 +148,14 @@ impl Store { } let mut result = RemovedConnectionState::default(); + result.user_id = connection.user_id; for project_id in connection.projects.clone() { if let Ok(project) = self.unregister_project(project_id, connection_id) { - result.contact_ids.extend(project.authorized_user_ids()); result.hosted_projects.insert(project_id, project); } else if let Ok(project) = self.leave_project(connection_id, project_id) { result .guest_project_ids .insert(project_id, project.connection_ids); - result.contact_ids.extend(project.authorized_user_ids); } } @@ -213,51 +209,123 @@ impl Store { .copied() } - pub fn contacts_for_user(&self, user_id: UserId) -> Vec { - let mut contacts = HashMap::default(); - for project_id in self - .visible_projects_by_user_id + pub fn is_user_online(&self, user_id: UserId) -> bool { + !self + .connections_by_user_id .get(&user_id) - .unwrap_or(&HashSet::default()) - { - let project = &self.projects[project_id]; + .unwrap_or(&Default::default()) + .is_empty() + } - let mut guests = HashSet::default(); - if let Ok(share) = project.share() { - for guest_connection_id in share.guests.keys() { - if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) { - guests.insert(user_id.to_proto()); - } - } - } + pub fn build_initial_contacts_update(&self, contacts: db::Contacts) -> proto::UpdateContacts { + let mut update = proto::UpdateContacts::default(); + for user_id in contacts.current { + update.contacts.push(self.contact_for_user(user_id)); + } - if let Ok(host_user_id) = self.user_id_for_connection(project.host_connection_id) { - let mut worktree_root_names = project - .worktrees - .values() - .filter(|worktree| worktree.visible) - .map(|worktree| worktree.root_name.clone()) - .collect::>(); - worktree_root_names.sort_unstable(); - contacts - .entry(host_user_id) - .or_insert_with(|| proto::Contact { - user_id: host_user_id.to_proto(), - projects: Vec::new(), - }) - .projects - .push(proto::ProjectMetadata { - id: *project_id, - worktree_root_names, - is_shared: project.share.is_some(), - guests: guests.into_iter().collect(), - }); - } + for request in contacts.incoming_requests { + update + .incoming_requests + .push(proto::IncomingContactRequest { + requester_id: request.requester_id.to_proto(), + should_notify: request.should_notify, + }) + } + + for requested_user_id in contacts.outgoing_requests { + update.outgoing_requests.push(requested_user_id.to_proto()) } - contacts.into_values().collect() + update } + pub fn contact_for_user(&self, user_id: UserId) -> proto::Contact { + proto::Contact { + user_id: user_id.to_proto(), + projects: self.project_metadata_for_user(user_id), + online: self.is_user_online(user_id), + } + } + + pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec { + let connection_ids = self.connections_by_user_id.get(&user_id); + let project_ids = connection_ids.iter().flat_map(|connection_ids| { + connection_ids + .iter() + .filter_map(|connection_id| self.connections.get(connection_id)) + .flat_map(|connection| connection.projects.iter().copied()) + }); + + let mut metadata = Vec::new(); + for project_id in project_ids { + if let Some(project) = self.projects.get(&project_id) { + metadata.push(proto::ProjectMetadata { + id: project_id, + is_shared: project.share.is_some(), + worktree_root_names: project + .worktrees + .values() + .map(|worktree| worktree.root_name.clone()) + .collect(), + guests: project + .share + .iter() + .flat_map(|share| { + share.guests.values().map(|(_, user_id)| user_id.to_proto()) + }) + .collect(), + }); + } + } + + metadata + } + + // pub fn contacts_for_user(&self, user_id: UserId) -> Vec { + // let mut contacts = HashMap::default(); + // for project_id in self + // .visible_projects_by_user_id + // .get(&user_id) + // .unwrap_or(&HashSet::default()) + // { + // let project = &self.projects[project_id]; + + // let mut guests = HashSet::default(); + // if let Ok(share) = project.share() { + // for guest_connection_id in share.guests.keys() { + // if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) { + // guests.insert(user_id.to_proto()); + // } + // } + // } + + // if let Ok(host_user_id) = self.user_id_for_connection(project.host_connection_id) { + // let mut worktree_root_names = project + // .worktrees + // .values() + // .filter(|worktree| worktree.visible) + // .map(|worktree| worktree.root_name.clone()) + // .collect::>(); + // worktree_root_names.sort_unstable(); + // contacts + // .entry(host_user_id) + // .or_insert_with(|| proto::Contact { + // user_id: host_user_id.to_proto(), + // projects: Vec::new(), + // }) + // .projects + // .push(proto::ProjectMetadata { + // id: *project_id, + // worktree_root_names, + // is_shared: project.share.is_some(), + // guests: guests.into_iter().collect(), + // }); + // } + // } + + // contacts.into_values().collect() + // } + pub fn register_project( &mut self, host_connection_id: ConnectionId, @@ -293,13 +361,6 @@ impl Store { .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { - for authorized_user_id in &worktree.authorized_user_ids { - self.visible_projects_by_user_id - .entry(*authorized_user_id) - .or_default() - .insert(project_id); - } - project.worktrees.insert(worktree_id, worktree); if let Ok(share) = project.share_mut() { share.worktrees.insert(worktree_id, Default::default()); @@ -319,14 +380,6 @@ impl Store { match self.projects.entry(project_id) { hash_map::Entry::Occupied(e) => { if e.get().host_connection_id == connection_id { - for user_id in e.get().authorized_user_ids() { - if let hash_map::Entry::Occupied(mut projects) = - self.visible_projects_by_user_id.entry(user_id) - { - projects.get_mut().remove(&project_id); - } - } - let project = e.remove(); if let Some(host_connection) = self.connections.get_mut(&connection_id) { @@ -375,16 +428,6 @@ impl Store { share.worktrees.remove(&worktree_id); } - for authorized_user_id in &worktree.authorized_user_ids { - if let Some(visible_projects) = - self.visible_projects_by_user_id.get_mut(authorized_user_id) - { - if !project.has_authorized_user_id(*authorized_user_id) { - visible_projects.remove(&project_id); - } - } - } - Ok((worktree, guest_connection_ids)) } @@ -400,9 +443,7 @@ impl Store { share.worktrees.insert(*worktree_id, Default::default()); } project.share = Some(share); - return Ok(SharedProject { - authorized_user_ids: project.authorized_user_ids(), - }); + return Ok(SharedProject {}); } } Err(anyhow!("no such project"))? @@ -424,7 +465,6 @@ impl Store { } let connection_ids = project.connection_ids(); - let authorized_user_ids = project.authorized_user_ids(); if let Some(share) = project.share.take() { for connection_id in share.guests.into_keys() { if let Some(connection) = self.connections.get_mut(&connection_id) { @@ -434,7 +474,7 @@ impl Store { Ok(UnsharedProject { connection_ids, - authorized_user_ids, + host_user_id: project.host_user_id, }) } else { Err(anyhow!("project is not shared"))? @@ -498,13 +538,6 @@ impl Store { let project = self .projects .get_mut(&project_id) - .and_then(|project| { - if project.has_authorized_user_id(user_id) { - Some(project) - } else { - None - } - }) .ok_or_else(|| anyhow!("no such project"))?; let share = project.share_mut()?; @@ -546,12 +579,9 @@ impl Store { connection.projects.remove(&project_id); } - let connection_ids = project.connection_ids(); - let authorized_user_ids = project.authorized_user_ids(); - Ok(LeftProject { - connection_ids, - authorized_user_ids, + connection_ids: project.connection_ids(), + host_user_id: project.host_user_id, }) } @@ -599,9 +629,10 @@ impl Store { .connection_ids()) } - #[cfg(test)] - pub fn project(&self, project_id: u64) -> Option<&Project> { - self.projects.get(&project_id) + pub fn project(&self, project_id: u64) -> Result<&Project> { + self.projects + .get(&project_id) + .ok_or_else(|| anyhow!("no such project")) } pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Result<&Project> { @@ -701,14 +732,6 @@ impl Store { let host_connection = self.connections.get(&project.host_connection_id).unwrap(); assert!(host_connection.projects.contains(project_id)); - for authorized_user_ids in project.authorized_user_ids() { - let visible_project_ids = self - .visible_projects_by_user_id - .get(&authorized_user_ids) - .unwrap(); - assert!(visible_project_ids.contains(project_id)); - } - if let Some(share) = &project.share { for guest_connection_id in share.guests.keys() { let guest_connection = self.connections.get(guest_connection_id).unwrap(); @@ -726,13 +749,6 @@ impl Store { } } - for (user_id, visible_project_ids) in &self.visible_projects_by_user_id { - for project_id in visible_project_ids { - let project = self.projects.get(project_id).unwrap(); - assert!(project.authorized_user_ids().contains(user_id)); - } - } - for (channel_id, channel) in &self.channels { for connection_id in &channel.connection_ids { let connection = self.connections.get(connection_id).unwrap(); @@ -743,24 +759,6 @@ impl Store { } impl Project { - pub fn has_authorized_user_id(&self, user_id: UserId) -> bool { - self.worktrees - .values() - .any(|worktree| worktree.authorized_user_ids.contains(&user_id)) - } - - pub fn authorized_user_ids(&self) -> Vec { - let mut ids = self - .worktrees - .values() - .flat_map(|worktree| worktree.authorized_user_ids.iter()) - .copied() - .collect::>(); - ids.sort_unstable(); - ids.dedup(); - ids - } - pub fn guest_connection_ids(&self) -> Vec { if let Some(share) = &self.share { share.guests.keys().copied().collect() diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 6a4dbf653dbb1751ca1a41303cf2fec08fb6f09e..619bcad3385255a232103efad30b954170e3f0da 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -9,8 +9,15 @@ doctest = false [dependencies] client = { path = "../client" } +editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } settings = { path = "../settings" } theme = { path = "../theme" } +util = { path = "../util" } workspace = { path = "../workspace" } +futures = "0.3" +log = "0.4" postage = { version = "0.4.1", features = ["futures-traits"] } +serde = { version = "1", features = ["derive"] } diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a480911d4a5ae3f6f2855da23bffdba29bf7216 --- /dev/null +++ b/crates/contacts_panel/src/contact_finder.rs @@ -0,0 +1,191 @@ +use client::{ContactRequestStatus, User, UserStore}; +use gpui::{ + actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, + ViewContext, ViewHandle, +}; +use picker::{Picker, PickerDelegate}; +use settings::Settings; +use std::sync::Arc; +use util::TryFutureExt; +use workspace::Workspace; + +use crate::render_icon_button; + +actions!(contact_finder, [Toggle]); + +pub fn init(cx: &mut MutableAppContext) { + Picker::::init(cx); + cx.add_action(ContactFinder::toggle); +} + +pub struct ContactFinder { + picker: ViewHandle>, + potential_contacts: Arc<[Arc]>, + user_store: ModelHandle, + selected_index: usize, +} + +pub enum Event { + Dismissed, +} + +impl Entity for ContactFinder { + type Event = Event; +} + +impl View for ContactFinder { + fn ui_name() -> &'static str { + "ContactFinder" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.picker); + } +} + +impl PickerDelegate for ContactFinder { + fn match_count(&self) -> usize { + self.potential_contacts.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + let search_users = self + .user_store + .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + + cx.spawn(|this, mut cx| async move { + async { + let potential_contacts = search_users.await?; + this.update(&mut cx, |this, cx| { + this.potential_contacts = potential_contacts.into(); + cx.notify(); + }); + Ok(()) + } + .log_err() + .await; + }) + } + + fn confirm(&mut self, cx: &mut ViewContext) { + if let Some(user) = self.potential_contacts.get(self.selected_index) { + let user_store = self.user_store.read(cx); + match user_store.contact_request_status(user) { + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { + self.user_store + .update(cx, |store, cx| store.request_contact(user.id, cx)) + .detach(); + } + ContactRequestStatus::RequestSent => { + self.user_store + .update(cx, |store, cx| store.remove_contact(user.id, cx)) + .detach(); + } + _ => {} + } + } + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + + fn render_match( + &self, + ix: usize, + mouse_state: &MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> ElementBox { + let theme = &cx.global::().theme; + let user = &self.potential_contacts[ix]; + let request_status = self.user_store.read(cx).contact_request_status(&user); + + let icon_path = match request_status { + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { + "icons/accept.svg" + } + ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => { + "icons/reject.svg" + } + }; + let button_style = if self.user_store.read(cx).is_contact_request_pending(&user) { + &theme.contact_finder.disabled_contact_button + } else { + &theme.contact_finder.contact_button + }; + let style = theme.picker.item.style_for(mouse_state, selected); + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_finder.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new(user.github_login.clone(), style.label.clone()) + .contained() + .with_style(theme.contact_finder.contact_username) + .aligned() + .left() + .boxed(), + ) + .with_child( + render_icon_button(button_style, icon_path) + .aligned() + .flex_float() + .boxed(), + ) + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.contact_finder.row_height) + .boxed() + } +} + +impl ContactFinder { + fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + workspace.toggle_modal(cx, |cx, workspace| { + let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx)); + cx.subscribe(&finder, Self::on_event).detach(); + finder + }); + } + + pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + let this = cx.weak_handle(); + Self { + picker: cx.add_view(|cx| Picker::new(this, cx)), + potential_contacts: Arc::from([]), + user_store, + selected_index: 0, + } + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } +} diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 171b4194960fc54f7c61b5d50e5e46d2a4b69c81..5d96a1b0c20f351c659e83256eb30610cde9ba10 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,63 +1,149 @@ -use client::{Contact, UserStore}; +mod contact_finder; + +use client::{Contact, User, UserStore}; +use editor::{Cancel, Editor}; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, + impl_actions, platform::CursorStyle, - Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View, - ViewContext, + Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, + Subscription, View, ViewContext, ViewHandle, }; +use serde::Deserialize; use settings::Settings; use std::sync::Arc; +use theme::IconButton; use workspace::{AppState, JoinProject}; +impl_actions!( + contacts_panel, + [RequestContact, RemoveContact, RespondToContactRequest] +); + +#[derive(Debug)] +enum ContactEntry { + Header(&'static str), + IncomingRequest(Arc), + OutgoingRequest(Arc), + Contact(Arc), +} + pub struct ContactsPanel { - contacts: ListState, + entries: Vec, + match_candidates: Vec, + list_state: ListState, user_store: ModelHandle, + filter_editor: ViewHandle, _maintain_contacts: Subscription, } +#[derive(Clone, Deserialize)] +pub struct RequestContact(pub u64); + +#[derive(Clone, Deserialize)] +pub struct RemoveContact(pub u64); + +#[derive(Clone, Deserialize)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + +pub fn init(cx: &mut MutableAppContext) { + contact_finder::init(cx); + cx.add_action(ContactsPanel::request_contact); + cx.add_action(ContactsPanel::remove_contact); + cx.add_action(ContactsPanel::respond_to_contact_request); + cx.add_action(ContactsPanel::clear_filter); +} + impl ContactsPanel { pub fn new(app_state: Arc, cx: &mut ViewContext) -> Self { - Self { - contacts: ListState::new( - app_state.user_store.read(cx).contacts().len(), - Orientation::Top, - 1000., - { - let app_state = app_state.clone(); - move |ix, cx| { - let user_store = app_state.user_store.read(cx); - let contacts = user_store.contacts().clone(); - let current_user_id = user_store.current_user().map(|user| user.id); - Self::render_collaborator( - &contacts[ix], + let user_query_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(|theme| theme.contacts_panel.user_query_editor.clone()), + cx, + ); + editor.set_placeholder_text("Filter contacts", cx); + editor + }); + + cx.subscribe(&user_query_editor, |this, _, event, cx| { + if let editor::Event::BufferEdited = event { + this.update_entries(cx) + } + }) + .detach(); + + let mut this = Self { + list_state: ListState::new(0, Orientation::Top, 1000., { + let this = cx.weak_handle(); + let app_state = app_state.clone(); + move |ix, cx| { + let this = this.upgrade(cx).unwrap(); + let this = this.read(cx); + let theme = cx.global::().theme.clone(); + let theme = &theme.contacts_panel; + let current_user_id = + this.user_store.read(cx).current_user().map(|user| user.id); + + match &this.entries[ix] { + ContactEntry::Header(text) => { + Label::new(text.to_string(), theme.header.text.clone()) + .contained() + .aligned() + .left() + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(theme.header.container) + .boxed() + } + ContactEntry::IncomingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + theme, + true, + cx, + ), + ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + theme, + false, + cx, + ), + ContactEntry::Contact(contact) => Self::render_contact( + contact.clone(), current_user_id, app_state.clone(), + theme, cx, - ) + ), } - }, - ), - _maintain_contacts: cx.observe(&app_state.user_store, Self::update_contacts), + } + }), + entries: Default::default(), + match_candidates: Default::default(), + filter_editor: user_query_editor, + _maintain_contacts: cx + .observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)), user_store: app_state.user_store.clone(), - } + }; + this.update_entries(cx); + this } - fn update_contacts(&mut self, _: ModelHandle, cx: &mut ViewContext) { - self.contacts - .reset(self.user_store.read(cx).contacts().len()); - cx.notify(); - } - - fn render_collaborator( - collaborator: &Contact, + fn render_contact( + contact: Arc, current_user_id: Option, app_state: Arc, + theme: &theme::ContactsPanel, cx: &mut LayoutContext, ) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contacts_panel; - let project_count = collaborator.projects.len(); + let project_count = contact.non_empty_projects().count(); let font_cache = cx.font_cache(); let line_height = theme.unshared_project.name.text.line_height(font_cache); let cap_height = theme.unshared_project.name.text.cap_height(font_cache); @@ -66,44 +152,42 @@ impl ContactsPanel { let tree_branch_width = theme.tree_branch_width; let tree_branch_color = theme.tree_branch_color; let host_avatar_height = theme - .host_avatar + .contact_avatar .width - .or(theme.host_avatar.height) + .or(theme.contact_avatar.height) .unwrap_or(0.); Flex::column() .with_child( Flex::row() - .with_children(collaborator.user.avatar.clone().map(|avatar| { + .with_children(contact.user.avatar.clone().map(|avatar| { Image::new(avatar) - .with_style(theme.host_avatar) + .with_style(theme.contact_avatar) .aligned() .left() .boxed() })) .with_child( Label::new( - collaborator.user.github_login.clone(), - theme.host_username.text.clone(), + contact.user.github_login.clone(), + theme.contact_username.text.clone(), ) .contained() - .with_style(theme.host_username.container) + .with_style(theme.contact_username.container) .aligned() .left() .boxed(), ) .constrained() - .with_height(theme.host_row_height) + .with_height(theme.row_height) .boxed(), ) .with_children( - collaborator - .projects - .iter() + contact + .non_empty_projects() .enumerate() .map(|(ix, project)| { let project_id = project.id; - Flex::row() .with_child( Canvas::new(move |bounds, _, cx| { @@ -145,7 +229,7 @@ impl ContactsPanel { .boxed(), ) .with_child({ - let is_host = Some(collaborator.user.id) == current_user_id; + let is_host = Some(contact.user.id) == current_user_id; let is_guest = !is_host && project .guests @@ -199,7 +283,7 @@ impl ContactsPanel { .boxed() }, ) - .with_cursor_style(if is_host || is_shared { + .with_cursor_style(if !is_host && is_shared { CursorStyle::PointingHand } else { CursorStyle::Arrow @@ -220,8 +304,281 @@ impl ContactsPanel { .boxed() }), ) + .contained() + .with_style(theme.row.clone()) + .boxed() + } + + fn render_contact_request( + user: Arc, + user_store: ModelHandle, + theme: &theme::ContactsPanel, + is_incoming: bool, + cx: &mut LayoutContext, + ) -> ElementBox { + enum Reject {} + enum Accept {} + enum Cancel {} + + let mut row = Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .boxed(), + ); + + let user_id = user.id; + let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); + + if is_incoming { + row.add_children([ + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/reject.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }) + }) + .flex_float() + .boxed(), + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/accept.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }) + }) + .boxed(), + ]); + } else { + row.add_child( + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/reject.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) + .flex_float() + .boxed(), + ); + } + + row.constrained() + .with_height(theme.row_height) + .contained() + .with_style(theme.row) .boxed() } + + fn update_entries(&mut self, cx: &mut ViewContext) { + let user_store = self.user_store.read(cx); + let query = self.filter_editor.read(cx).text(cx); + let executor = cx.background().clone(); + + self.entries.clear(); + + let mut request_entries = Vec::new(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + request_entries.extend( + matches.iter().map(|mat| { + ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone()) + }), + ); + } + } + + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + request_entries.extend( + matches.iter().map(|mat| { + ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone()) + }), + ); + } + } + + if !request_entries.is_empty() { + self.entries.push(ContactEntry::Header("Requests")); + self.entries.append(&mut request_entries); + } + + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + + let (online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + + if !online_contacts.is_empty() { + self.entries.push(ContactEntry::Header("Online")); + self.entries.extend( + online_contacts + .into_iter() + .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), + ); + } + + if !offline_contacts.is_empty() { + self.entries.push(ContactEntry::Header("Offline")); + self.entries.extend( + offline_contacts + .into_iter() + .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), + ); + } + } + + self.list_state.reset(self.entries.len()); + cx.notify(); + } + + fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext) { + self.user_store + .update(cx, |store, cx| store.request_contact(request.0, cx)) + .detach(); + } + + fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { + self.user_store + .update(cx, |store, cx| store.remove_contact(request.0, cx)) + .detach(); + } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } + + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.filter_editor + .update(cx, |editor, cx| editor.set_text("", cx)); + } +} + +fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { + Svg::new(svg_path) + .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) } pub enum Event {} @@ -236,9 +593,44 @@ impl View for ContactsPanel { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.contacts_panel; - Container::new(List::new(self.contacts.clone()).boxed()) - .with_style(theme.container) - .boxed() + enum AddContact {} + + let theme = cx.global::().theme.clone(); + let theme = &theme.contacts_panel; + Container::new( + Flex::column() + .with_child( + Flex::row() + .with_child( + ChildView::new(self.filter_editor.clone()) + .contained() + .with_style(theme.user_query_editor.container) + .flex(1., true) + .boxed(), + ) + .with_child( + MouseEventHandler::new::(0, cx, |_, _| { + Svg::new("icons/add-contact.svg") + .with_color(theme.add_contact_button.color) + .constrained() + .with_height(12.) + .contained() + .with_style(theme.add_contact_button.container) + .aligned() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(|_, cx| cx.dispatch_action(contact_finder::Toggle)) + .boxed(), + ) + .constrained() + .with_height(theme.user_query_editor_height) + .boxed(), + ) + .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) + .boxed(), + ) + .with_style(theme.container) + .boxed() } } diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index 7458f27c91521e24f7ff68478deba1c592659b66..f6abb22ddc4312f22f8d68013dab4082c7cea0c9 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -185,6 +185,18 @@ pub async fn match_strings( return Default::default(); } + if query.is_empty() { + return candidates + .iter() + .map(|candidate| StringMatch { + candidate_id: candidate.id, + score: 0., + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect(); + } + let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); @@ -195,7 +207,7 @@ pub async fn match_strings( let num_cpus = background.num_cpus().min(candidates.len()); let segment_size = (candidates.len() + num_cpus - 1) / num_cpus; let mut segment_results = (0..num_cpus) - .map(|_| Vec::with_capacity(max_results)) + .map(|_| Vec::with_capacity(max_results.min(candidates.len()))) .collect::>(); background diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 4f930dfb46679cacc7d627374cf7ed0dd3dc1cd6..3f42f984075a16589de9256aa5f414a97bf7bf05 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -117,14 +117,15 @@ impl Element for Flex { ) -> (Vector2F, Self::LayoutState) { let mut total_flex = None; let mut fixed_space = 0.0; + let mut contains_float = false; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; for child in &mut self.children { - if let Some(flex) = child - .metadata::() - .and_then(|metadata| metadata.flex.map(|(flex, _)| flex)) - { + let metadata = child.metadata::(); + contains_float |= metadata.map_or(false, |metadata| metadata.float); + + if let Some(flex) = metadata.and_then(|metadata| metadata.flex.map(|(flex, _)| flex)) { *total_flex.get_or_insert(0.) += flex; } else { let child_constraint = match self.axis { @@ -177,6 +178,13 @@ impl Element for Flex { } }; + if contains_float { + match self.axis { + Axis::Horizontal => size.set_x(size.x().max(constraint.max.x())), + Axis::Vertical => size.set_y(size.y().max(constraint.max.y())), + } + } + if constraint.min.x().is_finite() { size.set_x(size.x().max(constraint.min.x())); } @@ -225,7 +233,9 @@ impl Element for Flex { remaining_space: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - let overflowing = *remaining_space < 0.; + let mut remaining_space = *remaining_space; + + let overflowing = remaining_space < 0.; if overflowing { cx.scene.push_layer(Some(bounds)); } @@ -240,14 +250,14 @@ impl Element for Flex { } for child in &mut self.children { - if *remaining_space > 0. { + if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { match self.axis { - Axis::Horizontal => child_origin += vec2f(*remaining_space, 0.0), - Axis::Vertical => child_origin += vec2f(0.0, *remaining_space), + Axis::Horizontal => child_origin += vec2f(remaining_space, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, remaining_space), } - *remaining_space = 0.; + remaining_space = 0.; } } } @@ -257,6 +267,7 @@ impl Element for Flex { Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), } } + if overflowing { cx.scene.pop_layer(); } diff --git a/crates/gpui/src/platform/mac/atlas.rs b/crates/gpui/src/platform/mac/atlas.rs index a7a4de10006a7118469346e94e67898da1a8d0ca..a529513ef5ef38faab25cf983e4820066ce9d28b 100644 --- a/crates/gpui/src/platform/mac/atlas.rs +++ b/crates/gpui/src/platform/mac/atlas.rs @@ -12,10 +12,10 @@ pub struct AtlasAllocator { device: Device, texture_descriptor: TextureDescriptor, atlases: Vec, - free_atlases: Vec, + last_used_atlas_id: usize, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct AllocId { pub atlas_id: usize, alloc_id: etagere::AllocId, @@ -23,15 +23,15 @@ pub struct AllocId { impl AtlasAllocator { pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self { - let mut me = Self { + let mut this = Self { device, texture_descriptor, - atlases: Vec::new(), - free_atlases: Vec::new(), + atlases: vec![], + last_used_atlas_id: 0, }; - let atlas = me.new_atlas(Vector2I::zero()); - me.atlases.push(atlas); - me + let atlas = this.new_atlas(Vector2I::zero()); + this.atlases.push(atlas); + this } pub fn default_atlas_size(&self) -> Vector2I { @@ -42,17 +42,27 @@ impl AtlasAllocator { } pub fn allocate(&mut self, requested_size: Vector2I) -> Option<(AllocId, Vector2I)> { - let allocation = self - .atlases - .last_mut() - .unwrap() + let atlas_id = self.last_used_atlas_id; + if let Some((alloc_id, origin)) = self.atlases[atlas_id].allocate(requested_size) { + return Some((AllocId { atlas_id, alloc_id }, origin)); + } + + for (atlas_id, atlas) in self.atlases.iter_mut().enumerate() { + if atlas_id == self.last_used_atlas_id { + continue; + } + if let Some((alloc_id, origin)) = atlas.allocate(requested_size) { + self.last_used_atlas_id = atlas_id; + return Some((AllocId { atlas_id, alloc_id }, origin)); + } + } + + let atlas_id = self.atlases.len(); + let mut atlas = self.new_atlas(requested_size); + let allocation = atlas .allocate(requested_size) - .or_else(|| { - let mut atlas = self.new_atlas(requested_size); - let (id, origin) = atlas.allocate(requested_size)?; - self.atlases.push(atlas); - Some((id, origin)) - }); + .map(|(alloc_id, origin)| (AllocId { atlas_id, alloc_id }, origin)); + self.atlases.push(atlas); if allocation.is_none() { warn!( @@ -61,13 +71,7 @@ impl AtlasAllocator { ); } - let (alloc_id, origin) = allocation?; - - let id = AllocId { - atlas_id: self.atlases.len() - 1, - alloc_id, - }; - Some((id, origin)) + allocation } pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> Option<(AllocId, RectI)> { @@ -80,9 +84,6 @@ impl AtlasAllocator { pub fn deallocate(&mut self, id: AllocId) { if let Some(atlas) = self.atlases.get_mut(id.atlas_id) { atlas.deallocate(id.alloc_id); - if atlas.is_empty() { - self.free_atlases.push(self.atlases.remove(id.atlas_id)); - } } } @@ -90,7 +91,6 @@ impl AtlasAllocator { for atlas in &mut self.atlases { atlas.clear(); } - self.free_atlases.extend(self.atlases.drain(1..)); } pub fn texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> { @@ -98,28 +98,22 @@ impl AtlasAllocator { } fn new_atlas(&mut self, required_size: Vector2I) -> Atlas { - if let Some(i) = self.free_atlases.iter().rposition(|atlas| { - atlas.size().x() >= required_size.x() && atlas.size().y() >= required_size.y() - }) { - self.free_atlases.remove(i) - } else { - let size = self.default_atlas_size().max(required_size); - let texture = if size.x() as u64 > self.texture_descriptor.width() - || size.y() as u64 > self.texture_descriptor.height() - { - let descriptor = unsafe { - let descriptor_ptr: *mut metal::MTLTextureDescriptor = - msg_send![self.texture_descriptor, copy]; - metal::TextureDescriptor::from_ptr(descriptor_ptr) - }; - descriptor.set_width(size.x() as u64); - descriptor.set_height(size.y() as u64); - self.device.new_texture(&descriptor) - } else { - self.device.new_texture(&self.texture_descriptor) + let size = self.default_atlas_size().max(required_size); + let texture = if size.x() as u64 > self.texture_descriptor.width() + || size.y() as u64 > self.texture_descriptor.height() + { + let descriptor = unsafe { + let descriptor_ptr: *mut metal::MTLTextureDescriptor = + msg_send![self.texture_descriptor, copy]; + metal::TextureDescriptor::from_ptr(descriptor_ptr) }; - Atlas::new(size, texture) - } + descriptor.set_width(size.x() as u64); + descriptor.set_height(size.y() as u64); + self.device.new_texture(&descriptor) + } else { + self.device.new_texture(&self.texture_descriptor) + }; + Atlas::new(size, texture) } } @@ -136,11 +130,6 @@ impl Atlas { } } - fn size(&self) -> Vector2I { - let size = self.allocator.size(); - vec2i(size.width, size.height) - } - fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> { let alloc = self .allocator @@ -177,10 +166,6 @@ impl Atlas { self.allocator.deallocate(id); } - fn is_empty(&self) -> bool { - self.allocator.is_empty() - } - fn clear(&mut self) { self.allocator.clear(); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 14b5a288e1e08f2f9f333b3782bdf1c4b975da42..b9d28c76a1355044b2d12d44f38781f9087b4ad6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -289,10 +289,13 @@ impl LanguageRegistry { let servers_tx = servers_tx.clone(); cx.background() .spawn(async move { - fake_server - .receive_notification::() - .await; - servers_tx.unbounded_send(fake_server).ok(); + if fake_server + .try_receive_notification::() + .await + .is_some() + { + servers_tx.unbounded_send(fake_server).ok(); + } }) .detach(); Ok(server) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 02bc0486f1aaddae3fd5eed037a2461d8b8a139a..d5af202516c4134d02213bcb7841422edcaa1606 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -647,12 +647,18 @@ impl FakeLanguageServer { } pub async fn receive_notification(&mut self) -> T::Params { + self.try_receive_notification::().await.unwrap() + } + + pub async fn try_receive_notification( + &mut self, + ) -> Option { use futures::StreamExt as _; loop { - let (method, params) = self.notifications_rx.next().await.unwrap(); + let (method, params) = self.notifications_rx.next().await?; if &method == T::METHOD { - return serde_json::from_str::(¶ms).unwrap(); + return Some(serde_json::from_str::(¶ms).unwrap()); } else { log::info!("skipping message in fake language server {:?}", params); } diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 912dc65afeae289e4283a44f9d0a6d2bdc281956..7da2a38a83bc2d5a15a1d369024314508ff29f2c 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -376,6 +376,16 @@ impl FakeFs { .boxed() } + pub async fn files(&self) -> Vec { + self.state + .lock() + .await + .entries + .iter() + .filter_map(|(path, entry)| entry.content.as_ref().map(|_| path.clone())) + .collect() + } + async fn simulate_random_delay(&self) { self.executor .upgrade() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1fcd89fcde90bee62b0a3831b4e2df3d491ed956..d23122f45b337d5bc7152fef6cbec9abd3e4623e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -443,7 +443,7 @@ impl Project { .map(|peer| peer.user_id) .collect(); user_store - .update(cx, |user_store, cx| user_store.load_users(user_ids, cx)) + .update(cx, |user_store, cx| user_store.get_users(user_ids, cx)) .await?; let mut collaborators = HashMap::default(); for message in response.collaborators { @@ -6550,7 +6550,7 @@ mod tests { assert!(results.is_empty()); } - #[gpui::test] + #[gpui::test(iterations = 10)] async fn test_definition(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index bab41bbe278e21330b1f695a09feb9375efc105f..84fedbbde7facc527500c5137a8bdf2a8bf6bbe0 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -32,7 +32,6 @@ use postage::{ prelude::{Sink as _, Stream as _}, watch, }; -use serde::Deserialize; use smol::channel::{self, Sender}; use std::{ any::Any, @@ -64,7 +63,6 @@ pub enum Worktree { pub struct LocalWorktree { snapshot: LocalSnapshot, - config: WorktreeConfig, background_snapshot: Arc>, last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, @@ -143,11 +141,6 @@ struct ShareState { _maintain_remote_snapshot: Option>>, } -#[derive(Default, Deserialize)] -struct WorktreeConfig { - collaborators: Vec, -} - pub enum Event { UpdatedEntries, } @@ -460,13 +453,6 @@ impl LocalWorktree { .await .context("failed to stat worktree path")?; - let mut config = WorktreeConfig::default(); - if let Ok(zed_toml) = fs.load(&abs_path.join(".zed.toml")).await { - if let Ok(parsed) = toml::from_str(&zed_toml) { - config = parsed; - } - } - let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning); let tree = cx.add_model(move |cx: &mut ModelContext| { @@ -496,7 +482,6 @@ impl LocalWorktree { let tree = Self { snapshot: snapshot.clone(), - config, background_snapshot: Arc::new(Mutex::new(snapshot)), last_scan_state_rx, _background_scanner_task: None, @@ -544,10 +529,6 @@ impl LocalWorktree { } } - pub fn authorized_logins(&self) -> Vec { - self.config.collaborators.clone() - } - pub(crate) fn load_buffer( &mut self, path: &Path, @@ -879,7 +860,6 @@ impl LocalWorktree { project_id, worktree_id: self.id().to_proto(), root_name: self.root_name().to_string(), - authorized_logins: self.authorized_logins(), visible: self.visible, }; let request = client.request(register_message); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 4f083888d9d04d55e8c35f8ecea3afe1446c9491..157ea8ef7380795d318430bffbd2dc989a3b6596 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -319,15 +319,20 @@ mod tests { .into_iter() .map(|name| StringMatchCandidate::new(0, name.into())) .collect::>(); - let matches = fuzzy::match_strings( - &candidates, - ¶ms.query, - true, - 100, - &Default::default(), - executor.clone(), - ) - .await; + let matches = if params.query.is_empty() { + Vec::new() + } else { + fuzzy::match_strings( + &candidates, + ¶ms.query, + true, + 100, + &Default::default(), + executor.clone(), + ) + .await + }; + Ok(Some( matches.into_iter().map(|mat| symbol(&mat.string)).collect(), )) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fa0b587df486957d65b87c97caaf5b9a3eb5ed8f..8adba5fc80c10dbac3413e803750e086a9ba8563 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -87,12 +87,16 @@ message Envelope { UpdateContacts update_contacts = 75; GetUsers get_users = 76; - GetUsersResponse get_users_response = 77; - - Follow follow = 78; - FollowResponse follow_response = 79; - UpdateFollowers update_followers = 80; - Unfollow unfollow = 81; + FuzzySearchUsers fuzzy_search_users = 77; + UsersResponse users_response = 78; + RequestContact request_contact = 79; + RespondToContactRequest respond_to_contact_request = 80; + RemoveContact remove_contact = 81; + + Follow follow = 82; + FollowResponse follow_response = 83; + UpdateFollowers update_followers = 84; + Unfollow unfollow = 85; } } @@ -147,8 +151,7 @@ message RegisterWorktree { uint64 project_id = 1; uint64 worktree_id = 2; string root_name = 3; - repeated string authorized_logins = 4; - bool visible = 5; + bool visible = 4; } message UnregisterWorktree { @@ -538,10 +541,33 @@ message GetUsers { repeated uint64 user_ids = 1; } -message GetUsersResponse { +message FuzzySearchUsers { + string query = 1; +} + +message UsersResponse { repeated User users = 1; } +message RequestContact { + uint64 responder_id = 1; +} + +message RemoveContact { + uint64 user_id = 1; +} + +message RespondToContactRequest { + uint64 requester_id = 1; + ContactRequestResponse response = 2; +} + +enum ContactRequestResponse { + Accept = 0; + Reject = 1; + Block = 2; +} + message SendChannelMessage { uint64 channel_id = 1; string body = 2; @@ -569,6 +595,16 @@ message GetChannelMessagesResponse { message UpdateContacts { repeated Contact contacts = 1; + repeated uint64 remove_contacts = 2; + repeated IncomingContactRequest incoming_requests = 3; + repeated uint64 remove_incoming_requests = 4; + repeated uint64 outgoing_requests = 5; + repeated uint64 remove_outgoing_requests = 6; +} + +message IncomingContactRequest { + uint64 requester_id = 1; + bool should_notify = 2; } message UpdateDiagnostics { @@ -839,6 +875,7 @@ message ChannelMessage { message Contact { uint64 user_id = 1; repeated ProjectMetadata projects = 2; + bool online = 3; } message ProjectMetadata { diff --git a/crates/rpc/src/macros.rs b/crates/rpc/src/macros.rs new file mode 100644 index 0000000000000000000000000000000000000000..38d35893ee70c34615da14aabb561c417f83170b --- /dev/null +++ b/crates/rpc/src/macros.rs @@ -0,0 +1,67 @@ +#[macro_export] +macro_rules! messages { + ($(($name:ident, $priority:ident)),* $(,)?) => { + pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { + match envelope.payload { + $(Some(envelope::Payload::$name(payload)) => { + Some(Box::new(TypedEnvelope { + sender_id, + original_sender_id: envelope.original_sender_id.map(PeerId), + message_id: envelope.id, + payload, + })) + }, )* + _ => None + } + } + + $( + impl EnvelopedMessage for $name { + const NAME: &'static str = std::stringify!($name); + const PRIORITY: MessagePriority = MessagePriority::$priority; + + fn into_envelope( + self, + id: u32, + responding_to: Option, + original_sender_id: Option, + ) -> Envelope { + Envelope { + id, + responding_to, + original_sender_id, + payload: Some(envelope::Payload::$name(self)), + } + } + + fn from_envelope(envelope: Envelope) -> Option { + if let Some(envelope::Payload::$name(msg)) = envelope.payload { + Some(msg) + } else { + None + } + } + } + )* + }; +} + +#[macro_export] +macro_rules! request_messages { + ($(($request_name:ident, $response_name:ident)),* $(,)?) => { + $(impl RequestMessage for $request_name { + type Response = $response_name; + })* + }; +} + +#[macro_export] +macro_rules! entity_messages { + ($id_field:ident, $($name:ident),* $(,)?) => { + $(impl EntityMessage for $name { + fn remote_entity_id(&self) -> u64 { + self.$id_field + } + })* + }; +} diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index a2b88f795c69396770e1775473aef68d3e61f339..7d7d1c719495260845391f265770fdde3d76e6a3 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -173,7 +173,10 @@ impl Peer { Err(anyhow!("timed out writing message"))?; } } - None => return Ok(()), + None => { + log::info!("outgoing channel closed"); + return Ok(()) + }, }, incoming = read_message => { let incoming = incoming.context("received invalid RPC message")?; @@ -181,7 +184,10 @@ impl Peer { if let proto::Message::Envelope(incoming) = incoming { match incoming_tx.send(incoming).timeout(RECEIVE_TIMEOUT).await { Some(Ok(_)) => {}, - Some(Err(_)) => return Ok(()), + Some(Err(_)) => { + log::info!("incoming channel closed"); + return Ok(()) + }, None => Err(anyhow!("timed out processing incoming message"))?, } } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c505869c554744e1c89911b0a9c5f556ac3dd8b0..0b7ba21c4a22419d1f13ba909bbaeab7e24c512b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -1,4 +1,4 @@ -use super::{ConnectionId, PeerId, TypedEnvelope}; +use super::{entity_messages, messages, request_messages, ConnectionId, PeerId, TypedEnvelope}; use anyhow::{anyhow, Result}; use async_tungstenite::tungstenite::Message as WebSocketMessage; use futures::{SinkExt as _, StreamExt as _}; @@ -73,71 +73,6 @@ impl AnyTypedEnvelope for TypedEnvelope { } } -macro_rules! messages { - ($(($name:ident, $priority:ident)),* $(,)?) => { - pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { - match envelope.payload { - $(Some(envelope::Payload::$name(payload)) => { - Some(Box::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id.map(PeerId), - message_id: envelope.id, - payload, - })) - }, )* - _ => None - } - } - - $( - impl EnvelopedMessage for $name { - const NAME: &'static str = std::stringify!($name); - const PRIORITY: MessagePriority = MessagePriority::$priority; - - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope { - Envelope { - id, - responding_to, - original_sender_id, - payload: Some(envelope::Payload::$name(self)), - } - } - - fn from_envelope(envelope: Envelope) -> Option { - if let Some(envelope::Payload::$name(msg)) = envelope.payload { - Some(msg) - } else { - None - } - } - } - )* - }; -} - -macro_rules! request_messages { - ($(($request_name:ident, $response_name:ident)),* $(,)?) => { - $(impl RequestMessage for $request_name { - type Response = $response_name; - })* - }; -} - -macro_rules! entity_messages { - ($id_field:ident, $($name:ident),* $(,)?) => { - $(impl EntityMessage for $name { - fn remote_entity_id(&self) -> u64 { - self.$id_field - } - })* - }; -} - messages!( (Ack, Foreground), (AddProjectCollaborator, Foreground), @@ -147,6 +82,7 @@ messages!( (ApplyCompletionAdditionalEditsResponse, Background), (BufferReloaded, Foreground), (BufferSaved, Foreground), + (RemoveContact, Foreground), (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), (DeleteProjectEntry, Foreground), @@ -155,6 +91,7 @@ messages!( (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), + (FuzzySearchUsers, Foreground), (GetChannelMessages, Foreground), (GetChannelMessagesResponse, Foreground), (GetChannels, Foreground), @@ -172,7 +109,7 @@ messages!( (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), (GetUsers, Foreground), - (GetUsersResponse, Foreground), + (UsersResponse, Foreground), (JoinChannel, Foreground), (JoinChannelResponse, Foreground), (JoinProject, Foreground), @@ -197,6 +134,8 @@ messages!( (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), (RenameProjectEntry, Foreground), + (RequestContact, Foreground), + (RespondToContactRequest, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), @@ -236,7 +175,8 @@ request_messages!( (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetReferences, GetReferencesResponse), (GetProjectSymbols, GetProjectSymbolsResponse), - (GetUsers, GetUsersResponse), + (FuzzySearchUsers, UsersResponse), + (GetUsers, UsersResponse), (JoinChannel, JoinChannelResponse), (JoinProject, JoinProjectResponse), (OpenBufferById, OpenBufferResponse), @@ -248,6 +188,9 @@ request_messages!( (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (ReloadBuffers, ReloadBuffersResponse), + (RequestContact, Ack), + (RemoveContact, Ack), + (RespondToContactRequest, Ack), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index ffddcb9cd3ce1ac232b8eacd6b4ebeb08580c1b4..f21a0ba76e4d27cc82066ed4e5faa837789fcb15 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -4,5 +4,6 @@ mod peer; pub mod proto; pub use conn::Connection; pub use peer::*; +mod macros; pub const PROTOCOL_VERSION: u32 = 16; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d64c093144d752c1cb6a88105c3bd7cfea18be7c..72db11c4931436ad6bf90650561d3baa5fe1eef1 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -21,6 +21,7 @@ pub struct Theme { pub workspace: Workspace, pub chat_panel: ChatPanel, pub contacts_panel: ContactsPanel, + pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, pub picker: Picker, @@ -234,19 +235,44 @@ pub struct CommandPalette { pub struct ContactsPanel { #[serde(flatten)] pub container: ContainerStyle, - pub host_row_height: f32, - pub host_avatar: ImageStyle, - pub host_username: ContainedText, + pub header: ContainedText, + pub user_query_editor: FieldEditor, + pub user_query_editor_height: f32, + pub add_contact_button: IconButton, + pub row: ContainerStyle, + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainedText, + pub contact_button: Interactive, + pub disabled_contact_button: IconButton, pub tree_branch_width: f32, pub tree_branch_color: Color, - pub shared_project: WorktreeRow, - pub hovered_shared_project: WorktreeRow, - pub unshared_project: WorktreeRow, - pub hovered_unshared_project: WorktreeRow, + pub shared_project: ProjectRow, + pub hovered_shared_project: ProjectRow, + pub unshared_project: ProjectRow, + pub hovered_unshared_project: ProjectRow, } #[derive(Deserialize, Default)] -pub struct WorktreeRow { +pub struct ContactFinder { + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainerStyle, + pub contact_button: IconButton, + pub disabled_contact_button: IconButton, +} + +#[derive(Deserialize, Default)] +pub struct IconButton { + #[serde(flatten)] + pub container: ContainerStyle, + pub color: Color, + pub icon_width: f32, + pub button_width: f32, +} + +#[derive(Deserialize, Default)] +pub struct ProjectRow { #[serde(flatten)] pub container: ContainerStyle, pub height: f32, diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index bc7314e73286b48ed7f2ca95bb56cd7e93f71de8..c9cbcbb4fb073367c04ca63b9d951310a0ecaed4 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -106,10 +106,12 @@ impl Sidebar { .with_cursor_style(CursorStyle::ResizeLeftRight) .on_drag(move |delta, cx| { let prev_width = *actual_width.borrow(); - match side { - Side::Left => *custom_width.borrow_mut() = 0f32.max(prev_width + delta.x()), - Side::Right => *custom_width.borrow_mut() = 0f32.max(prev_width - delta.x()), - } + *custom_width.borrow_mut() = 0f32 + .max(match side { + Side::Left => prev_width + delta.x(), + Side::Right => prev_width - delta.x(), + }) + .round(); cx.notify(); }) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b23710834dfd38f00edcaa3ae7cf454613b79d6b..d5b0bf2ed54f8c999fa669e72a44dc8d1c7595eb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -824,6 +824,10 @@ impl Workspace { &self.status_bar } + pub fn user_store(&self) -> &ModelHandle { + &self.user_store + } + pub fn project(&self) -> &ModelHandle { &self.project } @@ -931,7 +935,7 @@ impl Workspace { }) } - // Returns the model that was toggled closed if it was open + /// Returns the modal that was toggled closed if it was open. pub fn toggle_modal( &mut self, cx: &mut ViewContext, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 53a0a92a2534ed091cb24638f2fb3e9b0269b86f..a4f85ab9bc7467b108092c01db7a26d5920098d9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -146,6 +146,7 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); + contacts_panel::init(cx); outline::init(cx); project_symbols::init(cx); project_panel::init(cx); diff --git a/script/seed-db b/script/seed-db index c69af799dd9ed32ebbac054584a499db7eededc9..2b12e0f480637ed583c51d516ac831e20125c135 100755 --- a/script/seed-db +++ b/script/seed-db @@ -6,4 +6,4 @@ cd crates/collab # Export contents of .env.toml eval "$(cargo run --bin dotenv)" -cargo run --package=collab --features seed-support --bin seed +cargo run --package=collab --features seed-support --bin seed -- $@ diff --git a/script/zed_with_local_servers b/script/zed-with-local-servers similarity index 100% rename from script/zed_with_local_servers rename to script/zed-with-local-servers diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 1f1dc74c308baa21f566044c9a30c0a38afed68a..0da6ada222d77ba1ec2cdbe7b1446ef76535cf05 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -1,6 +1,7 @@ import Theme from "../themes/theme"; import chatPanel from "./chatPanel"; import { text } from "./components"; +import contactFinder from "./contactFinder"; import contactsPanel from "./contactsPanel"; import commandPalette from "./commandPalette"; import editor from "./editor"; @@ -24,6 +25,7 @@ export default function app(theme: Theme): Object { projectPanel: projectPanel(theme), chatPanel: chatPanel(theme), contactsPanel: contactsPanel(theme), + contactFinder: contactFinder(theme), search: search(theme), breadcrumbs: { ...text(theme, "sans", "secondary"), diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts new file mode 100644 index 0000000000000000000000000000000000000000..853f87ca5e654d8824309158b0e97360e1ebc1fc --- /dev/null +++ b/styles/src/styleTree/contactFinder.ts @@ -0,0 +1,38 @@ +import Theme from "../themes/theme"; +import picker from "./picker"; +import { backgroundColor, iconColor } from "./components"; + +export default function contactFinder(theme: Theme) { + const contactButton = { + background: backgroundColor(theme, 100), + color: iconColor(theme, "primary"), + iconWidth: 8, + buttonWidth: 16, + cornerRadius: 8, + }; + + return { + ...picker(theme), + rowHeight: 28, + contactAvatar: { + cornerRadius: 10, + width: 18, + }, + contactUsername: { + padding: { + left: 8, + }, + }, + contactButton: { + ...contactButton, + hover: { + background: backgroundColor(theme, 100, "hovered") + } + }, + disabledContactButton: { + ...contactButton, + background: backgroundColor(theme, 100), + color: iconColor(theme, "muted"), + }, + } +} diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index e22a09e25f5db3c3ab5fb027b563d834913a3180..3cc0f35c3eb176911699741591042d4f34177293 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -1,8 +1,8 @@ import Theme from "../themes/theme"; import { panel } from "./app"; -import { backgroundColor, borderColor, text } from "./components"; +import { backgroundColor, border, borderColor, iconColor, player, text } from "./components"; -export default function(theme: Theme) { +export default function contactsPanel(theme: Theme) { const project = { guestAvatarSpacing: 4, height: 24, @@ -31,21 +31,68 @@ export default function(theme: Theme) { }, }; + const contactButton = { + background: backgroundColor(theme, 100), + color: iconColor(theme, "primary"), + iconWidth: 8, + buttonWidth: 16, + cornerRadius: 8, + }; + return { ...panel, - hostRowHeight: 28, + userQueryEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + }, + userQueryEditorHeight: 32, + addContactButton: { + margin: { left: 6 }, + color: iconColor(theme, "primary"), + buttonWidth: 8, + iconWidth: 8, + }, + row: { + padding: { left: 8 }, + }, + rowHeight: 28, + header: { + ...text(theme, "mono", "secondary", { size: "sm" }), + margin: { top: 8 }, + }, treeBranchColor: borderColor(theme, "muted"), treeBranchWidth: 1, - hostAvatar: { + contactAvatar: { cornerRadius: 10, width: 18, }, - hostUsername: { + contactUsername: { ...text(theme, "mono", "primary", { size: "sm" }), padding: { left: 8, }, }, + contactButton: { + ...contactButton, + hover: { + background: backgroundColor(theme, 100, "hovered"), + }, + }, + disabledContactButton: { + ...contactButton, + background: backgroundColor(theme, 100), + color: iconColor(theme, "muted"), + }, project, sharedProject, hoveredSharedProject: {