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