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