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