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