@@ -1,1889 +1,1884 @@
-//todo(partially ported)
-// use std::{
-// path::Path,
-// sync::{
-// atomic::{self, AtomicBool, AtomicUsize},
-// Arc,
-// },
-// };
-
-// use call::ActiveCall;
-// use editor::{
-// test::editor_test_context::{AssertionContextManager, EditorTestContext},
-// Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename,
-// ToggleCodeActions, Undo,
-// };
-// use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext};
-// use indoc::indoc;
-// use language::{
-// language_settings::{AllLanguageSettings, InlayHintSettings},
-// tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig,
-// };
-// use rpc::RECEIVE_TIMEOUT;
-// use serde_json::json;
-// use settings::SettingsStore;
-// use text::Point;
-// use workspace::Workspace;
-
-// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
-
-// #[gpui::test(iterations = 10)]
-// async fn test_host_disconnect(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// cx_c: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// let client_c = server.create_client(cx_c, "user_c").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
-// .await;
-
-// cx_b.update(editor::init);
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// serde_json::json!({
-// "a.txt": "a-contents",
-// "b.txt": "b-contents",
-// }),
-// )
-// .await;
-
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-
-// let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees().next().unwrap());
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// executor.run_until_parked();
-
-// assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
-
-// let workspace_b =
-// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
-// let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
-
-// let editor_b = workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "b.txt"), None, true, cx)
-// })
-// .unwrap()
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// //TODO: focus
-// assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
-// editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
-// //todo(is_edited)
-// // assert!(workspace_b.is_edited(cx_b));
-
-// // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
-// server.forbid_connections();
-// server.disconnect_client(client_a.peer_id().unwrap());
-// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-
-// project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
-
-// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
-
-// project_b.read_with(cx_b, |project, _| project.is_read_only());
-
-// assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
-
-// // Ensure client B's edited state is reset and that the whole window is blurred.
-
-// workspace_b.update(cx_b, |_, cx| {
-// assert_eq!(cx.focused_view_id(), None);
-// });
-// // assert!(!workspace_b.is_edited(cx_b));
-
-// // Ensure client B is not prompted to save edits when closing window after disconnecting.
-// let can_close = workspace_b
-// .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
-// .await
-// .unwrap();
-// assert!(can_close);
-
-// // Allow client A to reconnect to the server.
-// server.allow_connections();
-// executor.advance_clock(RECEIVE_TIMEOUT);
-
-// // Client B calls client A again after they reconnected.
-// let active_call_b = cx_b.read(ActiveCall::global);
-// active_call_b
-// .update(cx_b, |call, cx| {
-// call.invite(client_a.user_id().unwrap(), None, cx)
-// })
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// active_call_a
-// .update(cx_a, |call, cx| call.accept_incoming(cx))
-// .await
-// .unwrap();
-
-// active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// // Drop client A's connection again. We should still unshare it successfully.
-// server.forbid_connections();
-// server.disconnect_client(client_a.peer_id().unwrap());
-// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-
-// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
-// }
-
-// #[gpui::test]
-// async fn test_newline_above_or_below_does_not_move_guest_cursor(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// client_a
-// .fs()
-// .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-// // Open a buffer as client A
-// let buffer_a = project_a
-// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-// .await
-// .unwrap();
-// let window_a = cx_a.add_empty_window();
-// let editor_a =
-// window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
-// let mut editor_cx_a = EditorTestContext {
-// cx: cx_a,
-// window: window_a.into(),
-// editor: editor_a,
-// assertion_cx: AssertionContextManager::new(),
-// };
-
-// // Open a buffer as client B
-// let buffer_b = project_b
-// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
-// .await
-// .unwrap();
-// let window_b = cx_b.add_empty_window();
-// let editor_b =
-// window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
-// let mut editor_cx_b = EditorTestContext {
-// cx: cx_b,
-// window: window_b.into(),
-// editor: editor_b,
-// assertion_cx: AssertionContextManager::new(),
-// };
-
-// // Test newline above
-// editor_cx_a.set_selections_state(indoc! {"
-// Some textˇ
-// "});
-// editor_cx_b.set_selections_state(indoc! {"
-// Some textˇ
-// "});
-// editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
-// executor.run_until_parked();
-// editor_cx_a.assert_editor_state(indoc! {"
-// ˇ
-// Some text
-// "});
-// editor_cx_b.assert_editor_state(indoc! {"
-
-// Some textˇ
-// "});
-
-// // Test newline below
-// editor_cx_a.set_selections_state(indoc! {"
-
-// Some textˇ
-// "});
-// editor_cx_b.set_selections_state(indoc! {"
-
-// Some textˇ
-// "});
-// editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
-// executor.run_until_parked();
-// editor_cx_a.assert_editor_state(indoc! {"
-
-// Some text
-// ˇ
-// "});
-// editor_cx_b.assert_editor_state(indoc! {"
-
-// Some textˇ
-
-// "});
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_collaborating_with_completion(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// // Set up a fake language server.
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language
-// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-// capabilities: lsp::ServerCapabilities {
-// completion_provider: Some(lsp::CompletionOptions {
-// trigger_characters: Some(vec![".".to_string()]),
-// resolve_provider: Some(true),
-// ..Default::default()
-// }),
-// ..Default::default()
-// },
-// ..Default::default()
-// }))
-// .await;
-// client_a.language_registry().add(Arc::new(language));
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "main.rs": "fn main() { a }",
-// "other.rs": "",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-// // Open a file in an editor as the guest.
-// let buffer_b = project_b
-// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-// .await
-// .unwrap();
-// let window_b = cx_b.add_empty_window();
-// let editor_b = window_b.build_view(cx_b, |cx| {
-// Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
-// });
-
-// let fake_language_server = fake_language_servers.next().await.unwrap();
-// cx_a.foreground().run_until_parked();
-
-// buffer_b.read_with(cx_b, |buffer, _| {
-// assert!(!buffer.completion_triggers().is_empty())
-// });
-
-// // Type a completion trigger character as the guest.
-// editor_b.update(cx_b, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-// editor.handle_input(".", cx);
-// cx.focus(&editor_b);
-// });
-
-// // Receive a completion request as the host's language server.
-// // Return some completions from the host's language server.
-// cx_a.foreground().start_waiting();
-// fake_language_server
-// .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
-// assert_eq!(
-// params.text_document_position.text_document.uri,
-// lsp::Url::from_file_path("/a/main.rs").unwrap(),
-// );
-// assert_eq!(
-// params.text_document_position.position,
-// lsp::Position::new(0, 14),
-// );
-
-// Ok(Some(lsp::CompletionResponse::Array(vec![
-// lsp::CompletionItem {
-// label: "first_method(…)".into(),
-// detail: Some("fn(&mut self, B) -> C".into()),
-// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-// new_text: "first_method($1)".to_string(),
-// range: lsp::Range::new(
-// lsp::Position::new(0, 14),
-// lsp::Position::new(0, 14),
-// ),
-// })),
-// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-// ..Default::default()
-// },
-// lsp::CompletionItem {
-// label: "second_method(…)".into(),
-// detail: Some("fn(&mut self, C) -> D<E>".into()),
-// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-// new_text: "second_method()".to_string(),
-// range: lsp::Range::new(
-// lsp::Position::new(0, 14),
-// lsp::Position::new(0, 14),
-// ),
-// })),
-// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-// ..Default::default()
-// },
-// ])))
-// })
-// .next()
-// .await
-// .unwrap();
-// cx_a.foreground().finish_waiting();
-
-// // Open the buffer on the host.
-// let buffer_a = project_a
-// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-// .await
-// .unwrap();
-// cx_a.foreground().run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// assert_eq!(buffer.text(), "fn main() { a. }")
-// });
-
-// // Confirm a completion on the guest.
-
-// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
-// editor_b.update(cx_b, |editor, cx| {
-// editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
-// assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
-// });
-
-// // Return a resolved completion from the host's language server.
-// // The resolved completion has an additional text edit.
-// fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
-// |params, _| async move {
-// assert_eq!(params.label, "first_method(…)");
-// Ok(lsp::CompletionItem {
-// label: "first_method(…)".into(),
-// detail: Some("fn(&mut self, B) -> C".into()),
-// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-// new_text: "first_method($1)".to_string(),
-// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
-// })),
-// additional_text_edits: Some(vec![lsp::TextEdit {
-// new_text: "use d::SomeTrait;\n".to_string(),
-// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
-// }]),
-// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-// ..Default::default()
-// })
-// },
-// );
-
-// // The additional edit is applied.
-// cx_a.executor().run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// assert_eq!(
-// buffer.text(),
-// "use d::SomeTrait;\nfn main() { a.first_method() }"
-// );
-// });
-
-// buffer_b.read_with(cx_b, |buffer, _| {
-// assert_eq!(
-// buffer.text(),
-// "use d::SomeTrait;\nfn main() { a.first_method() }"
-// );
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_collaborating_with_code_actions(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// //
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// cx_b.update(editor::init);
-
-// // Set up a fake language server.
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-// client_a.language_registry().add(Arc::new(language));
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
-// "other.rs": "pub fn foo() -> usize { 4 }",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// // Join the project as client B.
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// let window_b =
-// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
-// let workspace_b = window_b.root(cx_b);
-// let editor_b = workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// let mut fake_language_server = fake_language_servers.next().await.unwrap();
-// let mut requests = fake_language_server
-// .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
-// 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));
-// Ok(None)
-// });
-// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
-// requests.next().await;
-
-// // Move cursor to a location that contains code actions.
-// editor_b.update(cx_b, |editor, cx| {
-// editor.change_selections(None, cx, |s| {
-// s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
-// });
-// cx.focus(&editor_b);
-// });
-
-// let mut requests = fake_language_server
-// .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
-// 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));
-
-// Ok(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()
-// },
-// )]))
-// });
-// executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
-// requests.next().await;
-
-// // Toggle code actions and wait for them to display.
-// editor_b.update(cx_b, |editor, cx| {
-// editor.toggle_code_actions(
-// &ToggleCodeActions {
-// deployed_from_indicator: false,
-// },
-// cx,
-// );
-// });
-// cx_a.foreground().run_until_parked();
-
-// editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
-
-// fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
-
-// // Confirming the code action will trigger a resolve request.
-// let confirm_action = workspace_b
-// .update(cx_b, |workspace, cx| {
-// Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
-// })
-// .unwrap();
-// fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
-// |_, _| async move {
-// Ok(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(cx_b, |editor, cx| {
-// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
-// editor.undo(&Undo, cx);
-// assert_eq!(
-// editor.text(cx),
-// "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
-// );
-// editor.redo(&Redo, cx);
-// assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_collaborating_with_renames(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// cx_b.update(editor::init);
-
-// // Set up a fake language server.
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language
-// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-// capabilities: lsp::ServerCapabilities {
-// rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
-// prepare_provider: Some(true),
-// work_done_progress_options: Default::default(),
-// })),
-// ..Default::default()
-// },
-// ..Default::default()
-// }))
-// .await;
-// client_a.language_registry().add(Arc::new(language));
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/dir",
-// json!({
-// "one.rs": "const ONE: usize = 1;",
-// "two.rs": "const TWO: usize = one::ONE + one::ONE;"
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-// let window_b =
-// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
-// let workspace_b = window_b.root(cx_b);
-// let editor_b = workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "one.rs"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-// let fake_language_server = fake_language_servers.next().await.unwrap();
-
-// // Move cursor to a location that can be renamed.
-// let prepare_rename = editor_b.update(cx_b, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
-// editor.rename(&Rename, cx).unwrap()
-// });
-
-// fake_language_server
-// .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
-// assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
-// assert_eq!(params.position, lsp::Position::new(0, 7));
-// Ok(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(cx_b, |editor, cx| {
-// use editor::ToOffset;
-// let rename = editor.pending_rename().unwrap();
-// let buffer = editor.buffer().read(cx).snapshot(cx);
-// assert_eq!(
-// rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
-// 6..9
-// );
-// rename.editor.update(cx, |rename_editor, cx| {
-// rename_editor.buffer().update(cx, |rename_buffer, cx| {
-// rename_buffer.edit([(0..3, "THREE")], None, cx);
-// });
-// });
-// });
-
-// let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
-// Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
-// });
-// fake_language_server
-// .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
-// 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");
-// Ok(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(cx_b, |editor, cx| {
-// assert_eq!(
-// editor.text(cx),
-// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
-// );
-// editor.undo(&Undo, cx);
-// assert_eq!(
-// editor.text(cx),
-// "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
-// );
-// editor.redo(&Redo, cx);
-// assert_eq!(
-// editor.text(cx),
-// "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
-// );
-// });
-
-// // Ensure temporary rename edits cannot be undone/redone.
-// editor_b.update(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_language_server_statuses(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// cx_b.update(editor::init);
-
-// // Set up a fake language server.
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language
-// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-// name: "the-language-server",
-// ..Default::default()
-// }))
-// .await;
-// client_a.language_registry().add(Arc::new(language));
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/dir",
-// json!({
-// "main.rs": "const ONE: usize = 1;",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
-
-// let _buffer_a = project_a
-// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-// .await
-// .unwrap();
-
-// let fake_language_server = fake_language_servers.next().await.unwrap();
-// fake_language_server.start_progress("the-token").await;
-// fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
-// token: lsp::NumberOrString::String("the-token".to_string()),
-// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
-// lsp::WorkDoneProgressReport {
-// message: Some("the-message".to_string()),
-// ..Default::default()
-// },
-// )),
-// });
-// executor.run_until_parked();
-
-// project_a.read_with(cx_a, |project, _| {
-// let status = project.language_server_statuses().next().unwrap();
-// assert_eq!(status.name, "the-language-server");
-// assert_eq!(status.pending_work.len(), 1);
-// assert_eq!(
-// status.pending_work["the-token"].message.as_ref().unwrap(),
-// "the-message"
-// );
-// });
-
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// executor.run_until_parked();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-// project_b.read_with(cx_b, |project, _| {
-// let status = project.language_server_statuses().next().unwrap();
-// assert_eq!(status.name, "the-language-server");
-// });
-
-// fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
-// token: lsp::NumberOrString::String("the-token".to_string()),
-// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
-// lsp::WorkDoneProgressReport {
-// message: Some("the-message-2".to_string()),
-// ..Default::default()
-// },
-// )),
-// });
-// executor.run_until_parked();
-
-// project_a.read_with(cx_a, |project, _| {
-// let status = project.language_server_statuses().next().unwrap();
-// assert_eq!(status.name, "the-language-server");
-// assert_eq!(status.pending_work.len(), 1);
-// assert_eq!(
-// status.pending_work["the-token"].message.as_ref().unwrap(),
-// "the-message-2"
-// );
-// });
-
-// project_b.read_with(cx_b, |project, _| {
-// let status = project.language_server_statuses().next().unwrap();
-// assert_eq!(status.name, "the-language-server");
-// assert_eq!(status.pending_work.len(), 1);
-// assert_eq!(
-// status.pending_work["the-token"].message.as_ref().unwrap(),
-// "the-message-2"
-// );
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_share_project(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// cx_c: &mut TestAppContext,
-// ) {
-// let window_b = cx_b.add_empty_window();
-// let mut server = TestServer::start(executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// let client_c = server.create_client(cx_c, "user_c").await;
-// server
-// .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-// let active_call_c = cx_c.read(ActiveCall::global);
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// ".gitignore": "ignored-dir",
-// "a.txt": "a-contents",
-// "b.txt": "b-contents",
-// "ignored-dir": {
-// "c.txt": "",
-// "d.txt": "",
-// }
-// }),
-// )
-// .await;
-
-// // Invite client B to collaborate on a project
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| {
-// call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
-// })
-// .await
-// .unwrap();
-
-// // Join that project as client B
-
-// let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
-// executor.run_until_parked();
-// let call = incoming_call_b.borrow().clone().unwrap();
-// assert_eq!(call.calling_user.github_login, "user_a");
-// let initial_project = call.initial_project.unwrap();
-// active_call_b
-// .update(cx_b, |call, cx| call.accept_incoming(cx))
-// .await
-// .unwrap();
-// let client_b_peer_id = client_b.peer_id().unwrap();
-// let project_b = client_b
-// .build_remote_project(initial_project.id, cx_b)
-// .await;
-
-// let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
-
-// executor.run_until_parked();
-
-// project_a.read_with(cx_a, |project, _| {
-// let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
-// assert_eq!(client_b_collaborator.replica_id, replica_id_b);
-// });
-
-// project_b.read_with(cx_b, |project, cx| {
-// let worktree = project.worktrees().next().unwrap().read(cx);
-// assert_eq!(
-// worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
-// [
-// Path::new(".gitignore"),
-// Path::new("a.txt"),
-// Path::new("b.txt"),
-// Path::new("ignored-dir"),
-// ]
-// );
-// });
-
-// project_b
-// .update(cx_b, |project, cx| {
-// let worktree = project.worktrees().next().unwrap();
-// let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
-// project.expand_entry(worktree_id, entry.id, cx).unwrap()
-// })
-// .await
-// .unwrap();
-
-// project_b.read_with(cx_b, |project, cx| {
-// let worktree = project.worktrees().next().unwrap().read(cx);
-// assert_eq!(
-// worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
-// [
-// Path::new(".gitignore"),
-// Path::new("a.txt"),
-// Path::new("b.txt"),
-// Path::new("ignored-dir"),
-// Path::new("ignored-dir/c.txt"),
-// Path::new("ignored-dir/d.txt"),
-// ]
-// );
-// });
-
-// // Open the same file as client B and client A.
-// let buffer_b = project_b
-// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
-// .await
-// .unwrap();
-
-// buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
-
-// project_a.read_with(cx_a, |project, cx| {
-// assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
-// });
-// let buffer_a = project_a
-// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
-// .await
-// .unwrap();
-
-// let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
-
-// // Client A sees client B's selection
-// executor.run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// buffer
-// .snapshot()
-// .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
-// .count()
-// == 1
-// });
-
-// // Edit the buffer as client B and see that edit as client A.
-// editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
-// executor.run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// assert_eq!(buffer.text(), "ok, b-contents")
-// });
-
-// // Client B can invite client C on a project shared by client A.
-// active_call_b
-// .update(cx_b, |call, cx| {
-// call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
-// })
-// .await
-// .unwrap();
-
-// let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
-// executor.run_until_parked();
-// let call = incoming_call_c.borrow().clone().unwrap();
-// assert_eq!(call.calling_user.github_login, "user_b");
-// let initial_project = call.initial_project.unwrap();
-// active_call_c
-// .update(cx_c, |call, cx| call.accept_incoming(cx))
-// .await
-// .unwrap();
-// let _project_c = client_c
-// .build_remote_project(initial_project.id, cx_c)
-// .await;
-
-// // Client B closes the editor, and client A sees client B's selections removed.
-// cx_b.update(move |_| drop(editor_b));
-// executor.run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// buffer
-// .snapshot()
-// .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
-// .count()
-// == 0
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_on_input_format_from_host_to_guest(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// // Set up a fake language server.
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language
-// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-// capabilities: lsp::ServerCapabilities {
-// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
-// first_trigger_character: ":".to_string(),
-// more_trigger_character: Some(vec![">".to_string()]),
-// }),
-// ..Default::default()
-// },
-// ..Default::default()
-// }))
-// .await;
-// client_a.language_registry().add(Arc::new(language));
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "main.rs": "fn main() { a }",
-// "other.rs": "// Test file",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-// // Open a file in an editor as the host.
-// let buffer_a = project_a
-// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-// .await
-// .unwrap();
-// let window_a = cx_a.add_empty_window();
-// let editor_a = window_a
-// .update(cx_a, |_, cx| {
-// cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx))
-// })
-// .unwrap();
-
-// let fake_language_server = fake_language_servers.next().await.unwrap();
-// executor.run_until_parked();
-
-// // Receive an OnTypeFormatting request as the host's language server.
-// // Return some formattings from the host's language server.
-// fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
-// |params, _| async move {
-// assert_eq!(
-// params.text_document_position.text_document.uri,
-// lsp::Url::from_file_path("/a/main.rs").unwrap(),
-// );
-// assert_eq!(
-// params.text_document_position.position,
-// lsp::Position::new(0, 14),
-// );
-
-// Ok(Some(vec![lsp::TextEdit {
-// new_text: "~<".to_string(),
-// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
-// }]))
-// },
-// );
-
-// // Open the buffer on the guest and see that the formattings worked
-// let buffer_b = project_b
-// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-// .await
-// .unwrap();
-
-// // Type a on type formatting trigger character as the guest.
-// editor_a.update(cx_a, |editor, cx| {
-// cx.focus(&editor_a);
-// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-// editor.handle_input(">", cx);
-// });
-
-// executor.run_until_parked();
-
-// buffer_b.read_with(cx_b, |buffer, _| {
-// assert_eq!(buffer.text(), "fn main() { a>~< }")
-// });
-
-// // Undo should remove LSP edits first
-// editor_a.update(cx_a, |editor, cx| {
-// assert_eq!(editor.text(cx), "fn main() { a>~< }");
-// editor.undo(&Undo, cx);
-// assert_eq!(editor.text(cx), "fn main() { a> }");
-// });
-// executor.run_until_parked();
-
-// buffer_b.read_with(cx_b, |buffer, _| {
-// assert_eq!(buffer.text(), "fn main() { a> }")
-// });
-
-// editor_a.update(cx_a, |editor, cx| {
-// assert_eq!(editor.text(cx), "fn main() { a> }");
-// editor.undo(&Undo, cx);
-// assert_eq!(editor.text(cx), "fn main() { a }");
-// });
-// executor.run_until_parked();
-
-// buffer_b.read_with(cx_b, |buffer, _| {
-// assert_eq!(buffer.text(), "fn main() { a }")
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_on_input_format_from_guest_to_host(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-
-// // Set up a fake language server.
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language
-// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-// capabilities: lsp::ServerCapabilities {
-// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
-// first_trigger_character: ":".to_string(),
-// more_trigger_character: Some(vec![">".to_string()]),
-// }),
-// ..Default::default()
-// },
-// ..Default::default()
-// }))
-// .await;
-// client_a.language_registry().add(Arc::new(language));
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "main.rs": "fn main() { a }",
-// "other.rs": "// Test file",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-// // Open a file in an editor as the guest.
-// let buffer_b = project_b
-// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-// .await
-// .unwrap();
-// let window_b = cx_b.add_empty_window();
-// let editor_b = window_b.build_view(cx_b, |cx| {
-// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
-// });
-
-// let fake_language_server = fake_language_servers.next().await.unwrap();
-// executor.run_until_parked();
-// // Type a on type formatting trigger character as the guest.
-// editor_b.update(cx_b, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-// editor.handle_input(":", cx);
-// cx.focus(&editor_b);
-// });
-
-// // Receive an OnTypeFormatting request as the host's language server.
-// // Return some formattings from the host's language server.
-// cx_a.foreground().start_waiting();
-// fake_language_server
-// .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
-// assert_eq!(
-// params.text_document_position.text_document.uri,
-// lsp::Url::from_file_path("/a/main.rs").unwrap(),
-// );
-// assert_eq!(
-// params.text_document_position.position,
-// lsp::Position::new(0, 14),
-// );
-
-// Ok(Some(vec![lsp::TextEdit {
-// new_text: "~:".to_string(),
-// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
-// }]))
-// })
-// .next()
-// .await
-// .unwrap();
-// cx_a.foreground().finish_waiting();
-
-// // Open the buffer on the host and see that the formattings worked
-// let buffer_a = project_a
-// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-// .await
-// .unwrap();
-// executor.run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// assert_eq!(buffer.text(), "fn main() { a:~: }")
-// });
-
-// // Undo should remove LSP edits first
-// editor_b.update(cx_b, |editor, cx| {
-// assert_eq!(editor.text(cx), "fn main() { a:~: }");
-// editor.undo(&Undo, cx);
-// assert_eq!(editor.text(cx), "fn main() { a: }");
-// });
-// executor.run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// assert_eq!(buffer.text(), "fn main() { a: }")
-// });
-
-// editor_b.update(cx_b, |editor, cx| {
-// assert_eq!(editor.text(cx), "fn main() { a: }");
-// editor.undo(&Undo, cx);
-// assert_eq!(editor.text(cx), "fn main() { a }");
-// });
-// executor.run_until_parked();
-
-// buffer_a.read_with(cx_a, |buffer, _| {
-// assert_eq!(buffer.text(), "fn main() { a }")
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_mutual_editor_inlay_hint_cache_update(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// cx_a.update(|cx| {
-// cx.update_global(|store: &mut SettingsStore, cx| {
-// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-// settings.defaults.inlay_hints = Some(InlayHintSettings {
-// enabled: true,
-// show_type_hints: true,
-// show_parameter_hints: false,
-// show_other_hints: true,
-// })
-// });
-// });
-// });
-// cx_b.update(|cx| {
-// cx.update_global(|store: &mut SettingsStore, cx| {
-// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-// settings.defaults.inlay_hints = Some(InlayHintSettings {
-// enabled: true,
-// show_type_hints: true,
-// show_parameter_hints: false,
-// show_other_hints: true,
-// })
-// });
-// });
-// });
-
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language
-// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-// capabilities: lsp::ServerCapabilities {
-// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-// ..Default::default()
-// },
-// ..Default::default()
-// }))
-// .await;
-// let language = Arc::new(language);
-// client_a.language_registry().add(Arc::clone(&language));
-// client_b.language_registry().add(language);
-
-// // Client A opens a project.
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
-// "other.rs": "// Test file",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// // Client B joins the project
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a);
-// cx_a.foreground().start_waiting();
-
-// // The host opens a rust file.
-// let _buffer_a = project_a
-// .update(cx_a, |project, cx| {
-// project.open_local_buffer("/a/main.rs", cx)
-// })
-// .await
-// .unwrap();
-// let fake_language_server = fake_language_servers.next().await.unwrap();
-// let editor_a = workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// // Set up the language server to return an additional inlay hint on each request.
-// let edits_made = Arc::new(AtomicUsize::new(0));
-// let closure_edits_made = Arc::clone(&edits_made);
-// fake_language_server
-// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-// let task_edits_made = Arc::clone(&closure_edits_made);
-// async move {
-// assert_eq!(
-// params.text_document.uri,
-// lsp::Url::from_file_path("/a/main.rs").unwrap(),
-// );
-// let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
-// Ok(Some(vec![lsp::InlayHint {
-// position: lsp::Position::new(0, edits_made as u32),
-// label: lsp::InlayHintLabel::String(edits_made.to_string()),
-// kind: None,
-// text_edits: None,
-// tooltip: None,
-// padding_left: None,
-// padding_right: None,
-// data: None,
-// }]))
-// }
-// })
-// .next()
-// .await
-// .unwrap();
-
-// executor.run_until_parked();
-
-// let initial_edit = edits_made.load(atomic::Ordering::Acquire);
-// editor_a.update(cx_a, |editor, _| {
-// assert_eq!(
-// vec![initial_edit.to_string()],
-// extract_hint_labels(editor),
-// "Host should get its first hints when opens an editor"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 1,
-// "Host editor update the cache version after every cache/view change",
-// );
-// });
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-// let editor_b = workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// executor.run_until_parked();
-// editor_b.update(cx_b, |editor, _| {
-// assert_eq!(
-// vec![initial_edit.to_string()],
-// extract_hint_labels(editor),
-// "Client should get its first hints when opens an editor"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 1,
-// "Guest editor update the cache version after every cache/view change"
-// );
-// });
-
-// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
-// editor_b.update(cx_b, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
-// editor.handle_input(":", cx);
-// cx.focus(&editor_b);
-// });
-
-// executor.run_until_parked();
-// editor_a.update(cx_a, |editor, _| {
-// assert_eq!(
-// vec![after_client_edit.to_string()],
-// extract_hint_labels(editor),
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(inlay_cache.version(), 2);
-// });
-// editor_b.update(cx_b, |editor, _| {
-// assert_eq!(
-// vec![after_client_edit.to_string()],
-// extract_hint_labels(editor),
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(inlay_cache.version(), 2);
-// });
-
-// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
-// editor_a.update(cx_a, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-// editor.handle_input("a change to increment both buffers' versions", cx);
-// cx.focus(&editor_a);
-// });
-
-// executor.run_until_parked();
-// editor_a.update(cx_a, |editor, _| {
-// assert_eq!(
-// vec![after_host_edit.to_string()],
-// extract_hint_labels(editor),
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(inlay_cache.version(), 3);
-// });
-// editor_b.update(cx_b, |editor, _| {
-// assert_eq!(
-// vec![after_host_edit.to_string()],
-// extract_hint_labels(editor),
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(inlay_cache.version(), 3);
-// });
-
-// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
-// fake_language_server
-// .request::<lsp::request::InlayHintRefreshRequest>(())
-// .await
-// .expect("inlay refresh request failed");
-
-// executor.run_until_parked();
-// editor_a.update(cx_a, |editor, _| {
-// assert_eq!(
-// vec![after_special_edit_for_refresh.to_string()],
-// extract_hint_labels(editor),
-// "Host should react to /refresh LSP request"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 4,
-// "Host should accepted all edits and bump its cache version every time"
-// );
-// });
-// editor_b.update(cx_b, |editor, _| {
-// assert_eq!(
-// vec![after_special_edit_for_refresh.to_string()],
-// extract_hint_labels(editor),
-// "Guest should get a /refresh LSP request propagated by host"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 4,
-// "Guest should accepted all edits and bump its cache version every time"
-// );
-// });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_inlay_hint_refresh_is_forwarded(
-// executor: BackgroundExecutor,
-// cx_a: &mut TestAppContext,
-// cx_b: &mut TestAppContext,
-// ) {
-// let mut server = TestServer::start(&executor).await;
-// let client_a = server.create_client(cx_a, "user_a").await;
-// let client_b = server.create_client(cx_b, "user_b").await;
-// server
-// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-// .await;
-// let active_call_a = cx_a.read(ActiveCall::global);
-// let active_call_b = cx_b.read(ActiveCall::global);
-
-// cx_a.update(editor::init);
-// cx_b.update(editor::init);
-
-// cx_a.update(|cx| {
-// cx.update_global(|store: &mut SettingsStore, cx| {
-// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-// settings.defaults.inlay_hints = Some(InlayHintSettings {
-// enabled: false,
-// show_type_hints: false,
-// show_parameter_hints: false,
-// show_other_hints: false,
-// })
-// });
-// });
-// });
-// cx_b.update(|cx| {
-// cx.update_global(|store: &mut SettingsStore, cx| {
-// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-// settings.defaults.inlay_hints = Some(InlayHintSettings {
-// enabled: true,
-// show_type_hints: true,
-// show_parameter_hints: true,
-// show_other_hints: true,
-// })
-// });
-// });
-// });
-
-// let mut language = Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// );
-// let mut fake_language_servers = language
-// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-// capabilities: lsp::ServerCapabilities {
-// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-// ..Default::default()
-// },
-// ..Default::default()
-// }))
-// .await;
-// let language = Arc::new(language);
-// client_a.language_registry().add(Arc::clone(&language));
-// client_b.language_registry().add(language);
-
-// client_a
-// .fs()
-// .insert_tree(
-// "/a",
-// json!({
-// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
-// "other.rs": "// Test file",
-// }),
-// )
-// .await;
-// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-// active_call_a
-// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-// .await
-// .unwrap();
-// let project_id = active_call_a
-// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-// .await
-// .unwrap();
-
-// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// active_call_b
-// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-// .await
-// .unwrap();
-
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-// cx_a.foreground().start_waiting();
-// cx_b.foreground().start_waiting();
-
-// let editor_a = workspace_a
-// .update(cx_a, |workspace, cx| {
-// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// let editor_b = workspace_b
-// .update(cx_b, |workspace, cx| {
-// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-// })
-// .await
-// .unwrap()
-// .downcast::<Editor>()
-// .unwrap();
-
-// let other_hints = Arc::new(AtomicBool::new(false));
-// let fake_language_server = fake_language_servers.next().await.unwrap();
-// let closure_other_hints = Arc::clone(&other_hints);
-// fake_language_server
-// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-// let task_other_hints = Arc::clone(&closure_other_hints);
-// async move {
-// assert_eq!(
-// params.text_document.uri,
-// lsp::Url::from_file_path("/a/main.rs").unwrap(),
-// );
-// let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
-// let character = if other_hints { 0 } else { 2 };
-// let label = if other_hints {
-// "other hint"
-// } else {
-// "initial hint"
-// };
-// Ok(Some(vec![lsp::InlayHint {
-// position: lsp::Position::new(0, character),
-// label: lsp::InlayHintLabel::String(label.to_string()),
-// kind: None,
-// text_edits: None,
-// tooltip: None,
-// padding_left: None,
-// padding_right: None,
-// data: None,
-// }]))
-// }
-// })
-// .next()
-// .await
-// .unwrap();
-// cx_a.foreground().finish_waiting();
-// cx_b.foreground().finish_waiting();
-
-// executor.run_until_parked();
-// editor_a.update(cx_a, |editor, _| {
-// assert!(
-// extract_hint_labels(editor).is_empty(),
-// "Host should get no hints due to them turned off"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 0,
-// "Turned off hints should not generate version updates"
-// );
-// });
-
-// executor.run_until_parked();
-// editor_b.update(cx_b, |editor, _| {
-// assert_eq!(
-// vec!["initial hint".to_string()],
-// extract_hint_labels(editor),
-// "Client should get its first hints when opens an editor"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 1,
-// "Should update cache verison after first hints"
-// );
-// });
-
-// other_hints.fetch_or(true, atomic::Ordering::Release);
-// fake_language_server
-// .request::<lsp::request::InlayHintRefreshRequest>(())
-// .await
-// .expect("inlay refresh request failed");
-// executor.run_until_parked();
-// editor_a.update(cx_a, |editor, _| {
-// assert!(
-// extract_hint_labels(editor).is_empty(),
-// "Host should get nop hints due to them turned off, even after the /refresh"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 0,
-// "Turned off hints should not generate version updates, again"
-// );
-// });
-
-// executor.run_until_parked();
-// editor_b.update(cx_b, |editor, _| {
-// assert_eq!(
-// vec!["other hint".to_string()],
-// extract_hint_labels(editor),
-// "Guest should get a /refresh LSP request propagated by host despite host hints are off"
-// );
-// let inlay_cache = editor.inlay_hint_cache();
-// assert_eq!(
-// inlay_cache.version(),
-// 2,
-// "Guest should accepted all edits and bump its cache version every time"
-// );
-// });
-// }
-
-// fn extract_hint_labels(editor: &Editor) -> Vec<String> {
-// let mut labels = Vec::new();
-// for hint in editor.inlay_hint_cache().hints() {
-// match hint.label {
-// project::InlayHintLabel::String(s) => labels.push(s),
-// _ => unreachable!(),
-// }
-// }
-// labels
-// }
+use std::{
+ path::Path,
+ sync::{
+ atomic::{self, AtomicBool, AtomicUsize},
+ Arc,
+ },
+};
+
+use call::ActiveCall;
+use editor::{
+ test::editor_test_context::{AssertionContextManager, EditorTestContext},
+ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToggleCodeActions,
+ Undo,
+};
+use futures::StreamExt;
+use gpui::{TestAppContext, VisualContext, VisualTestContext};
+use indoc::indoc;
+use language::{
+ language_settings::{AllLanguageSettings, InlayHintSettings},
+ tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig,
+};
+use rpc::RECEIVE_TIMEOUT;
+use serde_json::json;
+use settings::SettingsStore;
+use text::Point;
+use workspace::Workspace;
+
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+
+#[gpui::test(iterations = 10)]
+async fn test_host_disconnect(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
+ .await;
+
+ cx_b.update(editor::init);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ serde_json::json!({
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }),
+ )
+ .await;
+
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+
+ let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ cx_a.background_executor.run_until_parked();
+
+ assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
+
+ let workspace_b =
+ cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
+ let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
+
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "b.txt"), None, true, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ //TODO: focus
+ assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
+ editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
+ //todo(is_edited)
+ // assert!(workspace_b.is_edited(cx_b));
+
+ // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
+ server.forbid_connections();
+ server.disconnect_client(client_a.peer_id().unwrap());
+ cx_a.background_executor
+ .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+ project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
+
+ project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
+
+ project_b.read_with(cx_b, |project, _| project.is_read_only());
+
+ assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
+
+ // Ensure client B's edited state is reset and that the whole window is blurred.
+
+ workspace_b
+ .update(cx_b, |_, cx| {
+ assert_eq!(cx.focused(), None);
+ })
+ .unwrap();
+ // assert!(!workspace_b.is_edited(cx_b));
+
+ // Ensure client B is not prompted to save edits when closing window after disconnecting.
+ let can_close = workspace_b
+ .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
+ .unwrap()
+ .await
+ .unwrap();
+ assert!(can_close);
+
+ // Allow client A to reconnect to the server.
+ server.allow_connections();
+ cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);
+
+ // Client B calls client A again after they reconnected.
+ let active_call_b = cx_b.read(ActiveCall::global);
+ active_call_b
+ .update(cx_b, |call, cx| {
+ call.invite(client_a.user_id().unwrap(), None, cx)
+ })
+ .await
+ .unwrap();
+ cx_a.background_executor.run_until_parked();
+ active_call_a
+ .update(cx_a, |call, cx| call.accept_incoming(cx))
+ .await
+ .unwrap();
+
+ active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // Drop client A's connection again. We should still unshare it successfully.
+ server.forbid_connections();
+ server.disconnect_client(client_a.peer_id().unwrap());
+ cx_a.background_executor
+ .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+ project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
+}
+
+#[gpui::test]
+async fn test_newline_above_or_below_does_not_move_guest_cursor(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let executor = cx_a.executor();
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ client_a
+ .fs()
+ .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ // Open a buffer as client A
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .await
+ .unwrap();
+ let window_a = cx_a.add_empty_window();
+ let editor_a =
+ window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
+
+ let mut editor_cx_a = EditorTestContext {
+ cx: VisualTestContext::from_window(window_a, cx_a),
+ window: window_a.into(),
+ editor: editor_a,
+ assertion_cx: AssertionContextManager::new(),
+ };
+
+ let window_b = cx_b.add_empty_window();
+ let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
+
+ // Open a buffer as client B
+ let buffer_b = project_b
+ .update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .await
+ .unwrap();
+ let editor_b = window_b.build_view(&mut cx_b, |cx| {
+ Editor::for_buffer(buffer_b, Some(project_b), cx)
+ });
+ let mut editor_cx_b = EditorTestContext {
+ cx: cx_b,
+ window: window_b.into(),
+ editor: editor_b,
+ assertion_cx: AssertionContextManager::new(),
+ };
+
+ // Test newline above
+ editor_cx_a.set_selections_state(indoc! {"
+ Some textˇ
+ "});
+ editor_cx_b.set_selections_state(indoc! {"
+ Some textˇ
+ "});
+ editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
+ executor.run_until_parked();
+ editor_cx_a.assert_editor_state(indoc! {"
+ ˇ
+ Some text
+ "});
+ editor_cx_b.assert_editor_state(indoc! {"
+
+ Some textˇ
+ "});
+
+ // Test newline below
+ editor_cx_a.set_selections_state(indoc! {"
+
+ Some textˇ
+ "});
+ editor_cx_b.set_selections_state(indoc! {"
+
+ Some textˇ
+ "});
+ editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
+ executor.run_until_parked();
+ editor_cx_a.assert_editor_state(indoc! {"
+
+ Some text
+ ˇ
+ "});
+ editor_cx_b.assert_editor_state(indoc! {"
+
+ Some textˇ
+
+ "});
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ // Set up a fake language server.
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![".".to_string()]),
+ resolve_provider: Some(true),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ client_a.language_registry().add(Arc::new(language));
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a }",
+ "other.rs": "",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ // Open a file in an editor as the guest.
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .await
+ .unwrap();
+ let window_b = cx_b.add_empty_window();
+ let editor_b = window_b.build_view(cx_b, |cx| {
+ Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
+ });
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ cx_a.background_executor.run_until_parked();
+
+ buffer_b.read_with(cx_b, |buffer, _| {
+ assert!(!buffer.completion_triggers().is_empty())
+ });
+
+ let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
+
+ // Type a completion trigger character as the guest.
+ editor_b.update(&mut cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(".", cx);
+ });
+ cx_b.focus_view(&editor_b);
+
+ // Receive a completion request as the host's language server.
+ // Return some completions from the host's language server.
+ cx_a.executor().start_waiting();
+ fake_language_server
+ .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
+ assert_eq!(
+ params.text_document_position.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ assert_eq!(
+ params.text_document_position.position,
+ lsp::Position::new(0, 14),
+ );
+
+ Ok(Some(lsp::CompletionResponse::Array(vec![
+ lsp::CompletionItem {
+ label: "first_method(…)".into(),
+ detail: Some("fn(&mut self, B) -> C".into()),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ new_text: "first_method($1)".to_string(),
+ range: lsp::Range::new(
+ lsp::Position::new(0, 14),
+ lsp::Position::new(0, 14),
+ ),
+ })),
+ insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+ ..Default::default()
+ },
+ lsp::CompletionItem {
+ label: "second_method(…)".into(),
+ detail: Some("fn(&mut self, C) -> D<E>".into()),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ new_text: "second_method()".to_string(),
+ range: lsp::Range::new(
+ lsp::Position::new(0, 14),
+ lsp::Position::new(0, 14),
+ ),
+ })),
+ insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+ ..Default::default()
+ },
+ ])))
+ })
+ .next()
+ .await
+ .unwrap();
+ cx_a.executor().finish_waiting();
+
+ // Open the buffer on the host.
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .await
+ .unwrap();
+ cx_a.executor().run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(buffer.text(), "fn main() { a. }")
+ });
+
+ // Confirm a completion on the guest.
+
+ editor_b.update(&mut cx_b, |editor, cx| {
+ assert!(editor.context_menu_visible());
+ editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
+ assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
+ });
+
+ // Return a resolved completion from the host's language server.
+ // The resolved completion has an additional text edit.
+ fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
+ |params, _| async move {
+ assert_eq!(params.label, "first_method(…)");
+ Ok(lsp::CompletionItem {
+ label: "first_method(…)".into(),
+ detail: Some("fn(&mut self, B) -> C".into()),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ new_text: "first_method($1)".to_string(),
+ range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
+ })),
+ additional_text_edits: Some(vec![lsp::TextEdit {
+ new_text: "use d::SomeTrait;\n".to_string(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
+ }]),
+ insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+ ..Default::default()
+ })
+ },
+ );
+
+ // The additional edit is applied.
+ cx_a.executor().run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(
+ buffer.text(),
+ "use d::SomeTrait;\nfn main() { a.first_method() }"
+ );
+ });
+
+ buffer_b.read_with(&mut cx_b, |buffer, _| {
+ assert_eq!(
+ buffer.text(),
+ "use d::SomeTrait;\nfn main() { a.first_method() }"
+ );
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_code_actions(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ //
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ cx_b.update(editor::init);
+
+ // Set up a fake language server.
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
+ client_a.language_registry().add(Arc::new(language));
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
+ "other.rs": "pub fn foo() -> usize { 4 }",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // Join the project as client B.
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let mut fake_language_server = fake_language_servers.next().await.unwrap();
+ let mut requests = fake_language_server
+ .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
+ 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));
+ Ok(None)
+ });
+ cx_a.background_executor
+ .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
+ requests.next().await;
+
+ // Move cursor to a location that contains code actions.
+ editor_b.update(cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
+ });
+ });
+ cx_b.focus_view(&editor_b);
+
+ let mut requests = fake_language_server
+ .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
+ 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));
+
+ Ok(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()
+ },
+ )]))
+ });
+ cx_a.background_executor
+ .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
+ requests.next().await;
+
+ // Toggle code actions and wait for them to display.
+ editor_b.update(cx_b, |editor, cx| {
+ editor.toggle_code_actions(
+ &ToggleCodeActions {
+ deployed_from_indicator: false,
+ },
+ cx,
+ );
+ });
+ cx_a.background_executor.run_until_parked();
+
+ editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible()));
+
+ fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
+
+ // Confirming the code action will trigger a resolve request.
+ let confirm_action = editor_b
+ .update(cx_b, |editor, cx| {
+ Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
+ })
+ .unwrap();
+ fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
+ |_, _| async move {
+ Ok(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.update(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ code_action_editor.update(cx_b, |editor, cx| {
+ assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
+ editor.undo(&Undo, cx);
+ assert_eq!(
+ editor.text(cx),
+ "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
+ );
+ editor.redo(&Redo, cx);
+ assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ cx_b.update(editor::init);
+
+ // Set up a fake language server.
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
+ prepare_provider: Some(true),
+ work_done_progress_options: Default::default(),
+ })),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ client_a.language_registry().add(Arc::new(language));
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/dir",
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;"
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "one.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+
+ // Move cursor to a location that can be renamed.
+ let prepare_rename = editor_b.update(cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
+ editor.rename(&Rename, cx).unwrap()
+ });
+
+ fake_language_server
+ .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
+ assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
+ assert_eq!(params.position, lsp::Position::new(0, 7));
+ Ok(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(cx_b, |editor, cx| {
+ use editor::ToOffset;
+ let rename = editor.pending_rename().unwrap();
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ assert_eq!(
+ rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
+ 6..9
+ );
+ rename.editor.update(cx, |rename_editor, cx| {
+ rename_editor.buffer().update(cx, |rename_buffer, cx| {
+ rename_buffer.edit([(0..3, "THREE")], None, cx);
+ });
+ });
+ });
+
+ let confirm_rename = editor_b.update(cx_b, |editor, cx| {
+ Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
+ });
+ fake_language_server
+ .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
+ 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");
+ Ok(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.update(cx_b, |workspace, cx| {
+ workspace.active_item_as::<Editor>(cx).unwrap()
+ });
+
+ rename_editor.update(cx_b, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
+ );
+ editor.undo(&Undo, cx);
+ assert_eq!(
+ editor.text(cx),
+ "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
+ );
+ editor.redo(&Redo, cx);
+ assert_eq!(
+ editor.text(cx),
+ "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
+ );
+ });
+
+ // Ensure temporary rename edits cannot be undone/redone.
+ editor_b.update(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_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let executor = cx_a.executor();
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ cx_b.update(editor::init);
+
+ // Set up a fake language server.
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ name: "the-language-server",
+ ..Default::default()
+ }))
+ .await;
+ client_a.language_registry().add(Arc::new(language));
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/dir",
+ json!({
+ "main.rs": "const ONE: usize = 1;",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
+
+ let _buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .await
+ .unwrap();
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ fake_language_server.start_progress("the-token").await;
+ fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+ token: lsp::NumberOrString::String("the-token".to_string()),
+ value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
+ lsp::WorkDoneProgressReport {
+ message: Some("the-message".to_string()),
+ ..Default::default()
+ },
+ )),
+ });
+ executor.run_until_parked();
+
+ project_a.read_with(cx_a, |project, _| {
+ let status = project.language_server_statuses().next().unwrap();
+ assert_eq!(status.name, "the-language-server");
+ assert_eq!(status.pending_work.len(), 1);
+ assert_eq!(
+ status.pending_work["the-token"].message.as_ref().unwrap(),
+ "the-message"
+ );
+ });
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ executor.run_until_parked();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ project_b.read_with(cx_b, |project, _| {
+ let status = project.language_server_statuses().next().unwrap();
+ assert_eq!(status.name, "the-language-server");
+ });
+
+ fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+ token: lsp::NumberOrString::String("the-token".to_string()),
+ value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
+ lsp::WorkDoneProgressReport {
+ message: Some("the-message-2".to_string()),
+ ..Default::default()
+ },
+ )),
+ });
+ executor.run_until_parked();
+
+ project_a.read_with(cx_a, |project, _| {
+ let status = project.language_server_statuses().next().unwrap();
+ assert_eq!(status.name, "the-language-server");
+ assert_eq!(status.pending_work.len(), 1);
+ assert_eq!(
+ status.pending_work["the-token"].message.as_ref().unwrap(),
+ "the-message-2"
+ );
+ });
+
+ project_b.read_with(cx_b, |project, _| {
+ let status = project.language_server_statuses().next().unwrap();
+ assert_eq!(status.name, "the-language-server");
+ assert_eq!(status.pending_work.len(), 1);
+ assert_eq!(
+ status.pending_work["the-token"].message.as_ref().unwrap(),
+ "the-message-2"
+ );
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_share_project(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
+) {
+ let executor = cx_a.executor();
+ let window_b = cx_b.add_empty_window();
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
+ server
+ .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+ let active_call_c = cx_c.read(ActiveCall::global);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ ".gitignore": "ignored-dir",
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ "ignored-dir": {
+ "c.txt": "",
+ "d.txt": "",
+ }
+ }),
+ )
+ .await;
+
+ // Invite client B to collaborate on a project
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| {
+ call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
+ })
+ .await
+ .unwrap();
+
+ // Join that project as client B
+
+ let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
+ executor.run_until_parked();
+ let call = incoming_call_b.borrow().clone().unwrap();
+ assert_eq!(call.calling_user.github_login, "user_a");
+ let initial_project = call.initial_project.unwrap();
+ active_call_b
+ .update(cx_b, |call, cx| call.accept_incoming(cx))
+ .await
+ .unwrap();
+ let client_b_peer_id = client_b.peer_id().unwrap();
+ let project_b = client_b
+ .build_remote_project(initial_project.id, cx_b)
+ .await;
+
+ let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
+
+ executor.run_until_parked();
+
+ project_a.read_with(cx_a, |project, _| {
+ let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
+ assert_eq!(client_b_collaborator.replica_id, replica_id_b);
+ });
+
+ project_b.read_with(cx_b, |project, cx| {
+ let worktree = project.worktrees().next().unwrap().read(cx);
+ assert_eq!(
+ worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
+ [
+ Path::new(".gitignore"),
+ Path::new("a.txt"),
+ Path::new("b.txt"),
+ Path::new("ignored-dir"),
+ ]
+ );
+ });
+
+ project_b
+ .update(cx_b, |project, cx| {
+ let worktree = project.worktrees().next().unwrap();
+ let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
+ project.expand_entry(worktree_id, entry.id, cx).unwrap()
+ })
+ .await
+ .unwrap();
+
+ project_b.read_with(cx_b, |project, cx| {
+ let worktree = project.worktrees().next().unwrap().read(cx);
+ assert_eq!(
+ worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
+ [
+ Path::new(".gitignore"),
+ Path::new("a.txt"),
+ Path::new("b.txt"),
+ Path::new("ignored-dir"),
+ Path::new("ignored-dir/c.txt"),
+ Path::new("ignored-dir/d.txt"),
+ ]
+ );
+ });
+
+ // Open the same file as client B and client A.
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+ .await
+ .unwrap();
+
+ buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
+
+ project_a.read_with(cx_a, |project, cx| {
+ assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
+ });
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+ .await
+ .unwrap();
+
+ let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
+
+ // Client A sees client B's selection
+ executor.run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ buffer
+ .snapshot()
+ .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
+ .count()
+ == 1
+ });
+
+ // Edit the buffer as client B and see that edit as client A.
+ let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
+ editor_b.update(&mut cx_b, |editor, cx| editor.handle_input("ok, ", cx));
+ executor.run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(buffer.text(), "ok, b-contents")
+ });
+
+ // Client B can invite client C on a project shared by client A.
+ active_call_b
+ .update(&mut cx_b, |call, cx| {
+ call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
+ })
+ .await
+ .unwrap();
+
+ let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
+ executor.run_until_parked();
+ let call = incoming_call_c.borrow().clone().unwrap();
+ assert_eq!(call.calling_user.github_login, "user_b");
+ let initial_project = call.initial_project.unwrap();
+ active_call_c
+ .update(cx_c, |call, cx| call.accept_incoming(cx))
+ .await
+ .unwrap();
+ let _project_c = client_c
+ .build_remote_project(initial_project.id, cx_c)
+ .await;
+
+ // Client B closes the editor, and client A sees client B's selections removed.
+ cx_b.update(move |_| drop(editor_b));
+ executor.run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ buffer
+ .snapshot()
+ .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
+ .count()
+ == 0
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_on_input_format_from_host_to_guest(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let executor = cx_a.executor();
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ // Set up a fake language server.
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+ first_trigger_character: ":".to_string(),
+ more_trigger_character: Some(vec![">".to_string()]),
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ client_a.language_registry().add(Arc::new(language));
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a }",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ // Open a file in an editor as the host.
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .await
+ .unwrap();
+ let window_a = cx_a.add_empty_window();
+ let editor_a = window_a
+ .update(cx_a, |_, cx| {
+ cx.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx))
+ })
+ .unwrap();
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ executor.run_until_parked();
+
+ // Receive an OnTypeFormatting request as the host's language server.
+ // Return some formattings from the host's language server.
+ fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
+ |params, _| async move {
+ assert_eq!(
+ params.text_document_position.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ assert_eq!(
+ params.text_document_position.position,
+ lsp::Position::new(0, 14),
+ );
+
+ Ok(Some(vec![lsp::TextEdit {
+ new_text: "~<".to_string(),
+ range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
+ }]))
+ },
+ );
+
+ // Open the buffer on the guest and see that the formattings worked
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .await
+ .unwrap();
+
+ let mut cx_a = VisualTestContext::from_window(window_a, cx_a);
+ // Type a on type formatting trigger character as the guest.
+ cx_a.focus_view(&editor_a);
+ editor_a.update(&mut cx_a, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(">", cx);
+ });
+
+ executor.run_until_parked();
+
+ buffer_b.read_with(cx_b, |buffer, _| {
+ assert_eq!(buffer.text(), "fn main() { a>~< }")
+ });
+
+ // Undo should remove LSP edits first
+ editor_a.update(&mut cx_a, |editor, cx| {
+ assert_eq!(editor.text(cx), "fn main() { a>~< }");
+ editor.undo(&Undo, cx);
+ assert_eq!(editor.text(cx), "fn main() { a> }");
+ });
+ executor.run_until_parked();
+
+ buffer_b.read_with(cx_b, |buffer, _| {
+ assert_eq!(buffer.text(), "fn main() { a> }")
+ });
+
+ editor_a.update(&mut cx_a, |editor, cx| {
+ assert_eq!(editor.text(cx), "fn main() { a> }");
+ editor.undo(&Undo, cx);
+ assert_eq!(editor.text(cx), "fn main() { a }");
+ });
+ executor.run_until_parked();
+
+ buffer_b.read_with(cx_b, |buffer, _| {
+ assert_eq!(buffer.text(), "fn main() { a }")
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_on_input_format_from_guest_to_host(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let executor = cx_a.executor();
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ // Set up a fake language server.
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+ first_trigger_character: ":".to_string(),
+ more_trigger_character: Some(vec![">".to_string()]),
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ client_a.language_registry().add(Arc::new(language));
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a }",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ // Open a file in an editor as the guest.
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .await
+ .unwrap();
+ let window_b = cx_b.add_empty_window();
+ let editor_b = window_b.build_view(cx_b, |cx| {
+ Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
+ });
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ executor.run_until_parked();
+ let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
+ // Type a on type formatting trigger character as the guest.
+ cx_b.focus_view(&editor_b);
+ editor_b.update(&mut cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(":", cx);
+ });
+
+ // Receive an OnTypeFormatting request as the host's language server.
+ // Return some formattings from the host's language server.
+ executor.start_waiting();
+ fake_language_server
+ .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
+ assert_eq!(
+ params.text_document_position.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ assert_eq!(
+ params.text_document_position.position,
+ lsp::Position::new(0, 14),
+ );
+
+ Ok(Some(vec![lsp::TextEdit {
+ new_text: "~:".to_string(),
+ range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
+ }]))
+ })
+ .next()
+ .await
+ .unwrap();
+ executor.finish_waiting();
+
+ // Open the buffer on the host and see that the formattings worked
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+ .await
+ .unwrap();
+ executor.run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(buffer.text(), "fn main() { a:~: }")
+ });
+
+ // Undo should remove LSP edits first
+ editor_b.update(&mut cx_b, |editor, cx| {
+ assert_eq!(editor.text(cx), "fn main() { a:~: }");
+ editor.undo(&Undo, cx);
+ assert_eq!(editor.text(cx), "fn main() { a: }");
+ });
+ executor.run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(buffer.text(), "fn main() { a: }")
+ });
+
+ editor_b.update(&mut cx_b, |editor, cx| {
+ assert_eq!(editor.text(cx), "fn main() { a: }");
+ editor.undo(&Undo, cx);
+ assert_eq!(editor.text(cx), "fn main() { a }");
+ });
+ executor.run_until_parked();
+
+ buffer_a.read_with(cx_a, |buffer, _| {
+ assert_eq!(buffer.text(), "fn main() { a }")
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_mutual_editor_inlay_hint_cache_update(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let executor = cx_a.executor();
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ cx_a.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ client_a.language_registry().add(Arc::clone(&language));
+ client_b.language_registry().add(language);
+
+ // Client A opens a project.
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // Client B joins the project
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+ executor.start_waiting();
+
+ // The host opens a rust file.
+ let _buffer_a = project_a
+ .update(cx_a, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Set up the language server to return an additional inlay hint on each request.
+ let edits_made = Arc::new(AtomicUsize::new(0));
+ let closure_edits_made = Arc::clone(&edits_made);
+ fake_language_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_edits_made = Arc::clone(&closure_edits_made);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, edits_made as u32),
+ label: lsp::InlayHintLabel::String(edits_made.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await
+ .unwrap();
+
+ executor.run_until_parked();
+
+ let initial_edit = edits_made.load(atomic::Ordering::Acquire);
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![initial_edit.to_string()],
+ extract_hint_labels(editor),
+ "Host should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 1,
+ "Host editor update the cache version after every cache/view change",
+ );
+ });
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ executor.run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![initial_edit.to_string()],
+ extract_hint_labels(editor),
+ "Client should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 1,
+ "Guest editor update the cache version after every cache/view change"
+ );
+ });
+
+ let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
+ editor_b.update(cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
+ editor.handle_input(":", cx);
+ });
+ cx_b.focus_view(&editor_b);
+
+ executor.run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![after_client_edit.to_string()],
+ extract_hint_labels(editor),
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.version(), 2);
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![after_client_edit.to_string()],
+ extract_hint_labels(editor),
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.version(), 2);
+ });
+
+ let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
+ editor_a.update(cx_a, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("a change to increment both buffers' versions", cx);
+ });
+ cx_a.focus_view(&editor_a);
+
+ executor.run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![after_host_edit.to_string()],
+ extract_hint_labels(editor),
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.version(), 3);
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![after_host_edit.to_string()],
+ extract_hint_labels(editor),
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.version(), 3);
+ });
+
+ let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
+ fake_language_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+
+ executor.run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![after_special_edit_for_refresh.to_string()],
+ extract_hint_labels(editor),
+ "Host should react to /refresh LSP request"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 4,
+ "Host should accepted all edits and bump its cache version every time"
+ );
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![after_special_edit_for_refresh.to_string()],
+ extract_hint_labels(editor),
+ "Guest should get a /refresh LSP request propagated by host"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 4,
+ "Guest should accepted all edits and bump its cache version every time"
+ );
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_inlay_hint_refresh_is_forwarded(
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let executor = cx_a.executor();
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ cx_a.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: false,
+ show_type_hints: false,
+ show_parameter_hints: false,
+ show_other_hints: false,
+ })
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ client_a.language_registry().add(Arc::clone(&language));
+ client_b.language_registry().add(language);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+
+ cx_a.background_executor.start_waiting();
+
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let other_hints = Arc::new(AtomicBool::new(false));
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ let closure_other_hints = Arc::clone(&other_hints);
+ fake_language_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_other_hints = Arc::clone(&closure_other_hints);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
+ let character = if other_hints { 0 } else { 2 };
+ let label = if other_hints {
+ "other hint"
+ } else {
+ "initial hint"
+ };
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, character),
+ label: lsp::InlayHintLabel::String(label.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await
+ .unwrap();
+ executor.finish_waiting();
+
+ executor.run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert!(
+ extract_hint_labels(editor).is_empty(),
+ "Host should get no hints due to them turned off"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 0,
+ "Turned off hints should not generate version updates"
+ );
+ });
+
+ executor.run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["initial hint".to_string()],
+ extract_hint_labels(editor),
+ "Client should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 1,
+ "Should update cache verison after first hints"
+ );
+ });
+
+ other_hints.fetch_or(true, atomic::Ordering::Release);
+ fake_language_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ executor.run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert!(
+ extract_hint_labels(editor).is_empty(),
+ "Host should get nop hints due to them turned off, even after the /refresh"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 0,
+ "Turned off hints should not generate version updates, again"
+ );
+ });
+
+ executor.run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["other hint".to_string()],
+ extract_hint_labels(editor),
+ "Guest should get a /refresh LSP request propagated by host despite host hints are off"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.version(),
+ 2,
+ "Guest should accepted all edits and bump its cache version every time"
+ );
+ });
+}
+
+fn extract_hint_labels(editor: &Editor) -> Vec<String> {
+ let mut labels = Vec::new();
+ for hint in editor.inlay_hint_cache().hints() {
+ match hint.label {
+ project::InlayHintLabel::String(s) => labels.push(s),
+ _ => unreachable!(),
+ }
+ }
+ labels
+}