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!(expanded_hunks_background_highlights(editor, cx), Vec::new());
2114 assert_eq!(
2115 all_hunks,
2116 vec![
2117 ("".to_string(), DiffHunkStatus::Added, 1..3),
2118 ("struct Row2;\n".to_string(), DiffHunkStatus::Removed, 4..4),
2119 ("struct Row5;\n".to_string(), DiffHunkStatus::Modified, 6..7),
2120 ("struct Row8;\n".to_string(), DiffHunkStatus::Removed, 9..9),
2121 (
2122 "struct Row10;".to_string(),
2123 DiffHunkStatus::Modified,
2124 10..10,
2125 ),
2126 ]
2127 );
2128 assert_eq!(all_expanded_hunks, Vec::new());
2129 });
2130 editor_cx_b.update_editor(|editor, cx| {
2131 let snapshot = editor.snapshot(cx);
2132 let all_hunks = editor_hunks(editor, &snapshot, cx);
2133 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
2134 assert_eq!(
2135 expanded_hunks_background_highlights(editor, cx),
2136 vec![1..=2, 8..=8],
2137 );
2138 assert_eq!(
2139 all_hunks,
2140 vec![
2141 ("".to_string(), DiffHunkStatus::Added, 1..3),
2142 ("struct Row2;\n".to_string(), DiffHunkStatus::Removed, 5..5),
2143 ("struct Row5;\n".to_string(), DiffHunkStatus::Modified, 8..9),
2144 (
2145 "struct Row8;\n".to_string(),
2146 DiffHunkStatus::Removed,
2147 12..12
2148 ),
2149 (
2150 "struct Row10;".to_string(),
2151 DiffHunkStatus::Modified,
2152 13..13,
2153 ),
2154 ]
2155 );
2156 assert_eq!(all_expanded_hunks, &all_hunks[..all_hunks.len() - 1]);
2157 });
2158
2159 // the client reverts the hunks, removing the expanded diffs too
2160 // both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
2161 editor_cx_b.update_editor(|editor, cx| {
2162 editor.revert_selected_hunks(&RevertSelectedHunks, cx);
2163 });
2164 cx_a.executor().run_until_parked();
2165 cx_b.executor().run_until_parked();
2166 editor_cx_a.update_editor(|editor, cx| {
2167 let snapshot = editor.snapshot(cx);
2168 let all_hunks = editor_hunks(editor, &snapshot, cx);
2169 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
2170 assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
2171 assert_eq!(
2172 all_hunks,
2173 vec![(
2174 "struct Row10;".to_string(),
2175 DiffHunkStatus::Modified,
2176 10..10,
2177 )]
2178 );
2179 assert_eq!(all_expanded_hunks, Vec::new());
2180 });
2181 editor_cx_b.update_editor(|editor, cx| {
2182 let snapshot = editor.snapshot(cx);
2183 let all_hunks = editor_hunks(editor, &snapshot, cx);
2184 let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
2185 assert_eq!(
2186 expanded_hunks_background_highlights(editor, cx),
2187 vec![5..=5]
2188 );
2189 assert_eq!(
2190 all_hunks,
2191 vec![(
2192 "struct Row10;".to_string(),
2193 DiffHunkStatus::Modified,
2194 10..10,
2195 )]
2196 );
2197 assert_eq!(all_expanded_hunks, Vec::new());
2198 });
2199 editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
2200 struct Row1;
2201 struct Row2;
2202
2203 struct Row4;
2204 struct Row5;
2205 struct Row6;
2206
2207 struct Row8;
2208 struct Row9;
2209 struct Row1220;ˇ"#});
2210 editor_cx_b.assert_editor_state(indoc! {r#"«ˇstruct Row;
2211 struct Row1;
2212 struct Row2;
2213
2214 struct Row4;
2215 struct Row5;
2216 struct Row6;
2217
2218 struct Row8;
2219 struct R»ow9;
2220 struct Row1220;"#});
2221}
2222
2223#[gpui::test(iterations = 10)]
2224async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2225 let mut server = TestServer::start(cx_a.executor()).await;
2226 let client_a = server.create_client(cx_a, "user_a").await;
2227 let client_b = server.create_client(cx_b, "user_b").await;
2228 server
2229 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2230 .await;
2231 let active_call_a = cx_a.read(ActiveCall::global);
2232
2233 cx_a.update(editor::init);
2234 cx_b.update(editor::init);
2235 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
2236 let inline_blame_off_settings = Some(InlineBlameSettings {
2237 enabled: false,
2238 delay_ms: None,
2239 min_column: None,
2240 });
2241 cx_a.update(|cx| {
2242 cx.update_global(|store: &mut SettingsStore, cx| {
2243 store.update_user_settings::<ProjectSettings>(cx, |settings| {
2244 settings.git.inline_blame = inline_blame_off_settings;
2245 });
2246 });
2247 });
2248 cx_b.update(|cx| {
2249 cx.update_global(|store: &mut SettingsStore, cx| {
2250 store.update_user_settings::<ProjectSettings>(cx, |settings| {
2251 settings.git.inline_blame = inline_blame_off_settings;
2252 });
2253 });
2254 });
2255
2256 client_a
2257 .fs()
2258 .insert_tree(
2259 "/my-repo",
2260 json!({
2261 ".git": {},
2262 "file.txt": "line1\nline2\nline3\nline\n",
2263 }),
2264 )
2265 .await;
2266
2267 let blame = git::blame::Blame {
2268 entries: vec![
2269 blame_entry("1b1b1b", 0..1),
2270 blame_entry("0d0d0d", 1..2),
2271 blame_entry("3a3a3a", 2..3),
2272 blame_entry("4c4c4c", 3..4),
2273 ],
2274 permalinks: HashMap::default(), // This field is deprecrated
2275 messages: [
2276 ("1b1b1b", "message for idx-0"),
2277 ("0d0d0d", "message for idx-1"),
2278 ("3a3a3a", "message for idx-2"),
2279 ("4c4c4c", "message for idx-3"),
2280 ]
2281 .into_iter()
2282 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2283 .collect(),
2284 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2285 };
2286 client_a.fs().set_blame_for_repo(
2287 Path::new("/my-repo/.git"),
2288 vec![(Path::new("file.txt"), blame)],
2289 );
2290
2291 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2292 let project_id = active_call_a
2293 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2294 .await
2295 .unwrap();
2296
2297 // Create editor_a
2298 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2299 let editor_a = workspace_a
2300 .update(cx_a, |workspace, cx| {
2301 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2302 })
2303 .await
2304 .unwrap()
2305 .downcast::<Editor>()
2306 .unwrap();
2307
2308 // Join the project as client B.
2309 let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
2310 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2311 let editor_b = workspace_b
2312 .update(cx_b, |workspace, cx| {
2313 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2314 })
2315 .await
2316 .unwrap()
2317 .downcast::<Editor>()
2318 .unwrap();
2319
2320 // client_b now requests git blame for the open buffer
2321 editor_b.update(cx_b, |editor_b, cx| {
2322 assert!(editor_b.blame().is_none());
2323 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2324 });
2325
2326 cx_a.executor().run_until_parked();
2327 cx_b.executor().run_until_parked();
2328
2329 editor_b.update(cx_b, |editor_b, cx| {
2330 let blame = editor_b.blame().expect("editor_b should have blame now");
2331 let entries = blame.update(cx, |blame, cx| {
2332 blame
2333 .blame_for_rows((0..4).map(Some), cx)
2334 .collect::<Vec<_>>()
2335 });
2336
2337 assert_eq!(
2338 entries,
2339 vec![
2340 Some(blame_entry("1b1b1b", 0..1)),
2341 Some(blame_entry("0d0d0d", 1..2)),
2342 Some(blame_entry("3a3a3a", 2..3)),
2343 Some(blame_entry("4c4c4c", 3..4)),
2344 ]
2345 );
2346
2347 blame.update(cx, |blame, _| {
2348 for (idx, entry) in entries.iter().flatten().enumerate() {
2349 let details = blame.details_for_entry(entry).unwrap();
2350 assert_eq!(details.message, format!("message for idx-{}", idx));
2351 assert_eq!(
2352 details.permalink.unwrap().to_string(),
2353 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2354 );
2355 }
2356 });
2357 });
2358
2359 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2360 // which gets back to client_b.
2361 editor_b.update(cx_b, |editor_b, cx| {
2362 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2363 });
2364
2365 cx_a.executor().run_until_parked();
2366 cx_b.executor().run_until_parked();
2367
2368 editor_b.update(cx_b, |editor_b, cx| {
2369 let blame = editor_b.blame().expect("editor_b should have blame now");
2370 let entries = blame.update(cx, |blame, cx| {
2371 blame
2372 .blame_for_rows((0..4).map(Some), cx)
2373 .collect::<Vec<_>>()
2374 });
2375
2376 assert_eq!(
2377 entries,
2378 vec![
2379 None,
2380 Some(blame_entry("0d0d0d", 1..2)),
2381 Some(blame_entry("3a3a3a", 2..3)),
2382 Some(blame_entry("4c4c4c", 3..4)),
2383 ]
2384 );
2385 });
2386
2387 // Now editor_a also updates the file
2388 editor_a.update(cx_a, |editor_a, cx| {
2389 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2390 });
2391
2392 cx_a.executor().run_until_parked();
2393 cx_b.executor().run_until_parked();
2394
2395 editor_b.update(cx_b, |editor_b, cx| {
2396 let blame = editor_b.blame().expect("editor_b should have blame now");
2397 let entries = blame.update(cx, |blame, cx| {
2398 blame
2399 .blame_for_rows((0..4).map(Some), cx)
2400 .collect::<Vec<_>>()
2401 });
2402
2403 assert_eq!(
2404 entries,
2405 vec![
2406 None,
2407 None,
2408 Some(blame_entry("3a3a3a", 2..3)),
2409 Some(blame_entry("4c4c4c", 3..4)),
2410 ]
2411 );
2412 });
2413}
2414
2415fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2416 let mut labels = Vec::new();
2417 for hint in editor.inlay_hint_cache().hints() {
2418 match hint.label {
2419 project::InlayHintLabel::String(s) => labels.push(s),
2420 _ => unreachable!(),
2421 }
2422 }
2423 labels
2424}
2425
2426fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2427 git::blame::BlameEntry {
2428 sha: sha.parse().unwrap(),
2429 range,
2430 ..Default::default()
2431 }
2432}