diff --git a/gpui/src/image_data.rs b/gpui/src/image_data.rs index 352393e3b57328f8373134c23edcb9b9ed5790ae..d97820ab51b3f09ad8f2241243837ce1535dc6af 100644 --- a/gpui/src/image_data.rs +++ b/gpui/src/image_data.rs @@ -1,8 +1,11 @@ use crate::geometry::vector::{vec2i, Vector2I}; use image::{Bgra, ImageBuffer}; -use std::sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, +use std::{ + fmt, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, }; pub struct ImageData { @@ -29,3 +32,12 @@ impl ImageData { vec2i(width as i32, height as i32) } } + +impl fmt::Debug for ImageData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageData") + .field("id", &self.id) + .field("size", &self.data.dimensions()) + .finish() + } +} diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 539068f2d3d55e8cb11c085c52c0a84957ce4b6b..14e520d58973f09e9850e4b35dc56f8930a7f91d 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1025,6 +1025,7 @@ mod tests { language::LanguageRegistry, rpc::{self, Client}, settings, + test::FakeHttpClient, user::UserStore, worktree::Worktree, }; @@ -1486,6 +1487,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start().await; + let mut http = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await; let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await; diff --git a/zed/src/channel.rs b/zed/src/channel.rs index bf1237d8359f4e4c857b419fbc663b6182325c60..42727ea54b7d001c0df3f1384950b2c3ff1917ec 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -46,7 +46,7 @@ pub struct Channel { _subscription: rpc::Subscription, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct ChannelMessage { pub id: u64, pub body: String, @@ -495,15 +495,17 @@ impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count { #[cfg(test)] mod tests { use super::*; - use crate::test::FakeServer; + use crate::test::{FakeHttpClient, FakeServer}; use gpui::TestAppContext; + use surf::http::Response; #[gpui::test] async fn test_channel_messages(mut cx: TestAppContext) { let user_id = 5; let mut client = Client::new(); + let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); let server = FakeServer::for_client(user_id, &mut client, &cx).await; - let user_store = UserStore::new(client.clone(), cx.background().as_ref()); + let user_store = UserStore::new(client.clone(), http_client, cx.background().as_ref()); let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx)); channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None)); diff --git a/zed/src/http.rs b/zed/src/http.rs new file mode 100644 index 0000000000000000000000000000000000000000..68f1610c2749c8e374ddd5c8a81beef0281b06f9 --- /dev/null +++ b/zed/src/http.rs @@ -0,0 +1,26 @@ +pub use anyhow::{anyhow, Result}; +use futures::future::BoxFuture; +use std::sync::Arc; +pub use surf::{ + http::{Method, Request, Response as ServerResponse}, + Response, Url, +}; + +pub trait HttpClient: Send + Sync { + fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result>; +} + +pub fn client() -> Arc { + Arc::new(surf::client()) +} + +impl HttpClient for surf::Client { + fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result> { + Box::pin(async move { + Ok(self + .send(req) + .await + .map_err(|e| anyhow!("http request failed: {}", e))?) + }) + } +} diff --git a/zed/src/lib.rs b/zed/src/lib.rs index d672d234bfbcce21a64714f13ba46cd23926fe91..c9cec56f46da05851471cd20f9e4adb9133dc18c 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -5,6 +5,7 @@ pub mod editor; pub mod file_finder; pub mod fs; mod fuzzy; +pub mod http; pub mod language; pub mod menus; pub mod project_browser; diff --git a/zed/src/main.rs b/zed/src/main.rs index f585ed5b82e4afc0bd261f244a38504a58e2c5fb..9e73a961265e7347d652a6d016ece75ea513d3ce 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -13,7 +13,7 @@ use zed::{ channel::ChannelList, chat_panel, editor, file_finder, fs::RealFs, - language, menus, rpc, settings, theme_selector, + http, language, menus, rpc, settings, theme_selector, user::UserStore, workspace::{self, OpenNew, OpenParams, OpenPaths}, AppState, @@ -37,7 +37,8 @@ fn main() { app.run(move |cx| { let rpc = rpc::Client::new(); - let user_store = UserStore::new(rpc.clone(), cx.background()); + let http = http::client(); + let user_store = UserStore::new(rpc.clone(), http.clone(), cx.background()); let app_state = Arc::new(AppState { languages: languages.clone(), settings_tx: Arc::new(Mutex::new(settings_tx)), diff --git a/zed/src/test.rs b/zed/src/test.rs index 019969ee4f75ee1e463c3abbcc3f75950c9ac635..e8527a4ed762a057fbcd72355fa4783024e57f40 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -2,6 +2,7 @@ use crate::{ assets::Assets, channel::ChannelList, fs::RealFs, + http::{HttpClient, Request, Response, ServerResponse}, language::LanguageRegistry, rpc::{self, Client}, settings::{self, ThemeRegistry}, @@ -10,11 +11,13 @@ use crate::{ AppState, }; use anyhow::{anyhow, Result}; +use futures::{future::BoxFuture, Future}; use gpui::{AsyncAppContext, Entity, ModelHandle, MutableAppContext, TestAppContext}; use parking_lot::Mutex; use postage::{mpsc, prelude::Stream as _}; use smol::channel; use std::{ + fmt, marker::PhantomData, path::{Path, PathBuf}, sync::{ @@ -164,7 +167,8 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let languages = Arc::new(LanguageRegistry::new()); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let rpc = rpc::Client::new(); - let user_store = UserStore::new(rpc.clone(), cx.background()); + let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) }); + let user_store = UserStore::new(rpc.clone(), http, cx.background()); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, @@ -313,3 +317,33 @@ impl FakeServer { self.connection_id.lock().expect("not connected") } } + +pub struct FakeHttpClient { + handler: + Box BoxFuture<'static, Result>>, +} + +impl FakeHttpClient { + pub fn new(handler: F) -> Arc + where + Fut: 'static + Send + Future>, + F: 'static + Send + Sync + Fn(Request) -> Fut, + { + Arc::new(Self { + handler: Box::new(move |req| Box::pin(handler(req))), + }) + } +} + +impl fmt::Debug for FakeHttpClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FakeHttpClient").finish() + } +} + +impl HttpClient for FakeHttpClient { + fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result> { + let future = (self.handler)(req); + Box::pin(async move { future.await.map(Into::into) }) + } +} diff --git a/zed/src/user.rs b/zed/src/user.rs index 6c02c0a284e6dcca3ebd32d3ba601f15808fb948..ee9915ac3e6b21215fd98d3fb141e712a01c3907 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -1,22 +1,24 @@ use crate::{ + http::{HttpClient, Method, Request, Url}, rpc::{Client, Status}, util::TryFutureExt, }; -use anyhow::{anyhow, Result}; -use gpui::{elements::Image, executor, ImageData, Task}; +use anyhow::{anyhow, Context, Result}; +use futures::future; +use gpui::{executor, ImageData, Task}; use parking_lot::Mutex; -use postage::{prelude::Stream, sink::Sink, watch}; -use std::{collections::HashMap, sync::Arc}; -use surf::{ - http::{Method, Request}, - HttpClient, Url, +use postage::{oneshot, prelude::Stream, sink::Sink, watch}; +use std::{ + collections::HashMap, + sync::{Arc, Weak}, }; use zrpc::proto; +#[derive(Debug)] pub struct User { - id: u64, - github_login: String, - avatar: Option, + pub id: u64, + pub github_login: String, + pub avatar: Option>, } pub struct UserStore { @@ -24,7 +26,7 @@ pub struct UserStore { current_user: watch::Receiver>>, rpc: Arc, http: Arc, - _maintain_current_user: Option>, + _maintain_current_user: Task<()>, } impl UserStore { @@ -34,18 +36,18 @@ impl UserStore { executor: &executor::Background, ) -> Arc { let (mut current_user_tx, current_user_rx) = watch::channel(); - - let mut this = Arc::new(Self { + let (mut this_tx, mut this_rx) = oneshot::channel::>(); + let this = Arc::new(Self { users: Default::default(), current_user: current_user_rx, rpc: rpc.clone(), http, - _maintain_current_user: None, - }); - - let task = { - let this = Arc::downgrade(&this); - executor.spawn(async move { + _maintain_current_user: executor.spawn(async move { + let this = if let Some(this) = this_rx.recv().await { + this + } else { + return; + }; let mut status = rpc.status(); while let Some(status) = status.recv().await { match status { @@ -63,10 +65,12 @@ impl UserStore { _ => {} } } - }) - }; - Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task); - + }), + }); + let weak = Arc::downgrade(&this); + executor + .spawn(async move { this_tx.send(weak).await }) + .detach(); this } @@ -78,8 +82,15 @@ impl UserStore { if !user_ids.is_empty() { let response = self.rpc.request(proto::GetUsers { user_ids }).await?; + let new_users = future::join_all( + response + .users + .into_iter() + .map(|user| User::new(user, self.http.as_ref())), + ) + .await; let mut users = self.users.lock(); - for user in response.users { + for user in new_users { users.insert(user.id, Arc::new(user)); } } @@ -92,20 +103,12 @@ impl UserStore { return Ok(user); } - let response = self - .rpc - .request(proto::GetUsers { - user_ids: vec![user_id], - }) - .await?; - - if let Some(user) = response.users.into_iter().next() { - let user = Arc::new(user); - self.users.lock().insert(user_id, user.clone()); - Ok(user) - } else { - Err(anyhow!("server responded with no users")) - } + self.load_users(vec![user_id]).await?; + self.users + .lock() + .get(&user_id) + .cloned() + .ok_or_else(|| anyhow!("server responded with no users")) } pub fn current_user(&self) -> &watch::Receiver>> { @@ -115,20 +118,25 @@ impl UserStore { impl User { async fn new(message: proto::User, http: &dyn HttpClient) -> Self { - let avatar = fetch_avatar(http, &message.avatar_url).await.log_err(); User { id: message.id, github_login: message.github_login, - avatar, + avatar: fetch_avatar(http, &message.avatar_url).log_err().await, } } } async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { - let url = Url::parse(url)?; + let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?; let request = Request::new(Method::Get, url); - let response = http.send(request).await?; - let bytes = response.body_bytes().await?; + let mut response = http + .send(request) + .await + .map_err(|e| anyhow!("failed to send user avatar request: {}", e))?; + let bytes = response + .body_bytes() + .await + .map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?; let format = image::guess_format(&bytes)?; let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8(); Ok(ImageData::new(image)) diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1410b6ece55ea3a0bee1c87d9b67377ec07cbaa6..c38a2d78bb89acc4e23bccb2209639c59ddf8e0b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -956,8 +956,14 @@ impl Workspace { fn render_current_user(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; - let avatar = if let Some(current_user) = self.user_store.current_user().borrow().as_ref() { - todo!() + let avatar = if let Some(avatar) = self + .user_store + .current_user() + .borrow() + .as_ref() + .and_then(|user| user.avatar.clone()) + { + Image::new(avatar).boxed() } else { Svg::new("icons/signed-out-12.svg") .with_color(theme.workspace.titlebar.icon_signed_out)