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