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