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