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