Detailed changes
@@ -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::<Config>().expect("error loading config");
init_tracing(&config);
@@ -185,6 +185,14 @@ impl FollowableItem for Editor {
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
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)
@@ -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.
@@ -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<Self>,
+ buffer: Model<Buffer>,
+ peer_id: proto::PeerId,
+ cx: &mut AsyncAppContext,
+ ) -> Result<proto::OpenBufferResponse> {
+ 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(),
+ })
+ }
})?
}
@@ -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]
@@ -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();
@@ -216,6 +216,7 @@ enum ErrorCode {
BadPublicNesting = 9;
CircularNesting = 10;
WrongMoveTarget = 11;
+ UnsharedItem = 12;
}
message Test {
@@ -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::<RpcError>() {
+ rpc_error.cloned()
+ } else {
+ anyhow::anyhow!("{}", self)
+ }
+ }
}
impl From<proto::ErrorCode> 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 {
@@ -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!(