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