diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 517a0589a8..32c69de690 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -28,6 +28,9 @@ async fn main() -> Result<()> { Some("version") => { println!("collab v{VERSION}"); } + Some("migrate") => { + run_migrations().await?; + } Some("serve") => { let config = envy::from_env::().expect("error loading config"); init_tracing(&config); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3a4a14da0e..ac55ed8546 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -185,6 +185,14 @@ impl FollowableItem for Editor { fn to_state_proto(&self, cx: &WindowContext) -> Option { let buffer = self.buffer.read(cx); + if buffer + .as_singleton() + .and_then(|buffer| buffer.read(cx).file()) + .map_or(false, |file| file.is_private()) + { + return None; + } + let scroll_anchor = self.scroll_manager.anchor(); let excerpts = buffer .read(cx) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0bb069729f..cd8b239f5c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -384,7 +384,7 @@ pub trait File: Send + Sync { /// Converts this file into a protobuf message. fn to_proto(&self) -> rpc::proto::File; - /// Return whether Zed considers this to be a dotenv file. + /// Return whether Zed considers this to be a private file. fn is_private(&self) -> bool; } @@ -406,6 +406,11 @@ pub trait LocalFile: File { mtime: SystemTime, cx: &mut AppContext, ); + + /// Returns true if the file should not be shared with collaborators. + fn is_private(&self, _: &AppContext) -> bool { + false + } } /// The auto-indent behavior associated with an editing operation. diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bc13c5cf31..fdd273059b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -56,6 +56,7 @@ use postage::watch; use prettier_support::{DefaultPrettier, PrettierInstance}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; +use rpc::{ErrorCode, ErrorExt}; use search::SearchQuery; use serde::Serialize; use settings::{Settings, SettingsStore}; @@ -1760,7 +1761,7 @@ impl Project { cx.background_executor().spawn(async move { wait_for_loading_buffer(loading_watch) .await - .map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}")) + .map_err(|e| e.cloned()) }) } @@ -8011,11 +8012,20 @@ impl Project { .update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))? .await?; - Ok(proto::OpenBufferForSymbolResponse { - buffer_id: this.update(&mut cx, |this, cx| { - this.create_buffer_for_peer(&buffer, peer_id, cx).into() - })?, - }) + this.update(&mut cx, |this, cx| { + let is_private = buffer + .read(cx) + .file() + .map(|f| f.is_private()) + .unwrap_or_default(); + if is_private { + Err(anyhow!(ErrorCode::UnsharedItem)) + } else { + Ok(proto::OpenBufferForSymbolResponse { + buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(), + }) + } + })? } fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] { @@ -8037,11 +8047,7 @@ impl Project { let buffer = this .update(&mut cx, |this, cx| this.open_buffer_by_id(buffer_id, cx))? .await?; - this.update(&mut cx, |this, cx| { - Ok(proto::OpenBufferResponse { - buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(), - }) - })? + Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx) } async fn handle_open_buffer_by_path( @@ -8063,10 +8069,28 @@ impl Project { })?; let buffer = open_buffer.await?; - this.update(&mut cx, |this, cx| { - Ok(proto::OpenBufferResponse { - buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(), - }) + Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx) + } + + fn respond_to_open_buffer_request( + this: Model, + buffer: Model, + peer_id: proto::PeerId, + cx: &mut AsyncAppContext, + ) -> Result { + this.update(cx, |this, cx| { + let is_private = buffer + .read(cx) + .file() + .map(|f| f.is_private()) + .unwrap_or_default(); + if is_private { + Err(anyhow!(ErrorCode::UnsharedItem)) + } else { + Ok(proto::OpenBufferResponse { + buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(), + }) + } })? } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index d681b9b38c..a518a2c075 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -31,6 +31,7 @@ theme = { path = "../theme" } ui = { path = "../ui" } unicase = "2.6" util = { path = "../util" } +client = { path = "../client" } workspace = { path = "../workspace", package = "workspace" } [dev-dependencies] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 290969d0f4..f64099340c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,5 +1,6 @@ pub mod file_associations; mod project_panel_settings; +use client::{ErrorCode, ErrorExt}; use settings::Settings; use db::kvp::KEY_VALUE_STORE; @@ -35,6 +36,7 @@ use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, + notifications::DetachAndPromptErr, Workspace, }; @@ -259,6 +261,7 @@ impl ProjectPanel { } => { if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + let file_path = entry.path.clone(); workspace .open_path( ProjectPath { @@ -269,7 +272,15 @@ impl ProjectPanel { focus_opened_item, cx, ) - .detach_and_log_err(cx); + .detach_and_prompt_err("Failed to open file", cx, move |e, _| { + match e.error_code() { + ErrorCode::UnsharedItem => Some(format!( + "{} is not shared by the host. This could be because it has been marked as `private`", + file_path.display() + )), + _ => None, + } + }); if !focus_opened_item { if let Some(project_panel) = project_panel.upgrade() { let focus_handle = project_panel.read(cx).focus_handle.clone(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 18f9b1f1e8..3a513902e5 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -216,6 +216,7 @@ enum ErrorCode { BadPublicNesting = 9; CircularNesting = 10; WrongMoveTarget = 11; + UnsharedItem = 12; } message Test { diff --git a/crates/rpc/src/error.rs b/crates/rpc/src/error.rs index 97f93e7465..858029a02b 100644 --- a/crates/rpc/src/error.rs +++ b/crates/rpc/src/error.rs @@ -80,6 +80,8 @@ pub trait ErrorExt { fn error_tag(&self, k: &str) -> Option<&str>; /// to_proto() converts the error into a proto::Error fn to_proto(&self) -> proto::Error; + /// + fn cloned(&self) -> anyhow::Error; } impl ErrorExt for anyhow::Error { @@ -106,6 +108,14 @@ impl ErrorExt for anyhow::Error { ErrorCode::Internal.message(format!("{}", self)).to_proto() } } + + fn cloned(&self) -> anyhow::Error { + if let Some(rpc_error) = self.downcast_ref::() { + rpc_error.cloned() + } else { + anyhow::anyhow!("{}", self) + } + } } impl From for anyhow::Error { @@ -189,6 +199,10 @@ impl ErrorExt for RpcError { tags: self.tags.clone(), } } + + fn cloned(&self) -> anyhow::Error { + self.clone().into() + } } impl std::error::Error for RpcError { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 6963ed3cae..75db85f4a9 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -176,11 +176,19 @@ impl Member { return div().into_any(); } - let leader = follower_states.get(pane).and_then(|state| { + let follower_state = follower_states.get(pane); + + let leader = follower_state.and_then(|state| { let room = active_call?.read(cx).room()?.read(cx); room.remote_participant_for_peer_id(state.leader_id) }); + let is_in_unshared_view = follower_state.map_or(false, |state| { + state.active_view_id.is_some_and(|view_id| { + !state.items_by_leader_view_id.contains_key(&view_id) + }) + }); + let mut leader_border = None; let mut leader_status_box = None; let mut leader_join_data = None; @@ -198,7 +206,14 @@ impl Member { project_id: leader_project_id, } => { if Some(leader_project_id) == project.read(cx).remote_id() { - None + if is_in_unshared_view { + Some(Label::new(format!( + "{} is in an unshared pane", + leader.user.github_login + ))) + } else { + None + } } else { leader_join_data = Some((leader_project_id, leader.user.id)); Some(Label::new(format!(