diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 44f81f3345..702360f778 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -533,14 +533,14 @@ impl Client { match future.await { Ok(()) => { log::debug!( - "{}: rpc message '{}' handled", + "rpc message handled. client_id:{}, name:{}", client_id, type_name ); } Err(error) => { log::error!( - "{}: error handling rpc message '{}', {}", + "error handling rpc message. client_id:{}, name:{}, error:{}", client_id, type_name, error diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5972f245b2..1455da1cc9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2090,7 +2090,7 @@ impl Editor { })) } - fn toggle_code_actions( + pub fn toggle_code_actions( &mut self, &ToggleCodeActions(deployed_from_indicator): &ToggleCodeActions, cx: &mut ViewContext, @@ -2136,7 +2136,7 @@ impl Editor { .detach_and_log_err(cx); } - fn confirm_code_action( + pub fn confirm_code_action( workspace: &mut Workspace, ConfirmCodeAction(action_ix): &ConfirmCodeAction, cx: &mut ViewContext, @@ -2297,7 +2297,7 @@ impl Editor { self.completion_tasks.clear(); } self.context_menu = Some(menu); - cx.notify() + cx.notify(); } fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 337c9b29ea..1ba05c7b4a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2703,7 +2703,7 @@ impl ModelHandle { let (mut tx, mut rx) = mpsc::channel(1); let mut cx = cx.cx.borrow_mut(); let subscription = cx.observe(self, move |_, _| { - tx.blocking_send(()).ok(); + tx.try_send(()).ok(); }); let duration = if std::env::var("CI").is_ok() { @@ -3007,7 +3007,7 @@ impl ViewHandle { let (mut tx, mut rx) = mpsc::channel(1); let mut cx = cx.cx.borrow_mut(); let subscription = cx.observe(self, move |_, _| { - tx.blocking_send(()).ok(); + tx.try_send(()).ok(); }); let duration = if std::env::var("CI").is_ok() { diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 39ae4de5f8..0a614e0bed 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -179,7 +179,12 @@ impl Peer { let channel = response_channels.lock().as_mut()?.remove(&responding_to); if let Some(mut tx) = channel { let mut requester_resumed = barrier::channel(); - tx.send((incoming, requester_resumed.0)).await.ok(); + if let Err(error) = tx.send((incoming, requester_resumed.0)).await { + log::debug!( + "received RPC but request future was dropped {:?}", + error.0 .0 + ); + } // Drop response channel before awaiting on the barrier. This allows the // barrier to get dropped even if the request's future is dropped before it // has a chance to observe the response. diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index f5a8b4f8f7..c1d36ef3c6 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1089,7 +1089,10 @@ mod tests { self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials, EstablishConnectionError, UserStore, }, - editor::{ConfirmCompletion, Editor, EditorSettings, Input, MultiBuffer}, + editor::{ + self, ConfirmCodeAction, ConfirmCompletion, Editor, EditorSettings, Input, MultiBuffer, + Redo, ToggleCodeActions, Undo, + }, fs::{FakeFs, Fs as _}, language::{ tree_sitter_rust, AnchorRangeExt, Diagnostic, DiagnosticEntry, Language, @@ -1097,6 +1100,7 @@ mod tests { }, lsp, project::{worktree::WorktreeHandle, DiagnosticSummary, Project, ProjectPath}, + workspace::{Workspace, WorkspaceParams}, }; #[cfg(test)] @@ -2724,6 +2728,247 @@ mod tests { assert_eq!(definitions[0].target_buffer, buffer_b2); } + #[gpui::test(iterations = 10)] + async fn test_collaborating_with_code_actions( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + let mut lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new(cx_a.background())); + let mut path_openers_b = Vec::new(); + cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); + + // Set up a fake language server. + let (language_server_config, mut fake_language_server) = + LanguageServerConfig::fake_with_capabilities( + lsp::ServerCapabilities { + ..Default::default() + }, + &cx_a, + ) + .await; + Arc::get_mut(&mut lang_registry) + .unwrap() + .add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", + "other.rs": "pub fn foo() -> usize { 4 }", + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", false, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id()); + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let mut params = cx_b.update(WorkspaceParams::test); + params.languages = lang_registry.clone(); + params.client = client_b.client.clone(); + params.user_store = client_b.user_store.clone(); + params.project = project_b; + params.path_openers = path_openers_b.into(); + + let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); + let editor_b = workspace_b + .update(&mut cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs").into(), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + fake_language_server + .handle_request::(|params| { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(0, 0)); + assert_eq!(params.range.end, lsp::Position::new(0, 0)); + None + }) + .next() + .await; + + // Move cursor to a location that contains code actions. + editor_b.update(&mut cx_b, |editor, cx| { + editor.select_ranges([Point::new(1, 31)..Point::new(1, 31)], None, cx); + cx.focus(&editor_b); + }); + fake_language_server.handle_request::(|params| { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!(params.range.start, lsp::Position::new(1, 31)); + assert_eq!(params.range.end, lsp::Position::new(1, 31)); + + Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + data: Some(json!({ + "codeActionParams": { + "range": { + "start": {"line": 1, "column": 31}, + "end": {"line": 1, "column": 31}, + } + } + })), + ..Default::default() + }, + )]) + }); + + // Toggle code actions and wait for them to display. + editor_b.update(&mut cx_b, |editor, cx| { + editor.toggle_code_actions(&ToggleCodeActions(false), cx); + }); + editor_b + .condition(&cx_b, |editor, _| editor.context_menu_visible()) + .await; + + // Confirming the code action will trigger a resolve request. + let confirm_action = workspace_b + .update(&mut cx_b, |workspace, cx| { + Editor::confirm_code_action(workspace, &ConfirmCodeAction(Some(0)), cx) + }) + .unwrap(); + fake_language_server.handle_request::(|_| { + lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), + ..Default::default() + } + }); + + // After the action is confirmed, an editor containing both modified files is opened. + confirm_action.await.unwrap(); + let code_action_editor = workspace_b.read_with(&cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + code_action_editor.update(&mut cx_b, |editor, cx| { + assert_eq!(editor.text(cx), "\nmod other;\nfn main() { let foo = 4; }"); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "pub fn foo() -> usize { 4 }\nmod other;\nfn main() { let foo = other::foo(); }" + ); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "\nmod other;\nfn main() { let foo = 4; }"); + }); + } + #[gpui::test(iterations = 10)] async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking();