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