@@ -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;"
+ );
}
}
@@ -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(¶ms, 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!(