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