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