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