diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 47238646ea..79f2dac0ec 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4086,7 +4086,7 @@ impl Editor { .detach_and_log_err(cx); } - fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { 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, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 0c5c084060..0f2ed5bbce 100644 --- a/crates/project/src/lsp_command.rs +++ b/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, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a8bee2ff43..823ddff7c5 100644 --- a/crates/project/src/project.rs +++ b/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::(|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;" + ); } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 9c0a1e2ec7..35330cfc2f 100644 --- a/crates/server/src/rpc.rs +++ b/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(¶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::() + .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::(|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::(|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::() + .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::(|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!(