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.build_dev_server_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.build_dev_server_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.build_dev_server_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.build_dev_server_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.build_dev_server_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.build_dev_server_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
1130 .build_dev_server_project(initial_project.id, cx_b)
1131 .await;
1132
1133 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1134
1135 executor.run_until_parked();
1136
1137 project_a.read_with(cx_a, |project, _| {
1138 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1139 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1140 });
1141
1142 project_b.read_with(cx_b, |project, cx| {
1143 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1144 assert_eq!(
1145 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1146 [
1147 Path::new(".gitignore"),
1148 Path::new("a.txt"),
1149 Path::new("b.txt"),
1150 Path::new("ignored-dir"),
1151 ]
1152 );
1153 });
1154
1155 project_b
1156 .update(cx_b, |project, cx| {
1157 let worktree = project.worktrees(cx).next().unwrap();
1158 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1159 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1160 })
1161 .await
1162 .unwrap();
1163
1164 project_b.read_with(cx_b, |project, cx| {
1165 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1166 assert_eq!(
1167 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1168 [
1169 Path::new(".gitignore"),
1170 Path::new("a.txt"),
1171 Path::new("b.txt"),
1172 Path::new("ignored-dir"),
1173 Path::new("ignored-dir/c.txt"),
1174 Path::new("ignored-dir/d.txt"),
1175 ]
1176 );
1177 });
1178
1179 // Open the same file as client B and client A.
1180 let buffer_b = project_b
1181 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1182 .await
1183 .unwrap();
1184
1185 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1186
1187 project_a.read_with(cx_a, |project, cx| {
1188 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1189 });
1190 let buffer_a = project_a
1191 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1192 .await
1193 .unwrap();
1194
1195 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1196
1197 // Client A sees client B's selection
1198 executor.run_until_parked();
1199
1200 buffer_a.read_with(cx_a, |buffer, _| {
1201 buffer
1202 .snapshot()
1203 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1204 .count()
1205 == 1
1206 });
1207
1208 // Edit the buffer as client B and see that edit as client A.
1209 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1210 executor.run_until_parked();
1211
1212 buffer_a.read_with(cx_a, |buffer, _| {
1213 assert_eq!(buffer.text(), "ok, b-contents")
1214 });
1215
1216 // Client B can invite client C on a project shared by client A.
1217 active_call_b
1218 .update(cx_b, |call, cx| {
1219 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1220 })
1221 .await
1222 .unwrap();
1223
1224 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1225 executor.run_until_parked();
1226 let call = incoming_call_c.borrow().clone().unwrap();
1227 assert_eq!(call.calling_user.github_login, "user_b");
1228 let initial_project = call.initial_project.unwrap();
1229 active_call_c
1230 .update(cx_c, |call, cx| call.accept_incoming(cx))
1231 .await
1232 .unwrap();
1233 let _project_c = client_c
1234 .build_dev_server_project(initial_project.id, cx_c)
1235 .await;
1236
1237 // Client B closes the editor, and client A sees client B's selections removed.
1238 cx_b.update(move |_| drop(editor_b));
1239 executor.run_until_parked();
1240
1241 buffer_a.read_with(cx_a, |buffer, _| {
1242 buffer
1243 .snapshot()
1244 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1245 .count()
1246 == 0
1247 });
1248}
1249
1250#[gpui::test(iterations = 10)]
1251async fn test_on_input_format_from_host_to_guest(
1252 cx_a: &mut TestAppContext,
1253 cx_b: &mut TestAppContext,
1254) {
1255 let mut server = TestServer::start(cx_a.executor()).await;
1256 let executor = cx_a.executor();
1257 let client_a = server.create_client(cx_a, "user_a").await;
1258 let client_b = server.create_client(cx_b, "user_b").await;
1259 server
1260 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1261 .await;
1262 let active_call_a = cx_a.read(ActiveCall::global);
1263
1264 client_a.language_registry().add(rust_lang());
1265 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1266 "Rust",
1267 FakeLspAdapter {
1268 capabilities: lsp::ServerCapabilities {
1269 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1270 first_trigger_character: ":".to_string(),
1271 more_trigger_character: Some(vec![">".to_string()]),
1272 }),
1273 ..Default::default()
1274 },
1275 ..Default::default()
1276 },
1277 );
1278
1279 client_a
1280 .fs()
1281 .insert_tree(
1282 "/a",
1283 json!({
1284 "main.rs": "fn main() { a }",
1285 "other.rs": "// Test file",
1286 }),
1287 )
1288 .await;
1289 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1290 let project_id = active_call_a
1291 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1292 .await
1293 .unwrap();
1294 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1295
1296 // Open a file in an editor as the host.
1297 let buffer_a = project_a
1298 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1299 .await
1300 .unwrap();
1301 let cx_a = cx_a.add_empty_window();
1302 let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1303
1304 let fake_language_server = fake_language_servers.next().await.unwrap();
1305 executor.run_until_parked();
1306
1307 // Receive an OnTypeFormatting request as the host's language server.
1308 // Return some formatting from the host's language server.
1309 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1310 |params, _| async move {
1311 assert_eq!(
1312 params.text_document_position.text_document.uri,
1313 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1314 );
1315 assert_eq!(
1316 params.text_document_position.position,
1317 lsp::Position::new(0, 14),
1318 );
1319
1320 Ok(Some(vec![lsp::TextEdit {
1321 new_text: "~<".to_string(),
1322 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1323 }]))
1324 },
1325 );
1326
1327 // Open the buffer on the guest and see that the formatting worked
1328 let buffer_b = project_b
1329 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1330 .await
1331 .unwrap();
1332
1333 // Type a on type formatting trigger character as the guest.
1334 cx_a.focus_view(&editor_a);
1335 editor_a.update(cx_a, |editor, cx| {
1336 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1337 editor.handle_input(">", cx);
1338 });
1339
1340 executor.run_until_parked();
1341
1342 buffer_b.read_with(cx_b, |buffer, _| {
1343 assert_eq!(buffer.text(), "fn main() { a>~< }")
1344 });
1345
1346 // Undo should remove LSP edits first
1347 editor_a.update(cx_a, |editor, cx| {
1348 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1349 editor.undo(&Undo, cx);
1350 assert_eq!(editor.text(cx), "fn main() { a> }");
1351 });
1352 executor.run_until_parked();
1353
1354 buffer_b.read_with(cx_b, |buffer, _| {
1355 assert_eq!(buffer.text(), "fn main() { a> }")
1356 });
1357
1358 editor_a.update(cx_a, |editor, cx| {
1359 assert_eq!(editor.text(cx), "fn main() { a> }");
1360 editor.undo(&Undo, cx);
1361 assert_eq!(editor.text(cx), "fn main() { a }");
1362 });
1363 executor.run_until_parked();
1364
1365 buffer_b.read_with(cx_b, |buffer, _| {
1366 assert_eq!(buffer.text(), "fn main() { a }")
1367 });
1368}
1369
1370#[gpui::test(iterations = 10)]
1371async fn test_on_input_format_from_guest_to_host(
1372 cx_a: &mut TestAppContext,
1373 cx_b: &mut TestAppContext,
1374) {
1375 let mut server = TestServer::start(cx_a.executor()).await;
1376 let executor = cx_a.executor();
1377 let client_a = server.create_client(cx_a, "user_a").await;
1378 let client_b = server.create_client(cx_b, "user_b").await;
1379 server
1380 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1381 .await;
1382 let active_call_a = cx_a.read(ActiveCall::global);
1383
1384 client_a.language_registry().add(rust_lang());
1385 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1386 "Rust",
1387 FakeLspAdapter {
1388 capabilities: lsp::ServerCapabilities {
1389 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1390 first_trigger_character: ":".to_string(),
1391 more_trigger_character: Some(vec![">".to_string()]),
1392 }),
1393 ..Default::default()
1394 },
1395 ..Default::default()
1396 },
1397 );
1398
1399 client_a
1400 .fs()
1401 .insert_tree(
1402 "/a",
1403 json!({
1404 "main.rs": "fn main() { a }",
1405 "other.rs": "// Test file",
1406 }),
1407 )
1408 .await;
1409 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1410 let project_id = active_call_a
1411 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1412 .await
1413 .unwrap();
1414 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1415
1416 // Open a file in an editor as the guest.
1417 let buffer_b = project_b
1418 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1419 .await
1420 .unwrap();
1421 let cx_b = cx_b.add_empty_window();
1422 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1423
1424 let fake_language_server = fake_language_servers.next().await.unwrap();
1425 executor.run_until_parked();
1426
1427 // Type a on type formatting trigger character as the guest.
1428 cx_b.focus_view(&editor_b);
1429 editor_b.update(cx_b, |editor, cx| {
1430 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1431 editor.handle_input(":", cx);
1432 });
1433
1434 // Receive an OnTypeFormatting request as the host's language server.
1435 // Return some formatting from the host's language server.
1436 executor.start_waiting();
1437 fake_language_server
1438 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1439 assert_eq!(
1440 params.text_document_position.text_document.uri,
1441 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1442 );
1443 assert_eq!(
1444 params.text_document_position.position,
1445 lsp::Position::new(0, 14),
1446 );
1447
1448 Ok(Some(vec![lsp::TextEdit {
1449 new_text: "~:".to_string(),
1450 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1451 }]))
1452 })
1453 .next()
1454 .await
1455 .unwrap();
1456 executor.finish_waiting();
1457
1458 // Open the buffer on the host and see that the formatting worked
1459 let buffer_a = project_a
1460 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1461 .await
1462 .unwrap();
1463 executor.run_until_parked();
1464
1465 buffer_a.read_with(cx_a, |buffer, _| {
1466 assert_eq!(buffer.text(), "fn main() { a:~: }")
1467 });
1468
1469 // Undo should remove LSP edits first
1470 editor_b.update(cx_b, |editor, cx| {
1471 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1472 editor.undo(&Undo, cx);
1473 assert_eq!(editor.text(cx), "fn main() { a: }");
1474 });
1475 executor.run_until_parked();
1476
1477 buffer_a.read_with(cx_a, |buffer, _| {
1478 assert_eq!(buffer.text(), "fn main() { a: }")
1479 });
1480
1481 editor_b.update(cx_b, |editor, cx| {
1482 assert_eq!(editor.text(cx), "fn main() { a: }");
1483 editor.undo(&Undo, cx);
1484 assert_eq!(editor.text(cx), "fn main() { a }");
1485 });
1486 executor.run_until_parked();
1487
1488 buffer_a.read_with(cx_a, |buffer, _| {
1489 assert_eq!(buffer.text(), "fn main() { a }")
1490 });
1491}
1492
1493#[gpui::test(iterations = 10)]
1494async fn test_mutual_editor_inlay_hint_cache_update(
1495 cx_a: &mut TestAppContext,
1496 cx_b: &mut TestAppContext,
1497) {
1498 let mut server = TestServer::start(cx_a.executor()).await;
1499 let executor = cx_a.executor();
1500 let client_a = server.create_client(cx_a, "user_a").await;
1501 let client_b = server.create_client(cx_b, "user_b").await;
1502 server
1503 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1504 .await;
1505 let active_call_a = cx_a.read(ActiveCall::global);
1506 let active_call_b = cx_b.read(ActiveCall::global);
1507
1508 cx_a.update(editor::init);
1509 cx_b.update(editor::init);
1510
1511 cx_a.update(|cx| {
1512 SettingsStore::update_global(cx, |store, cx| {
1513 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1514 settings.defaults.inlay_hints = Some(InlayHintSettings {
1515 enabled: true,
1516 edit_debounce_ms: 0,
1517 scroll_debounce_ms: 0,
1518 show_type_hints: true,
1519 show_parameter_hints: false,
1520 show_other_hints: true,
1521 show_background: false,
1522 })
1523 });
1524 });
1525 });
1526 cx_b.update(|cx| {
1527 SettingsStore::update_global(cx, |store, cx| {
1528 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1529 settings.defaults.inlay_hints = Some(InlayHintSettings {
1530 enabled: true,
1531 edit_debounce_ms: 0,
1532 scroll_debounce_ms: 0,
1533 show_type_hints: true,
1534 show_parameter_hints: false,
1535 show_other_hints: true,
1536 show_background: false,
1537 })
1538 });
1539 });
1540 });
1541
1542 client_a.language_registry().add(rust_lang());
1543 client_b.language_registry().add(rust_lang());
1544 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1545 "Rust",
1546 FakeLspAdapter {
1547 capabilities: lsp::ServerCapabilities {
1548 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1549 ..Default::default()
1550 },
1551 ..Default::default()
1552 },
1553 );
1554
1555 // Client A opens a project.
1556 client_a
1557 .fs()
1558 .insert_tree(
1559 "/a",
1560 json!({
1561 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1562 "other.rs": "// Test file",
1563 }),
1564 )
1565 .await;
1566 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1567 active_call_a
1568 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1569 .await
1570 .unwrap();
1571 let project_id = active_call_a
1572 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1573 .await
1574 .unwrap();
1575
1576 // Client B joins the project
1577 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1578 active_call_b
1579 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1580 .await
1581 .unwrap();
1582
1583 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1584 executor.start_waiting();
1585
1586 // The host opens a rust file.
1587 let _buffer_a = project_a
1588 .update(cx_a, |project, cx| {
1589 project.open_local_buffer("/a/main.rs", cx)
1590 })
1591 .await
1592 .unwrap();
1593 let fake_language_server = fake_language_servers.next().await.unwrap();
1594 let editor_a = workspace_a
1595 .update(cx_a, |workspace, cx| {
1596 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1597 })
1598 .await
1599 .unwrap()
1600 .downcast::<Editor>()
1601 .unwrap();
1602
1603 // Set up the language server to return an additional inlay hint on each request.
1604 let edits_made = Arc::new(AtomicUsize::new(0));
1605 let closure_edits_made = Arc::clone(&edits_made);
1606 fake_language_server
1607 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1608 let task_edits_made = Arc::clone(&closure_edits_made);
1609 async move {
1610 assert_eq!(
1611 params.text_document.uri,
1612 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1613 );
1614 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1615 Ok(Some(vec![lsp::InlayHint {
1616 position: lsp::Position::new(0, edits_made as u32),
1617 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1618 kind: None,
1619 text_edits: None,
1620 tooltip: None,
1621 padding_left: None,
1622 padding_right: None,
1623 data: None,
1624 }]))
1625 }
1626 })
1627 .next()
1628 .await
1629 .unwrap();
1630
1631 executor.run_until_parked();
1632
1633 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1634 editor_a.update(cx_a, |editor, _| {
1635 assert_eq!(
1636 vec![initial_edit.to_string()],
1637 extract_hint_labels(editor),
1638 "Host should get its first hints when opens an editor"
1639 );
1640 let inlay_cache = editor.inlay_hint_cache();
1641 assert_eq!(
1642 inlay_cache.version(),
1643 1,
1644 "Host editor update the cache version after every cache/view change",
1645 );
1646 });
1647 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1648 let editor_b = workspace_b
1649 .update(cx_b, |workspace, cx| {
1650 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1651 })
1652 .await
1653 .unwrap()
1654 .downcast::<Editor>()
1655 .unwrap();
1656
1657 executor.run_until_parked();
1658 editor_b.update(cx_b, |editor, _| {
1659 assert_eq!(
1660 vec![initial_edit.to_string()],
1661 extract_hint_labels(editor),
1662 "Client should get its first hints when opens an editor"
1663 );
1664 let inlay_cache = editor.inlay_hint_cache();
1665 assert_eq!(
1666 inlay_cache.version(),
1667 1,
1668 "Guest editor update the cache version after every cache/view change"
1669 );
1670 });
1671
1672 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1673 editor_b.update(cx_b, |editor, cx| {
1674 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1675 editor.handle_input(":", cx);
1676 });
1677 cx_b.focus_view(&editor_b);
1678
1679 executor.run_until_parked();
1680 editor_a.update(cx_a, |editor, _| {
1681 assert_eq!(
1682 vec![after_client_edit.to_string()],
1683 extract_hint_labels(editor),
1684 );
1685 let inlay_cache = editor.inlay_hint_cache();
1686 assert_eq!(inlay_cache.version(), 2);
1687 });
1688 editor_b.update(cx_b, |editor, _| {
1689 assert_eq!(
1690 vec![after_client_edit.to_string()],
1691 extract_hint_labels(editor),
1692 );
1693 let inlay_cache = editor.inlay_hint_cache();
1694 assert_eq!(inlay_cache.version(), 2);
1695 });
1696
1697 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1698 editor_a.update(cx_a, |editor, cx| {
1699 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1700 editor.handle_input("a change to increment both buffers' versions", cx);
1701 });
1702 cx_a.focus_view(&editor_a);
1703
1704 executor.run_until_parked();
1705 editor_a.update(cx_a, |editor, _| {
1706 assert_eq!(
1707 vec![after_host_edit.to_string()],
1708 extract_hint_labels(editor),
1709 );
1710 let inlay_cache = editor.inlay_hint_cache();
1711 assert_eq!(inlay_cache.version(), 3);
1712 });
1713 editor_b.update(cx_b, |editor, _| {
1714 assert_eq!(
1715 vec![after_host_edit.to_string()],
1716 extract_hint_labels(editor),
1717 );
1718 let inlay_cache = editor.inlay_hint_cache();
1719 assert_eq!(inlay_cache.version(), 3);
1720 });
1721
1722 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1723 fake_language_server
1724 .request::<lsp::request::InlayHintRefreshRequest>(())
1725 .await
1726 .expect("inlay refresh request failed");
1727
1728 executor.run_until_parked();
1729 editor_a.update(cx_a, |editor, _| {
1730 assert_eq!(
1731 vec![after_special_edit_for_refresh.to_string()],
1732 extract_hint_labels(editor),
1733 "Host should react to /refresh LSP request"
1734 );
1735 let inlay_cache = editor.inlay_hint_cache();
1736 assert_eq!(
1737 inlay_cache.version(),
1738 4,
1739 "Host should accepted all edits and bump its cache version every time"
1740 );
1741 });
1742 editor_b.update(cx_b, |editor, _| {
1743 assert_eq!(
1744 vec![after_special_edit_for_refresh.to_string()],
1745 extract_hint_labels(editor),
1746 "Guest should get a /refresh LSP request propagated by host"
1747 );
1748 let inlay_cache = editor.inlay_hint_cache();
1749 assert_eq!(
1750 inlay_cache.version(),
1751 4,
1752 "Guest should accepted all edits and bump its cache version every time"
1753 );
1754 });
1755}
1756
1757#[gpui::test(iterations = 10)]
1758async fn test_inlay_hint_refresh_is_forwarded(
1759 cx_a: &mut TestAppContext,
1760 cx_b: &mut TestAppContext,
1761) {
1762 let mut server = TestServer::start(cx_a.executor()).await;
1763 let executor = cx_a.executor();
1764 let client_a = server.create_client(cx_a, "user_a").await;
1765 let client_b = server.create_client(cx_b, "user_b").await;
1766 server
1767 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1768 .await;
1769 let active_call_a = cx_a.read(ActiveCall::global);
1770 let active_call_b = cx_b.read(ActiveCall::global);
1771
1772 cx_a.update(editor::init);
1773 cx_b.update(editor::init);
1774
1775 cx_a.update(|cx| {
1776 SettingsStore::update_global(cx, |store, cx| {
1777 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1778 settings.defaults.inlay_hints = Some(InlayHintSettings {
1779 enabled: false,
1780 edit_debounce_ms: 0,
1781 scroll_debounce_ms: 0,
1782 show_type_hints: false,
1783 show_parameter_hints: false,
1784 show_other_hints: false,
1785 show_background: false,
1786 })
1787 });
1788 });
1789 });
1790 cx_b.update(|cx| {
1791 SettingsStore::update_global(cx, |store, cx| {
1792 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1793 settings.defaults.inlay_hints = Some(InlayHintSettings {
1794 enabled: true,
1795 edit_debounce_ms: 0,
1796 scroll_debounce_ms: 0,
1797 show_type_hints: true,
1798 show_parameter_hints: true,
1799 show_other_hints: true,
1800 show_background: false,
1801 })
1802 });
1803 });
1804 });
1805
1806 client_a.language_registry().add(rust_lang());
1807 client_b.language_registry().add(rust_lang());
1808 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1809 "Rust",
1810 FakeLspAdapter {
1811 capabilities: lsp::ServerCapabilities {
1812 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1813 ..Default::default()
1814 },
1815 ..Default::default()
1816 },
1817 );
1818
1819 client_a
1820 .fs()
1821 .insert_tree(
1822 "/a",
1823 json!({
1824 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1825 "other.rs": "// Test file",
1826 }),
1827 )
1828 .await;
1829 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1830 active_call_a
1831 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1832 .await
1833 .unwrap();
1834 let project_id = active_call_a
1835 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1836 .await
1837 .unwrap();
1838
1839 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1840 active_call_b
1841 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1842 .await
1843 .unwrap();
1844
1845 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1846 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1847
1848 cx_a.background_executor.start_waiting();
1849
1850 let editor_a = workspace_a
1851 .update(cx_a, |workspace, cx| {
1852 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1853 })
1854 .await
1855 .unwrap()
1856 .downcast::<Editor>()
1857 .unwrap();
1858
1859 let editor_b = workspace_b
1860 .update(cx_b, |workspace, cx| {
1861 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1862 })
1863 .await
1864 .unwrap()
1865 .downcast::<Editor>()
1866 .unwrap();
1867
1868 let other_hints = Arc::new(AtomicBool::new(false));
1869 let fake_language_server = fake_language_servers.next().await.unwrap();
1870 let closure_other_hints = Arc::clone(&other_hints);
1871 fake_language_server
1872 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1873 let task_other_hints = Arc::clone(&closure_other_hints);
1874 async move {
1875 assert_eq!(
1876 params.text_document.uri,
1877 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1878 );
1879 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1880 let character = if other_hints { 0 } else { 2 };
1881 let label = if other_hints {
1882 "other hint"
1883 } else {
1884 "initial hint"
1885 };
1886 Ok(Some(vec![lsp::InlayHint {
1887 position: lsp::Position::new(0, character),
1888 label: lsp::InlayHintLabel::String(label.to_string()),
1889 kind: None,
1890 text_edits: None,
1891 tooltip: None,
1892 padding_left: None,
1893 padding_right: None,
1894 data: None,
1895 }]))
1896 }
1897 })
1898 .next()
1899 .await
1900 .unwrap();
1901 executor.finish_waiting();
1902
1903 executor.run_until_parked();
1904 editor_a.update(cx_a, |editor, _| {
1905 assert!(
1906 extract_hint_labels(editor).is_empty(),
1907 "Host should get no hints due to them turned off"
1908 );
1909 let inlay_cache = editor.inlay_hint_cache();
1910 assert_eq!(
1911 inlay_cache.version(),
1912 0,
1913 "Turned off hints should not generate version updates"
1914 );
1915 });
1916
1917 executor.run_until_parked();
1918 editor_b.update(cx_b, |editor, _| {
1919 assert_eq!(
1920 vec!["initial hint".to_string()],
1921 extract_hint_labels(editor),
1922 "Client should get its first hints when opens an editor"
1923 );
1924 let inlay_cache = editor.inlay_hint_cache();
1925 assert_eq!(
1926 inlay_cache.version(),
1927 1,
1928 "Should update cache version after first hints"
1929 );
1930 });
1931
1932 other_hints.fetch_or(true, atomic::Ordering::Release);
1933 fake_language_server
1934 .request::<lsp::request::InlayHintRefreshRequest>(())
1935 .await
1936 .expect("inlay refresh request failed");
1937 executor.run_until_parked();
1938 editor_a.update(cx_a, |editor, _| {
1939 assert!(
1940 extract_hint_labels(editor).is_empty(),
1941 "Host should get nop hints due to them turned off, even after the /refresh"
1942 );
1943 let inlay_cache = editor.inlay_hint_cache();
1944 assert_eq!(
1945 inlay_cache.version(),
1946 0,
1947 "Turned off hints should not generate version updates, again"
1948 );
1949 });
1950
1951 executor.run_until_parked();
1952 editor_b.update(cx_b, |editor, _| {
1953 assert_eq!(
1954 vec!["other hint".to_string()],
1955 extract_hint_labels(editor),
1956 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1957 );
1958 let inlay_cache = editor.inlay_hint_cache();
1959 assert_eq!(
1960 inlay_cache.version(),
1961 2,
1962 "Guest should accepted all edits and bump its cache version every time"
1963 );
1964 });
1965}
1966
1967#[gpui::test(iterations = 10)]
1968async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1969 let mut server = TestServer::start(cx_a.executor()).await;
1970 let client_a = server.create_client(cx_a, "user_a").await;
1971 let client_b = server.create_client(cx_b, "user_b").await;
1972 server
1973 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1974 .await;
1975 let active_call_a = cx_a.read(ActiveCall::global);
1976
1977 cx_a.update(editor::init);
1978 cx_b.update(editor::init);
1979 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1980 let inline_blame_off_settings = Some(InlineBlameSettings {
1981 enabled: false,
1982 delay_ms: None,
1983 min_column: None,
1984 });
1985 cx_a.update(|cx| {
1986 SettingsStore::update_global(cx, |store, cx| {
1987 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1988 settings.git.inline_blame = inline_blame_off_settings;
1989 });
1990 });
1991 });
1992 cx_b.update(|cx| {
1993 SettingsStore::update_global(cx, |store, cx| {
1994 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1995 settings.git.inline_blame = inline_blame_off_settings;
1996 });
1997 });
1998 });
1999
2000 client_a
2001 .fs()
2002 .insert_tree(
2003 "/my-repo",
2004 json!({
2005 ".git": {},
2006 "file.txt": "line1\nline2\nline3\nline\n",
2007 }),
2008 )
2009 .await;
2010
2011 let blame = git::blame::Blame {
2012 entries: vec![
2013 blame_entry("1b1b1b", 0..1),
2014 blame_entry("0d0d0d", 1..2),
2015 blame_entry("3a3a3a", 2..3),
2016 blame_entry("4c4c4c", 3..4),
2017 ],
2018 permalinks: HashMap::default(), // This field is deprecrated
2019 messages: [
2020 ("1b1b1b", "message for idx-0"),
2021 ("0d0d0d", "message for idx-1"),
2022 ("3a3a3a", "message for idx-2"),
2023 ("4c4c4c", "message for idx-3"),
2024 ]
2025 .into_iter()
2026 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2027 .collect(),
2028 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2029 };
2030 client_a.fs().set_blame_for_repo(
2031 Path::new("/my-repo/.git"),
2032 vec![(Path::new("file.txt"), blame)],
2033 );
2034
2035 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2036 let project_id = active_call_a
2037 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2038 .await
2039 .unwrap();
2040
2041 // Create editor_a
2042 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2043 let editor_a = workspace_a
2044 .update(cx_a, |workspace, cx| {
2045 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2046 })
2047 .await
2048 .unwrap()
2049 .downcast::<Editor>()
2050 .unwrap();
2051
2052 // Join the project as client B.
2053 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
2054 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2055 let editor_b = workspace_b
2056 .update(cx_b, |workspace, cx| {
2057 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2058 })
2059 .await
2060 .unwrap()
2061 .downcast::<Editor>()
2062 .unwrap();
2063
2064 // client_b now requests git blame for the open buffer
2065 editor_b.update(cx_b, |editor_b, cx| {
2066 assert!(editor_b.blame().is_none());
2067 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2068 });
2069
2070 cx_a.executor().run_until_parked();
2071 cx_b.executor().run_until_parked();
2072
2073 editor_b.update(cx_b, |editor_b, cx| {
2074 let blame = editor_b.blame().expect("editor_b should have blame now");
2075 let entries = blame.update(cx, |blame, cx| {
2076 blame
2077 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2078 .collect::<Vec<_>>()
2079 });
2080
2081 assert_eq!(
2082 entries,
2083 vec![
2084 Some(blame_entry("1b1b1b", 0..1)),
2085 Some(blame_entry("0d0d0d", 1..2)),
2086 Some(blame_entry("3a3a3a", 2..3)),
2087 Some(blame_entry("4c4c4c", 3..4)),
2088 ]
2089 );
2090
2091 blame.update(cx, |blame, _| {
2092 for (idx, entry) in entries.iter().flatten().enumerate() {
2093 let details = blame.details_for_entry(entry).unwrap();
2094 assert_eq!(details.message, format!("message for idx-{}", idx));
2095 assert_eq!(
2096 details.permalink.unwrap().to_string(),
2097 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2098 );
2099 }
2100 });
2101 });
2102
2103 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2104 // which gets back to client_b.
2105 editor_b.update(cx_b, |editor_b, cx| {
2106 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2107 });
2108
2109 cx_a.executor().run_until_parked();
2110 cx_b.executor().run_until_parked();
2111
2112 editor_b.update(cx_b, |editor_b, cx| {
2113 let blame = editor_b.blame().expect("editor_b should have blame now");
2114 let entries = blame.update(cx, |blame, cx| {
2115 blame
2116 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2117 .collect::<Vec<_>>()
2118 });
2119
2120 assert_eq!(
2121 entries,
2122 vec![
2123 None,
2124 Some(blame_entry("0d0d0d", 1..2)),
2125 Some(blame_entry("3a3a3a", 2..3)),
2126 Some(blame_entry("4c4c4c", 3..4)),
2127 ]
2128 );
2129 });
2130
2131 // Now editor_a also updates the file
2132 editor_a.update(cx_a, |editor_a, cx| {
2133 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2134 });
2135
2136 cx_a.executor().run_until_parked();
2137 cx_b.executor().run_until_parked();
2138
2139 editor_b.update(cx_b, |editor_b, cx| {
2140 let blame = editor_b.blame().expect("editor_b should have blame now");
2141 let entries = blame.update(cx, |blame, cx| {
2142 blame
2143 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2144 .collect::<Vec<_>>()
2145 });
2146
2147 assert_eq!(
2148 entries,
2149 vec![
2150 None,
2151 None,
2152 Some(blame_entry("3a3a3a", 2..3)),
2153 Some(blame_entry("4c4c4c", 3..4)),
2154 ]
2155 );
2156 });
2157}
2158
2159fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2160 let mut labels = Vec::new();
2161 for hint in editor.inlay_hint_cache().hints() {
2162 match hint.label {
2163 project::InlayHintLabel::String(s) => labels.push(s),
2164 _ => unreachable!(),
2165 }
2166 }
2167 labels
2168}
2169
2170fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2171 git::blame::BlameEntry {
2172 sha: sha.parse().unwrap(),
2173 range,
2174 ..Default::default()
2175 }
2176}