diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e338b72e74..c40b78987c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -127,6 +127,12 @@ pub enum Status { ReconnectionError { next_reconnection: Instant }, } +impl Status { + pub fn is_connected(&self) -> bool { + matches!(self, Self::Connected { .. }) + } +} + struct ClientState { credentials: Option, status: (watch::Sender, watch::Receiver), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a89317db90..2ca1a7ce88 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -92,6 +92,7 @@ enum ProjectClientState { sharing_has_stopped: bool, remote_id: u64, replica_id: ReplicaId, + _detect_unshare_task: Task>, }, } @@ -244,7 +245,7 @@ impl Project { let mut status = rpc.status(); while let Some(status) = status.next().await { if let Some(this) = this.upgrade(&cx) { - let remote_id = if let client::Status::Connected { .. } = status { + let remote_id = if status.is_connected() { let response = rpc.request(proto::RegisterProject {}).await?; Some(response.project_id) } else { @@ -333,7 +334,7 @@ impl Project { } let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); - let this = cx.add_model(|cx| { + let this = cx.add_model(|cx: &mut ModelContext| { let mut this = Self { worktrees: Vec::new(), loading_buffers: Default::default(), @@ -346,11 +347,26 @@ impl Project { user_store: user_store.clone(), fs, subscriptions: vec![client.add_model_for_remote_entity(remote_id, cx)], - client, + client: client.clone(), client_state: ProjectClientState::Remote { sharing_has_stopped: false, remote_id, replica_id, + _detect_unshare_task: cx.spawn_weak(move |this, mut cx| { + async move { + let mut status = client.status(); + let is_connected = + status.next().await.map_or(false, |s| s.is_connected()); + // Even if we're initially connected, any future change of the status means we momentarily disconnected. + if !is_connected || status.next().await.is_some() { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.project_unshared(cx)) + } + } + Ok(()) + } + .log_err() + }), }, language_servers_with_diagnostics_running: 0, language_servers: Default::default(), @@ -666,6 +682,18 @@ impl Project { }) } + fn project_unshared(&mut self, cx: &mut ModelContext) { + if let ProjectClientState::Remote { + sharing_has_stopped, + .. + } = &mut self.client_state + { + *sharing_has_stopped = true; + self.collaborators.clear(); + cx.notify(); + } + } + pub fn is_read_only(&self) -> bool { match &self.client_state { ProjectClientState::Local { .. } => false, @@ -2628,20 +2656,7 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - if let ProjectClientState::Remote { - sharing_has_stopped, - .. - } = &mut this.client_state - { - *sharing_has_stopped = true; - this.collaborators.clear(); - cx.notify(); - } else { - unreachable!() - } - }); - + this.update(&mut cx, |this, cx| this.project_unshared(cx)); Ok(()) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1ca2e3a604..c96db5aab2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -39,6 +39,7 @@ pub struct Workspace { pub right_sidebar: Sidebar, pub status_bar: StatusBar, pub toolbar: Toolbar, + pub disconnected_overlay: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2691516e2f..2188d0b88d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1297,6 +1297,24 @@ impl Workspace { None } } + + fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { + if self.project.read(cx).is_read_only() { + let theme = &self.settings.borrow().theme; + Some( + Label::new( + "Your connection to the remote project has been lost.".to_string(), + theme.workspace.disconnected_overlay.text.clone(), + ) + .aligned() + .contained() + .with_style(theme.workspace.disconnected_overlay.container) + .boxed(), + ) + } else { + None + } + } } impl Entity for Workspace { @@ -1339,6 +1357,7 @@ impl View for Workspace { content.boxed() }) .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed())) + .with_children(self.render_disconnected_overlay(cx)) .flexible(1.0, true) .boxed(), ) diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml index 769076645f..34de16627e 100644 --- a/crates/zed/assets/themes/black.toml +++ b/crates/zed/assets/themes/black.toml @@ -60,3 +60,8 @@ emphasis = "#4ec9b0" link_uri = { color = "#6a9955", underline = true } link_text = { color = "#cb8f77", italic = true } list_marker = "#4e94ce" + +[workspace.disconnected_overlay] +extends = "$text.base" +color = "#ffffff" +background = "#000000aa" diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml index ed6deed040..fa673ac446 100644 --- a/crates/zed/assets/themes/dark.toml +++ b/crates/zed/assets/themes/dark.toml @@ -60,3 +60,8 @@ emphasis = "#4ec9b0" link_uri = { color = "#6a9955", underline = true } link_text = { color = "#cb8f77", italic = true } list_marker = "#4e94ce" + +[workspace.disconnected_overlay] +extends = "$text.base" +color = "#ffffff" +background = "#000000aa" diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml index f51b3f4656..2884515e09 100644 --- a/crates/zed/assets/themes/light.toml +++ b/crates/zed/assets/themes/light.toml @@ -60,3 +60,8 @@ emphasis = "#267f29" link_uri = { color = "#6a9955", underline = true } link_text = { color = "#a82121", italic = true } list_marker = "#4e94ce" + +[workspace.disconnected_overlay] +extends = "$text.base" +color = "#ffffff" +background = "#000000cc"