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