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_DEBOUNCE_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 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1010 token: lsp::NumberOrString::String("the-token".to_string()),
1011 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1012 lsp::WorkDoneProgressReport {
1013 message: Some("the-message".to_string()),
1014 ..Default::default()
1015 },
1016 )),
1017 });
1018 executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
1019 executor.run_until_parked();
1020
1021 project_a.read_with(cx_a, |project, _| {
1022 let status = project.language_server_statuses().next().unwrap();
1023 assert_eq!(status.name, "the-language-server");
1024 assert_eq!(status.pending_work.len(), 1);
1025 assert_eq!(
1026 status.pending_work["the-token"].message.as_ref().unwrap(),
1027 "the-message"
1028 );
1029 });
1030
1031 let project_id = active_call_a
1032 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1033 .await
1034 .unwrap();
1035 executor.run_until_parked();
1036 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1037
1038 project_b.read_with(cx_b, |project, _| {
1039 let status = project.language_server_statuses().next().unwrap();
1040 assert_eq!(status.name, "the-language-server");
1041 });
1042
1043 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1044 token: lsp::NumberOrString::String("the-token".to_string()),
1045 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1046 lsp::WorkDoneProgressReport {
1047 message: Some("the-message-2".to_string()),
1048 ..Default::default()
1049 },
1050 )),
1051 });
1052 executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
1053 executor.run_until_parked();
1054
1055 project_a.read_with(cx_a, |project, _| {
1056 let status = project.language_server_statuses().next().unwrap();
1057 assert_eq!(status.name, "the-language-server");
1058 assert_eq!(status.pending_work.len(), 1);
1059 assert_eq!(
1060 status.pending_work["the-token"].message.as_ref().unwrap(),
1061 "the-message-2"
1062 );
1063 });
1064
1065 project_b.read_with(cx_b, |project, _| {
1066 let status = project.language_server_statuses().next().unwrap();
1067 assert_eq!(status.name, "the-language-server");
1068 assert_eq!(status.pending_work.len(), 1);
1069 assert_eq!(
1070 status.pending_work["the-token"].message.as_ref().unwrap(),
1071 "the-message-2"
1072 );
1073 });
1074}
1075
1076#[gpui::test(iterations = 10)]
1077async fn test_share_project(
1078 cx_a: &mut TestAppContext,
1079 cx_b: &mut TestAppContext,
1080 cx_c: &mut TestAppContext,
1081) {
1082 let executor = cx_a.executor();
1083 let cx_b = cx_b.add_empty_window();
1084 let mut server = TestServer::start(executor.clone()).await;
1085 let client_a = server.create_client(cx_a, "user_a").await;
1086 let client_b = server.create_client(cx_b, "user_b").await;
1087 let client_c = server.create_client(cx_c, "user_c").await;
1088 server
1089 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1090 .await;
1091 let active_call_a = cx_a.read(ActiveCall::global);
1092 let active_call_b = cx_b.read(ActiveCall::global);
1093 let active_call_c = cx_c.read(ActiveCall::global);
1094
1095 client_a
1096 .fs()
1097 .insert_tree(
1098 "/a",
1099 json!({
1100 ".gitignore": "ignored-dir",
1101 "a.txt": "a-contents",
1102 "b.txt": "b-contents",
1103 "ignored-dir": {
1104 "c.txt": "",
1105 "d.txt": "",
1106 }
1107 }),
1108 )
1109 .await;
1110
1111 // Invite client B to collaborate on a project
1112 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1113 active_call_a
1114 .update(cx_a, |call, cx| {
1115 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1116 })
1117 .await
1118 .unwrap();
1119
1120 // Join that project as client B
1121
1122 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1123 executor.run_until_parked();
1124 let call = incoming_call_b.borrow().clone().unwrap();
1125 assert_eq!(call.calling_user.github_login, "user_a");
1126 let initial_project = call.initial_project.unwrap();
1127 active_call_b
1128 .update(cx_b, |call, cx| call.accept_incoming(cx))
1129 .await
1130 .unwrap();
1131 let client_b_peer_id = client_b.peer_id().unwrap();
1132 let project_b = client_b
1133 .build_dev_server_project(initial_project.id, cx_b)
1134 .await;
1135
1136 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1137
1138 executor.run_until_parked();
1139
1140 project_a.read_with(cx_a, |project, _| {
1141 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1142 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1143 });
1144
1145 project_b.read_with(cx_b, |project, cx| {
1146 let worktree = project.worktrees().next().unwrap().read(cx);
1147 assert_eq!(
1148 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1149 [
1150 Path::new(".gitignore"),
1151 Path::new("a.txt"),
1152 Path::new("b.txt"),
1153 Path::new("ignored-dir"),
1154 ]
1155 );
1156 });
1157
1158 project_b
1159 .update(cx_b, |project, cx| {
1160 let worktree = project.worktrees().next().unwrap();
1161 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1162 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1163 })
1164 .await
1165 .unwrap();
1166
1167 project_b.read_with(cx_b, |project, cx| {
1168 let worktree = project.worktrees().next().unwrap().read(cx);
1169 assert_eq!(
1170 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1171 [
1172 Path::new(".gitignore"),
1173 Path::new("a.txt"),
1174 Path::new("b.txt"),
1175 Path::new("ignored-dir"),
1176 Path::new("ignored-dir/c.txt"),
1177 Path::new("ignored-dir/d.txt"),
1178 ]
1179 );
1180 });
1181
1182 // Open the same file as client B and client A.
1183 let buffer_b = project_b
1184 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1185 .await
1186 .unwrap();
1187
1188 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1189
1190 project_a.read_with(cx_a, |project, cx| {
1191 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1192 });
1193 let buffer_a = project_a
1194 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1195 .await
1196 .unwrap();
1197
1198 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1199
1200 // Client A sees client B's selection
1201 executor.run_until_parked();
1202
1203 buffer_a.read_with(cx_a, |buffer, _| {
1204 buffer
1205 .snapshot()
1206 .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1207 .count()
1208 == 1
1209 });
1210
1211 // Edit the buffer as client B and see that edit as client A.
1212 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1213 executor.run_until_parked();
1214
1215 buffer_a.read_with(cx_a, |buffer, _| {
1216 assert_eq!(buffer.text(), "ok, b-contents")
1217 });
1218
1219 // Client B can invite client C on a project shared by client A.
1220 active_call_b
1221 .update(cx_b, |call, cx| {
1222 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1223 })
1224 .await
1225 .unwrap();
1226
1227 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1228 executor.run_until_parked();
1229 let call = incoming_call_c.borrow().clone().unwrap();
1230 assert_eq!(call.calling_user.github_login, "user_b");
1231 let initial_project = call.initial_project.unwrap();
1232 active_call_c
1233 .update(cx_c, |call, cx| call.accept_incoming(cx))
1234 .await
1235 .unwrap();
1236 let _project_c = client_c
1237 .build_dev_server_project(initial_project.id, cx_c)
1238 .await;
1239
1240 // Client B closes the editor, and client A sees client B's selections removed.
1241 cx_b.update(move |_| drop(editor_b));
1242 executor.run_until_parked();
1243
1244 buffer_a.read_with(cx_a, |buffer, _| {
1245 buffer
1246 .snapshot()
1247 .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1248 .count()
1249 == 0
1250 });
1251}
1252
1253#[gpui::test(iterations = 10)]
1254async fn test_on_input_format_from_host_to_guest(
1255 cx_a: &mut TestAppContext,
1256 cx_b: &mut TestAppContext,
1257) {
1258 let mut server = TestServer::start(cx_a.executor()).await;
1259 let executor = cx_a.executor();
1260 let client_a = server.create_client(cx_a, "user_a").await;
1261 let client_b = server.create_client(cx_b, "user_b").await;
1262 server
1263 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1264 .await;
1265 let active_call_a = cx_a.read(ActiveCall::global);
1266
1267 client_a.language_registry().add(rust_lang());
1268 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1269 "Rust",
1270 FakeLspAdapter {
1271 capabilities: lsp::ServerCapabilities {
1272 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1273 first_trigger_character: ":".to_string(),
1274 more_trigger_character: Some(vec![">".to_string()]),
1275 }),
1276 ..Default::default()
1277 },
1278 ..Default::default()
1279 },
1280 );
1281
1282 client_a
1283 .fs()
1284 .insert_tree(
1285 "/a",
1286 json!({
1287 "main.rs": "fn main() { a }",
1288 "other.rs": "// Test file",
1289 }),
1290 )
1291 .await;
1292 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1293 let project_id = active_call_a
1294 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1295 .await
1296 .unwrap();
1297 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1298
1299 // Open a file in an editor as the host.
1300 let buffer_a = project_a
1301 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1302 .await
1303 .unwrap();
1304 let cx_a = cx_a.add_empty_window();
1305 let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1306
1307 let fake_language_server = fake_language_servers.next().await.unwrap();
1308 executor.run_until_parked();
1309
1310 // Receive an OnTypeFormatting request as the host's language server.
1311 // Return some formatting from the host's language server.
1312 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1313 |params, _| async move {
1314 assert_eq!(
1315 params.text_document_position.text_document.uri,
1316 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1317 );
1318 assert_eq!(
1319 params.text_document_position.position,
1320 lsp::Position::new(0, 14),
1321 );
1322
1323 Ok(Some(vec![lsp::TextEdit {
1324 new_text: "~<".to_string(),
1325 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1326 }]))
1327 },
1328 );
1329
1330 // Open the buffer on the guest and see that the formatting worked
1331 let buffer_b = project_b
1332 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1333 .await
1334 .unwrap();
1335
1336 // Type a on type formatting trigger character as the guest.
1337 cx_a.focus_view(&editor_a);
1338 editor_a.update(cx_a, |editor, cx| {
1339 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1340 editor.handle_input(">", cx);
1341 });
1342
1343 executor.run_until_parked();
1344
1345 buffer_b.read_with(cx_b, |buffer, _| {
1346 assert_eq!(buffer.text(), "fn main() { a>~< }")
1347 });
1348
1349 // Undo should remove LSP edits first
1350 editor_a.update(cx_a, |editor, cx| {
1351 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1352 editor.undo(&Undo, cx);
1353 assert_eq!(editor.text(cx), "fn main() { a> }");
1354 });
1355 executor.run_until_parked();
1356
1357 buffer_b.read_with(cx_b, |buffer, _| {
1358 assert_eq!(buffer.text(), "fn main() { a> }")
1359 });
1360
1361 editor_a.update(cx_a, |editor, cx| {
1362 assert_eq!(editor.text(cx), "fn main() { a> }");
1363 editor.undo(&Undo, cx);
1364 assert_eq!(editor.text(cx), "fn main() { a }");
1365 });
1366 executor.run_until_parked();
1367
1368 buffer_b.read_with(cx_b, |buffer, _| {
1369 assert_eq!(buffer.text(), "fn main() { a }")
1370 });
1371}
1372
1373#[gpui::test(iterations = 10)]
1374async fn test_on_input_format_from_guest_to_host(
1375 cx_a: &mut TestAppContext,
1376 cx_b: &mut TestAppContext,
1377) {
1378 let mut server = TestServer::start(cx_a.executor()).await;
1379 let executor = cx_a.executor();
1380 let client_a = server.create_client(cx_a, "user_a").await;
1381 let client_b = server.create_client(cx_b, "user_b").await;
1382 server
1383 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1384 .await;
1385 let active_call_a = cx_a.read(ActiveCall::global);
1386
1387 client_a.language_registry().add(rust_lang());
1388 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1389 "Rust",
1390 FakeLspAdapter {
1391 capabilities: lsp::ServerCapabilities {
1392 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1393 first_trigger_character: ":".to_string(),
1394 more_trigger_character: Some(vec![">".to_string()]),
1395 }),
1396 ..Default::default()
1397 },
1398 ..Default::default()
1399 },
1400 );
1401
1402 client_a
1403 .fs()
1404 .insert_tree(
1405 "/a",
1406 json!({
1407 "main.rs": "fn main() { a }",
1408 "other.rs": "// Test file",
1409 }),
1410 )
1411 .await;
1412 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1413 let project_id = active_call_a
1414 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1415 .await
1416 .unwrap();
1417 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1418
1419 // Open a file in an editor as the guest.
1420 let buffer_b = project_b
1421 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1422 .await
1423 .unwrap();
1424 let cx_b = cx_b.add_empty_window();
1425 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1426
1427 let fake_language_server = fake_language_servers.next().await.unwrap();
1428 executor.run_until_parked();
1429
1430 // Type a on type formatting trigger character as the guest.
1431 cx_b.focus_view(&editor_b);
1432 editor_b.update(cx_b, |editor, cx| {
1433 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1434 editor.handle_input(":", cx);
1435 });
1436
1437 // Receive an OnTypeFormatting request as the host's language server.
1438 // Return some formatting from the host's language server.
1439 executor.start_waiting();
1440 fake_language_server
1441 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1442 assert_eq!(
1443 params.text_document_position.text_document.uri,
1444 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1445 );
1446 assert_eq!(
1447 params.text_document_position.position,
1448 lsp::Position::new(0, 14),
1449 );
1450
1451 Ok(Some(vec![lsp::TextEdit {
1452 new_text: "~:".to_string(),
1453 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1454 }]))
1455 })
1456 .next()
1457 .await
1458 .unwrap();
1459 executor.finish_waiting();
1460
1461 // Open the buffer on the host and see that the formatting worked
1462 let buffer_a = project_a
1463 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1464 .await
1465 .unwrap();
1466 executor.run_until_parked();
1467
1468 buffer_a.read_with(cx_a, |buffer, _| {
1469 assert_eq!(buffer.text(), "fn main() { a:~: }")
1470 });
1471
1472 // Undo should remove LSP edits first
1473 editor_b.update(cx_b, |editor, cx| {
1474 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1475 editor.undo(&Undo, cx);
1476 assert_eq!(editor.text(cx), "fn main() { a: }");
1477 });
1478 executor.run_until_parked();
1479
1480 buffer_a.read_with(cx_a, |buffer, _| {
1481 assert_eq!(buffer.text(), "fn main() { a: }")
1482 });
1483
1484 editor_b.update(cx_b, |editor, cx| {
1485 assert_eq!(editor.text(cx), "fn main() { a: }");
1486 editor.undo(&Undo, cx);
1487 assert_eq!(editor.text(cx), "fn main() { a }");
1488 });
1489 executor.run_until_parked();
1490
1491 buffer_a.read_with(cx_a, |buffer, _| {
1492 assert_eq!(buffer.text(), "fn main() { a }")
1493 });
1494}
1495
1496#[gpui::test(iterations = 10)]
1497async fn test_mutual_editor_inlay_hint_cache_update(
1498 cx_a: &mut TestAppContext,
1499 cx_b: &mut TestAppContext,
1500) {
1501 let mut server = TestServer::start(cx_a.executor()).await;
1502 let executor = cx_a.executor();
1503 let client_a = server.create_client(cx_a, "user_a").await;
1504 let client_b = server.create_client(cx_b, "user_b").await;
1505 server
1506 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1507 .await;
1508 let active_call_a = cx_a.read(ActiveCall::global);
1509 let active_call_b = cx_b.read(ActiveCall::global);
1510
1511 cx_a.update(editor::init);
1512 cx_b.update(editor::init);
1513
1514 cx_a.update(|cx| {
1515 SettingsStore::update_global(cx, |store, cx| {
1516 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1517 settings.defaults.inlay_hints = Some(InlayHintSettings {
1518 enabled: true,
1519 edit_debounce_ms: 0,
1520 scroll_debounce_ms: 0,
1521 show_type_hints: true,
1522 show_parameter_hints: false,
1523 show_other_hints: true,
1524 })
1525 });
1526 });
1527 });
1528 cx_b.update(|cx| {
1529 SettingsStore::update_global(cx, |store, cx| {
1530 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1531 settings.defaults.inlay_hints = Some(InlayHintSettings {
1532 enabled: true,
1533 edit_debounce_ms: 0,
1534 scroll_debounce_ms: 0,
1535 show_type_hints: true,
1536 show_parameter_hints: false,
1537 show_other_hints: true,
1538 })
1539 });
1540 });
1541 });
1542
1543 client_a.language_registry().add(rust_lang());
1544 client_b.language_registry().add(rust_lang());
1545 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1546 "Rust",
1547 FakeLspAdapter {
1548 capabilities: lsp::ServerCapabilities {
1549 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1550 ..Default::default()
1551 },
1552 ..Default::default()
1553 },
1554 );
1555
1556 // Client A opens a project.
1557 client_a
1558 .fs()
1559 .insert_tree(
1560 "/a",
1561 json!({
1562 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1563 "other.rs": "// Test file",
1564 }),
1565 )
1566 .await;
1567 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1568 active_call_a
1569 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1570 .await
1571 .unwrap();
1572 let project_id = active_call_a
1573 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1574 .await
1575 .unwrap();
1576
1577 // Client B joins the project
1578 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1579 active_call_b
1580 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1581 .await
1582 .unwrap();
1583
1584 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1585 executor.start_waiting();
1586
1587 // The host opens a rust file.
1588 let _buffer_a = project_a
1589 .update(cx_a, |project, cx| {
1590 project.open_local_buffer("/a/main.rs", cx)
1591 })
1592 .await
1593 .unwrap();
1594 let fake_language_server = fake_language_servers.next().await.unwrap();
1595 let editor_a = workspace_a
1596 .update(cx_a, |workspace, cx| {
1597 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1598 })
1599 .await
1600 .unwrap()
1601 .downcast::<Editor>()
1602 .unwrap();
1603
1604 // Set up the language server to return an additional inlay hint on each request.
1605 let edits_made = Arc::new(AtomicUsize::new(0));
1606 let closure_edits_made = Arc::clone(&edits_made);
1607 fake_language_server
1608 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1609 let task_edits_made = Arc::clone(&closure_edits_made);
1610 async move {
1611 assert_eq!(
1612 params.text_document.uri,
1613 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1614 );
1615 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1616 Ok(Some(vec![lsp::InlayHint {
1617 position: lsp::Position::new(0, edits_made as u32),
1618 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1619 kind: None,
1620 text_edits: None,
1621 tooltip: None,
1622 padding_left: None,
1623 padding_right: None,
1624 data: None,
1625 }]))
1626 }
1627 })
1628 .next()
1629 .await
1630 .unwrap();
1631
1632 executor.run_until_parked();
1633
1634 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1635 editor_a.update(cx_a, |editor, _| {
1636 assert_eq!(
1637 vec![initial_edit.to_string()],
1638 extract_hint_labels(editor),
1639 "Host should get its first hints when opens an editor"
1640 );
1641 let inlay_cache = editor.inlay_hint_cache();
1642 assert_eq!(
1643 inlay_cache.version(),
1644 1,
1645 "Host editor update the cache version after every cache/view change",
1646 );
1647 });
1648 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1649 let editor_b = workspace_b
1650 .update(cx_b, |workspace, cx| {
1651 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1652 })
1653 .await
1654 .unwrap()
1655 .downcast::<Editor>()
1656 .unwrap();
1657
1658 executor.run_until_parked();
1659 editor_b.update(cx_b, |editor, _| {
1660 assert_eq!(
1661 vec![initial_edit.to_string()],
1662 extract_hint_labels(editor),
1663 "Client should get its first hints when opens an editor"
1664 );
1665 let inlay_cache = editor.inlay_hint_cache();
1666 assert_eq!(
1667 inlay_cache.version(),
1668 1,
1669 "Guest editor update the cache version after every cache/view change"
1670 );
1671 });
1672
1673 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1674 editor_b.update(cx_b, |editor, cx| {
1675 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1676 editor.handle_input(":", cx);
1677 });
1678 cx_b.focus_view(&editor_b);
1679
1680 executor.run_until_parked();
1681 editor_a.update(cx_a, |editor, _| {
1682 assert_eq!(
1683 vec![after_client_edit.to_string()],
1684 extract_hint_labels(editor),
1685 );
1686 let inlay_cache = editor.inlay_hint_cache();
1687 assert_eq!(inlay_cache.version(), 2);
1688 });
1689 editor_b.update(cx_b, |editor, _| {
1690 assert_eq!(
1691 vec![after_client_edit.to_string()],
1692 extract_hint_labels(editor),
1693 );
1694 let inlay_cache = editor.inlay_hint_cache();
1695 assert_eq!(inlay_cache.version(), 2);
1696 });
1697
1698 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1699 editor_a.update(cx_a, |editor, cx| {
1700 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1701 editor.handle_input("a change to increment both buffers' versions", cx);
1702 });
1703 cx_a.focus_view(&editor_a);
1704
1705 executor.run_until_parked();
1706 editor_a.update(cx_a, |editor, _| {
1707 assert_eq!(
1708 vec![after_host_edit.to_string()],
1709 extract_hint_labels(editor),
1710 );
1711 let inlay_cache = editor.inlay_hint_cache();
1712 assert_eq!(inlay_cache.version(), 3);
1713 });
1714 editor_b.update(cx_b, |editor, _| {
1715 assert_eq!(
1716 vec![after_host_edit.to_string()],
1717 extract_hint_labels(editor),
1718 );
1719 let inlay_cache = editor.inlay_hint_cache();
1720 assert_eq!(inlay_cache.version(), 3);
1721 });
1722
1723 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1724 fake_language_server
1725 .request::<lsp::request::InlayHintRefreshRequest>(())
1726 .await
1727 .expect("inlay refresh request failed");
1728
1729 executor.run_until_parked();
1730 editor_a.update(cx_a, |editor, _| {
1731 assert_eq!(
1732 vec![after_special_edit_for_refresh.to_string()],
1733 extract_hint_labels(editor),
1734 "Host should react to /refresh LSP request"
1735 );
1736 let inlay_cache = editor.inlay_hint_cache();
1737 assert_eq!(
1738 inlay_cache.version(),
1739 4,
1740 "Host should accepted all edits and bump its cache version every time"
1741 );
1742 });
1743 editor_b.update(cx_b, |editor, _| {
1744 assert_eq!(
1745 vec![after_special_edit_for_refresh.to_string()],
1746 extract_hint_labels(editor),
1747 "Guest should get a /refresh LSP request propagated by host"
1748 );
1749 let inlay_cache = editor.inlay_hint_cache();
1750 assert_eq!(
1751 inlay_cache.version(),
1752 4,
1753 "Guest should accepted all edits and bump its cache version every time"
1754 );
1755 });
1756}
1757
1758#[gpui::test(iterations = 10)]
1759async fn test_inlay_hint_refresh_is_forwarded(
1760 cx_a: &mut TestAppContext,
1761 cx_b: &mut TestAppContext,
1762) {
1763 let mut server = TestServer::start(cx_a.executor()).await;
1764 let executor = cx_a.executor();
1765 let client_a = server.create_client(cx_a, "user_a").await;
1766 let client_b = server.create_client(cx_b, "user_b").await;
1767 server
1768 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1769 .await;
1770 let active_call_a = cx_a.read(ActiveCall::global);
1771 let active_call_b = cx_b.read(ActiveCall::global);
1772
1773 cx_a.update(editor::init);
1774 cx_b.update(editor::init);
1775
1776 cx_a.update(|cx| {
1777 SettingsStore::update_global(cx, |store, cx| {
1778 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1779 settings.defaults.inlay_hints = Some(InlayHintSettings {
1780 enabled: false,
1781 edit_debounce_ms: 0,
1782 scroll_debounce_ms: 0,
1783 show_type_hints: false,
1784 show_parameter_hints: false,
1785 show_other_hints: 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 })
1801 });
1802 });
1803 });
1804
1805 client_a.language_registry().add(rust_lang());
1806 client_b.language_registry().add(rust_lang());
1807 let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
1808 "Rust",
1809 FakeLspAdapter {
1810 capabilities: lsp::ServerCapabilities {
1811 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1812 ..Default::default()
1813 },
1814 ..Default::default()
1815 },
1816 );
1817
1818 client_a
1819 .fs()
1820 .insert_tree(
1821 "/a",
1822 json!({
1823 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1824 "other.rs": "// Test file",
1825 }),
1826 )
1827 .await;
1828 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1829 active_call_a
1830 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1831 .await
1832 .unwrap();
1833 let project_id = active_call_a
1834 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1835 .await
1836 .unwrap();
1837
1838 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
1839 active_call_b
1840 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1841 .await
1842 .unwrap();
1843
1844 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1845 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1846
1847 cx_a.background_executor.start_waiting();
1848
1849 let editor_a = workspace_a
1850 .update(cx_a, |workspace, cx| {
1851 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1852 })
1853 .await
1854 .unwrap()
1855 .downcast::<Editor>()
1856 .unwrap();
1857
1858 let editor_b = workspace_b
1859 .update(cx_b, |workspace, cx| {
1860 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1861 })
1862 .await
1863 .unwrap()
1864 .downcast::<Editor>()
1865 .unwrap();
1866
1867 let other_hints = Arc::new(AtomicBool::new(false));
1868 let fake_language_server = fake_language_servers.next().await.unwrap();
1869 let closure_other_hints = Arc::clone(&other_hints);
1870 fake_language_server
1871 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1872 let task_other_hints = Arc::clone(&closure_other_hints);
1873 async move {
1874 assert_eq!(
1875 params.text_document.uri,
1876 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1877 );
1878 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1879 let character = if other_hints { 0 } else { 2 };
1880 let label = if other_hints {
1881 "other hint"
1882 } else {
1883 "initial hint"
1884 };
1885 Ok(Some(vec![lsp::InlayHint {
1886 position: lsp::Position::new(0, character),
1887 label: lsp::InlayHintLabel::String(label.to_string()),
1888 kind: None,
1889 text_edits: None,
1890 tooltip: None,
1891 padding_left: None,
1892 padding_right: None,
1893 data: None,
1894 }]))
1895 }
1896 })
1897 .next()
1898 .await
1899 .unwrap();
1900 executor.finish_waiting();
1901
1902 executor.run_until_parked();
1903 editor_a.update(cx_a, |editor, _| {
1904 assert!(
1905 extract_hint_labels(editor).is_empty(),
1906 "Host should get no hints due to them turned off"
1907 );
1908 let inlay_cache = editor.inlay_hint_cache();
1909 assert_eq!(
1910 inlay_cache.version(),
1911 0,
1912 "Turned off hints should not generate version updates"
1913 );
1914 });
1915
1916 executor.run_until_parked();
1917 editor_b.update(cx_b, |editor, _| {
1918 assert_eq!(
1919 vec!["initial hint".to_string()],
1920 extract_hint_labels(editor),
1921 "Client should get its first hints when opens an editor"
1922 );
1923 let inlay_cache = editor.inlay_hint_cache();
1924 assert_eq!(
1925 inlay_cache.version(),
1926 1,
1927 "Should update cache version after first hints"
1928 );
1929 });
1930
1931 other_hints.fetch_or(true, atomic::Ordering::Release);
1932 fake_language_server
1933 .request::<lsp::request::InlayHintRefreshRequest>(())
1934 .await
1935 .expect("inlay refresh request failed");
1936 executor.run_until_parked();
1937 editor_a.update(cx_a, |editor, _| {
1938 assert!(
1939 extract_hint_labels(editor).is_empty(),
1940 "Host should get nop hints due to them turned off, even after the /refresh"
1941 );
1942 let inlay_cache = editor.inlay_hint_cache();
1943 assert_eq!(
1944 inlay_cache.version(),
1945 0,
1946 "Turned off hints should not generate version updates, again"
1947 );
1948 });
1949
1950 executor.run_until_parked();
1951 editor_b.update(cx_b, |editor, _| {
1952 assert_eq!(
1953 vec!["other hint".to_string()],
1954 extract_hint_labels(editor),
1955 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1956 );
1957 let inlay_cache = editor.inlay_hint_cache();
1958 assert_eq!(
1959 inlay_cache.version(),
1960 2,
1961 "Guest should accepted all edits and bump its cache version every time"
1962 );
1963 });
1964}
1965
1966#[gpui::test]
1967async fn test_multiple_hunk_types_revert(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1968 let mut server = TestServer::start(cx_a.executor()).await;
1969 let client_a = server.create_client(cx_a, "user_a").await;
1970 let client_b = server.create_client(cx_b, "user_b").await;
1971 server
1972 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1973 .await;
1974 let active_call_a = cx_a.read(ActiveCall::global);
1975 let active_call_b = cx_b.read(ActiveCall::global);
1976
1977 cx_a.update(editor::init);
1978 cx_b.update(editor::init);
1979
1980 client_a.language_registry().add(rust_lang());
1981 client_b.language_registry().add(rust_lang());
1982
1983 let base_text = indoc! {r#"struct Row;
1984struct Row1;
1985struct Row2;
1986
1987struct Row4;
1988struct Row5;
1989struct Row6;
1990
1991struct Row8;
1992struct Row9;
1993struct Row10;"#};
1994
1995 client_a
1996 .fs()
1997 .insert_tree(
1998 "/a",
1999 json!({
2000 "main.rs": base_text,
2001 }),
2002 )
2003 .await;
2004 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2005 active_call_a
2006 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2007 .await
2008 .unwrap();
2009 let project_id = active_call_a
2010 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2011 .await
2012 .unwrap();
2013
2014 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
2015 active_call_b
2016 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2017 .await
2018 .unwrap();
2019
2020 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2021 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2022
2023 let editor_a = workspace_a
2024 .update(cx_a, |workspace, cx| {
2025 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2026 })
2027 .await
2028 .unwrap()
2029 .downcast::<Editor>()
2030 .unwrap();
2031
2032 let editor_b = workspace_b
2033 .update(cx_b, |workspace, cx| {
2034 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
2035 })
2036 .await
2037 .unwrap()
2038 .downcast::<Editor>()
2039 .unwrap();
2040
2041 let mut editor_cx_a = EditorTestContext {
2042 cx: cx_a.clone(),
2043 window: cx_a.handle(),
2044 editor: editor_a,
2045 assertion_cx: AssertionContextManager::new(),
2046 };
2047 let mut editor_cx_b = EditorTestContext {
2048 cx: cx_b.clone(),
2049 window: cx_b.handle(),
2050 editor: editor_b,
2051 assertion_cx: AssertionContextManager::new(),
2052 };
2053
2054 // host edits the file, that differs from the base text, producing diff hunks
2055 editor_cx_a.set_state(indoc! {r#"struct Row;
2056 struct Row0.1;
2057 struct Row0.2;
2058 struct Row1;
2059
2060 struct Row4;
2061 struct Row5444;
2062 struct Row6;
2063
2064 struct Row9;
2065 struct Row1220;ˇ"#});
2066 editor_cx_a.update_editor(|editor, cx| {
2067 editor
2068 .buffer()
2069 .read(cx)
2070 .as_singleton()
2071 .unwrap()
2072 .update(cx, |buffer, cx| {
2073 buffer.set_diff_base(Some(base_text.into()), cx);
2074 });
2075 });
2076 editor_cx_b.update_editor(|editor, cx| {
2077 editor
2078 .buffer()
2079 .read(cx)
2080 .as_singleton()
2081 .unwrap()
2082 .update(cx, |buffer, cx| {
2083 buffer.set_diff_base(Some(base_text.into()), cx);
2084 });
2085 });
2086 cx_a.executor().run_until_parked();
2087 cx_b.executor().run_until_parked();
2088
2089 // the client selects a range in the updated buffer, expands it to see the diff for each hunk in the selection
2090 // the host does not see the diffs toggled
2091 editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row;
2092 struct Row0.1;
2093 struct Row0.2;
2094 struct Row1;
2095
2096 struct Row4;
2097 struct Row5444;
2098 struct Row6;
2099
2100 struct R»ow9;
2101 struct Row1220;"#});
2102 editor_cx_b
2103 .update_editor(|editor, cx| editor.toggle_hunk_diff(&editor::actions::ToggleHunkDiff, cx));
2104 cx_a.executor().run_until_parked();
2105 cx_b.executor().run_until_parked();
2106 editor_cx_a.update_editor(|editor, cx| {
2107 let snapshot = editor.snapshot(cx);
2108 let all_hunks = editor_hunks(editor, &snapshot, cx);
2109 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
2110 assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
2111 assert_eq!(
2112 all_hunks,
2113 vec![
2114 (
2115 "".to_string(),
2116 DiffHunkStatus::Added,
2117 DisplayRow(1)..DisplayRow(3)
2118 ),
2119 (
2120 "struct Row2;\n".to_string(),
2121 DiffHunkStatus::Removed,
2122 DisplayRow(4)..DisplayRow(4)
2123 ),
2124 (
2125 "struct Row5;\n".to_string(),
2126 DiffHunkStatus::Modified,
2127 DisplayRow(6)..DisplayRow(7)
2128 ),
2129 (
2130 "struct Row8;\n".to_string(),
2131 DiffHunkStatus::Removed,
2132 DisplayRow(9)..DisplayRow(9)
2133 ),
2134 (
2135 "struct Row10;".to_string(),
2136 DiffHunkStatus::Modified,
2137 DisplayRow(10)..DisplayRow(10),
2138 ),
2139 ]
2140 );
2141 assert_eq!(all_expanded_hunks, Vec::new());
2142 });
2143 editor_cx_b.update_editor(|editor, cx| {
2144 let snapshot = editor.snapshot(cx);
2145 let all_hunks = editor_hunks(editor, &snapshot, cx);
2146 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
2147 assert_eq!(
2148 expanded_hunks_background_highlights(editor, cx),
2149 vec![DisplayRow(1)..=DisplayRow(2), DisplayRow(8)..=DisplayRow(8)],
2150 );
2151 assert_eq!(
2152 all_hunks,
2153 vec![
2154 (
2155 "".to_string(),
2156 DiffHunkStatus::Added,
2157 DisplayRow(1)..DisplayRow(3)
2158 ),
2159 (
2160 "struct Row2;\n".to_string(),
2161 DiffHunkStatus::Removed,
2162 DisplayRow(5)..DisplayRow(5)
2163 ),
2164 (
2165 "struct Row5;\n".to_string(),
2166 DiffHunkStatus::Modified,
2167 DisplayRow(8)..DisplayRow(9)
2168 ),
2169 (
2170 "struct Row8;\n".to_string(),
2171 DiffHunkStatus::Removed,
2172 DisplayRow(12)..DisplayRow(12)
2173 ),
2174 (
2175 "struct Row10;".to_string(),
2176 DiffHunkStatus::Modified,
2177 DisplayRow(13)..DisplayRow(13),
2178 ),
2179 ]
2180 );
2181 assert_eq!(all_expanded_hunks, &all_hunks[..all_hunks.len() - 1]);
2182 });
2183
2184 // the client reverts the hunks, removing the expanded diffs too
2185 // both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
2186 editor_cx_b.update_editor(|editor, cx| {
2187 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
2188 });
2189 cx_a.executor().run_until_parked();
2190 cx_b.executor().run_until_parked();
2191 editor_cx_a.update_editor(|editor, cx| {
2192 let snapshot = editor.snapshot(cx);
2193 let all_hunks = editor_hunks(editor, &snapshot, cx);
2194 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
2195 assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
2196 assert_eq!(
2197 all_hunks,
2198 vec![(
2199 "struct Row10;".to_string(),
2200 DiffHunkStatus::Modified,
2201 DisplayRow(10)..DisplayRow(10),
2202 )]
2203 );
2204 assert_eq!(all_expanded_hunks, Vec::new());
2205 });
2206 editor_cx_b.update_editor(|editor, cx| {
2207 let snapshot = editor.snapshot(cx);
2208 let all_hunks = editor_hunks(editor, &snapshot, cx);
2209 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
2210 assert_eq!(
2211 expanded_hunks_background_highlights(editor, cx),
2212 vec![DisplayRow(5)..=DisplayRow(5)]
2213 );
2214 assert_eq!(
2215 all_hunks,
2216 vec![(
2217 "struct Row10;".to_string(),
2218 DiffHunkStatus::Modified,
2219 DisplayRow(10)..DisplayRow(10),
2220 )]
2221 );
2222 assert_eq!(all_expanded_hunks, Vec::new());
2223 });
2224 editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
2225 struct Row1;
2226 struct Row2;
2227
2228 struct Row4;
2229 struct Row5;
2230 struct Row6;
2231
2232 struct Row8;
2233 struct Row9;
2234 struct Row1220;ˇ"#});
2235 editor_cx_b.assert_editor_state(indoc! {r#"«ˇstruct Row;
2236 struct Row1;
2237 struct Row2;
2238
2239 struct Row4;
2240 struct Row5;
2241 struct Row6;
2242
2243 struct Row8;
2244 struct R»ow9;
2245 struct Row1220;"#});
2246}
2247
2248#[gpui::test(iterations = 10)]
2249async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2250 let mut server = TestServer::start(cx_a.executor()).await;
2251 let client_a = server.create_client(cx_a, "user_a").await;
2252 let client_b = server.create_client(cx_b, "user_b").await;
2253 server
2254 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2255 .await;
2256 let active_call_a = cx_a.read(ActiveCall::global);
2257
2258 cx_a.update(editor::init);
2259 cx_b.update(editor::init);
2260 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
2261 let inline_blame_off_settings = Some(InlineBlameSettings {
2262 enabled: false,
2263 delay_ms: None,
2264 min_column: None,
2265 });
2266 cx_a.update(|cx| {
2267 SettingsStore::update_global(cx, |store, cx| {
2268 store.update_user_settings::<ProjectSettings>(cx, |settings| {
2269 settings.git.inline_blame = inline_blame_off_settings;
2270 });
2271 });
2272 });
2273 cx_b.update(|cx| {
2274 SettingsStore::update_global(cx, |store, cx| {
2275 store.update_user_settings::<ProjectSettings>(cx, |settings| {
2276 settings.git.inline_blame = inline_blame_off_settings;
2277 });
2278 });
2279 });
2280
2281 client_a
2282 .fs()
2283 .insert_tree(
2284 "/my-repo",
2285 json!({
2286 ".git": {},
2287 "file.txt": "line1\nline2\nline3\nline\n",
2288 }),
2289 )
2290 .await;
2291
2292 let blame = git::blame::Blame {
2293 entries: vec![
2294 blame_entry("1b1b1b", 0..1),
2295 blame_entry("0d0d0d", 1..2),
2296 blame_entry("3a3a3a", 2..3),
2297 blame_entry("4c4c4c", 3..4),
2298 ],
2299 permalinks: HashMap::default(), // This field is deprecrated
2300 messages: [
2301 ("1b1b1b", "message for idx-0"),
2302 ("0d0d0d", "message for idx-1"),
2303 ("3a3a3a", "message for idx-2"),
2304 ("4c4c4c", "message for idx-3"),
2305 ]
2306 .into_iter()
2307 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2308 .collect(),
2309 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2310 };
2311 client_a.fs().set_blame_for_repo(
2312 Path::new("/my-repo/.git"),
2313 vec![(Path::new("file.txt"), blame)],
2314 );
2315
2316 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2317 let project_id = active_call_a
2318 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2319 .await
2320 .unwrap();
2321
2322 // Create editor_a
2323 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2324 let editor_a = workspace_a
2325 .update(cx_a, |workspace, cx| {
2326 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2327 })
2328 .await
2329 .unwrap()
2330 .downcast::<Editor>()
2331 .unwrap();
2332
2333 // Join the project as client B.
2334 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
2335 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2336 let editor_b = workspace_b
2337 .update(cx_b, |workspace, cx| {
2338 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2339 })
2340 .await
2341 .unwrap()
2342 .downcast::<Editor>()
2343 .unwrap();
2344
2345 // client_b now requests git blame for the open buffer
2346 editor_b.update(cx_b, |editor_b, cx| {
2347 assert!(editor_b.blame().is_none());
2348 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2349 });
2350
2351 cx_a.executor().run_until_parked();
2352 cx_b.executor().run_until_parked();
2353
2354 editor_b.update(cx_b, |editor_b, cx| {
2355 let blame = editor_b.blame().expect("editor_b should have blame now");
2356 let entries = blame.update(cx, |blame, cx| {
2357 blame
2358 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2359 .collect::<Vec<_>>()
2360 });
2361
2362 assert_eq!(
2363 entries,
2364 vec![
2365 Some(blame_entry("1b1b1b", 0..1)),
2366 Some(blame_entry("0d0d0d", 1..2)),
2367 Some(blame_entry("3a3a3a", 2..3)),
2368 Some(blame_entry("4c4c4c", 3..4)),
2369 ]
2370 );
2371
2372 blame.update(cx, |blame, _| {
2373 for (idx, entry) in entries.iter().flatten().enumerate() {
2374 let details = blame.details_for_entry(entry).unwrap();
2375 assert_eq!(details.message, format!("message for idx-{}", idx));
2376 assert_eq!(
2377 details.permalink.unwrap().to_string(),
2378 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2379 );
2380 }
2381 });
2382 });
2383
2384 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2385 // which gets back to client_b.
2386 editor_b.update(cx_b, |editor_b, cx| {
2387 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2388 });
2389
2390 cx_a.executor().run_until_parked();
2391 cx_b.executor().run_until_parked();
2392
2393 editor_b.update(cx_b, |editor_b, cx| {
2394 let blame = editor_b.blame().expect("editor_b should have blame now");
2395 let entries = blame.update(cx, |blame, cx| {
2396 blame
2397 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2398 .collect::<Vec<_>>()
2399 });
2400
2401 assert_eq!(
2402 entries,
2403 vec![
2404 None,
2405 Some(blame_entry("0d0d0d", 1..2)),
2406 Some(blame_entry("3a3a3a", 2..3)),
2407 Some(blame_entry("4c4c4c", 3..4)),
2408 ]
2409 );
2410 });
2411
2412 // Now editor_a also updates the file
2413 editor_a.update(cx_a, |editor_a, cx| {
2414 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2415 });
2416
2417 cx_a.executor().run_until_parked();
2418 cx_b.executor().run_until_parked();
2419
2420 editor_b.update(cx_b, |editor_b, cx| {
2421 let blame = editor_b.blame().expect("editor_b should have blame now");
2422 let entries = blame.update(cx, |blame, cx| {
2423 blame
2424 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2425 .collect::<Vec<_>>()
2426 });
2427
2428 assert_eq!(
2429 entries,
2430 vec![
2431 None,
2432 None,
2433 Some(blame_entry("3a3a3a", 2..3)),
2434 Some(blame_entry("4c4c4c", 3..4)),
2435 ]
2436 );
2437 });
2438}
2439
2440fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2441 let mut labels = Vec::new();
2442 for hint in editor.inlay_hint_cache().hints() {
2443 match hint.label {
2444 project::InlayHintLabel::String(s) => labels.push(s),
2445 _ => unreachable!(),
2446 }
2447 }
2448 labels
2449}
2450
2451fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2452 git::blame::BlameEntry {
2453 sha: sha.parse().unwrap(),
2454 range,
2455 ..Default::default()
2456 }
2457}