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 workspace_b =
52 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
53
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
125#[gpui::test]
126async fn test_newline_above_or_below_does_not_move_guest_cursor(
127 executor: BackgroundExecutor,
128 cx_a: &mut TestAppContext,
129 cx_b: &mut TestAppContext,
130) {
131 let mut server = TestServer::start(&executor).await;
132 let client_a = server.create_client(cx_a, "user_a").await;
133 let client_b = server.create_client(cx_b, "user_b").await;
134 server
135 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
136 .await;
137 let active_call_a = cx_a.read(ActiveCall::global);
138
139 client_a
140 .fs()
141 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
142 .await;
143 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
144 let project_id = active_call_a
145 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
146 .await
147 .unwrap();
148
149 let project_b = client_b.build_remote_project(project_id, cx_b).await;
150
151 // Open a buffer as client A
152 let buffer_a = project_a
153 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
154 .await
155 .unwrap();
156 let window_a = cx_a.add_window(|_| EmptyView);
157 let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
158 let mut editor_cx_a = EditorTestContext {
159 cx: cx_a,
160 window: window_a.into(),
161 editor: editor_a,
162 };
163
164 // Open a buffer as client B
165 let buffer_b = project_b
166 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
167 .await
168 .unwrap();
169 let window_b = cx_b.add_window(|_| EmptyView);
170 let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
171 let mut editor_cx_b = EditorTestContext {
172 cx: cx_b,
173 window: window_b.into(),
174 editor: editor_b,
175 };
176
177 // Test newline above
178 editor_cx_a.set_selections_state(indoc! {"
179 Some textˇ
180 "});
181 editor_cx_b.set_selections_state(indoc! {"
182 Some textˇ
183 "});
184 editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
185 executor.run_until_parked();
186 editor_cx_a.assert_editor_state(indoc! {"
187 ˇ
188 Some text
189 "});
190 editor_cx_b.assert_editor_state(indoc! {"
191
192 Some textˇ
193 "});
194
195 // Test newline below
196 editor_cx_a.set_selections_state(indoc! {"
197
198 Some textˇ
199 "});
200 editor_cx_b.set_selections_state(indoc! {"
201
202 Some textˇ
203 "});
204 editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
205 executor.run_until_parked();
206 editor_cx_a.assert_editor_state(indoc! {"
207
208 Some text
209 ˇ
210 "});
211 editor_cx_b.assert_editor_state(indoc! {"
212
213 Some textˇ
214
215 "});
216}
217
218#[gpui::test(iterations = 10)]
219async fn test_collaborating_with_completion(
220 executor: BackgroundExecutor,
221 cx_a: &mut TestAppContext,
222 cx_b: &mut TestAppContext,
223) {
224 let mut server = TestServer::start(&executor).await;
225 let client_a = server.create_client(cx_a, "user_a").await;
226 let client_b = server.create_client(cx_b, "user_b").await;
227 server
228 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
229 .await;
230 let active_call_a = cx_a.read(ActiveCall::global);
231
232 // Set up a fake language server.
233 let mut language = Language::new(
234 LanguageConfig {
235 name: "Rust".into(),
236 path_suffixes: vec!["rs".to_string()],
237 ..Default::default()
238 },
239 Some(tree_sitter_rust::language()),
240 );
241 let mut fake_language_servers = language
242 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
243 capabilities: lsp::ServerCapabilities {
244 completion_provider: Some(lsp::CompletionOptions {
245 trigger_characters: Some(vec![".".to_string()]),
246 resolve_provider: Some(true),
247 ..Default::default()
248 }),
249 ..Default::default()
250 },
251 ..Default::default()
252 }))
253 .await;
254 client_a.language_registry().add(Arc::new(language));
255
256 client_a
257 .fs()
258 .insert_tree(
259 "/a",
260 json!({
261 "main.rs": "fn main() { a }",
262 "other.rs": "",
263 }),
264 )
265 .await;
266 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
267 let project_id = active_call_a
268 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
269 .await
270 .unwrap();
271 let project_b = client_b.build_remote_project(project_id, cx_b).await;
272
273 // Open a file in an editor as the guest.
274 let buffer_b = project_b
275 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
276 .await
277 .unwrap();
278 let window_b = cx_b.add_window(|_| EmptyView);
279 let editor_b = window_b.add_view(cx_b, |cx| {
280 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
281 });
282
283 let fake_language_server = fake_language_servers.next().await.unwrap();
284 cx_a.foreground().run_until_parked();
285
286 buffer_b.read_with(cx_b, |buffer, _| {
287 assert!(!buffer.completion_triggers().is_empty())
288 });
289
290 // Type a completion trigger character as the guest.
291 editor_b.update(cx_b, |editor, cx| {
292 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
293 editor.handle_input(".", cx);
294 cx.focus(&editor_b);
295 });
296
297 // Receive a completion request as the host's language server.
298 // Return some completions from the host's language server.
299 cx_a.foreground().start_waiting();
300 fake_language_server
301 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
302 assert_eq!(
303 params.text_document_position.text_document.uri,
304 lsp::Url::from_file_path("/a/main.rs").unwrap(),
305 );
306 assert_eq!(
307 params.text_document_position.position,
308 lsp::Position::new(0, 14),
309 );
310
311 Ok(Some(lsp::CompletionResponse::Array(vec![
312 lsp::CompletionItem {
313 label: "first_method(…)".into(),
314 detail: Some("fn(&mut self, B) -> C".into()),
315 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
316 new_text: "first_method($1)".to_string(),
317 range: lsp::Range::new(
318 lsp::Position::new(0, 14),
319 lsp::Position::new(0, 14),
320 ),
321 })),
322 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
323 ..Default::default()
324 },
325 lsp::CompletionItem {
326 label: "second_method(…)".into(),
327 detail: Some("fn(&mut self, C) -> D<E>".into()),
328 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
329 new_text: "second_method()".to_string(),
330 range: lsp::Range::new(
331 lsp::Position::new(0, 14),
332 lsp::Position::new(0, 14),
333 ),
334 })),
335 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
336 ..Default::default()
337 },
338 ])))
339 })
340 .next()
341 .await
342 .unwrap();
343 cx_a.foreground().finish_waiting();
344
345 // Open the buffer on the host.
346 let buffer_a = project_a
347 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
348 .await
349 .unwrap();
350 cx_a.foreground().run_until_parked();
351
352 buffer_a.read_with(cx_a, |buffer, _| {
353 assert_eq!(buffer.text(), "fn main() { a. }")
354 });
355
356 // Confirm a completion on the guest.
357
358 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
359 editor_b.update(cx_b, |editor, cx| {
360 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
361 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
362 });
363
364 // Return a resolved completion from the host's language server.
365 // The resolved completion has an additional text edit.
366 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
367 |params, _| async move {
368 assert_eq!(params.label, "first_method(…)");
369 Ok(lsp::CompletionItem {
370 label: "first_method(…)".into(),
371 detail: Some("fn(&mut self, B) -> C".into()),
372 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
373 new_text: "first_method($1)".to_string(),
374 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
375 })),
376 additional_text_edits: Some(vec![lsp::TextEdit {
377 new_text: "use d::SomeTrait;\n".to_string(),
378 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
379 }]),
380 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
381 ..Default::default()
382 })
383 },
384 );
385
386 // The additional edit is applied.
387 cx_a.foreground().run_until_parked();
388
389 buffer_a.read_with(cx_a, |buffer, _| {
390 assert_eq!(
391 buffer.text(),
392 "use d::SomeTrait;\nfn main() { a.first_method() }"
393 );
394 });
395
396 buffer_b.read_with(cx_b, |buffer, _| {
397 assert_eq!(
398 buffer.text(),
399 "use d::SomeTrait;\nfn main() { a.first_method() }"
400 );
401 });
402}
403
404#[gpui::test(iterations = 10)]
405async fn test_collaborating_with_code_actions(
406 executor: BackgroundExecutor,
407 cx_a: &mut TestAppContext,
408 cx_b: &mut TestAppContext,
409) {
410 let mut server = TestServer::start(&executor).await;
411 let client_a = server.create_client(cx_a, "user_a").await;
412 //
413 let client_b = server.create_client(cx_b, "user_b").await;
414 server
415 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
416 .await;
417 let active_call_a = cx_a.read(ActiveCall::global);
418
419 cx_b.update(editor::init);
420
421 // Set up a fake language server.
422 let mut language = Language::new(
423 LanguageConfig {
424 name: "Rust".into(),
425 path_suffixes: vec!["rs".to_string()],
426 ..Default::default()
427 },
428 Some(tree_sitter_rust::language()),
429 );
430 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
431 client_a.language_registry().add(Arc::new(language));
432
433 client_a
434 .fs()
435 .insert_tree(
436 "/a",
437 json!({
438 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
439 "other.rs": "pub fn foo() -> usize { 4 }",
440 }),
441 )
442 .await;
443 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
444 let project_id = active_call_a
445 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
446 .await
447 .unwrap();
448
449 // Join the project as client B.
450 let project_b = client_b.build_remote_project(project_id, cx_b).await;
451 let window_b =
452 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
453 let workspace_b = window_b.root(cx_b);
454 let editor_b = workspace_b
455 .update(cx_b, |workspace, cx| {
456 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
457 })
458 .await
459 .unwrap()
460 .downcast::<Editor>()
461 .unwrap();
462
463 let mut fake_language_server = fake_language_servers.next().await.unwrap();
464 let mut requests = fake_language_server
465 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
466 assert_eq!(
467 params.text_document.uri,
468 lsp::Url::from_file_path("/a/main.rs").unwrap(),
469 );
470 assert_eq!(params.range.start, lsp::Position::new(0, 0));
471 assert_eq!(params.range.end, lsp::Position::new(0, 0));
472 Ok(None)
473 });
474 executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
475 requests.next().await;
476
477 // Move cursor to a location that contains code actions.
478 editor_b.update(cx_b, |editor, cx| {
479 editor.change_selections(None, cx, |s| {
480 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
481 });
482 cx.focus(&editor_b);
483 });
484
485 let mut requests = fake_language_server
486 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
487 assert_eq!(
488 params.text_document.uri,
489 lsp::Url::from_file_path("/a/main.rs").unwrap(),
490 );
491 assert_eq!(params.range.start, lsp::Position::new(1, 31));
492 assert_eq!(params.range.end, lsp::Position::new(1, 31));
493
494 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
495 lsp::CodeAction {
496 title: "Inline into all callers".to_string(),
497 edit: Some(lsp::WorkspaceEdit {
498 changes: Some(
499 [
500 (
501 lsp::Url::from_file_path("/a/main.rs").unwrap(),
502 vec![lsp::TextEdit::new(
503 lsp::Range::new(
504 lsp::Position::new(1, 22),
505 lsp::Position::new(1, 34),
506 ),
507 "4".to_string(),
508 )],
509 ),
510 (
511 lsp::Url::from_file_path("/a/other.rs").unwrap(),
512 vec![lsp::TextEdit::new(
513 lsp::Range::new(
514 lsp::Position::new(0, 0),
515 lsp::Position::new(0, 27),
516 ),
517 "".to_string(),
518 )],
519 ),
520 ]
521 .into_iter()
522 .collect(),
523 ),
524 ..Default::default()
525 }),
526 data: Some(json!({
527 "codeActionParams": {
528 "range": {
529 "start": {"line": 1, "column": 31},
530 "end": {"line": 1, "column": 31},
531 }
532 }
533 })),
534 ..Default::default()
535 },
536 )]))
537 });
538 executor.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
539 requests.next().await;
540
541 // Toggle code actions and wait for them to display.
542 editor_b.update(cx_b, |editor, cx| {
543 editor.toggle_code_actions(
544 &ToggleCodeActions {
545 deployed_from_indicator: false,
546 },
547 cx,
548 );
549 });
550 cx_a.foreground().run_until_parked();
551
552 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
553
554 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
555
556 // Confirming the code action will trigger a resolve request.
557 let confirm_action = workspace_b
558 .update(cx_b, |workspace, cx| {
559 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
560 })
561 .unwrap();
562 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
563 |_, _| async move {
564 Ok(lsp::CodeAction {
565 title: "Inline into all callers".to_string(),
566 edit: Some(lsp::WorkspaceEdit {
567 changes: Some(
568 [
569 (
570 lsp::Url::from_file_path("/a/main.rs").unwrap(),
571 vec![lsp::TextEdit::new(
572 lsp::Range::new(
573 lsp::Position::new(1, 22),
574 lsp::Position::new(1, 34),
575 ),
576 "4".to_string(),
577 )],
578 ),
579 (
580 lsp::Url::from_file_path("/a/other.rs").unwrap(),
581 vec![lsp::TextEdit::new(
582 lsp::Range::new(
583 lsp::Position::new(0, 0),
584 lsp::Position::new(0, 27),
585 ),
586 "".to_string(),
587 )],
588 ),
589 ]
590 .into_iter()
591 .collect(),
592 ),
593 ..Default::default()
594 }),
595 ..Default::default()
596 })
597 },
598 );
599
600 // After the action is confirmed, an editor containing both modified files is opened.
601 confirm_action.await.unwrap();
602
603 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
604 workspace
605 .active_item(cx)
606 .unwrap()
607 .downcast::<Editor>()
608 .unwrap()
609 });
610 code_action_editor.update(cx_b, |editor, cx| {
611 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
612 editor.undo(&Undo, cx);
613 assert_eq!(
614 editor.text(cx),
615 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
616 );
617 editor.redo(&Redo, cx);
618 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
619 });
620}
621
622#[gpui::test(iterations = 10)]
623async fn test_collaborating_with_renames(
624 executor: BackgroundExecutor,
625 cx_a: &mut TestAppContext,
626 cx_b: &mut TestAppContext,
627) {
628 let mut server = TestServer::start(&executor).await;
629 let client_a = server.create_client(cx_a, "user_a").await;
630 let client_b = server.create_client(cx_b, "user_b").await;
631 server
632 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
633 .await;
634 let active_call_a = cx_a.read(ActiveCall::global);
635
636 cx_b.update(editor::init);
637
638 // Set up a fake language server.
639 let mut language = Language::new(
640 LanguageConfig {
641 name: "Rust".into(),
642 path_suffixes: vec!["rs".to_string()],
643 ..Default::default()
644 },
645 Some(tree_sitter_rust::language()),
646 );
647 let mut fake_language_servers = language
648 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
649 capabilities: lsp::ServerCapabilities {
650 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
651 prepare_provider: Some(true),
652 work_done_progress_options: Default::default(),
653 })),
654 ..Default::default()
655 },
656 ..Default::default()
657 }))
658 .await;
659 client_a.language_registry().add(Arc::new(language));
660
661 client_a
662 .fs()
663 .insert_tree(
664 "/dir",
665 json!({
666 "one.rs": "const ONE: usize = 1;",
667 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
668 }),
669 )
670 .await;
671 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
672 let project_id = active_call_a
673 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
674 .await
675 .unwrap();
676 let project_b = client_b.build_remote_project(project_id, cx_b).await;
677
678 let window_b =
679 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
680 let workspace_b = window_b.root(cx_b);
681 let editor_b = workspace_b
682 .update(cx_b, |workspace, cx| {
683 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
684 })
685 .await
686 .unwrap()
687 .downcast::<Editor>()
688 .unwrap();
689 let fake_language_server = fake_language_servers.next().await.unwrap();
690
691 // Move cursor to a location that can be renamed.
692 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
693 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
694 editor.rename(&Rename, cx).unwrap()
695 });
696
697 fake_language_server
698 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
699 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
700 assert_eq!(params.position, lsp::Position::new(0, 7));
701 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
702 lsp::Position::new(0, 6),
703 lsp::Position::new(0, 9),
704 ))))
705 })
706 .next()
707 .await
708 .unwrap();
709 prepare_rename.await.unwrap();
710 editor_b.update(cx_b, |editor, cx| {
711 use editor::ToOffset;
712 let rename = editor.pending_rename().unwrap();
713 let buffer = editor.buffer().read(cx).snapshot(cx);
714 assert_eq!(
715 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
716 6..9
717 );
718 rename.editor.update(cx, |rename_editor, cx| {
719 rename_editor.buffer().update(cx, |rename_buffer, cx| {
720 rename_buffer.edit([(0..3, "THREE")], None, cx);
721 });
722 });
723 });
724
725 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
726 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
727 });
728 fake_language_server
729 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
730 assert_eq!(
731 params.text_document_position.text_document.uri.as_str(),
732 "file:///dir/one.rs"
733 );
734 assert_eq!(
735 params.text_document_position.position,
736 lsp::Position::new(0, 6)
737 );
738 assert_eq!(params.new_name, "THREE");
739 Ok(Some(lsp::WorkspaceEdit {
740 changes: Some(
741 [
742 (
743 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
744 vec![lsp::TextEdit::new(
745 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
746 "THREE".to_string(),
747 )],
748 ),
749 (
750 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
751 vec![
752 lsp::TextEdit::new(
753 lsp::Range::new(
754 lsp::Position::new(0, 24),
755 lsp::Position::new(0, 27),
756 ),
757 "THREE".to_string(),
758 ),
759 lsp::TextEdit::new(
760 lsp::Range::new(
761 lsp::Position::new(0, 35),
762 lsp::Position::new(0, 38),
763 ),
764 "THREE".to_string(),
765 ),
766 ],
767 ),
768 ]
769 .into_iter()
770 .collect(),
771 ),
772 ..Default::default()
773 }))
774 })
775 .next()
776 .await
777 .unwrap();
778 confirm_rename.await.unwrap();
779
780 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
781 workspace
782 .active_item(cx)
783 .unwrap()
784 .downcast::<Editor>()
785 .unwrap()
786 });
787 rename_editor.update(cx_b, |editor, cx| {
788 assert_eq!(
789 editor.text(cx),
790 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
791 );
792 editor.undo(&Undo, cx);
793 assert_eq!(
794 editor.text(cx),
795 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
796 );
797 editor.redo(&Redo, cx);
798 assert_eq!(
799 editor.text(cx),
800 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
801 );
802 });
803
804 // Ensure temporary rename edits cannot be undone/redone.
805 editor_b.update(cx_b, |editor, cx| {
806 editor.undo(&Undo, cx);
807 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
808 editor.undo(&Undo, cx);
809 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
810 editor.redo(&Redo, cx);
811 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
812 })
813}
814
815#[gpui::test(iterations = 10)]
816async fn test_language_server_statuses(
817 executor: BackgroundExecutor,
818 cx_a: &mut TestAppContext,
819 cx_b: &mut TestAppContext,
820) {
821 let mut server = TestServer::start(&executor).await;
822 let client_a = server.create_client(cx_a, "user_a").await;
823 let client_b = server.create_client(cx_b, "user_b").await;
824 server
825 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
826 .await;
827 let active_call_a = cx_a.read(ActiveCall::global);
828
829 cx_b.update(editor::init);
830
831 // Set up a fake language server.
832 let mut language = Language::new(
833 LanguageConfig {
834 name: "Rust".into(),
835 path_suffixes: vec!["rs".to_string()],
836 ..Default::default()
837 },
838 Some(tree_sitter_rust::language()),
839 );
840 let mut fake_language_servers = language
841 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
842 name: "the-language-server",
843 ..Default::default()
844 }))
845 .await;
846 client_a.language_registry().add(Arc::new(language));
847
848 client_a
849 .fs()
850 .insert_tree(
851 "/dir",
852 json!({
853 "main.rs": "const ONE: usize = 1;",
854 }),
855 )
856 .await;
857 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
858
859 let _buffer_a = project_a
860 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
861 .await
862 .unwrap();
863
864 let fake_language_server = fake_language_servers.next().await.unwrap();
865 fake_language_server.start_progress("the-token").await;
866 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
867 token: lsp::NumberOrString::String("the-token".to_string()),
868 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
869 lsp::WorkDoneProgressReport {
870 message: Some("the-message".to_string()),
871 ..Default::default()
872 },
873 )),
874 });
875 executor.run_until_parked();
876
877 project_a.read_with(cx_a, |project, _| {
878 let status = project.language_server_statuses().next().unwrap();
879 assert_eq!(status.name, "the-language-server");
880 assert_eq!(status.pending_work.len(), 1);
881 assert_eq!(
882 status.pending_work["the-token"].message.as_ref().unwrap(),
883 "the-message"
884 );
885 });
886
887 let project_id = active_call_a
888 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
889 .await
890 .unwrap();
891 executor.run_until_parked();
892 let project_b = client_b.build_remote_project(project_id, cx_b).await;
893
894 project_b.read_with(cx_b, |project, _| {
895 let status = project.language_server_statuses().next().unwrap();
896 assert_eq!(status.name, "the-language-server");
897 });
898
899 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
900 token: lsp::NumberOrString::String("the-token".to_string()),
901 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
902 lsp::WorkDoneProgressReport {
903 message: Some("the-message-2".to_string()),
904 ..Default::default()
905 },
906 )),
907 });
908 executor.run_until_parked();
909
910 project_a.read_with(cx_a, |project, _| {
911 let status = project.language_server_statuses().next().unwrap();
912 assert_eq!(status.name, "the-language-server");
913 assert_eq!(status.pending_work.len(), 1);
914 assert_eq!(
915 status.pending_work["the-token"].message.as_ref().unwrap(),
916 "the-message-2"
917 );
918 });
919
920 project_b.read_with(cx_b, |project, _| {
921 let status = project.language_server_statuses().next().unwrap();
922 assert_eq!(status.name, "the-language-server");
923 assert_eq!(status.pending_work.len(), 1);
924 assert_eq!(
925 status.pending_work["the-token"].message.as_ref().unwrap(),
926 "the-message-2"
927 );
928 });
929}
930
931#[gpui::test(iterations = 10)]
932async fn test_share_project(
933 executor: BackgroundExecutor,
934 cx_a: &mut TestAppContext,
935 cx_b: &mut TestAppContext,
936 cx_c: &mut TestAppContext,
937) {
938 let window_b = cx_b.add_window(|_| EmptyView);
939 let mut server = TestServer::start(&executor).await;
940 let client_a = server.create_client(cx_a, "user_a").await;
941 let client_b = server.create_client(cx_b, "user_b").await;
942 let client_c = server.create_client(cx_c, "user_c").await;
943 server
944 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
945 .await;
946 let active_call_a = cx_a.read(ActiveCall::global);
947 let active_call_b = cx_b.read(ActiveCall::global);
948 let active_call_c = cx_c.read(ActiveCall::global);
949
950 client_a
951 .fs()
952 .insert_tree(
953 "/a",
954 json!({
955 ".gitignore": "ignored-dir",
956 "a.txt": "a-contents",
957 "b.txt": "b-contents",
958 "ignored-dir": {
959 "c.txt": "",
960 "d.txt": "",
961 }
962 }),
963 )
964 .await;
965
966 // Invite client B to collaborate on a project
967 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
968 active_call_a
969 .update(cx_a, |call, cx| {
970 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
971 })
972 .await
973 .unwrap();
974
975 // Join that project as client B
976
977 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
978 executor.run_until_parked();
979 let call = incoming_call_b.borrow().clone().unwrap();
980 assert_eq!(call.calling_user.github_login, "user_a");
981 let initial_project = call.initial_project.unwrap();
982 active_call_b
983 .update(cx_b, |call, cx| call.accept_incoming(cx))
984 .await
985 .unwrap();
986 let client_b_peer_id = client_b.peer_id().unwrap();
987 let project_b = client_b
988 .build_remote_project(initial_project.id, cx_b)
989 .await;
990
991 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
992
993 executor.run_until_parked();
994
995 project_a.read_with(cx_a, |project, _| {
996 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
997 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
998 });
999
1000 project_b.read_with(cx_b, |project, cx| {
1001 let worktree = project.worktrees().next().unwrap().read(cx);
1002 assert_eq!(
1003 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1004 [
1005 Path::new(".gitignore"),
1006 Path::new("a.txt"),
1007 Path::new("b.txt"),
1008 Path::new("ignored-dir"),
1009 ]
1010 );
1011 });
1012
1013 project_b
1014 .update(cx_b, |project, cx| {
1015 let worktree = project.worktrees().next().unwrap();
1016 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1017 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1018 })
1019 .await
1020 .unwrap();
1021
1022 project_b.read_with(cx_b, |project, cx| {
1023 let worktree = project.worktrees().next().unwrap().read(cx);
1024 assert_eq!(
1025 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1026 [
1027 Path::new(".gitignore"),
1028 Path::new("a.txt"),
1029 Path::new("b.txt"),
1030 Path::new("ignored-dir"),
1031 Path::new("ignored-dir/c.txt"),
1032 Path::new("ignored-dir/d.txt"),
1033 ]
1034 );
1035 });
1036
1037 // Open the same file as client B and client A.
1038 let buffer_b = project_b
1039 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1040 .await
1041 .unwrap();
1042
1043 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1044
1045 project_a.read_with(cx_a, |project, cx| {
1046 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1047 });
1048 let buffer_a = project_a
1049 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1050 .await
1051 .unwrap();
1052
1053 let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
1054
1055 // Client A sees client B's selection
1056 executor.run_until_parked();
1057
1058 buffer_a.read_with(cx_a, |buffer, _| {
1059 buffer
1060 .snapshot()
1061 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1062 .count()
1063 == 1
1064 });
1065
1066 // Edit the buffer as client B and see that edit as client A.
1067 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1068 executor.run_until_parked();
1069
1070 buffer_a.read_with(cx_a, |buffer, _| {
1071 assert_eq!(buffer.text(), "ok, b-contents")
1072 });
1073
1074 // Client B can invite client C on a project shared by client A.
1075 active_call_b
1076 .update(cx_b, |call, cx| {
1077 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1078 })
1079 .await
1080 .unwrap();
1081
1082 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1083 executor.run_until_parked();
1084 let call = incoming_call_c.borrow().clone().unwrap();
1085 assert_eq!(call.calling_user.github_login, "user_b");
1086 let initial_project = call.initial_project.unwrap();
1087 active_call_c
1088 .update(cx_c, |call, cx| call.accept_incoming(cx))
1089 .await
1090 .unwrap();
1091 let _project_c = client_c
1092 .build_remote_project(initial_project.id, cx_c)
1093 .await;
1094
1095 // Client B closes the editor, and client A sees client B's selections removed.
1096 cx_b.update(move |_| drop(editor_b));
1097 executor.run_until_parked();
1098
1099 buffer_a.read_with(cx_a, |buffer, _| {
1100 buffer
1101 .snapshot()
1102 .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
1103 .count()
1104 == 0
1105 });
1106}
1107
1108#[gpui::test(iterations = 10)]
1109async fn test_on_input_format_from_host_to_guest(
1110 executor: BackgroundExecutor,
1111 cx_a: &mut TestAppContext,
1112 cx_b: &mut TestAppContext,
1113) {
1114 let mut server = TestServer::start(&executor).await;
1115 let client_a = server.create_client(cx_a, "user_a").await;
1116 let client_b = server.create_client(cx_b, "user_b").await;
1117 server
1118 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1119 .await;
1120 let active_call_a = cx_a.read(ActiveCall::global);
1121
1122 // Set up a fake language server.
1123 let mut language = Language::new(
1124 LanguageConfig {
1125 name: "Rust".into(),
1126 path_suffixes: vec!["rs".to_string()],
1127 ..Default::default()
1128 },
1129 Some(tree_sitter_rust::language()),
1130 );
1131 let mut fake_language_servers = language
1132 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1133 capabilities: lsp::ServerCapabilities {
1134 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1135 first_trigger_character: ":".to_string(),
1136 more_trigger_character: Some(vec![">".to_string()]),
1137 }),
1138 ..Default::default()
1139 },
1140 ..Default::default()
1141 }))
1142 .await;
1143 client_a.language_registry().add(Arc::new(language));
1144
1145 client_a
1146 .fs()
1147 .insert_tree(
1148 "/a",
1149 json!({
1150 "main.rs": "fn main() { a }",
1151 "other.rs": "// Test file",
1152 }),
1153 )
1154 .await;
1155 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1156 let project_id = active_call_a
1157 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1158 .await
1159 .unwrap();
1160 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1161
1162 // Open a file in an editor as the host.
1163 let buffer_a = project_a
1164 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1165 .await
1166 .unwrap();
1167 let window_a = cx_a.add_window(|_| EmptyView);
1168 let editor_a = window_a.add_view(cx_a, |cx| {
1169 Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
1170 });
1171
1172 let fake_language_server = fake_language_servers.next().await.unwrap();
1173 executor.run_until_parked();
1174
1175 // Receive an OnTypeFormatting request as the host's language server.
1176 // Return some formattings from the host's language server.
1177 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1178 |params, _| async move {
1179 assert_eq!(
1180 params.text_document_position.text_document.uri,
1181 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1182 );
1183 assert_eq!(
1184 params.text_document_position.position,
1185 lsp::Position::new(0, 14),
1186 );
1187
1188 Ok(Some(vec![lsp::TextEdit {
1189 new_text: "~<".to_string(),
1190 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1191 }]))
1192 },
1193 );
1194
1195 // Open the buffer on the guest and see that the formattings worked
1196 let buffer_b = project_b
1197 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1198 .await
1199 .unwrap();
1200
1201 // Type a on type formatting trigger character as the guest.
1202 editor_a.update(cx_a, |editor, cx| {
1203 cx.focus(&editor_a);
1204 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1205 editor.handle_input(">", cx);
1206 });
1207
1208 executor.run_until_parked();
1209
1210 buffer_b.read_with(cx_b, |buffer, _| {
1211 assert_eq!(buffer.text(), "fn main() { a>~< }")
1212 });
1213
1214 // Undo should remove LSP edits first
1215 editor_a.update(cx_a, |editor, cx| {
1216 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1217 editor.undo(&Undo, cx);
1218 assert_eq!(editor.text(cx), "fn main() { a> }");
1219 });
1220 executor.run_until_parked();
1221
1222 buffer_b.read_with(cx_b, |buffer, _| {
1223 assert_eq!(buffer.text(), "fn main() { a> }")
1224 });
1225
1226 editor_a.update(cx_a, |editor, cx| {
1227 assert_eq!(editor.text(cx), "fn main() { a> }");
1228 editor.undo(&Undo, cx);
1229 assert_eq!(editor.text(cx), "fn main() { a }");
1230 });
1231 executor.run_until_parked();
1232
1233 buffer_b.read_with(cx_b, |buffer, _| {
1234 assert_eq!(buffer.text(), "fn main() { a }")
1235 });
1236}
1237
1238#[gpui::test(iterations = 10)]
1239async fn test_on_input_format_from_guest_to_host(
1240 executor: BackgroundExecutor,
1241 cx_a: &mut TestAppContext,
1242 cx_b: &mut TestAppContext,
1243) {
1244 let mut server = TestServer::start(&executor).await;
1245 let client_a = server.create_client(cx_a, "user_a").await;
1246 let client_b = server.create_client(cx_b, "user_b").await;
1247 server
1248 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1249 .await;
1250 let active_call_a = cx_a.read(ActiveCall::global);
1251
1252 // Set up a fake language server.
1253 let mut language = Language::new(
1254 LanguageConfig {
1255 name: "Rust".into(),
1256 path_suffixes: vec!["rs".to_string()],
1257 ..Default::default()
1258 },
1259 Some(tree_sitter_rust::language()),
1260 );
1261 let mut fake_language_servers = language
1262 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1263 capabilities: lsp::ServerCapabilities {
1264 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1265 first_trigger_character: ":".to_string(),
1266 more_trigger_character: Some(vec![">".to_string()]),
1267 }),
1268 ..Default::default()
1269 },
1270 ..Default::default()
1271 }))
1272 .await;
1273 client_a.language_registry().add(Arc::new(language));
1274
1275 client_a
1276 .fs()
1277 .insert_tree(
1278 "/a",
1279 json!({
1280 "main.rs": "fn main() { a }",
1281 "other.rs": "// Test file",
1282 }),
1283 )
1284 .await;
1285 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1286 let project_id = active_call_a
1287 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1288 .await
1289 .unwrap();
1290 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1291
1292 // Open a file in an editor as the guest.
1293 let buffer_b = project_b
1294 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1295 .await
1296 .unwrap();
1297 let window_b = cx_b.add_window(|_| EmptyView);
1298 let editor_b = window_b.add_view(cx_b, |cx| {
1299 Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
1300 });
1301
1302 let fake_language_server = fake_language_servers.next().await.unwrap();
1303 executor.run_until_parked();
1304 // Type a on type formatting trigger character as the guest.
1305 editor_b.update(cx_b, |editor, cx| {
1306 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1307 editor.handle_input(":", cx);
1308 cx.focus(&editor_b);
1309 });
1310
1311 // Receive an OnTypeFormatting request as the host's language server.
1312 // Return some formattings from the host's language server.
1313 cx_a.foreground().start_waiting();
1314 fake_language_server
1315 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1316 assert_eq!(
1317 params.text_document_position.text_document.uri,
1318 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1319 );
1320 assert_eq!(
1321 params.text_document_position.position,
1322 lsp::Position::new(0, 14),
1323 );
1324
1325 Ok(Some(vec![lsp::TextEdit {
1326 new_text: "~:".to_string(),
1327 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1328 }]))
1329 })
1330 .next()
1331 .await
1332 .unwrap();
1333 cx_a.foreground().finish_waiting();
1334
1335 // Open the buffer on the host and see that the formattings worked
1336 let buffer_a = project_a
1337 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1338 .await
1339 .unwrap();
1340 executor.run_until_parked();
1341
1342 buffer_a.read_with(cx_a, |buffer, _| {
1343 assert_eq!(buffer.text(), "fn main() { a:~: }")
1344 });
1345
1346 // Undo should remove LSP edits first
1347 editor_b.update(cx_b, |editor, cx| {
1348 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1349 editor.undo(&Undo, cx);
1350 assert_eq!(editor.text(cx), "fn main() { a: }");
1351 });
1352 executor.run_until_parked();
1353
1354 buffer_a.read_with(cx_a, |buffer, _| {
1355 assert_eq!(buffer.text(), "fn main() { a: }")
1356 });
1357
1358 editor_b.update(cx_b, |editor, cx| {
1359 assert_eq!(editor.text(cx), "fn main() { a: }");
1360 editor.undo(&Undo, cx);
1361 assert_eq!(editor.text(cx), "fn main() { a }");
1362 });
1363 executor.run_until_parked();
1364
1365 buffer_a.read_with(cx_a, |buffer, _| {
1366 assert_eq!(buffer.text(), "fn main() { a }")
1367 });
1368}
1369
1370#[gpui::test(iterations = 10)]
1371async fn test_mutual_editor_inlay_hint_cache_update(
1372 executor: BackgroundExecutor,
1373 cx_a: &mut TestAppContext,
1374 cx_b: &mut TestAppContext,
1375) {
1376 let mut server = TestServer::start(&executor).await;
1377 let client_a = server.create_client(cx_a, "user_a").await;
1378 let client_b = server.create_client(cx_b, "user_b").await;
1379 server
1380 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1381 .await;
1382 let active_call_a = cx_a.read(ActiveCall::global);
1383 let active_call_b = cx_b.read(ActiveCall::global);
1384
1385 cx_a.update(editor::init);
1386 cx_b.update(editor::init);
1387
1388 cx_a.update(|cx| {
1389 cx.update_global(|store: &mut SettingsStore, cx| {
1390 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1391 settings.defaults.inlay_hints = Some(InlayHintSettings {
1392 enabled: true,
1393 show_type_hints: true,
1394 show_parameter_hints: false,
1395 show_other_hints: true,
1396 })
1397 });
1398 });
1399 });
1400 cx_b.update(|cx| {
1401 cx.update_global(|store: &mut SettingsStore, cx| {
1402 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1403 settings.defaults.inlay_hints = Some(InlayHintSettings {
1404 enabled: true,
1405 show_type_hints: true,
1406 show_parameter_hints: false,
1407 show_other_hints: true,
1408 })
1409 });
1410 });
1411 });
1412
1413 let mut language = Language::new(
1414 LanguageConfig {
1415 name: "Rust".into(),
1416 path_suffixes: vec!["rs".to_string()],
1417 ..Default::default()
1418 },
1419 Some(tree_sitter_rust::language()),
1420 );
1421 let mut fake_language_servers = language
1422 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1423 capabilities: lsp::ServerCapabilities {
1424 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1425 ..Default::default()
1426 },
1427 ..Default::default()
1428 }))
1429 .await;
1430 let language = Arc::new(language);
1431 client_a.language_registry().add(Arc::clone(&language));
1432 client_b.language_registry().add(language);
1433
1434 // Client A opens a project.
1435 client_a
1436 .fs()
1437 .insert_tree(
1438 "/a",
1439 json!({
1440 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1441 "other.rs": "// Test file",
1442 }),
1443 )
1444 .await;
1445 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1446 active_call_a
1447 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1448 .await
1449 .unwrap();
1450 let project_id = active_call_a
1451 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1452 .await
1453 .unwrap();
1454
1455 // Client B joins the project
1456 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1457 active_call_b
1458 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1459 .await
1460 .unwrap();
1461
1462 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1463 cx_a.foreground().start_waiting();
1464
1465 // The host opens a rust file.
1466 let _buffer_a = project_a
1467 .update(cx_a, |project, cx| {
1468 project.open_local_buffer("/a/main.rs", cx)
1469 })
1470 .await
1471 .unwrap();
1472 let fake_language_server = fake_language_servers.next().await.unwrap();
1473 let editor_a = workspace_a
1474 .update(cx_a, |workspace, cx| {
1475 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1476 })
1477 .await
1478 .unwrap()
1479 .downcast::<Editor>()
1480 .unwrap();
1481
1482 // Set up the language server to return an additional inlay hint on each request.
1483 let edits_made = Arc::new(AtomicUsize::new(0));
1484 let closure_edits_made = Arc::clone(&edits_made);
1485 fake_language_server
1486 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1487 let task_edits_made = Arc::clone(&closure_edits_made);
1488 async move {
1489 assert_eq!(
1490 params.text_document.uri,
1491 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1492 );
1493 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1494 Ok(Some(vec![lsp::InlayHint {
1495 position: lsp::Position::new(0, edits_made as u32),
1496 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1497 kind: None,
1498 text_edits: None,
1499 tooltip: None,
1500 padding_left: None,
1501 padding_right: None,
1502 data: None,
1503 }]))
1504 }
1505 })
1506 .next()
1507 .await
1508 .unwrap();
1509
1510 executor.run_until_parked();
1511
1512 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1513 editor_a.update(cx_a, |editor, _| {
1514 assert_eq!(
1515 vec![initial_edit.to_string()],
1516 extract_hint_labels(editor),
1517 "Host should get its first hints when opens an editor"
1518 );
1519 let inlay_cache = editor.inlay_hint_cache();
1520 assert_eq!(
1521 inlay_cache.version(),
1522 1,
1523 "Host editor update the cache version after every cache/view change",
1524 );
1525 });
1526 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1527 let editor_b = workspace_b
1528 .update(cx_b, |workspace, cx| {
1529 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1530 })
1531 .await
1532 .unwrap()
1533 .downcast::<Editor>()
1534 .unwrap();
1535
1536 executor.run_until_parked();
1537 editor_b.update(cx_b, |editor, _| {
1538 assert_eq!(
1539 vec![initial_edit.to_string()],
1540 extract_hint_labels(editor),
1541 "Client should get its first hints when opens an editor"
1542 );
1543 let inlay_cache = editor.inlay_hint_cache();
1544 assert_eq!(
1545 inlay_cache.version(),
1546 1,
1547 "Guest editor update the cache version after every cache/view change"
1548 );
1549 });
1550
1551 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1552 editor_b.update(cx_b, |editor, cx| {
1553 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1554 editor.handle_input(":", cx);
1555 cx.focus(&editor_b);
1556 });
1557
1558 executor.run_until_parked();
1559 editor_a.update(cx_a, |editor, _| {
1560 assert_eq!(
1561 vec![after_client_edit.to_string()],
1562 extract_hint_labels(editor),
1563 );
1564 let inlay_cache = editor.inlay_hint_cache();
1565 assert_eq!(inlay_cache.version(), 2);
1566 });
1567 editor_b.update(cx_b, |editor, _| {
1568 assert_eq!(
1569 vec![after_client_edit.to_string()],
1570 extract_hint_labels(editor),
1571 );
1572 let inlay_cache = editor.inlay_hint_cache();
1573 assert_eq!(inlay_cache.version(), 2);
1574 });
1575
1576 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1577 editor_a.update(cx_a, |editor, cx| {
1578 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1579 editor.handle_input("a change to increment both buffers' versions", cx);
1580 cx.focus(&editor_a);
1581 });
1582
1583 executor.run_until_parked();
1584 editor_a.update(cx_a, |editor, _| {
1585 assert_eq!(
1586 vec![after_host_edit.to_string()],
1587 extract_hint_labels(editor),
1588 );
1589 let inlay_cache = editor.inlay_hint_cache();
1590 assert_eq!(inlay_cache.version(), 3);
1591 });
1592 editor_b.update(cx_b, |editor, _| {
1593 assert_eq!(
1594 vec![after_host_edit.to_string()],
1595 extract_hint_labels(editor),
1596 );
1597 let inlay_cache = editor.inlay_hint_cache();
1598 assert_eq!(inlay_cache.version(), 3);
1599 });
1600
1601 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1602 fake_language_server
1603 .request::<lsp::request::InlayHintRefreshRequest>(())
1604 .await
1605 .expect("inlay refresh request failed");
1606
1607 executor.run_until_parked();
1608 editor_a.update(cx_a, |editor, _| {
1609 assert_eq!(
1610 vec![after_special_edit_for_refresh.to_string()],
1611 extract_hint_labels(editor),
1612 "Host should react to /refresh LSP request"
1613 );
1614 let inlay_cache = editor.inlay_hint_cache();
1615 assert_eq!(
1616 inlay_cache.version(),
1617 4,
1618 "Host should accepted all edits and bump its cache version every time"
1619 );
1620 });
1621 editor_b.update(cx_b, |editor, _| {
1622 assert_eq!(
1623 vec![after_special_edit_for_refresh.to_string()],
1624 extract_hint_labels(editor),
1625 "Guest should get a /refresh LSP request propagated by host"
1626 );
1627 let inlay_cache = editor.inlay_hint_cache();
1628 assert_eq!(
1629 inlay_cache.version(),
1630 4,
1631 "Guest should accepted all edits and bump its cache version every time"
1632 );
1633 });
1634}
1635
1636#[gpui::test(iterations = 10)]
1637async fn test_inlay_hint_refresh_is_forwarded(
1638 executor: BackgroundExecutor,
1639 cx_a: &mut TestAppContext,
1640 cx_b: &mut TestAppContext,
1641) {
1642 let mut server = TestServer::start(&executor).await;
1643 let client_a = server.create_client(cx_a, "user_a").await;
1644 let client_b = server.create_client(cx_b, "user_b").await;
1645 server
1646 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1647 .await;
1648 let active_call_a = cx_a.read(ActiveCall::global);
1649 let active_call_b = cx_b.read(ActiveCall::global);
1650
1651 cx_a.update(editor::init);
1652 cx_b.update(editor::init);
1653
1654 cx_a.update(|cx| {
1655 cx.update_global(|store: &mut SettingsStore, cx| {
1656 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1657 settings.defaults.inlay_hints = Some(InlayHintSettings {
1658 enabled: false,
1659 show_type_hints: false,
1660 show_parameter_hints: false,
1661 show_other_hints: false,
1662 })
1663 });
1664 });
1665 });
1666 cx_b.update(|cx| {
1667 cx.update_global(|store: &mut SettingsStore, cx| {
1668 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1669 settings.defaults.inlay_hints = Some(InlayHintSettings {
1670 enabled: true,
1671 show_type_hints: true,
1672 show_parameter_hints: true,
1673 show_other_hints: true,
1674 })
1675 });
1676 });
1677 });
1678
1679 let mut language = Language::new(
1680 LanguageConfig {
1681 name: "Rust".into(),
1682 path_suffixes: vec!["rs".to_string()],
1683 ..Default::default()
1684 },
1685 Some(tree_sitter_rust::language()),
1686 );
1687 let mut fake_language_servers = language
1688 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1689 capabilities: lsp::ServerCapabilities {
1690 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1691 ..Default::default()
1692 },
1693 ..Default::default()
1694 }))
1695 .await;
1696 let language = Arc::new(language);
1697 client_a.language_registry().add(Arc::clone(&language));
1698 client_b.language_registry().add(language);
1699
1700 client_a
1701 .fs()
1702 .insert_tree(
1703 "/a",
1704 json!({
1705 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1706 "other.rs": "// Test file",
1707 }),
1708 )
1709 .await;
1710 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1711 active_call_a
1712 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1713 .await
1714 .unwrap();
1715 let project_id = active_call_a
1716 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1717 .await
1718 .unwrap();
1719
1720 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1721 active_call_b
1722 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1723 .await
1724 .unwrap();
1725
1726 let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
1727 let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
1728 cx_a.foreground().start_waiting();
1729 cx_b.foreground().start_waiting();
1730
1731 let editor_a = workspace_a
1732 .update(cx_a, |workspace, cx| {
1733 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1734 })
1735 .await
1736 .unwrap()
1737 .downcast::<Editor>()
1738 .unwrap();
1739
1740 let editor_b = workspace_b
1741 .update(cx_b, |workspace, cx| {
1742 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1743 })
1744 .await
1745 .unwrap()
1746 .downcast::<Editor>()
1747 .unwrap();
1748
1749 let other_hints = Arc::new(AtomicBool::new(false));
1750 let fake_language_server = fake_language_servers.next().await.unwrap();
1751 let closure_other_hints = Arc::clone(&other_hints);
1752 fake_language_server
1753 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1754 let task_other_hints = Arc::clone(&closure_other_hints);
1755 async move {
1756 assert_eq!(
1757 params.text_document.uri,
1758 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1759 );
1760 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1761 let character = if other_hints { 0 } else { 2 };
1762 let label = if other_hints {
1763 "other hint"
1764 } else {
1765 "initial hint"
1766 };
1767 Ok(Some(vec![lsp::InlayHint {
1768 position: lsp::Position::new(0, character),
1769 label: lsp::InlayHintLabel::String(label.to_string()),
1770 kind: None,
1771 text_edits: None,
1772 tooltip: None,
1773 padding_left: None,
1774 padding_right: None,
1775 data: None,
1776 }]))
1777 }
1778 })
1779 .next()
1780 .await
1781 .unwrap();
1782 cx_a.foreground().finish_waiting();
1783 cx_b.foreground().finish_waiting();
1784
1785 executor.run_until_parked();
1786 editor_a.update(cx_a, |editor, _| {
1787 assert!(
1788 extract_hint_labels(editor).is_empty(),
1789 "Host should get no hints due to them turned off"
1790 );
1791 let inlay_cache = editor.inlay_hint_cache();
1792 assert_eq!(
1793 inlay_cache.version(),
1794 0,
1795 "Turned off hints should not generate version updates"
1796 );
1797 });
1798
1799 executor.run_until_parked();
1800 editor_b.update(cx_b, |editor, _| {
1801 assert_eq!(
1802 vec!["initial hint".to_string()],
1803 extract_hint_labels(editor),
1804 "Client should get its first hints when opens an editor"
1805 );
1806 let inlay_cache = editor.inlay_hint_cache();
1807 assert_eq!(
1808 inlay_cache.version(),
1809 1,
1810 "Should update cache verison after first hints"
1811 );
1812 });
1813
1814 other_hints.fetch_or(true, atomic::Ordering::Release);
1815 fake_language_server
1816 .request::<lsp::request::InlayHintRefreshRequest>(())
1817 .await
1818 .expect("inlay refresh request failed");
1819 executor.run_until_parked();
1820 editor_a.update(cx_a, |editor, _| {
1821 assert!(
1822 extract_hint_labels(editor).is_empty(),
1823 "Host should get nop hints due to them turned off, even after the /refresh"
1824 );
1825 let inlay_cache = editor.inlay_hint_cache();
1826 assert_eq!(
1827 inlay_cache.version(),
1828 0,
1829 "Turned off hints should not generate version updates, again"
1830 );
1831 });
1832
1833 executor.run_until_parked();
1834 editor_b.update(cx_b, |editor, _| {
1835 assert_eq!(
1836 vec!["other hint".to_string()],
1837 extract_hint_labels(editor),
1838 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1839 );
1840 let inlay_cache = editor.inlay_hint_cache();
1841 assert_eq!(
1842 inlay_cache.version(),
1843 2,
1844 "Guest should accepted all edits and bump its cache version every time"
1845 );
1846 });
1847}
1848
1849fn extract_hint_labels(editor: &Editor) -> Vec<String> {
1850 let mut labels = Vec::new();
1851 for hint in editor.inlay_hint_cache().hints() {
1852 match hint.label {
1853 project::InlayHintLabel::String(s) => labels.push(s),
1854 _ => unreachable!(),
1855 }
1856 }
1857 labels
1858}