Add test for preparing and performing a rename

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs       |   4 
crates/project/src/lsp_command.rs |  10 +
crates/project/src/project.rs     |  75 ++++++++++
crates/server/src/rpc.rs          | 243 ++++++++++++++++++++++++++++++++
4 files changed, 326 insertions(+), 6 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -4086,7 +4086,7 @@ impl Editor {
         .detach_and_log_err(cx);
     }
 
-    fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+    pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
         use language::ToOffset as _;
 
         let project = self.project.clone()?;
@@ -4130,7 +4130,7 @@ impl Editor {
         }))
     }
 
-    fn confirm_rename(
+    pub fn confirm_rename(
         workspace: &mut Workspace,
         _: &ConfirmRename,
         cx: &mut ViewContext<Workspace>,

crates/project/src/lsp_command.rs 🔗

@@ -4,7 +4,7 @@ use client::proto;
 use futures::{future::LocalBoxFuture, FutureExt};
 use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
-    proto::deserialize_anchor, range_from_lsp, Anchor, Buffer, PointUtf16, ToLspPosition,
+    proto::deserialize_anchor, range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition,
 };
 use std::{ops::Range, path::Path};
 
@@ -84,7 +84,13 @@ impl LspCommand for PrepareRename {
                 | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => {
                     self.buffer.read_with(&cx, |buffer, _| {
                         let range = range_from_lsp(range);
-                        Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end))
+                        if buffer.clip_point_utf16(range.start, Bias::Left) == range.start
+                            && buffer.clip_point_utf16(range.end, Bias::Left) == range.end
+                        {
+                            Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end))
+                        } else {
+                            None
+                        }
                     })
                 }
                 _ => None,

crates/project/src/project.rs 🔗

@@ -4332,5 +4332,80 @@ mod tests {
         let range = response.await.unwrap().unwrap();
         let range = buffer.read_with(&cx, |buffer, _| range.to_offset(buffer));
         assert_eq!(range, 6..9);
+
+        let response = project.update(&mut cx, |project, cx| {
+            project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
+        });
+        fake_server
+            .handle_request::<lsp::request::Rename, _>(|params| {
+                assert_eq!(
+                    params.text_document_position.text_document.uri.as_str(),
+                    "file:///dir/one.rs"
+                );
+                assert_eq!(
+                    params.text_document_position.position,
+                    lsp::Position::new(0, 7)
+                );
+                assert_eq!(params.new_name, "THREE");
+                Some(lsp::WorkspaceEdit {
+                    changes: Some(
+                        [
+                            (
+                                lsp::Url::from_file_path("/dir/one.rs").unwrap(),
+                                vec![lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(0, 6),
+                                        lsp::Position::new(0, 9),
+                                    ),
+                                    "THREE".to_string(),
+                                )],
+                            ),
+                            (
+                                lsp::Url::from_file_path("/dir/two.rs").unwrap(),
+                                vec![
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 24),
+                                            lsp::Position::new(0, 27),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 35),
+                                            lsp::Position::new(0, 38),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                ],
+                            ),
+                        ]
+                        .into_iter()
+                        .collect(),
+                    ),
+                    ..Default::default()
+                })
+            })
+            .next()
+            .await
+            .unwrap();
+        let mut transaction = response.await.unwrap().0;
+        assert_eq!(transaction.len(), 2);
+        assert_eq!(
+            transaction
+                .remove_entry(&buffer)
+                .unwrap()
+                .0
+                .read_with(&cx, |buffer, _| buffer.text()),
+            "const THREE: usize = 1;"
+        );
+        assert_eq!(
+            transaction
+                .into_keys()
+                .next()
+                .unwrap()
+                .read_with(&cx, |buffer, _| buffer.text()),
+            "const TWO: usize = one::THREE + one::THREE;"
+        );
     }
 }

crates/server/src/rpc.rs 🔗

@@ -1152,8 +1152,8 @@ mod tests {
             EstablishConnectionError, UserStore,
         },
         editor::{
-            self, ConfirmCodeAction, ConfirmCompletion, Editor, EditorSettings, Input, MultiBuffer,
-            Redo, ToggleCodeActions, Undo,
+            self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, EditorSettings,
+            Input, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
         },
         fs::{FakeFs, Fs as _},
         language::{
@@ -3029,6 +3029,218 @@ mod tests {
         });
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_collaborating_with_renames(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let mut lang_registry = Arc::new(LanguageRegistry::new());
+        let fs = 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_servers) = LanguageServerConfig::fake();
+        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(), cx_a.background()).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(
+            "/dir",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "one.rs": "const ONE: usize = 1;",
+                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
+            }),
+        )
+        .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("/dir", 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, "one.rs").into(), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+
+        // Move cursor to a location that can be renamed.
+        let prepare_rename = editor_b.update(&mut cx_b, |editor, cx| {
+            editor.select_ranges([7..7], None, cx);
+            editor.rename(&Rename, cx).unwrap()
+        });
+
+        fake_language_server
+            .handle_request::<lsp::request::PrepareRenameRequest, _>(|params| {
+                assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
+                assert_eq!(params.position, lsp::Position::new(0, 7));
+                Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                    lsp::Position::new(0, 6),
+                    lsp::Position::new(0, 9),
+                )))
+            })
+            .next()
+            .await
+            .unwrap();
+        prepare_rename.await.unwrap();
+        editor_b.update(&mut cx_b, |editor, cx| {
+            assert_eq!(editor.selected_ranges(cx), [6..9]);
+            editor.handle_input(&Input("T".to_string()), cx);
+            editor.handle_input(&Input("H".to_string()), cx);
+            editor.handle_input(&Input("R".to_string()), cx);
+            editor.handle_input(&Input("E".to_string()), cx);
+            editor.handle_input(&Input("E".to_string()), cx);
+        });
+
+        let confirm_rename = workspace_b.update(&mut cx_b, |workspace, cx| {
+            Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
+        });
+        fake_language_server
+            .handle_request::<lsp::request::Rename, _>(|params| {
+                assert_eq!(
+                    params.text_document_position.text_document.uri.as_str(),
+                    "file:///dir/one.rs"
+                );
+                assert_eq!(
+                    params.text_document_position.position,
+                    lsp::Position::new(0, 6)
+                );
+                assert_eq!(params.new_name, "THREE");
+                Some(lsp::WorkspaceEdit {
+                    changes: Some(
+                        [
+                            (
+                                lsp::Url::from_file_path("/dir/one.rs").unwrap(),
+                                vec![lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(0, 6),
+                                        lsp::Position::new(0, 9),
+                                    ),
+                                    "THREE".to_string(),
+                                )],
+                            ),
+                            (
+                                lsp::Url::from_file_path("/dir/two.rs").unwrap(),
+                                vec![
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 24),
+                                            lsp::Position::new(0, 27),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 35),
+                                            lsp::Position::new(0, 38),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                ],
+                            ),
+                        ]
+                        .into_iter()
+                        .collect(),
+                    ),
+                    ..Default::default()
+                })
+            })
+            .next()
+            .await
+            .unwrap();
+        confirm_rename.await.unwrap();
+
+        let rename_editor = workspace_b.read_with(&cx_b, |workspace, cx| {
+            workspace
+                .active_item(cx)
+                .unwrap()
+                .downcast::<Editor>()
+                .unwrap()
+        });
+        rename_editor.update(&mut cx_b, |editor, cx| {
+            assert_eq!(
+                editor.text(cx),
+                "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;"
+            );
+            editor.undo(&Undo, cx);
+            assert_eq!(
+                editor.text(cx),
+                "const TWO: usize = one::ONE + one::ONE;\nconst ONE: usize = 1;"
+            );
+            editor.redo(&Redo, cx);
+            assert_eq!(
+                editor.text(cx),
+                "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;"
+            );
+        });
+
+        // Ensure temporary rename edits cannot be undone/redone.
+        editor_b.update(&mut cx_b, |editor, cx| {
+            editor.undo(&Undo, cx);
+            assert_eq!(editor.text(cx), "const ONE: usize = 1;");
+            editor.undo(&Undo, cx);
+            assert_eq!(editor.text(cx), "const ONE: usize = 1;");
+            editor.redo(&Redo, cx);
+            assert_eq!(editor.text(cx), "const THREE: usize = 1;");
+        })
+    }
+
     #[gpui::test(iterations = 10)]
     async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
@@ -3619,6 +3831,13 @@ mod tests {
                     },
                 )])
             });
+
+            fake_server.handle_request::<lsp::request::PrepareRenameRequest, _>(|params| {
+                Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                    params.position,
+                    params.position,
+                )))
+            });
         });
 
         Arc::get_mut(&mut host_lang_registry)
@@ -4251,6 +4470,26 @@ mod tests {
                             save.await;
                         }
                     }
+                    40..=45 => {
+                        let prepare_rename = project.update(&mut cx, |project, cx| {
+                            log::info!(
+                                "Guest {}: preparing rename for buffer {:?}",
+                                guest_id,
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.borrow_mut().gen_range(0..=buffer.read(cx).len());
+                            project.prepare_rename(buffer, offset, cx)
+                        });
+                        let prepare_rename = cx.background().spawn(async move {
+                            prepare_rename.await.expect("prepare rename request failed");
+                        });
+                        if rng.borrow_mut().gen_bool(0.3) {
+                            log::info!("Guest {}: detaching prepare rename request", guest_id);
+                            prepare_rename.detach();
+                        } else {
+                            prepare_rename.await;
+                        }
+                    }
                     _ => {
                         buffer.update(&mut cx, |buffer, cx| {
                             log::info!(