diff --git a/Cargo.lock b/Cargo.lock index 30de4e97ae..3620ea1aac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,6 @@ dependencies = [ "project", "smallvec", "ui", - "util", "workspace", ] diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index b4fb2ec5b0..9761a08238 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -23,7 +23,6 @@ language.workspace = true project.workspace = true smallvec.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 4c5818afde..ade2bf7c71 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -12,7 +12,6 @@ use project::{LanguageServerProgress, Project}; use smallvec::SmallVec; use std::{cmp::Reverse, fmt::Write, sync::Arc}; use ui::prelude::*; -use util::ResultExt; use workspace::{item::ItemHandle, StatusItemView, Workspace}; actions!(activity_indicator, [ShowErrorMessage]); @@ -82,27 +81,37 @@ impl ActivityIndicator { } }); - cx.subscribe(&this, move |workspace, _, event, cx| match event { + cx.subscribe(&this, move |_, _, event, cx| match event { Event::ShowError { lsp_name, error } => { - if let Some(buffer) = project - .update(cx, |project, cx| project.create_buffer(error, None, cx)) - .log_err() - { - buffer.update(cx, |buffer, cx| { + let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); + let project = project.clone(); + let error = error.clone(); + let lsp_name = lsp_name.clone(); + cx.spawn(|workspace, mut cx| async move { + let buffer = create_buffer.await?; + buffer.update(&mut cx, |buffer, cx| { buffer.edit( - [(0..0, format!("Language server error: {}\n\n", lsp_name))], + [( + 0..0, + format!("Language server error: {}\n\n{}", lsp_name, error), + )], None, cx, ); - }); - workspace.add_item_to_active_pane( - Box::new( - cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), - ), - None, - cx, - ); - } + })?; + workspace.update(&mut cx, |workspace, cx| { + workspace.add_item_to_active_pane( + Box::new(cx.new_view(|cx| { + Editor::for_buffer(buffer, Some(project.clone()), cx) + })), + None, + cx, + ); + })?; + + anyhow::Ok(()) + }) + .detach(); } }) .detach(); diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 5fb5c99c7e..2f04324bdb 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -221,9 +221,9 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext, )) + .add_request_handler(user_handler( + forward_versioned_mutating_project_request::, + )) .add_request_handler(user_handler( forward_mutating_project_request::, )) @@ -505,7 +510,7 @@ impl Server { forward_mutating_project_request::, )) .add_request_handler(user_handler( - forward_mutating_project_request::, + forward_versioned_mutating_project_request::, )) .add_request_handler(user_handler( forward_mutating_project_request::, @@ -2677,6 +2682,7 @@ where T: EntityMessage + RequestMessage, { let project_id = ProjectId::from_proto(request.remote_entity_id()); + let host_connection_id = session .db() .await @@ -2690,6 +2696,45 @@ where Ok(()) } +/// forward a project request to the host. These requests are disallowed +/// for guests. +async fn forward_versioned_mutating_project_request( + request: T, + response: Response, + session: UserSession, +) -> Result<()> +where + T: EntityMessage + RequestMessage + VersionedMessage, +{ + let project_id = ProjectId::from_proto(request.remote_entity_id()); + + let host_connection_id = session + .db() + .await + .host_for_mutating_project_request(project_id, session.connection_id, session.user_id()) + .await?; + if let Some(host_version) = session + .connection_pool() + .await + .connection(host_connection_id) + .map(|c| c.zed_version) + { + if let Some(min_required_version) = request.required_host_version() { + if min_required_version > host_version { + return Err(anyhow!(ErrorCode::RemoteUpgradeRequired + .with_tag("required", &min_required_version.to_string())))?; + } + } + } + + let payload = session + .peer + .forward_request(session.connection_id, host_connection_id, request) + .await?; + response.send(payload)?; + Ok(()) +} + /// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, diff --git a/crates/collab/src/rpc/connection_pool.rs b/crates/collab/src/rpc/connection_pool.rs index 5a7632f391..197e82af98 100644 --- a/crates/collab/src/rpc/connection_pool.rs +++ b/crates/collab/src/rpc/connection_pool.rs @@ -21,7 +21,7 @@ struct ConnectedPrincipal { connection_ids: HashSet, } -#[derive(Debug, Serialize)] +#[derive(Copy, Clone, Debug, Serialize, PartialOrd, PartialEq, Eq, Ord)] pub struct ZedVersion(pub SemanticVersion); impl fmt::Display for ZedVersion { @@ -34,6 +34,32 @@ impl ZedVersion { pub fn can_collaborate(&self) -> bool { self.0 >= SemanticVersion::new(0, 129, 2) } + + pub fn with_save_as() -> ZedVersion { + ZedVersion(SemanticVersion::new(0, 134, 0)) + } +} + +pub trait VersionedMessage { + fn required_host_version(&self) -> Option { + None + } +} + +impl VersionedMessage for proto::SaveBuffer { + fn required_host_version(&self) -> Option { + if self.new_path.is_some() { + Some(ZedVersion::with_save_as()) + } else { + None + } + } +} + +impl VersionedMessage for proto::OpenNewBuffer { + fn required_host_version(&self) -> Option { + Some(ZedVersion::with_save_as()) + } } #[derive(Serialize)] @@ -50,6 +76,10 @@ impl ConnectionPool { self.channels.clear(); } + pub fn connection(&mut self, connection_id: ConnectionId) -> Option<&Connection> { + self.connections.get(&connection_id) + } + #[instrument(skip(self))] pub fn add_connection( &mut self, diff --git a/crates/collab/src/tests/dev_server_tests.rs b/crates/collab/src/tests/dev_server_tests.rs index 8769b721b3..e6709b6e2a 100644 --- a/crates/collab/src/tests/dev_server_tests.rs +++ b/crates/collab/src/tests/dev_server_tests.rs @@ -398,3 +398,33 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes "remote\nremote\nremote" ); } + +#[gpui::test] +async fn test_new_file_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) { + let (server, client1) = TestServer::start1(cx1).await; + + // Creating a project with a path that does exist should not fail + let (dev_server, remote_workspace) = + create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await; + + let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1); + + cx.simulate_keystrokes("cmd-n"); + cx.simulate_input("new!"); + cx.simulate_keystrokes("cmd-shift-s"); + cx.simulate_input("2.txt"); + cx.simulate_keystrokes("enter"); + + cx.executor().run_until_parked(); + + let title = remote_workspace + .update(&mut cx, |ws, cx| { + ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap() + }) + .unwrap(); + + assert_eq!(title, "2.txt"); + + let path = Path::new("/remote/2.txt"); + assert_eq!(dev_server.fs().load(&path).await.unwrap(), "new!"); +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 98a12a7f73..a9a67ef2e3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2450,7 +2450,8 @@ async fn test_propagate_saves_and_fs_changes( }); let new_buffer_a = project_a - .update(cx_a, |p, cx| p.create_buffer("", None, cx)) + .update(cx_a, |p, cx| p.create_buffer(cx)) + .await .unwrap(); let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 57be49d21b..e9e9088806 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -99,7 +99,7 @@ use project::{ CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction, }; use rand::prelude::*; -use rpc::proto::*; +use rpc::{proto::*, ErrorExt}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; @@ -131,7 +131,7 @@ use ui::{ }; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::item::{ItemHandle, PreviewTabsSettings}; -use workspace::notifications::NotificationId; +use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::{ searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId, }; @@ -1610,18 +1610,27 @@ impl Editor { cx: &mut ViewContext, ) { let project = workspace.project().clone(); - if project.read(cx).is_remote() { - cx.propagate(); - } else if let Some(buffer) = project - .update(cx, |project, cx| project.create_buffer("", None, cx)) - .log_err() - { - workspace.add_item_to_active_pane( - Box::new(cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), - None, - cx, - ); - } + let create = project.update(cx, |project, cx| project.create_buffer(cx)); + + cx.spawn(|workspace, mut cx| async move { + let buffer = create.await?; + workspace.update(&mut cx, |workspace, cx| { + workspace.add_item_to_active_pane( + Box::new( + cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), + ), + None, + cx, + ) + }) + }) + .detach_and_prompt_err("Failed to create buffer", cx, |e, _| match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( + "The remote instance of Zed does not support this yet. It must be upgraded to {}", + e.error_tag("required").unwrap_or("the latest version") + )), + _ => None, + }); } pub fn new_file_in_direction( @@ -1630,18 +1639,29 @@ impl Editor { cx: &mut ViewContext, ) { let project = workspace.project().clone(); - if project.read(cx).is_remote() { - cx.propagate(); - } else if let Some(buffer) = project - .update(cx, |project, cx| project.create_buffer("", None, cx)) - .log_err() - { - workspace.split_item( - action.0, - Box::new(cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), - cx, - ); - } + let create = project.update(cx, |project, cx| project.create_buffer(cx)); + let direction = action.0; + + cx.spawn(|workspace, mut cx| async move { + let buffer = create.await?; + workspace.update(&mut cx, move |workspace, cx| { + workspace.split_item( + direction, + Box::new( + cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), + ), + cx, + ) + })?; + anyhow::Ok(()) + }) + .detach_and_prompt_err("Failed to create buffer", cx, |e, _| match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( + "The remote instance of Zed does not support this yet. It must be upgraded to {}", + e.error_tag("required").unwrap_or("the latest version") + )), + _ => None, + }); } pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f443146dd1..05d7aa243e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7356,9 +7356,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let buffer = project.update(cx, |project, cx| { - let buffer = project - .create_buffer(&sample_text(16, 8, 'a'), None, cx) - .unwrap(); + let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx); cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)) }); let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); @@ -7565,12 +7563,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { let (buffer_1, buffer_2) = project.update(cx, |project, cx| { ( - project - .create_buffer("abc\ndef\nghi\njkl\n", None, cx) - .unwrap(), - project - .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) - .unwrap(), + project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx), + project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx), ) }); diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index fd82ff5b10..817a70190f 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -108,10 +108,9 @@ mod tests { let project = Project::test(fs, [], cx).await; // buffer has two modified hunks with two rows each - let buffer_1 = project - .update(cx, |project, cx| { - project.create_buffer( - " + let buffer_1 = project.update(cx, |project, cx| { + project.create_local_buffer( + " 1.zero 1.ONE 1.TWO @@ -120,13 +119,12 @@ mod tests { 1.FIVE 1.six " - .unindent() - .as_str(), - None, - cx, - ) - }) - .unwrap(); + .unindent() + .as_str(), + None, + cx, + ) + }); buffer_1.update(cx, |buffer, cx| { buffer.set_diff_base( Some( @@ -146,10 +144,9 @@ mod tests { }); // buffer has a deletion hunk and an insertion hunk - let buffer_2 = project - .update(cx, |project, cx| { - project.create_buffer( - " + let buffer_2 = project.update(cx, |project, cx| { + project.create_local_buffer( + " 2.zero 2.one 2.two @@ -158,13 +155,12 @@ mod tests { 2.five 2.six " - .unindent() - .as_str(), - None, - cx, - ) - }) - .unwrap(); + .unindent() + .as_str(), + None, + cx, + ) + }); buffer_2.update(cx, |buffer, cx| { buffer.set_diff_base( Some( diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 4544d1d785..32fd03a385 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -97,15 +97,19 @@ pub fn expand_macro_recursively( return Ok(()); } - let buffer = project.update(&mut cx, |project, cx| { - project.create_buffer(¯o_expansion.expansion, Some(rust_language), cx) - })??; + let buffer = project + .update(&mut cx, |project, cx| project.create_buffer(cx))? + .await?; workspace.update(&mut cx, |workspace, cx| { - let buffer = cx.new_model(|cx| { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, macro_expansion.expansion)], None, cx); + buffer.set_language(Some(rust_language), cx) + }); + let multibuffer = cx.new_model(|cx| { MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name) }); workspace.add_item_to_active_pane( - Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), + Box::new(cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))), None, cx, ); diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index bb109ef6ef..273b293dfb 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -142,11 +142,9 @@ impl FeedbackModal { cx.spawn(|workspace, mut cx| async move { let markdown = markdown.await.log_err(); - let buffer = project - .update(&mut cx, |project, cx| { - project.create_buffer("", markdown, cx) - })? - .expect("creating buffers on a local workspace always succeeds"); + let buffer = project.update(&mut cx, |project, cx| { + project.create_local_buffer("", markdown, cx) + })?; workspace.update(&mut cx, |workspace, cx| { let system_specs = SystemSpecs::new(cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9ec6373539..ae2b2ba57d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -665,6 +665,7 @@ impl Project { client.add_model_request_handler(Self::handle_open_buffer_for_symbol); client.add_model_request_handler(Self::handle_open_buffer_by_id); client.add_model_request_handler(Self::handle_open_buffer_by_path); + client.add_model_request_handler(Self::handle_open_new_buffer); client.add_model_request_handler(Self::handle_save_buffer); client.add_model_message_handler(Self::handle_update_diff_base); client.add_model_request_handler(Self::handle_lsp_command::); @@ -1955,21 +1956,41 @@ impl Project { !self.is_local() } - pub fn create_buffer( + pub fn create_buffer(&mut self, cx: &mut ModelContext) -> Task>> { + if self.is_remote() { + let create = self.client.request(proto::OpenNewBuffer { + project_id: self.remote_id().unwrap(), + }); + cx.spawn(|this, mut cx| async move { + let response = create.await?; + let buffer_id = BufferId::new(response.buffer_id)?; + + this.update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(buffer_id, cx) + })? + .await + }) + } else { + Task::ready(Ok(self.create_local_buffer("", None, cx))) + } + } + + pub fn create_local_buffer( &mut self, text: &str, language: Option>, cx: &mut ModelContext, - ) -> Result> { + ) -> Model { if self.is_remote() { - return Err(anyhow!("creating buffers as a guest is not supported yet")); + panic!("called create_local_buffer on a remote project") } let buffer = cx.new_model(|cx| { Buffer::local(text, cx) .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) }); - self.register_buffer(&buffer, cx)?; - Ok(buffer) + self.register_buffer(&buffer, cx) + .expect("creating local buffers always succeeds"); + buffer } pub fn open_path( @@ -9415,6 +9436,18 @@ impl Project { Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx) } + async fn handle_open_new_buffer( + this: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let buffer = this.update(&mut cx, |this, cx| this.create_local_buffer("", None, cx))?; + let peer_id = envelope.original_sender_id()?; + + Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx) + } + fn respond_to_open_buffer_request( this: Model, buffer: Model, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 275b2f3f97..763e316e7a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2931,9 +2931,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { let languages = project.update(cx, |project, _| project.languages().clone()); languages.add(rust_lang()); - let buffer = project.update(cx, |project, cx| { - project.create_buffer("", None, cx).unwrap() - }); + let buffer = project.update(cx, |project, cx| project.create_local_buffer("", None, cx)); buffer.update(cx, |buffer, cx| { buffer.edit([(0..0, "abc")], None, cx); assert!(buffer.is_dirty()); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index f096b0868e..dd9750759e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3394,7 +3394,9 @@ mod tests { }) .unwrap(); - // "Save as"" the buffer, creating a new backing file for it + cx.executor().run_until_parked(); + + // "Save as" the buffer, creating a new backing file for it let save_task = workspace .update(cx, |workspace, cx| { workspace.save_active_item(workspace::SaveIntent::Save, cx) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index e5f95850e2..5dcf3ad740 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -235,8 +235,9 @@ message Envelope { RejoinRemoteProjectsResponse rejoin_remote_projects_response = 187; RemoteProjectsUpdate remote_projects_update = 193; - ValidateRemoteProjectRequest validate_remote_project_request = 194; // Current max + ValidateRemoteProjectRequest validate_remote_project_request = 194; DeleteDevServer delete_dev_server = 195; + OpenNewBuffer open_new_buffer = 196; // Current max } reserved 158 to 161; @@ -275,6 +276,7 @@ enum ErrorCode { DevServerAlreadyOnline = 14; DevServerOffline = 15; RemoteProjectPathDoesNotExist = 16; + RemoteUpgradeRequired = 17; reserved 6; } @@ -736,6 +738,10 @@ message OpenBufferById { uint64 id = 2; } +message OpenNewBuffer { + uint64 project_id = 1; +} + message OpenBufferResponse { uint64 buffer_id = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 25074083f3..317941cb8c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -319,7 +319,8 @@ messages!( (MultiLspQueryResponse, Background), (RemoteProjectsUpdate, Foreground), (ValidateRemoteProjectRequest, Background), - (DeleteDevServer, Foreground) + (DeleteDevServer, Foreground), + (OpenNewBuffer, Foreground) ); request_messages!( @@ -377,6 +378,7 @@ request_messages!( (OpenBufferById, OpenBufferResponse), (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), + (OpenNewBuffer, OpenBufferResponse), (PerformRename, PerformRenameResponse), (Ping, Ack), (PrepareRename, PrepareRenameResponse), @@ -453,6 +455,7 @@ entity_messages!( LeaveProject, MultiLspQuery, OnTypeFormatting, + OpenNewBuffer, OpenBufferById, OpenBufferByPath, OpenBufferForSymbol, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d0bb0c43d9..9a5d657906 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -599,9 +599,9 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { return; }; let project = workspace.project().clone(); - let buffer = project - .update(cx, |project, cx| project.create_buffer(&log, None, cx)) - .expect("creating buffers on a local workspace always succeeds"); + let buffer = project.update(cx, |project, cx| { + project.create_local_buffer(&log, None, cx) + }); let buffer = cx.new_model(|cx| { MultiBuffer::singleton(buffer, cx).with_title("Log".into()) @@ -812,8 +812,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext())