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