1use editor::{
2 test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
3 ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo,
4};
5use gpui::{BackgroundExecutor, TestAppContext};
6
7use crate::tests::TestServer;
8
9#[gpui::test(iterations = 10)]
10async fn test_host_disconnect(
11 executor: BackgroundExecutor,
12 cx_a: &mut TestAppContext,
13 cx_b: &mut TestAppContext,
14 cx_c: &mut TestAppContext,
15) {
16 let mut server = TestServer::start(&executor).await;
17 let client_a = server.create_client(cx_a, "user_a").await;
18 let client_b = server.create_client(cx_b, "user_b").await;
19 let client_c = server.create_client(cx_c, "user_c").await;
20 server
21 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
22 .await;
23
24 cx_b.update(editor::init);
25
26 client_a
27 .fs()
28 .insert_tree(
29 "/a",
30 json!({
31 "a.txt": "a-contents",
32 "b.txt": "b-contents",
33 }),
34 )
35 .await;
36
37 let active_call_a = cx_a.read(ActiveCall::global);
38 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
39
40 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
41 let project_id = active_call_a
42 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
43 .await
44 .unwrap();
45
46 let project_b = client_b.build_remote_project(project_id, cx_b).await;
47 executor.run_until_parked();
48
49 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
50
51 let window_b =
52 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
53 let workspace_b = window_b.root(cx_b);
54 let editor_b = workspace_b
55 .update(cx_b, |workspace, cx| {
56 workspace.open_path((worktree_id, "b.txt"), None, true, cx)
57 })
58 .await
59 .unwrap()
60 .downcast::<Editor>()
61 .unwrap();
62
63 assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx)));
64 editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
65 assert!(window_b.is_edited(cx_b));
66
67 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
68 server.forbid_connections();
69 server.disconnect_client(client_a.peer_id().unwrap());
70 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
71
72 project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
73
74 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
75
76 project_b.read_with(cx_b, |project, _| project.is_read_only());
77
78 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
79
80 // Ensure client B's edited state is reset and that the whole window is blurred.
81
82 window_b.read_with(cx_b, |cx| {
83 assert_eq!(cx.focused_view_id(), None);
84 });
85 assert!(!window_b.is_edited(cx_b));
86
87 // Ensure client B is not prompted to save edits when closing window after disconnecting.
88 let can_close = workspace_b
89 .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
90 .await
91 .unwrap();
92 assert!(can_close);
93
94 // Allow client A to reconnect to the server.
95 server.allow_connections();
96 executor.advance_clock(RECEIVE_TIMEOUT);
97
98 // Client B calls client A again after they reconnected.
99 let active_call_b = cx_b.read(ActiveCall::global);
100 active_call_b
101 .update(cx_b, |call, cx| {
102 call.invite(client_a.user_id().unwrap(), None, cx)
103 })
104 .await
105 .unwrap();
106 executor.run_until_parked();
107 active_call_a
108 .update(cx_a, |call, cx| call.accept_incoming(cx))
109 .await
110 .unwrap();
111
112 active_call_a
113 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
114 .await
115 .unwrap();
116
117 // Drop client A's connection again. We should still unshare it successfully.
118 server.forbid_connections();
119 server.disconnect_client(client_a.peer_id().unwrap());
120 executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
121
122 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
123}
124
125todo!(editor)
126#[gpui::test]
127async fn test_newline_above_or_below_does_not_move_guest_cursor(
128 executor: BackgroundExecutor,
129 cx_a: &mut TestAppContext,
130 cx_b: &mut TestAppContext,
131) {
132 let mut server = TestServer::start(&executor).await;
133 let client_a = server.create_client(cx_a, "user_a").await;
134 let client_b = server.create_client(cx_b, "user_b").await;
135 server
136 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
137 .await;
138 let active_call_a = cx_a.read(ActiveCall::global);
139
140 client_a
141 .fs()
142 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
143 .await;
144 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
145 let project_id = active_call_a
146 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
147 .await
148 .unwrap();
149
150 let project_b = client_b.build_remote_project(project_id, cx_b).await;
151
152 // Open a buffer as client A
153 let buffer_a = project_a
154 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
155 .await
156 .unwrap();
157 let window_a = cx_a.add_window(|_| EmptyView);
158 let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
159 let mut editor_cx_a = EditorTestContext {
160 cx: cx_a,
161 window: window_a.into(),
162 editor: editor_a,
163 };
164
165 // Open a buffer as client B
166 let buffer_b = project_b
167 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
168 .await
169 .unwrap();
170 let window_b = cx_b.add_window(|_| EmptyView);
171 let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
172 let mut editor_cx_b = EditorTestContext {
173 cx: cx_b,
174 window: window_b.into(),
175 editor: editor_b,
176 };
177
178 // Test newline above
179 editor_cx_a.set_selections_state(indoc! {"
180 Some textˇ
181 "});
182 editor_cx_b.set_selections_state(indoc! {"
183 Some textˇ
184 "});
185 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
186 executor.run_until_parked();
187 editor_cx_a.assert_editor_state(indoc! {"
188 ˇ
189 Some text
190 "});
191 editor_cx_b.assert_editor_state(indoc! {"
192
193 Some textˇ
194 "});
195
196 // Test newline below
197 editor_cx_a.set_selections_state(indoc! {"
198
199 Some textˇ
200 "});
201 editor_cx_b.set_selections_state(indoc! {"
202
203 Some textˇ
204 "});
205 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
206 executor.run_until_parked();
207 editor_cx_a.assert_editor_state(indoc! {"
208
209 Some text
210 ˇ
211 "});
212 editor_cx_b.assert_editor_state(indoc! {"
213
214 Some textˇ
215
216 "});
217}
218
219todo!(editor)
220#[gpui::test(iterations = 10)]
221async fn test_collaborating_with_completion(
222 executor: BackgroundExecutor,
223 cx_a: &mut TestAppContext,
224 cx_b: &mut TestAppContext,
225) {
226 let mut server = TestServer::start(&executor).await;
227 let client_a = server.create_client(cx_a, "user_a").await;
228 let client_b = server.create_client(cx_b, "user_b").await;
229 server
230 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
231 .await;
232 let active_call_a = cx_a.read(ActiveCall::global);
233
234 // Set up a fake language server.
235 let mut language = Language::new(
236 LanguageConfig {
237 name: "Rust".into(),
238 path_suffixes: vec!["rs".to_string()],
239 ..Default::default()
240 },
241 Some(tree_sitter_rust::language()),
242 );
243 let mut fake_language_servers = language
244 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
245 capabilities: lsp::ServerCapabilities {
246 completion_provider: Some(lsp::CompletionOptions {
247 trigger_characters: Some(vec![".".to_string()]),
248 resolve_provider: Some(true),
249 ..Default::default()
250 }),
251 ..Default::default()
252 },
253 ..Default::default()
254 }))
255 .await;
256 client_a.language_registry().add(Arc::new(language));
257
258 client_a
259 .fs()
260 .insert_tree(
261 "/a",
262 json!({
263 "main.rs": "fn main() { a }",
264 "other.rs": "",
265 }),
266 )
267 .await;
268 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
269 let project_id = active_call_a
270 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
271 .await
272 .unwrap();
273 let project_b = client_b.build_remote_project(project_id, cx_b).await;
274
275 // Open a file in an editor as the guest.
276 let buffer_b = project_b
277 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
278 .await
279 .unwrap();
280 let window_b = cx_b.add_window(|_| EmptyView);
281 let editor_b = window_b.add_view(cx_b, |cx| {
282 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
283 });
284
285 let fake_language_server = fake_language_servers.next().await.unwrap();
286 cx_a.foreground().run_until_parked();
287
288 buffer_b.read_with(cx_b, |buffer, _| {
289 assert!(!buffer.completion_triggers().is_empty())
290 });
291
292 // Type a completion trigger character as the guest.
293 editor_b.update(cx_b, |editor, cx| {
294 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
295 editor.handle_input(".", cx);
296 cx.focus(&editor_b);
297 });
298
299 // Receive a completion request as the host's language server.
300 // Return some completions from the host's language server.
301 cx_a.foreground().start_waiting();
302 fake_language_server
303 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
304 assert_eq!(
305 params.text_document_position.text_document.uri,
306 lsp::Url::from_file_path("/a/main.rs").unwrap(),
307 );
308 assert_eq!(
309 params.text_document_position.position,
310 lsp::Position::new(0, 14),
311 );
312
313 Ok(Some(lsp::CompletionResponse::Array(vec![
314 lsp::CompletionItem {
315 label: "first_method(…)".into(),
316 detail: Some("fn(&mut self, B) -> C".into()),
317 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
318 new_text: "first_method($1)".to_string(),
319 range: lsp::Range::new(
320 lsp::Position::new(0, 14),
321 lsp::Position::new(0, 14),
322 ),
323 })),
324 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
325 ..Default::default()
326 },
327 lsp::CompletionItem {
328 label: "second_method(…)".into(),
329 detail: Some("fn(&mut self, C) -> D<E>".into()),
330 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
331 new_text: "second_method()".to_string(),
332 range: lsp::Range::new(
333 lsp::Position::new(0, 14),
334 lsp::Position::new(0, 14),
335 ),
336 })),
337 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
338 ..Default::default()
339 },
340 ])))
341 })
342 .next()
343 .await
344 .unwrap();
345 cx_a.foreground().finish_waiting();
346
347 // Open the buffer on the host.
348 let buffer_a = project_a
349 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
350 .await
351 .unwrap();
352 cx_a.foreground().run_until_parked();
353
354 buffer_a.read_with(cx_a, |buffer, _| {
355 assert_eq!(buffer.text(), "fn main() { a. }")
356 });
357
358 // Confirm a completion on the guest.
359
360 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
361 editor_b.update(cx_b, |editor, cx| {
362 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
363 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
364 });
365
366 // Return a resolved completion from the host's language server.
367 // The resolved completion has an additional text edit.
368 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
369 |params, _| async move {
370 assert_eq!(params.label, "first_method(…)");
371 Ok(lsp::CompletionItem {
372 label: "first_method(…)".into(),
373 detail: Some("fn(&mut self, B) -> C".into()),
374 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
375 new_text: "first_method($1)".to_string(),
376 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
377 })),
378 additional_text_edits: Some(vec![lsp::TextEdit {
379 new_text: "use d::SomeTrait;\n".to_string(),
380 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
381 }]),
382 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
383 ..Default::default()
384 })
385 },
386 );
387
388 // The additional edit is applied.
389 cx_a.foreground().run_until_parked();
390
391 buffer_a.read_with(cx_a, |buffer, _| {
392 assert_eq!(
393 buffer.text(),
394 "use d::SomeTrait;\nfn main() { a.first_method() }"
395 );
396 });
397
398 buffer_b.read_with(cx_b, |buffer, _| {
399 assert_eq!(
400 buffer.text(),
401 "use d::SomeTrait;\nfn main() { a.first_method() }"
402 );
403 });
404}
405todo!(editor)
406#[gpui::test(iterations = 10)]
407async fn test_collaborating_with_code_actions(
408 executor: BackgroundExecutor,
409 cx_a: &mut TestAppContext,
410 cx_b: &mut TestAppContext,
411) {
412 let mut server = TestServer::start(&executor).await;
413 let client_a = server.create_client(cx_a, "user_a").await;
414 //
415 let client_b = server.create_client(cx_b, "user_b").await;
416 server
417 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
418 .await;
419 let active_call_a = cx_a.read(ActiveCall::global);
420
421 cx_b.update(editor::init);
422
423 // Set up a fake language server.
424 let mut language = Language::new(
425 LanguageConfig {
426 name: "Rust".into(),
427 path_suffixes: vec!["rs".to_string()],
428 ..Default::default()
429 },
430 Some(tree_sitter_rust::language()),
431 );
432 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
433 client_a.language_registry().add(Arc::new(language));
434
435 client_a
436 .fs()
437 .insert_tree(
438 "/a",
439 json!({
440 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
441 "other.rs": "pub fn foo() -> usize { 4 }",
442 }),
443 )
444 .await;
445 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
446 let project_id = active_call_a
447 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
448 .await
449 .unwrap();
450
451 // Join the project as client B.
452 let project_b = client_b.build_remote_project(project_id, cx_b).await;
453 let window_b =
454 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
455 let workspace_b = window_b.root(cx_b);
456 let editor_b = workspace_b
457 .update(cx_b, |workspace, cx| {
458 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
459 })
460 .await
461 .unwrap()
462 .downcast::<Editor>()
463 .unwrap();
464
465 let mut fake_language_server = fake_language_servers.next().await.unwrap();
466 let mut requests = fake_language_server
467 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
468 assert_eq!(
469 params.text_document.uri,
470 lsp::Url::from_file_path("/a/main.rs").unwrap(),
471 );
472 assert_eq!(params.range.start, lsp::Position::new(0, 0));
473 assert_eq!(params.range.end, lsp::Position::new(0, 0));
474 Ok(None)
475 });
476 executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
477 requests.next().await;
478
479 // Move cursor to a location that contains code actions.
480 editor_b.update(cx_b, |editor, cx| {
481 editor.change_selections(None, cx, |s| {
482 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
483 });
484 cx.focus(&editor_b);
485 });
486
487 let mut requests = fake_language_server
488 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
489 assert_eq!(
490 params.text_document.uri,
491 lsp::Url::from_file_path("/a/main.rs").unwrap(),
492 );
493 assert_eq!(params.range.start, lsp::Position::new(1, 31));
494 assert_eq!(params.range.end, lsp::Position::new(1, 31));
495
496 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
497 lsp::CodeAction {
498 title: "Inline into all callers".to_string(),
499 edit: Some(lsp::WorkspaceEdit {
500 changes: Some(
501 [
502 (
503 lsp::Url::from_file_path("/a/main.rs").unwrap(),
504 vec![lsp::TextEdit::new(
505 lsp::Range::new(
506 lsp::Position::new(1, 22),
507 lsp::Position::new(1, 34),
508 ),
509 "4".to_string(),
510 )],
511 ),
512 (
513 lsp::Url::from_file_path("/a/other.rs").unwrap(),
514 vec![lsp::TextEdit::new(
515 lsp::Range::new(
516 lsp::Position::new(0, 0),
517 lsp::Position::new(0, 27),
518 ),
519 "".to_string(),
520 )],
521 ),
522 ]
523 .into_iter()
524 .collect(),
525 ),
526 ..Default::default()
527 }),
528 data: Some(json!({
529 "codeActionParams": {
530 "range": {
531 "start": {"line": 1, "column": 31},
532 "end": {"line": 1, "column": 31},
533 }
534 }
535 })),
536 ..Default::default()
537 },
538 )]))
539 });
540 executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
541 requests.next().await;
542
543 // Toggle code actions and wait for them to display.
544 editor_b.update(cx_b, |editor, cx| {
545 editor.toggle_code_actions(
546 &ToggleCodeActions {
547 deployed_from_indicator: false,
548 },
549 cx,
550 );
551 });
552 cx_a.foreground().run_until_parked();
553
554 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
555
556 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
557
558 // Confirming the code action will trigger a resolve request.
559 let confirm_action = workspace_b
560 .update(cx_b, |workspace, cx| {
561 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
562 })
563 .unwrap();
564 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
565 |_, _| async move {
566 Ok(lsp::CodeAction {
567 title: "Inline into all callers".to_string(),
568 edit: Some(lsp::WorkspaceEdit {
569 changes: Some(
570 [
571 (
572 lsp::Url::from_file_path("/a/main.rs").unwrap(),
573 vec![lsp::TextEdit::new(
574 lsp::Range::new(
575 lsp::Position::new(1, 22),
576 lsp::Position::new(1, 34),
577 ),
578 "4".to_string(),
579 )],
580 ),
581 (
582 lsp::Url::from_file_path("/a/other.rs").unwrap(),
583 vec![lsp::TextEdit::new(
584 lsp::Range::new(
585 lsp::Position::new(0, 0),
586 lsp::Position::new(0, 27),
587 ),
588 "".to_string(),
589 )],
590 ),
591 ]
592 .into_iter()
593 .collect(),
594 ),
595 ..Default::default()
596 }),
597 ..Default::default()
598 })
599 },
600 );
601
602 // After the action is confirmed, an editor containing both modified files is opened.
603 confirm_action.await.unwrap();
604
605 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
606 workspace
607 .active_item(cx)
608 .unwrap()
609 .downcast::<Editor>()
610 .unwrap()
611 });
612 code_action_editor.update(cx_b, |editor, cx| {
613 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
614 editor.undo(&Undo, cx);
615 assert_eq!(
616 editor.text(cx),
617 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
618 );
619 editor.redo(&Redo, cx);
620 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
621 });
622}
623
624todo!(editor)
625#[gpui::test(iterations = 10)]
626async fn test_collaborating_with_renames(
627 executor: BackgroundExecutor,
628 cx_a: &mut TestAppContext,
629 cx_b: &mut TestAppContext,
630) {
631 let mut server = TestServer::start(&executor).await;
632 let client_a = server.create_client(cx_a, "user_a").await;
633 let client_b = server.create_client(cx_b, "user_b").await;
634 server
635 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
636 .await;
637 let active_call_a = cx_a.read(ActiveCall::global);
638
639 cx_b.update(editor::init);
640
641 // Set up a fake language server.
642 let mut language = Language::new(
643 LanguageConfig {
644 name: "Rust".into(),
645 path_suffixes: vec!["rs".to_string()],
646 ..Default::default()
647 },
648 Some(tree_sitter_rust::language()),
649 );
650 let mut fake_language_servers = language
651 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
652 capabilities: lsp::ServerCapabilities {
653 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
654 prepare_provider: Some(true),
655 work_done_progress_options: Default::default(),
656 })),
657 ..Default::default()
658 },
659 ..Default::default()
660 }))
661 .await;
662 client_a.language_registry().add(Arc::new(language));
663
664 client_a
665 .fs()
666 .insert_tree(
667 "/dir",
668 json!({
669 "one.rs": "const ONE: usize = 1;",
670 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
671 }),
672 )
673 .await;
674 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
675 let project_id = active_call_a
676 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
677 .await
678 .unwrap();
679 let project_b = client_b.build_remote_project(project_id, cx_b).await;
680
681 let window_b =
682 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
683 let workspace_b = window_b.root(cx_b);
684 let editor_b = workspace_b
685 .update(cx_b, |workspace, cx| {
686 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
687 })
688 .await
689 .unwrap()
690 .downcast::<Editor>()
691 .unwrap();
692 let fake_language_server = fake_language_servers.next().await.unwrap();
693
694 // Move cursor to a location that can be renamed.
695 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
696 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
697 editor.rename(&Rename, cx).unwrap()
698 });
699
700 fake_language_server
701 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
702 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
703 assert_eq!(params.position, lsp::Position::new(0, 7));
704 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
705 lsp::Position::new(0, 6),
706 lsp::Position::new(0, 9),
707 ))))
708 })
709 .next()
710 .await
711 .unwrap();
712 prepare_rename.await.unwrap();
713 editor_b.update(cx_b, |editor, cx| {
714 use editor::ToOffset;
715 let rename = editor.pending_rename().unwrap();
716 let buffer = editor.buffer().read(cx).snapshot(cx);
717 assert_eq!(
718 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
719 6..9
720 );
721 rename.editor.update(cx, |rename_editor, cx| {
722 rename_editor.buffer().update(cx, |rename_buffer, cx| {
723 rename_buffer.edit([(0..3, "THREE")], None, cx);
724 });
725 });
726 });
727
728 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
729 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
730 });
731 fake_language_server
732 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
733 assert_eq!(
734 params.text_document_position.text_document.uri.as_str(),
735 "file:///dir/one.rs"
736 );
737 assert_eq!(
738 params.text_document_position.position,
739 lsp::Position::new(0, 6)
740 );
741 assert_eq!(params.new_name, "THREE");
742 Ok(Some(lsp::WorkspaceEdit {
743 changes: Some(
744 [
745 (
746 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
747 vec![lsp::TextEdit::new(
748 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
749 "THREE".to_string(),
750 )],
751 ),
752 (
753 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
754 vec![
755 lsp::TextEdit::new(
756 lsp::Range::new(
757 lsp::Position::new(0, 24),
758 lsp::Position::new(0, 27),
759 ),
760 "THREE".to_string(),
761 ),
762 lsp::TextEdit::new(
763 lsp::Range::new(
764 lsp::Position::new(0, 35),
765 lsp::Position::new(0, 38),
766 ),
767 "THREE".to_string(),
768 ),
769 ],
770 ),
771 ]
772 .into_iter()
773 .collect(),
774 ),
775 ..Default::default()
776 }))
777 })
778 .next()
779 .await
780 .unwrap();
781 confirm_rename.await.unwrap();
782
783 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
784 workspace
785 .active_item(cx)
786 .unwrap()
787 .downcast::<Editor>()
788 .unwrap()
789 });
790 rename_editor.update(cx_b, |editor, cx| {
791 assert_eq!(
792 editor.text(cx),
793 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
794 );
795 editor.undo(&Undo, cx);
796 assert_eq!(
797 editor.text(cx),
798 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
799 );
800 editor.redo(&Redo, cx);
801 assert_eq!(
802 editor.text(cx),
803 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
804 );
805 });
806
807 // Ensure temporary rename edits cannot be undone/redone.
808 editor_b.update(cx_b, |editor, cx| {
809 editor.undo(&Undo, cx);
810 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
811 editor.undo(&Undo, cx);
812 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
813 editor.redo(&Redo, cx);
814 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
815 })
816}
817
818todo!(editor)
819#[gpui::test(iterations = 10)]
820async fn test_language_server_statuses(
821 executor: BackgroundExecutor,
822 cx_a: &mut TestAppContext,
823 cx_b: &mut TestAppContext,
824) {
825 let mut server = TestServer::start(&executor).await;
826 let client_a = server.create_client(cx_a, "user_a").await;
827 let client_b = server.create_client(cx_b, "user_b").await;
828 server
829 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
830 .await;
831 let active_call_a = cx_a.read(ActiveCall::global);
832
833 cx_b.update(editor::init);
834
835 // Set up a fake language server.
836 let mut language = Language::new(
837 LanguageConfig {
838 name: "Rust".into(),
839 path_suffixes: vec!["rs".to_string()],
840 ..Default::default()
841 },
842 Some(tree_sitter_rust::language()),
843 );
844 let mut fake_language_servers = language
845 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
846 name: "the-language-server",
847 ..Default::default()
848 }))
849 .await;
850 client_a.language_registry().add(Arc::new(language));
851
852 client_a
853 .fs()
854 .insert_tree(
855 "/dir",
856 json!({
857 "main.rs": "const ONE: usize = 1;",
858 }),
859 )
860 .await;
861 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
862
863 let _buffer_a = project_a
864 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
865 .await
866 .unwrap();
867
868 let fake_language_server = fake_language_servers.next().await.unwrap();
869 fake_language_server.start_progress("the-token").await;
870 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
871 token: lsp::NumberOrString::String("the-token".to_string()),
872 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
873 lsp::WorkDoneProgressReport {
874 message: Some("the-message".to_string()),
875 ..Default::default()
876 },
877 )),
878 });
879 executor.run_until_parked();
880
881 project_a.read_with(cx_a, |project, _| {
882 let status = project.language_server_statuses().next().unwrap();
883 assert_eq!(status.name, "the-language-server");
884 assert_eq!(status.pending_work.len(), 1);
885 assert_eq!(
886 status.pending_work["the-token"].message.as_ref().unwrap(),
887 "the-message"
888 );
889 });
890
891 let project_id = active_call_a
892 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
893 .await
894 .unwrap();
895 executor.run_until_parked();
896 let project_b = client_b.build_remote_project(project_id, cx_b).await;
897
898 project_b.read_with(cx_b, |project, _| {
899 let status = project.language_server_statuses().next().unwrap();
900 assert_eq!(status.name, "the-language-server");
901 });
902
903 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
904 token: lsp::NumberOrString::String("the-token".to_string()),
905 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
906 lsp::WorkDoneProgressReport {
907 message: Some("the-message-2".to_string()),
908 ..Default::default()
909 },
910 )),
911 });
912 executor.run_until_parked();
913
914 project_a.read_with(cx_a, |project, _| {
915 let status = project.language_server_statuses().next().unwrap();
916 assert_eq!(status.name, "the-language-server");
917 assert_eq!(status.pending_work.len(), 1);
918 assert_eq!(
919 status.pending_work["the-token"].message.as_ref().unwrap(),
920 "the-message-2"
921 );
922 });
923
924 project_b.read_with(cx_b, |project, _| {
925 let status = project.language_server_statuses().next().unwrap();
926 assert_eq!(status.name, "the-language-server");
927 assert_eq!(status.pending_work.len(), 1);
928 assert_eq!(
929 status.pending_work["the-token"].message.as_ref().unwrap(),
930 "the-message-2"
931 );
932 });
933}
934
935#[gpui::test(iterations = 10)]
936async fn test_share_project(
937 executor: BackgroundExecutor,
938 cx_a: &mut TestAppContext,
939 cx_b: &mut TestAppContext,
940 cx_c: &mut TestAppContext,
941) {
942 let window_b = cx_b.add_window(|_| EmptyView);
943 let mut server = TestServer::start(&executor).await;
944 let client_a = server.create_client(cx_a, "user_a").await;
945 let client_b = server.create_client(cx_b, "user_b").await;
946 let client_c = server.create_client(cx_c, "user_c").await;
947 server
948 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
949 .await;
950 let active_call_a = cx_a.read(ActiveCall::global);
951 let active_call_b = cx_b.read(ActiveCall::global);
952 let active_call_c = cx_c.read(ActiveCall::global);
953
954 client_a
955 .fs()
956 .insert_tree(
957 "/a",
958 json!({
959 ".gitignore": "ignored-dir",
960 "a.txt": "a-contents",
961 "b.txt": "b-contents",
962 "ignored-dir": {
963 "c.txt": "",
964 "d.txt": "",
965 }
966 }),
967 )
968 .await;
969
970 // Invite client B to collaborate on a project
971 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
972 active_call_a
973 .update(cx_a, |call, cx| {
974 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
975 })
976 .await
977 .unwrap();
978
979 // Join that project as client B
980
981 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
982 executor.run_until_parked();
983 let call = incoming_call_b.borrow().clone().unwrap();
984 assert_eq!(call.calling_user.github_login, "user_a");
985 let initial_project = call.initial_project.unwrap();
986 active_call_b
987 .update(cx_b, |call, cx| call.accept_incoming(cx))
988 .await
989 .unwrap();
990 let client_b_peer_id = client_b.peer_id().unwrap();
991 let project_b = client_b
992 .build_remote_project(initial_project.id, cx_b)
993 .await;
994
995 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
996
997 executor.run_until_parked();
998
999 project_a.read_with(cx_a, |project, _| {
1000 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1001 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1002 });
1003
1004 project_b.read_with(cx_b, |project, cx| {
1005 let worktree = project.worktrees().next().unwrap().read(cx);
1006 assert_eq!(
1007 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1008 [
1009 Path::new(".gitignore"),
1010 Path::new("a.txt"),
1011 Path::new("b.txt"),
1012 Path::new("ignored-dir"),
1013 ]
1014 );
1015 });
1016
1017 project_b
1018 .update(cx_b, |project, cx| {
1019 let worktree = project.worktrees().next().unwrap();
1020 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1021 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1022 })
1023 .await
1024 .unwrap();
1025
1026 project_b.read_with(cx_b, |project, cx| {
1027 let worktree = project.worktrees().next().unwrap().read(cx);
1028 assert_eq!(
1029 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1030 [
1031 Path::new(".gitignore"),
1032 Path::new("a.txt"),
1033 Path::new("b.txt"),
1034 Path::new("ignored-dir"),
1035 Path::new("ignored-dir/c.txt"),
1036 Path::new("ignored-dir/d.txt"),
1037 ]
1038 );
1039 });
1040
1041 // Open the same file as client B and client A.
1042 let buffer_b = project_b
1043 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1044 .await
1045 .unwrap();
1046
1047 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1048
1049 project_a.read_with(cx_a, |project, cx| {
1050 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1051 });
1052 let buffer_a = project_a
1053 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1054 .await
1055 .unwrap();
1056
1057 let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
1058
1059 // Client A sees client B's selection
1060 executor.run_until_parked();
1061
1062 buffer_a.read_with(cx_a, |buffer, _| {
1063 buffer
1064 .snapshot()
1065 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1066 .count()
1067 == 1
1068 });
1069
1070 // Edit the buffer as client B and see that edit as client A.
1071 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1072 executor.run_until_parked();
1073
1074 buffer_a.read_with(cx_a, |buffer, _| {
1075 assert_eq!(buffer.text(), "ok, b-contents")
1076 });
1077
1078 // Client B can invite client C on a project shared by client A.
1079 active_call_b
1080 .update(cx_b, |call, cx| {
1081 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1082 })
1083 .await
1084 .unwrap();
1085
1086 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1087 executor.run_until_parked();
1088 let call = incoming_call_c.borrow().clone().unwrap();
1089 assert_eq!(call.calling_user.github_login, "user_b");
1090 let initial_project = call.initial_project.unwrap();
1091 active_call_c
1092 .update(cx_c, |call, cx| call.accept_incoming(cx))
1093 .await
1094 .unwrap();
1095 let _project_c = client_c
1096 .build_remote_project(initial_project.id, cx_c)
1097 .await;
1098
1099 // Client B closes the editor, and client A sees client B's selections removed.
1100 cx_b.update(move |_| drop(editor_b));
1101 executor.run_until_parked();
1102
1103 buffer_a.read_with(cx_a, |buffer, _| {
1104 buffer
1105 .snapshot()
1106 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1107 .count()
1108 == 0
1109 });
1110}