Add integration test for code actions

Antonio Scandurra created

Change summary

crates/client/src/client.rs |   4 
crates/editor/src/editor.rs |   6 
crates/gpui/src/app.rs      |   4 
crates/rpc/src/peer.rs      |   7 
crates/server/src/rpc.rs    | 247 ++++++++++++++++++++++++++++++++++++++
5 files changed, 259 insertions(+), 9 deletions(-)

Detailed changes

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

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<Self>,
@@ -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<Workspace>,
@@ -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<Self>) -> Option<ContextMenu> {

crates/gpui/src/app.rs 🔗

@@ -2703,7 +2703,7 @@ impl<T: Entity> ModelHandle<T> {
         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<T: View> ViewHandle<T> {
         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() {

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.

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(&params, cx));
+        let editor_b = workspace_b
+            .update(&mut cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs").into(), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        fake_language_server
+            .handle_request::<lsp::request::CodeActionRequest, _>(|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::<lsp::request::CodeActionRequest, _>(|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::request::CodeActionResolveRequest, _>(|_| {
+            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::<Editor>()
+                .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();