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