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