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