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 let rename_selection = rename_editor.selections.newest::<usize>(cx);
740 assert_eq!(
741 rename_selection.range(),
742 0..3,
743 "Rename that was triggered from zero selection caret, should propose the whole word."
744 );
745 rename_editor.buffer().update(cx, |rename_buffer, cx| {
746 rename_buffer.edit([(0..3, "THREE")], None, cx);
747 });
748 });
749 });
750
751 // Cancel the rename, and repeat the same, but use selections instead of cursor movement
752 editor_b.update(cx_b, |editor, cx| {
753 editor.cancel(&editor::actions::Cancel, cx);
754 });
755 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
756 editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
757 editor.rename(&Rename, cx).unwrap()
758 });
759
760 fake_language_server
761 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
762 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
763 assert_eq!(params.position, lsp::Position::new(0, 8));
764 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
765 lsp::Position::new(0, 6),
766 lsp::Position::new(0, 9),
767 ))))
768 })
769 .next()
770 .await
771 .unwrap();
772 prepare_rename.await.unwrap();
773 editor_b.update(cx_b, |editor, cx| {
774 use editor::ToOffset;
775 let rename = editor.pending_rename().unwrap();
776 let buffer = editor.buffer().read(cx).snapshot(cx);
777 let lsp_rename_start = rename.range.start.to_offset(&buffer);
778 let lsp_rename_end = rename.range.end.to_offset(&buffer);
779 assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
780 rename.editor.update(cx, |rename_editor, cx| {
781 let rename_selection = rename_editor.selections.newest::<usize>(cx);
782 assert_eq!(
783 rename_selection.range(),
784 1..2,
785 "Rename that was triggered from a selection, should have the same selection range in the rename proposal"
786 );
787 rename_editor.buffer().update(cx, |rename_buffer, cx| {
788 rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
789 });
790 });
791 });
792
793 let confirm_rename = editor_b.update(cx_b, |editor, cx| {
794 Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
795 });
796 fake_language_server
797 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
798 assert_eq!(
799 params.text_document_position.text_document.uri.as_str(),
800 "file:///dir/one.rs"
801 );
802 assert_eq!(
803 params.text_document_position.position,
804 lsp::Position::new(0, 6)
805 );
806 assert_eq!(params.new_name, "THREE");
807 Ok(Some(lsp::WorkspaceEdit {
808 changes: Some(
809 [
810 (
811 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
812 vec![lsp::TextEdit::new(
813 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
814 "THREE".to_string(),
815 )],
816 ),
817 (
818 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
819 vec![
820 lsp::TextEdit::new(
821 lsp::Range::new(
822 lsp::Position::new(0, 24),
823 lsp::Position::new(0, 27),
824 ),
825 "THREE".to_string(),
826 ),
827 lsp::TextEdit::new(
828 lsp::Range::new(
829 lsp::Position::new(0, 35),
830 lsp::Position::new(0, 38),
831 ),
832 "THREE".to_string(),
833 ),
834 ],
835 ),
836 ]
837 .into_iter()
838 .collect(),
839 ),
840 ..Default::default()
841 }))
842 })
843 .next()
844 .await
845 .unwrap();
846 confirm_rename.await.unwrap();
847
848 let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
849 workspace.active_item_as::<Editor>(cx).unwrap()
850 });
851
852 rename_editor.update(cx_b, |editor, cx| {
853 assert_eq!(
854 editor.text(cx),
855 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
856 );
857 editor.undo(&Undo, cx);
858 assert_eq!(
859 editor.text(cx),
860 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
861 );
862 editor.redo(&Redo, cx);
863 assert_eq!(
864 editor.text(cx),
865 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
866 );
867 });
868
869 // Ensure temporary rename edits cannot be undone/redone.
870 editor_b.update(cx_b, |editor, cx| {
871 editor.undo(&Undo, cx);
872 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
873 editor.undo(&Undo, cx);
874 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
875 editor.redo(&Redo, cx);
876 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
877 })
878}
879
880#[gpui::test(iterations = 10)]
881async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
882 let mut server = TestServer::start(cx_a.executor()).await;
883 let executor = cx_a.executor();
884 let client_a = server.create_client(cx_a, "user_a").await;
885 let client_b = server.create_client(cx_b, "user_b").await;
886 server
887 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
888 .await;
889 let active_call_a = cx_a.read(ActiveCall::global);
890
891 cx_b.update(editor::init);
892
893 client_a.language_registry().add(rust_lang());
894 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
895 "Rust",
896 FakeLspAdapter {
897 name: "the-language-server",
898 ..Default::default()
899 },
900 );
901
902 client_a
903 .fs()
904 .insert_tree(
905 "/dir",
906 json!({
907 "main.rs": "const ONE: usize = 1;",
908 }),
909 )
910 .await;
911 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
912
913 let _buffer_a = project_a
914 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
915 .await
916 .unwrap();
917
918 let fake_language_server = fake_language_servers.next().await.unwrap();
919 fake_language_server.start_progress("the-token").await;
920 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
921 token: lsp::NumberOrString::String("the-token".to_string()),
922 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
923 lsp::WorkDoneProgressReport {
924 message: Some("the-message".to_string()),
925 ..Default::default()
926 },
927 )),
928 });
929 executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
930 executor.run_until_parked();
931
932 project_a.read_with(cx_a, |project, _| {
933 let status = project.language_server_statuses().next().unwrap();
934 assert_eq!(status.name, "the-language-server");
935 assert_eq!(status.pending_work.len(), 1);
936 assert_eq!(
937 status.pending_work["the-token"].message.as_ref().unwrap(),
938 "the-message"
939 );
940 });
941
942 let project_id = active_call_a
943 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
944 .await
945 .unwrap();
946 executor.run_until_parked();
947 let project_b = client_b.build_remote_project(project_id, cx_b).await;
948
949 project_b.read_with(cx_b, |project, _| {
950 let status = project.language_server_statuses().next().unwrap();
951 assert_eq!(status.name, "the-language-server");
952 });
953
954 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
955 token: lsp::NumberOrString::String("the-token".to_string()),
956 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
957 lsp::WorkDoneProgressReport {
958 message: Some("the-message-2".to_string()),
959 ..Default::default()
960 },
961 )),
962 });
963 executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
964 executor.run_until_parked();
965
966 project_a.read_with(cx_a, |project, _| {
967 let status = project.language_server_statuses().next().unwrap();
968 assert_eq!(status.name, "the-language-server");
969 assert_eq!(status.pending_work.len(), 1);
970 assert_eq!(
971 status.pending_work["the-token"].message.as_ref().unwrap(),
972 "the-message-2"
973 );
974 });
975
976 project_b.read_with(cx_b, |project, _| {
977 let status = project.language_server_statuses().next().unwrap();
978 assert_eq!(status.name, "the-language-server");
979 assert_eq!(status.pending_work.len(), 1);
980 assert_eq!(
981 status.pending_work["the-token"].message.as_ref().unwrap(),
982 "the-message-2"
983 );
984 });
985}
986
987#[gpui::test(iterations = 10)]
988async fn test_share_project(
989 cx_a: &mut TestAppContext,
990 cx_b: &mut TestAppContext,
991 cx_c: &mut TestAppContext,
992) {
993 let executor = cx_a.executor();
994 let cx_b = cx_b.add_empty_window();
995 let mut server = TestServer::start(executor.clone()).await;
996 let client_a = server.create_client(cx_a, "user_a").await;
997 let client_b = server.create_client(cx_b, "user_b").await;
998 let client_c = server.create_client(cx_c, "user_c").await;
999 server
1000 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1001 .await;
1002 let active_call_a = cx_a.read(ActiveCall::global);
1003 let active_call_b = cx_b.read(ActiveCall::global);
1004 let active_call_c = cx_c.read(ActiveCall::global);
1005
1006 client_a
1007 .fs()
1008 .insert_tree(
1009 "/a",
1010 json!({
1011 ".gitignore": "ignored-dir",
1012 "a.txt": "a-contents",
1013 "b.txt": "b-contents",
1014 "ignored-dir": {
1015 "c.txt": "",
1016 "d.txt": "",
1017 }
1018 }),
1019 )
1020 .await;
1021
1022 // Invite client B to collaborate on a project
1023 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1024 active_call_a
1025 .update(cx_a, |call, cx| {
1026 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1027 })
1028 .await
1029 .unwrap();
1030
1031 // Join that project as client B
1032
1033 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1034 executor.run_until_parked();
1035 let call = incoming_call_b.borrow().clone().unwrap();
1036 assert_eq!(call.calling_user.github_login, "user_a");
1037 let initial_project = call.initial_project.unwrap();
1038 active_call_b
1039 .update(cx_b, |call, cx| call.accept_incoming(cx))
1040 .await
1041 .unwrap();
1042 let client_b_peer_id = client_b.peer_id().unwrap();
1043 let project_b = client_b
1044 .build_remote_project(initial_project.id, cx_b)
1045 .await;
1046
1047 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1048
1049 executor.run_until_parked();
1050
1051 project_a.read_with(cx_a, |project, _| {
1052 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1053 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1054 });
1055
1056 project_b.read_with(cx_b, |project, cx| {
1057 let worktree = project.worktrees().next().unwrap().read(cx);
1058 assert_eq!(
1059 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1060 [
1061 Path::new(".gitignore"),
1062 Path::new("a.txt"),
1063 Path::new("b.txt"),
1064 Path::new("ignored-dir"),
1065 ]
1066 );
1067 });
1068
1069 project_b
1070 .update(cx_b, |project, cx| {
1071 let worktree = project.worktrees().next().unwrap();
1072 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1073 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1074 })
1075 .await
1076 .unwrap();
1077
1078 project_b.read_with(cx_b, |project, cx| {
1079 let worktree = project.worktrees().next().unwrap().read(cx);
1080 assert_eq!(
1081 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1082 [
1083 Path::new(".gitignore"),
1084 Path::new("a.txt"),
1085 Path::new("b.txt"),
1086 Path::new("ignored-dir"),
1087 Path::new("ignored-dir/c.txt"),
1088 Path::new("ignored-dir/d.txt"),
1089 ]
1090 );
1091 });
1092
1093 // Open the same file as client B and client A.
1094 let buffer_b = project_b
1095 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1096 .await
1097 .unwrap();
1098
1099 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1100
1101 project_a.read_with(cx_a, |project, cx| {
1102 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1103 });
1104 let buffer_a = project_a
1105 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1106 .await
1107 .unwrap();
1108
1109 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1110
1111 // Client A sees client B's selection
1112 executor.run_until_parked();
1113
1114 buffer_a.read_with(cx_a, |buffer, _| {
1115 buffer
1116 .snapshot()
1117 .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1118 .count()
1119 == 1
1120 });
1121
1122 // Edit the buffer as client B and see that edit as client A.
1123 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1124 executor.run_until_parked();
1125
1126 buffer_a.read_with(cx_a, |buffer, _| {
1127 assert_eq!(buffer.text(), "ok, b-contents")
1128 });
1129
1130 // Client B can invite client C on a project shared by client A.
1131 active_call_b
1132 .update(cx_b, |call, cx| {
1133 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1134 })
1135 .await
1136 .unwrap();
1137
1138 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1139 executor.run_until_parked();
1140 let call = incoming_call_c.borrow().clone().unwrap();
1141 assert_eq!(call.calling_user.github_login, "user_b");
1142 let initial_project = call.initial_project.unwrap();
1143 active_call_c
1144 .update(cx_c, |call, cx| call.accept_incoming(cx))
1145 .await
1146 .unwrap();
1147 let _project_c = client_c
1148 .build_remote_project(initial_project.id, cx_c)
1149 .await;
1150
1151 // Client B closes the editor, and client A sees client B's selections removed.
1152 cx_b.update(move |_| drop(editor_b));
1153 executor.run_until_parked();
1154
1155 buffer_a.read_with(cx_a, |buffer, _| {
1156 buffer
1157 .snapshot()
1158 .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1159 .count()
1160 == 0
1161 });
1162}
1163
1164#[gpui::test(iterations = 10)]
1165async fn test_on_input_format_from_host_to_guest(
1166 cx_a: &mut TestAppContext,
1167 cx_b: &mut TestAppContext,
1168) {
1169 let mut server = TestServer::start(cx_a.executor()).await;
1170 let executor = cx_a.executor();
1171 let client_a = server.create_client(cx_a, "user_a").await;
1172 let client_b = server.create_client(cx_b, "user_b").await;
1173 server
1174 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1175 .await;
1176 let active_call_a = cx_a.read(ActiveCall::global);
1177
1178 client_a.language_registry().add(rust_lang());
1179 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1180 "Rust",
1181 FakeLspAdapter {
1182 capabilities: lsp::ServerCapabilities {
1183 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1184 first_trigger_character: ":".to_string(),
1185 more_trigger_character: Some(vec![">".to_string()]),
1186 }),
1187 ..Default::default()
1188 },
1189 ..Default::default()
1190 },
1191 );
1192
1193 client_a
1194 .fs()
1195 .insert_tree(
1196 "/a",
1197 json!({
1198 "main.rs": "fn main() { a }",
1199 "other.rs": "// Test file",
1200 }),
1201 )
1202 .await;
1203 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1204 let project_id = active_call_a
1205 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1206 .await
1207 .unwrap();
1208 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1209
1210 // Open a file in an editor as the host.
1211 let buffer_a = project_a
1212 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1213 .await
1214 .unwrap();
1215 let cx_a = cx_a.add_empty_window();
1216 let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1217
1218 let fake_language_server = fake_language_servers.next().await.unwrap();
1219 executor.run_until_parked();
1220
1221 // Receive an OnTypeFormatting request as the host's language server.
1222 // Return some formatting from the host's language server.
1223 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1224 |params, _| async move {
1225 assert_eq!(
1226 params.text_document_position.text_document.uri,
1227 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1228 );
1229 assert_eq!(
1230 params.text_document_position.position,
1231 lsp::Position::new(0, 14),
1232 );
1233
1234 Ok(Some(vec![lsp::TextEdit {
1235 new_text: "~<".to_string(),
1236 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1237 }]))
1238 },
1239 );
1240
1241 // Open the buffer on the guest and see that the formatting worked
1242 let buffer_b = project_b
1243 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1244 .await
1245 .unwrap();
1246
1247 // Type a on type formatting trigger character as the guest.
1248 cx_a.focus_view(&editor_a);
1249 editor_a.update(cx_a, |editor, cx| {
1250 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1251 editor.handle_input(">", cx);
1252 });
1253
1254 executor.run_until_parked();
1255
1256 buffer_b.read_with(cx_b, |buffer, _| {
1257 assert_eq!(buffer.text(), "fn main() { a>~< }")
1258 });
1259
1260 // Undo should remove LSP edits first
1261 editor_a.update(cx_a, |editor, cx| {
1262 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1263 editor.undo(&Undo, cx);
1264 assert_eq!(editor.text(cx), "fn main() { a> }");
1265 });
1266 executor.run_until_parked();
1267
1268 buffer_b.read_with(cx_b, |buffer, _| {
1269 assert_eq!(buffer.text(), "fn main() { a> }")
1270 });
1271
1272 editor_a.update(cx_a, |editor, cx| {
1273 assert_eq!(editor.text(cx), "fn main() { a> }");
1274 editor.undo(&Undo, cx);
1275 assert_eq!(editor.text(cx), "fn main() { a }");
1276 });
1277 executor.run_until_parked();
1278
1279 buffer_b.read_with(cx_b, |buffer, _| {
1280 assert_eq!(buffer.text(), "fn main() { a }")
1281 });
1282}
1283
1284#[gpui::test(iterations = 10)]
1285async fn test_on_input_format_from_guest_to_host(
1286 cx_a: &mut TestAppContext,
1287 cx_b: &mut TestAppContext,
1288) {
1289 let mut server = TestServer::start(cx_a.executor()).await;
1290 let executor = cx_a.executor();
1291 let client_a = server.create_client(cx_a, "user_a").await;
1292 let client_b = server.create_client(cx_b, "user_b").await;
1293 server
1294 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1295 .await;
1296 let active_call_a = cx_a.read(ActiveCall::global);
1297
1298 client_a.language_registry().add(rust_lang());
1299 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1300 "Rust",
1301 FakeLspAdapter {
1302 capabilities: lsp::ServerCapabilities {
1303 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1304 first_trigger_character: ":".to_string(),
1305 more_trigger_character: Some(vec![">".to_string()]),
1306 }),
1307 ..Default::default()
1308 },
1309 ..Default::default()
1310 },
1311 );
1312
1313 client_a
1314 .fs()
1315 .insert_tree(
1316 "/a",
1317 json!({
1318 "main.rs": "fn main() { a }",
1319 "other.rs": "// Test file",
1320 }),
1321 )
1322 .await;
1323 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1324 let project_id = active_call_a
1325 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1326 .await
1327 .unwrap();
1328 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1329
1330 // Open a file in an editor as the guest.
1331 let buffer_b = project_b
1332 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1333 .await
1334 .unwrap();
1335 let cx_b = cx_b.add_empty_window();
1336 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1337
1338 let fake_language_server = fake_language_servers.next().await.unwrap();
1339 executor.run_until_parked();
1340
1341 // Type a on type formatting trigger character as the guest.
1342 cx_b.focus_view(&editor_b);
1343 editor_b.update(cx_b, |editor, cx| {
1344 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1345 editor.handle_input(":", cx);
1346 });
1347
1348 // Receive an OnTypeFormatting request as the host's language server.
1349 // Return some formatting from the host's language server.
1350 executor.start_waiting();
1351 fake_language_server
1352 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1353 assert_eq!(
1354 params.text_document_position.text_document.uri,
1355 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1356 );
1357 assert_eq!(
1358 params.text_document_position.position,
1359 lsp::Position::new(0, 14),
1360 );
1361
1362 Ok(Some(vec![lsp::TextEdit {
1363 new_text: "~:".to_string(),
1364 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1365 }]))
1366 })
1367 .next()
1368 .await
1369 .unwrap();
1370 executor.finish_waiting();
1371
1372 // Open the buffer on the host and see that the formatting worked
1373 let buffer_a = project_a
1374 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1375 .await
1376 .unwrap();
1377 executor.run_until_parked();
1378
1379 buffer_a.read_with(cx_a, |buffer, _| {
1380 assert_eq!(buffer.text(), "fn main() { a:~: }")
1381 });
1382
1383 // Undo should remove LSP edits first
1384 editor_b.update(cx_b, |editor, cx| {
1385 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1386 editor.undo(&Undo, cx);
1387 assert_eq!(editor.text(cx), "fn main() { a: }");
1388 });
1389 executor.run_until_parked();
1390
1391 buffer_a.read_with(cx_a, |buffer, _| {
1392 assert_eq!(buffer.text(), "fn main() { a: }")
1393 });
1394
1395 editor_b.update(cx_b, |editor, cx| {
1396 assert_eq!(editor.text(cx), "fn main() { a: }");
1397 editor.undo(&Undo, cx);
1398 assert_eq!(editor.text(cx), "fn main() { a }");
1399 });
1400 executor.run_until_parked();
1401
1402 buffer_a.read_with(cx_a, |buffer, _| {
1403 assert_eq!(buffer.text(), "fn main() { a }")
1404 });
1405}
1406
1407#[gpui::test(iterations = 10)]
1408async fn test_mutual_editor_inlay_hint_cache_update(
1409 cx_a: &mut TestAppContext,
1410 cx_b: &mut TestAppContext,
1411) {
1412 let mut server = TestServer::start(cx_a.executor()).await;
1413 let executor = cx_a.executor();
1414 let client_a = server.create_client(cx_a, "user_a").await;
1415 let client_b = server.create_client(cx_b, "user_b").await;
1416 server
1417 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1418 .await;
1419 let active_call_a = cx_a.read(ActiveCall::global);
1420 let active_call_b = cx_b.read(ActiveCall::global);
1421
1422 cx_a.update(editor::init);
1423 cx_b.update(editor::init);
1424
1425 cx_a.update(|cx| {
1426 cx.update_global(|store: &mut SettingsStore, cx| {
1427 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1428 settings.defaults.inlay_hints = Some(InlayHintSettings {
1429 enabled: true,
1430 edit_debounce_ms: 0,
1431 scroll_debounce_ms: 0,
1432 show_type_hints: true,
1433 show_parameter_hints: false,
1434 show_other_hints: true,
1435 })
1436 });
1437 });
1438 });
1439 cx_b.update(|cx| {
1440 cx.update_global(|store: &mut SettingsStore, cx| {
1441 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1442 settings.defaults.inlay_hints = Some(InlayHintSettings {
1443 enabled: true,
1444 edit_debounce_ms: 0,
1445 scroll_debounce_ms: 0,
1446 show_type_hints: true,
1447 show_parameter_hints: false,
1448 show_other_hints: true,
1449 })
1450 });
1451 });
1452 });
1453
1454 client_a.language_registry().add(rust_lang());
1455 client_b.language_registry().add(rust_lang());
1456 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1457 "Rust",
1458 FakeLspAdapter {
1459 capabilities: lsp::ServerCapabilities {
1460 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1461 ..Default::default()
1462 },
1463 ..Default::default()
1464 },
1465 );
1466
1467 // Client A opens a project.
1468 client_a
1469 .fs()
1470 .insert_tree(
1471 "/a",
1472 json!({
1473 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1474 "other.rs": "// Test file",
1475 }),
1476 )
1477 .await;
1478 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1479 active_call_a
1480 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1481 .await
1482 .unwrap();
1483 let project_id = active_call_a
1484 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1485 .await
1486 .unwrap();
1487
1488 // Client B joins the project
1489 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1490 active_call_b
1491 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1492 .await
1493 .unwrap();
1494
1495 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1496 executor.start_waiting();
1497
1498 // The host opens a rust file.
1499 let _buffer_a = project_a
1500 .update(cx_a, |project, cx| {
1501 project.open_local_buffer("/a/main.rs", cx)
1502 })
1503 .await
1504 .unwrap();
1505 let fake_language_server = fake_language_servers.next().await.unwrap();
1506 let editor_a = workspace_a
1507 .update(cx_a, |workspace, cx| {
1508 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1509 })
1510 .await
1511 .unwrap()
1512 .downcast::<Editor>()
1513 .unwrap();
1514
1515 // Set up the language server to return an additional inlay hint on each request.
1516 let edits_made = Arc::new(AtomicUsize::new(0));
1517 let closure_edits_made = Arc::clone(&edits_made);
1518 fake_language_server
1519 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1520 let task_edits_made = Arc::clone(&closure_edits_made);
1521 async move {
1522 assert_eq!(
1523 params.text_document.uri,
1524 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1525 );
1526 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1527 Ok(Some(vec![lsp::InlayHint {
1528 position: lsp::Position::new(0, edits_made as u32),
1529 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1530 kind: None,
1531 text_edits: None,
1532 tooltip: None,
1533 padding_left: None,
1534 padding_right: None,
1535 data: None,
1536 }]))
1537 }
1538 })
1539 .next()
1540 .await
1541 .unwrap();
1542
1543 executor.run_until_parked();
1544
1545 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1546 editor_a.update(cx_a, |editor, _| {
1547 assert_eq!(
1548 vec![initial_edit.to_string()],
1549 extract_hint_labels(editor),
1550 "Host should get its first hints when opens an editor"
1551 );
1552 let inlay_cache = editor.inlay_hint_cache();
1553 assert_eq!(
1554 inlay_cache.version(),
1555 1,
1556 "Host editor update the cache version after every cache/view change",
1557 );
1558 });
1559 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1560 let editor_b = workspace_b
1561 .update(cx_b, |workspace, cx| {
1562 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1563 })
1564 .await
1565 .unwrap()
1566 .downcast::<Editor>()
1567 .unwrap();
1568
1569 executor.run_until_parked();
1570 editor_b.update(cx_b, |editor, _| {
1571 assert_eq!(
1572 vec![initial_edit.to_string()],
1573 extract_hint_labels(editor),
1574 "Client should get its first hints when opens an editor"
1575 );
1576 let inlay_cache = editor.inlay_hint_cache();
1577 assert_eq!(
1578 inlay_cache.version(),
1579 1,
1580 "Guest editor update the cache version after every cache/view change"
1581 );
1582 });
1583
1584 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1585 editor_b.update(cx_b, |editor, cx| {
1586 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1587 editor.handle_input(":", cx);
1588 });
1589 cx_b.focus_view(&editor_b);
1590
1591 executor.run_until_parked();
1592 editor_a.update(cx_a, |editor, _| {
1593 assert_eq!(
1594 vec![after_client_edit.to_string()],
1595 extract_hint_labels(editor),
1596 );
1597 let inlay_cache = editor.inlay_hint_cache();
1598 assert_eq!(inlay_cache.version(), 2);
1599 });
1600 editor_b.update(cx_b, |editor, _| {
1601 assert_eq!(
1602 vec![after_client_edit.to_string()],
1603 extract_hint_labels(editor),
1604 );
1605 let inlay_cache = editor.inlay_hint_cache();
1606 assert_eq!(inlay_cache.version(), 2);
1607 });
1608
1609 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1610 editor_a.update(cx_a, |editor, cx| {
1611 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1612 editor.handle_input("a change to increment both buffers' versions", cx);
1613 });
1614 cx_a.focus_view(&editor_a);
1615
1616 executor.run_until_parked();
1617 editor_a.update(cx_a, |editor, _| {
1618 assert_eq!(
1619 vec![after_host_edit.to_string()],
1620 extract_hint_labels(editor),
1621 );
1622 let inlay_cache = editor.inlay_hint_cache();
1623 assert_eq!(inlay_cache.version(), 3);
1624 });
1625 editor_b.update(cx_b, |editor, _| {
1626 assert_eq!(
1627 vec![after_host_edit.to_string()],
1628 extract_hint_labels(editor),
1629 );
1630 let inlay_cache = editor.inlay_hint_cache();
1631 assert_eq!(inlay_cache.version(), 3);
1632 });
1633
1634 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1635 fake_language_server
1636 .request::<lsp::request::InlayHintRefreshRequest>(())
1637 .await
1638 .expect("inlay refresh request failed");
1639
1640 executor.run_until_parked();
1641 editor_a.update(cx_a, |editor, _| {
1642 assert_eq!(
1643 vec![after_special_edit_for_refresh.to_string()],
1644 extract_hint_labels(editor),
1645 "Host should react to /refresh LSP request"
1646 );
1647 let inlay_cache = editor.inlay_hint_cache();
1648 assert_eq!(
1649 inlay_cache.version(),
1650 4,
1651 "Host should accepted all edits and bump its cache version every time"
1652 );
1653 });
1654 editor_b.update(cx_b, |editor, _| {
1655 assert_eq!(
1656 vec![after_special_edit_for_refresh.to_string()],
1657 extract_hint_labels(editor),
1658 "Guest should get a /refresh LSP request propagated by host"
1659 );
1660 let inlay_cache = editor.inlay_hint_cache();
1661 assert_eq!(
1662 inlay_cache.version(),
1663 4,
1664 "Guest should accepted all edits and bump its cache version every time"
1665 );
1666 });
1667}
1668
1669#[gpui::test(iterations = 10)]
1670async fn test_inlay_hint_refresh_is_forwarded(
1671 cx_a: &mut TestAppContext,
1672 cx_b: &mut TestAppContext,
1673) {
1674 let mut server = TestServer::start(cx_a.executor()).await;
1675 let executor = cx_a.executor();
1676 let client_a = server.create_client(cx_a, "user_a").await;
1677 let client_b = server.create_client(cx_b, "user_b").await;
1678 server
1679 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1680 .await;
1681 let active_call_a = cx_a.read(ActiveCall::global);
1682 let active_call_b = cx_b.read(ActiveCall::global);
1683
1684 cx_a.update(editor::init);
1685 cx_b.update(editor::init);
1686
1687 cx_a.update(|cx| {
1688 cx.update_global(|store: &mut SettingsStore, cx| {
1689 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1690 settings.defaults.inlay_hints = Some(InlayHintSettings {
1691 enabled: false,
1692 edit_debounce_ms: 0,
1693 scroll_debounce_ms: 0,
1694 show_type_hints: false,
1695 show_parameter_hints: false,
1696 show_other_hints: false,
1697 })
1698 });
1699 });
1700 });
1701 cx_b.update(|cx| {
1702 cx.update_global(|store: &mut SettingsStore, cx| {
1703 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1704 settings.defaults.inlay_hints = Some(InlayHintSettings {
1705 enabled: true,
1706 edit_debounce_ms: 0,
1707 scroll_debounce_ms: 0,
1708 show_type_hints: true,
1709 show_parameter_hints: true,
1710 show_other_hints: true,
1711 })
1712 });
1713 });
1714 });
1715
1716 client_a.language_registry().add(rust_lang());
1717 client_b.language_registry().add(rust_lang());
1718 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1719 "Rust",
1720 FakeLspAdapter {
1721 capabilities: lsp::ServerCapabilities {
1722 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1723 ..Default::default()
1724 },
1725 ..Default::default()
1726 },
1727 );
1728
1729 client_a
1730 .fs()
1731 .insert_tree(
1732 "/a",
1733 json!({
1734 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1735 "other.rs": "// Test file",
1736 }),
1737 )
1738 .await;
1739 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1740 active_call_a
1741 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1742 .await
1743 .unwrap();
1744 let project_id = active_call_a
1745 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1746 .await
1747 .unwrap();
1748
1749 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1750 active_call_b
1751 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1752 .await
1753 .unwrap();
1754
1755 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1756 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1757
1758 cx_a.background_executor.start_waiting();
1759
1760 let editor_a = workspace_a
1761 .update(cx_a, |workspace, cx| {
1762 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1763 })
1764 .await
1765 .unwrap()
1766 .downcast::<Editor>()
1767 .unwrap();
1768
1769 let editor_b = workspace_b
1770 .update(cx_b, |workspace, cx| {
1771 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1772 })
1773 .await
1774 .unwrap()
1775 .downcast::<Editor>()
1776 .unwrap();
1777
1778 let other_hints = Arc::new(AtomicBool::new(false));
1779 let fake_language_server = fake_language_servers.next().await.unwrap();
1780 let closure_other_hints = Arc::clone(&other_hints);
1781 fake_language_server
1782 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1783 let task_other_hints = Arc::clone(&closure_other_hints);
1784 async move {
1785 assert_eq!(
1786 params.text_document.uri,
1787 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1788 );
1789 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1790 let character = if other_hints { 0 } else { 2 };
1791 let label = if other_hints {
1792 "other hint"
1793 } else {
1794 "initial hint"
1795 };
1796 Ok(Some(vec![lsp::InlayHint {
1797 position: lsp::Position::new(0, character),
1798 label: lsp::InlayHintLabel::String(label.to_string()),
1799 kind: None,
1800 text_edits: None,
1801 tooltip: None,
1802 padding_left: None,
1803 padding_right: None,
1804 data: None,
1805 }]))
1806 }
1807 })
1808 .next()
1809 .await
1810 .unwrap();
1811 executor.finish_waiting();
1812
1813 executor.run_until_parked();
1814 editor_a.update(cx_a, |editor, _| {
1815 assert!(
1816 extract_hint_labels(editor).is_empty(),
1817 "Host should get no hints due to them turned off"
1818 );
1819 let inlay_cache = editor.inlay_hint_cache();
1820 assert_eq!(
1821 inlay_cache.version(),
1822 0,
1823 "Turned off hints should not generate version updates"
1824 );
1825 });
1826
1827 executor.run_until_parked();
1828 editor_b.update(cx_b, |editor, _| {
1829 assert_eq!(
1830 vec!["initial hint".to_string()],
1831 extract_hint_labels(editor),
1832 "Client should get its first hints when opens an editor"
1833 );
1834 let inlay_cache = editor.inlay_hint_cache();
1835 assert_eq!(
1836 inlay_cache.version(),
1837 1,
1838 "Should update cache version after first hints"
1839 );
1840 });
1841
1842 other_hints.fetch_or(true, atomic::Ordering::Release);
1843 fake_language_server
1844 .request::<lsp::request::InlayHintRefreshRequest>(())
1845 .await
1846 .expect("inlay refresh request failed");
1847 executor.run_until_parked();
1848 editor_a.update(cx_a, |editor, _| {
1849 assert!(
1850 extract_hint_labels(editor).is_empty(),
1851 "Host should get nop hints due to them turned off, even after the /refresh"
1852 );
1853 let inlay_cache = editor.inlay_hint_cache();
1854 assert_eq!(
1855 inlay_cache.version(),
1856 0,
1857 "Turned off hints should not generate version updates, again"
1858 );
1859 });
1860
1861 executor.run_until_parked();
1862 editor_b.update(cx_b, |editor, _| {
1863 assert_eq!(
1864 vec!["other hint".to_string()],
1865 extract_hint_labels(editor),
1866 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1867 );
1868 let inlay_cache = editor.inlay_hint_cache();
1869 assert_eq!(
1870 inlay_cache.version(),
1871 2,
1872 "Guest should accepted all edits and bump its cache version every time"
1873 );
1874 });
1875}
1876
1877#[gpui::test]
1878async fn test_multiple_types_reverts(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1879 let mut server = TestServer::start(cx_a.executor()).await;
1880 let client_a = server.create_client(cx_a, "user_a").await;
1881 let client_b = server.create_client(cx_b, "user_b").await;
1882 server
1883 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1884 .await;
1885 let active_call_a = cx_a.read(ActiveCall::global);
1886 let active_call_b = cx_b.read(ActiveCall::global);
1887
1888 cx_a.update(editor::init);
1889 cx_b.update(editor::init);
1890
1891 client_a.language_registry().add(rust_lang());
1892 client_b.language_registry().add(rust_lang());
1893
1894 let base_text = indoc! {r#"struct Row;
1895struct Row1;
1896struct Row2;
1897
1898struct Row4;
1899struct Row5;
1900struct Row6;
1901
1902struct Row8;
1903struct Row9;
1904struct Row10;"#};
1905
1906 client_a
1907 .fs()
1908 .insert_tree(
1909 "/a",
1910 json!({
1911 "main.rs": base_text,
1912 }),
1913 )
1914 .await;
1915 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1916 active_call_a
1917 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1918 .await
1919 .unwrap();
1920 let project_id = active_call_a
1921 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1922 .await
1923 .unwrap();
1924
1925 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1926 active_call_b
1927 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1928 .await
1929 .unwrap();
1930
1931 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1932 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1933
1934 let editor_a = workspace_a
1935 .update(cx_a, |workspace, cx| {
1936 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1937 })
1938 .await
1939 .unwrap()
1940 .downcast::<Editor>()
1941 .unwrap();
1942
1943 let editor_b = workspace_b
1944 .update(cx_b, |workspace, cx| {
1945 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1946 })
1947 .await
1948 .unwrap()
1949 .downcast::<Editor>()
1950 .unwrap();
1951
1952 let mut editor_cx_a = EditorTestContext {
1953 cx: cx_a.clone(),
1954 window: cx_a.handle(),
1955 editor: editor_a,
1956 assertion_cx: AssertionContextManager::new(),
1957 };
1958 let mut editor_cx_b = EditorTestContext {
1959 cx: cx_b.clone(),
1960 window: cx_b.handle(),
1961 editor: editor_b,
1962 assertion_cx: AssertionContextManager::new(),
1963 };
1964
1965 // host edits the file, that differs from the base text, producing diff hunks
1966 editor_cx_a.set_state(indoc! {r#"struct Row;
1967 struct Row0.1;
1968 struct Row0.2;
1969 struct Row1;
1970
1971 struct Row4;
1972 struct Row5444;
1973 struct Row6;
1974
1975 struct Row9;
1976 struct Row1220;ˇ"#});
1977 editor_cx_a.update_editor(|editor, cx| {
1978 editor
1979 .buffer()
1980 .read(cx)
1981 .as_singleton()
1982 .unwrap()
1983 .update(cx, |buffer, cx| {
1984 buffer.set_diff_base(Some(base_text.to_string()), cx);
1985 });
1986 });
1987 editor_cx_b.update_editor(|editor, cx| {
1988 editor
1989 .buffer()
1990 .read(cx)
1991 .as_singleton()
1992 .unwrap()
1993 .update(cx, |buffer, cx| {
1994 buffer.set_diff_base(Some(base_text.to_string()), cx);
1995 });
1996 });
1997 cx_a.executor().run_until_parked();
1998 cx_b.executor().run_until_parked();
1999
2000 // client, selects a range in the updated buffer, and reverts it
2001 // both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
2002 editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row;
2003 struct Row0.1;
2004 struct Row0.2;
2005 struct Row1;
2006
2007 struct Row4;
2008 struct Row5444;
2009 struct Row6;
2010
2011 struct R»ow9;
2012 struct Row1220;"#});
2013 editor_cx_b.update_editor(|editor, cx| {
2014 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
2015 });
2016 cx_a.executor().run_until_parked();
2017 cx_b.executor().run_until_parked();
2018 editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
2019 struct Row1;
2020 struct Row2;
2021
2022 struct Row4;
2023 struct Row5;
2024 struct Row6;
2025
2026 struct Row8;
2027 struct Row9;
2028 struct Row1220;ˇ"#});
2029 editor_cx_b.assert_editor_state(indoc! {r#"«ˇstruct Row;
2030 struct Row1;
2031 struct Row2;
2032
2033 struct Row4;
2034 struct Row5;
2035 struct Row6;
2036
2037 struct Row8;
2038 struct R»ow9;
2039 struct Row1220;"#});
2040}
2041
2042#[gpui::test(iterations = 10)]
2043async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2044 let mut server = TestServer::start(cx_a.executor()).await;
2045 let client_a = server.create_client(cx_a, "user_a").await;
2046 let client_b = server.create_client(cx_b, "user_b").await;
2047 server
2048 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2049 .await;
2050 let active_call_a = cx_a.read(ActiveCall::global);
2051
2052 cx_a.update(editor::init);
2053 cx_b.update(editor::init);
2054 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
2055 let inline_blame_off_settings = Some(InlineBlameSettings {
2056 enabled: false,
2057 delay_ms: None,
2058 min_column: None,
2059 });
2060 cx_a.update(|cx| {
2061 cx.update_global(|store: &mut SettingsStore, cx| {
2062 store.update_user_settings::<ProjectSettings>(cx, |settings| {
2063 settings.git.inline_blame = inline_blame_off_settings;
2064 });
2065 });
2066 });
2067 cx_b.update(|cx| {
2068 cx.update_global(|store: &mut SettingsStore, cx| {
2069 store.update_user_settings::<ProjectSettings>(cx, |settings| {
2070 settings.git.inline_blame = inline_blame_off_settings;
2071 });
2072 });
2073 });
2074
2075 client_a
2076 .fs()
2077 .insert_tree(
2078 "/my-repo",
2079 json!({
2080 ".git": {},
2081 "file.txt": "line1\nline2\nline3\nline\n",
2082 }),
2083 )
2084 .await;
2085
2086 let blame = git::blame::Blame {
2087 entries: vec![
2088 blame_entry("1b1b1b", 0..1),
2089 blame_entry("0d0d0d", 1..2),
2090 blame_entry("3a3a3a", 2..3),
2091 blame_entry("4c4c4c", 3..4),
2092 ],
2093 permalinks: HashMap::default(), // This field is deprecrated
2094 messages: [
2095 ("1b1b1b", "message for idx-0"),
2096 ("0d0d0d", "message for idx-1"),
2097 ("3a3a3a", "message for idx-2"),
2098 ("4c4c4c", "message for idx-3"),
2099 ]
2100 .into_iter()
2101 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2102 .collect(),
2103 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2104 };
2105 client_a.fs().set_blame_for_repo(
2106 Path::new("/my-repo/.git"),
2107 vec![(Path::new("file.txt"), blame)],
2108 );
2109
2110 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2111 let project_id = active_call_a
2112 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2113 .await
2114 .unwrap();
2115
2116 // Create editor_a
2117 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2118 let editor_a = workspace_a
2119 .update(cx_a, |workspace, cx| {
2120 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2121 })
2122 .await
2123 .unwrap()
2124 .downcast::<Editor>()
2125 .unwrap();
2126
2127 // Join the project as client B.
2128 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2129 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2130 let editor_b = workspace_b
2131 .update(cx_b, |workspace, cx| {
2132 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2133 })
2134 .await
2135 .unwrap()
2136 .downcast::<Editor>()
2137 .unwrap();
2138
2139 // client_b now requests git blame for the open buffer
2140 editor_b.update(cx_b, |editor_b, cx| {
2141 assert!(editor_b.blame().is_none());
2142 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2143 });
2144
2145 cx_a.executor().run_until_parked();
2146 cx_b.executor().run_until_parked();
2147
2148 editor_b.update(cx_b, |editor_b, cx| {
2149 let blame = editor_b.blame().expect("editor_b should have blame now");
2150 let entries = blame.update(cx, |blame, cx| {
2151 blame
2152 .blame_for_rows((0..4).map(Some), cx)
2153 .collect::<Vec<_>>()
2154 });
2155
2156 assert_eq!(
2157 entries,
2158 vec![
2159 Some(blame_entry("1b1b1b", 0..1)),
2160 Some(blame_entry("0d0d0d", 1..2)),
2161 Some(blame_entry("3a3a3a", 2..3)),
2162 Some(blame_entry("4c4c4c", 3..4)),
2163 ]
2164 );
2165
2166 blame.update(cx, |blame, _| {
2167 for (idx, entry) in entries.iter().flatten().enumerate() {
2168 let details = blame.details_for_entry(entry).unwrap();
2169 assert_eq!(details.message, format!("message for idx-{}", idx));
2170 assert_eq!(
2171 details.permalink.unwrap().to_string(),
2172 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2173 );
2174 }
2175 });
2176 });
2177
2178 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2179 // which gets back to client_b.
2180 editor_b.update(cx_b, |editor_b, cx| {
2181 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2182 });
2183
2184 cx_a.executor().run_until_parked();
2185 cx_b.executor().run_until_parked();
2186
2187 editor_b.update(cx_b, |editor_b, cx| {
2188 let blame = editor_b.blame().expect("editor_b should have blame now");
2189 let entries = blame.update(cx, |blame, cx| {
2190 blame
2191 .blame_for_rows((0..4).map(Some), cx)
2192 .collect::<Vec<_>>()
2193 });
2194
2195 assert_eq!(
2196 entries,
2197 vec![
2198 None,
2199 Some(blame_entry("0d0d0d", 1..2)),
2200 Some(blame_entry("3a3a3a", 2..3)),
2201 Some(blame_entry("4c4c4c", 3..4)),
2202 ]
2203 );
2204 });
2205
2206 // Now editor_a also updates the file
2207 editor_a.update(cx_a, |editor_a, cx| {
2208 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2209 });
2210
2211 cx_a.executor().run_until_parked();
2212 cx_b.executor().run_until_parked();
2213
2214 editor_b.update(cx_b, |editor_b, cx| {
2215 let blame = editor_b.blame().expect("editor_b should have blame now");
2216 let entries = blame.update(cx, |blame, cx| {
2217 blame
2218 .blame_for_rows((0..4).map(Some), cx)
2219 .collect::<Vec<_>>()
2220 });
2221
2222 assert_eq!(
2223 entries,
2224 vec![
2225 None,
2226 None,
2227 Some(blame_entry("3a3a3a", 2..3)),
2228 Some(blame_entry("4c4c4c", 3..4)),
2229 ]
2230 );
2231 });
2232}
2233
2234fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2235 let mut labels = Vec::new();
2236 for hint in editor.inlay_hint_cache().hints() {
2237 match hint.label {
2238 project::InlayHintLabel::String(s) => labels.push(s),
2239 _ => unreachable!(),
2240 }
2241 }
2242 labels
2243}
2244
2245fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2246 git::blame::BlameEntry {
2247 sha: sha.parse().unwrap(),
2248 range,
2249 ..Default::default()
2250 }
2251}