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