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