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(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
698 })
699 .unwrap();
700 fake_language_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
701 |_, _| async move {
702 Ok(lsp::CodeAction {
703 title: "Inline into all callers".to_string(),
704 edit: Some(lsp::WorkspaceEdit {
705 changes: Some(
706 [
707 (
708 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
709 vec![lsp::TextEdit::new(
710 lsp::Range::new(
711 lsp::Position::new(1, 22),
712 lsp::Position::new(1, 34),
713 ),
714 "4".to_string(),
715 )],
716 ),
717 (
718 lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
719 vec![lsp::TextEdit::new(
720 lsp::Range::new(
721 lsp::Position::new(0, 0),
722 lsp::Position::new(0, 27),
723 ),
724 "".to_string(),
725 )],
726 ),
727 ]
728 .into_iter()
729 .collect(),
730 ),
731 ..Default::default()
732 }),
733 ..Default::default()
734 })
735 },
736 );
737
738 // After the action is confirmed, an editor containing both modified files is opened.
739 confirm_action.await.unwrap();
740
741 let code_action_editor = workspace_b.update(cx_b, |workspace, cx| {
742 workspace
743 .active_item(cx)
744 .unwrap()
745 .downcast::<Editor>()
746 .unwrap()
747 });
748 code_action_editor.update_in(cx_b, |editor, window, cx| {
749 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
750 editor.undo(&Undo, window, cx);
751 assert_eq!(
752 editor.text(cx),
753 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
754 );
755 editor.redo(&Redo, window, cx);
756 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
757 });
758}
759
760#[gpui::test(iterations = 10)]
761async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
762 let mut server = TestServer::start(cx_a.executor()).await;
763 let client_a = server.create_client(cx_a, "user_a").await;
764 let client_b = server.create_client(cx_b, "user_b").await;
765 server
766 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
767 .await;
768 let active_call_a = cx_a.read(ActiveCall::global);
769
770 cx_b.update(editor::init);
771
772 // Set up a fake language server.
773 client_a.language_registry().add(rust_lang());
774 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
775 "Rust",
776 FakeLspAdapter {
777 capabilities: lsp::ServerCapabilities {
778 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
779 prepare_provider: Some(true),
780 work_done_progress_options: Default::default(),
781 })),
782 ..Default::default()
783 },
784 ..Default::default()
785 },
786 );
787
788 client_a
789 .fs()
790 .insert_tree(
791 path!("/dir"),
792 json!({
793 "one.rs": "const ONE: usize = 1;",
794 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
795 }),
796 )
797 .await;
798 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
799 let project_id = active_call_a
800 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
801 .await
802 .unwrap();
803 let project_b = client_b.join_remote_project(project_id, cx_b).await;
804
805 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
806 let editor_b = workspace_b
807 .update_in(cx_b, |workspace, window, cx| {
808 workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
809 })
810 .await
811 .unwrap()
812 .downcast::<Editor>()
813 .unwrap();
814 let fake_language_server = fake_language_servers.next().await.unwrap();
815
816 // Move cursor to a location that can be renamed.
817 let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
818 editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
819 editor.rename(&Rename, window, cx).unwrap()
820 });
821
822 fake_language_server
823 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
824 assert_eq!(
825 params.text_document.uri.as_str(),
826 uri!("file:///dir/one.rs")
827 );
828 assert_eq!(params.position, lsp::Position::new(0, 7));
829 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
830 lsp::Position::new(0, 6),
831 lsp::Position::new(0, 9),
832 ))))
833 })
834 .next()
835 .await
836 .unwrap();
837 prepare_rename.await.unwrap();
838 editor_b.update(cx_b, |editor, cx| {
839 use editor::ToOffset;
840 let rename = editor.pending_rename().unwrap();
841 let buffer = editor.buffer().read(cx).snapshot(cx);
842 assert_eq!(
843 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
844 6..9
845 );
846 rename.editor.update(cx, |rename_editor, cx| {
847 let rename_selection = rename_editor.selections.newest::<usize>(cx);
848 assert_eq!(
849 rename_selection.range(),
850 0..3,
851 "Rename that was triggered from zero selection caret, should propose the whole word."
852 );
853 rename_editor.buffer().update(cx, |rename_buffer, cx| {
854 rename_buffer.edit([(0..3, "THREE")], None, cx);
855 });
856 });
857 });
858
859 // Cancel the rename, and repeat the same, but use selections instead of cursor movement
860 editor_b.update_in(cx_b, |editor, window, cx| {
861 editor.cancel(&editor::actions::Cancel, window, cx);
862 });
863 let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
864 editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
865 editor.rename(&Rename, window, cx).unwrap()
866 });
867
868 fake_language_server
869 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
870 assert_eq!(
871 params.text_document.uri.as_str(),
872 uri!("file:///dir/one.rs")
873 );
874 assert_eq!(params.position, lsp::Position::new(0, 8));
875 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
876 lsp::Position::new(0, 6),
877 lsp::Position::new(0, 9),
878 ))))
879 })
880 .next()
881 .await
882 .unwrap();
883 prepare_rename.await.unwrap();
884 editor_b.update(cx_b, |editor, cx| {
885 use editor::ToOffset;
886 let rename = editor.pending_rename().unwrap();
887 let buffer = editor.buffer().read(cx).snapshot(cx);
888 let lsp_rename_start = rename.range.start.to_offset(&buffer);
889 let lsp_rename_end = rename.range.end.to_offset(&buffer);
890 assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
891 rename.editor.update(cx, |rename_editor, cx| {
892 let rename_selection = rename_editor.selections.newest::<usize>(cx);
893 assert_eq!(
894 rename_selection.range(),
895 1..2,
896 "Rename that was triggered from a selection, should have the same selection range in the rename proposal"
897 );
898 rename_editor.buffer().update(cx, |rename_buffer, cx| {
899 rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
900 });
901 });
902 });
903
904 let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| {
905 Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap()
906 });
907 fake_language_server
908 .set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
909 assert_eq!(
910 params.text_document_position.text_document.uri.as_str(),
911 uri!("file:///dir/one.rs")
912 );
913 assert_eq!(
914 params.text_document_position.position,
915 lsp::Position::new(0, 6)
916 );
917 assert_eq!(params.new_name, "THREE");
918 Ok(Some(lsp::WorkspaceEdit {
919 changes: Some(
920 [
921 (
922 lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
923 vec![lsp::TextEdit::new(
924 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
925 "THREE".to_string(),
926 )],
927 ),
928 (
929 lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
930 vec![
931 lsp::TextEdit::new(
932 lsp::Range::new(
933 lsp::Position::new(0, 24),
934 lsp::Position::new(0, 27),
935 ),
936 "THREE".to_string(),
937 ),
938 lsp::TextEdit::new(
939 lsp::Range::new(
940 lsp::Position::new(0, 35),
941 lsp::Position::new(0, 38),
942 ),
943 "THREE".to_string(),
944 ),
945 ],
946 ),
947 ]
948 .into_iter()
949 .collect(),
950 ),
951 ..Default::default()
952 }))
953 })
954 .next()
955 .await
956 .unwrap();
957 confirm_rename.await.unwrap();
958
959 let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
960 workspace.active_item_as::<Editor>(cx).unwrap()
961 });
962
963 rename_editor.update_in(cx_b, |editor, window, cx| {
964 assert_eq!(
965 editor.text(cx),
966 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
967 );
968 editor.undo(&Undo, window, cx);
969 assert_eq!(
970 editor.text(cx),
971 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
972 );
973 editor.redo(&Redo, window, cx);
974 assert_eq!(
975 editor.text(cx),
976 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
977 );
978 });
979
980 // Ensure temporary rename edits cannot be undone/redone.
981 editor_b.update_in(cx_b, |editor, window, cx| {
982 editor.undo(&Undo, window, cx);
983 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
984 editor.undo(&Undo, window, cx);
985 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
986 editor.redo(&Redo, window, cx);
987 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
988 })
989}
990
991#[gpui::test(iterations = 10)]
992async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
993 let mut server = TestServer::start(cx_a.executor()).await;
994 let executor = cx_a.executor();
995 let client_a = server.create_client(cx_a, "user_a").await;
996 let client_b = server.create_client(cx_b, "user_b").await;
997 server
998 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
999 .await;
1000 let active_call_a = cx_a.read(ActiveCall::global);
1001
1002 cx_b.update(editor::init);
1003
1004 client_a.language_registry().add(rust_lang());
1005 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1006 "Rust",
1007 FakeLspAdapter {
1008 name: "the-language-server",
1009 ..Default::default()
1010 },
1011 );
1012
1013 client_a
1014 .fs()
1015 .insert_tree(
1016 path!("/dir"),
1017 json!({
1018 "main.rs": "const ONE: usize = 1;",
1019 }),
1020 )
1021 .await;
1022 let (project_a, _) = client_a.build_local_project(path!("/dir"), cx_a).await;
1023
1024 let _buffer_a = project_a
1025 .update(cx_a, |p, cx| {
1026 p.open_local_buffer_with_lsp(path!("/dir/main.rs"), cx)
1027 })
1028 .await
1029 .unwrap();
1030
1031 let fake_language_server = fake_language_servers.next().await.unwrap();
1032 fake_language_server.start_progress("the-token").await;
1033
1034 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1035 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1036 token: lsp::NumberOrString::String("the-token".to_string()),
1037 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1038 lsp::WorkDoneProgressReport {
1039 message: Some("the-message".to_string()),
1040 ..Default::default()
1041 },
1042 )),
1043 });
1044 executor.run_until_parked();
1045
1046 project_a.read_with(cx_a, |project, cx| {
1047 let status = project.language_server_statuses(cx).next().unwrap().1;
1048 assert_eq!(status.name, "the-language-server");
1049 assert_eq!(status.pending_work.len(), 1);
1050 assert_eq!(
1051 status.pending_work["the-token"].message.as_ref().unwrap(),
1052 "the-message"
1053 );
1054 });
1055
1056 let project_id = active_call_a
1057 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1058 .await
1059 .unwrap();
1060 executor.run_until_parked();
1061 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1062
1063 project_b.read_with(cx_b, |project, cx| {
1064 let status = project.language_server_statuses(cx).next().unwrap().1;
1065 assert_eq!(status.name, "the-language-server");
1066 });
1067
1068 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1069 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1070 token: lsp::NumberOrString::String("the-token".to_string()),
1071 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1072 lsp::WorkDoneProgressReport {
1073 message: Some("the-message-2".to_string()),
1074 ..Default::default()
1075 },
1076 )),
1077 });
1078 executor.run_until_parked();
1079
1080 project_a.read_with(cx_a, |project, cx| {
1081 let status = project.language_server_statuses(cx).next().unwrap().1;
1082 assert_eq!(status.name, "the-language-server");
1083 assert_eq!(status.pending_work.len(), 1);
1084 assert_eq!(
1085 status.pending_work["the-token"].message.as_ref().unwrap(),
1086 "the-message-2"
1087 );
1088 });
1089
1090 project_b.read_with(cx_b, |project, cx| {
1091 let status = project.language_server_statuses(cx).next().unwrap().1;
1092 assert_eq!(status.name, "the-language-server");
1093 assert_eq!(status.pending_work.len(), 1);
1094 assert_eq!(
1095 status.pending_work["the-token"].message.as_ref().unwrap(),
1096 "the-message-2"
1097 );
1098 });
1099}
1100
1101#[gpui::test(iterations = 10)]
1102async fn test_share_project(
1103 cx_a: &mut TestAppContext,
1104 cx_b: &mut TestAppContext,
1105 cx_c: &mut TestAppContext,
1106) {
1107 let executor = cx_a.executor();
1108 let cx_b = cx_b.add_empty_window();
1109 let mut server = TestServer::start(executor.clone()).await;
1110 let client_a = server.create_client(cx_a, "user_a").await;
1111 let client_b = server.create_client(cx_b, "user_b").await;
1112 let client_c = server.create_client(cx_c, "user_c").await;
1113 server
1114 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1115 .await;
1116 let active_call_a = cx_a.read(ActiveCall::global);
1117 let active_call_b = cx_b.read(ActiveCall::global);
1118 let active_call_c = cx_c.read(ActiveCall::global);
1119
1120 client_a
1121 .fs()
1122 .insert_tree(
1123 path!("/a"),
1124 json!({
1125 ".gitignore": "ignored-dir",
1126 "a.txt": "a-contents",
1127 "b.txt": "b-contents",
1128 "ignored-dir": {
1129 "c.txt": "",
1130 "d.txt": "",
1131 }
1132 }),
1133 )
1134 .await;
1135
1136 // Invite client B to collaborate on a project
1137 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1138 active_call_a
1139 .update(cx_a, |call, cx| {
1140 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1141 })
1142 .await
1143 .unwrap();
1144
1145 // Join that project as client B
1146
1147 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1148 executor.run_until_parked();
1149 let call = incoming_call_b.borrow().clone().unwrap();
1150 assert_eq!(call.calling_user.github_login, "user_a");
1151 let initial_project = call.initial_project.unwrap();
1152 active_call_b
1153 .update(cx_b, |call, cx| call.accept_incoming(cx))
1154 .await
1155 .unwrap();
1156 let client_b_peer_id = client_b.peer_id().unwrap();
1157 let project_b = client_b.join_remote_project(initial_project.id, cx_b).await;
1158
1159 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1160
1161 executor.run_until_parked();
1162
1163 project_a.read_with(cx_a, |project, _| {
1164 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1165 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1166 });
1167
1168 project_b.read_with(cx_b, |project, cx| {
1169 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1170 assert_eq!(
1171 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1172 [
1173 Path::new(".gitignore"),
1174 Path::new("a.txt"),
1175 Path::new("b.txt"),
1176 Path::new("ignored-dir"),
1177 ]
1178 );
1179 });
1180
1181 project_b
1182 .update(cx_b, |project, cx| {
1183 let worktree = project.worktrees(cx).next().unwrap();
1184 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1185 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1186 })
1187 .await
1188 .unwrap();
1189
1190 project_b.read_with(cx_b, |project, cx| {
1191 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1192 assert_eq!(
1193 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1194 [
1195 Path::new(".gitignore"),
1196 Path::new("a.txt"),
1197 Path::new("b.txt"),
1198 Path::new("ignored-dir"),
1199 Path::new("ignored-dir/c.txt"),
1200 Path::new("ignored-dir/d.txt"),
1201 ]
1202 );
1203 });
1204
1205 // Open the same file as client B and client A.
1206 let buffer_b = project_b
1207 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1208 .await
1209 .unwrap();
1210
1211 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1212
1213 project_a.read_with(cx_a, |project, cx| {
1214 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1215 });
1216 let buffer_a = project_a
1217 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1218 .await
1219 .unwrap();
1220
1221 let editor_b =
1222 cx_b.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
1223
1224 // Client A sees client B's selection
1225 executor.run_until_parked();
1226
1227 buffer_a.read_with(cx_a, |buffer, _| {
1228 buffer
1229 .snapshot()
1230 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1231 .count()
1232 == 1
1233 });
1234
1235 // Edit the buffer as client B and see that edit as client A.
1236 editor_b.update_in(cx_b, |editor, window, cx| {
1237 editor.handle_input("ok, ", window, cx)
1238 });
1239 executor.run_until_parked();
1240
1241 buffer_a.read_with(cx_a, |buffer, _| {
1242 assert_eq!(buffer.text(), "ok, b-contents")
1243 });
1244
1245 // Client B can invite client C on a project shared by client A.
1246 active_call_b
1247 .update(cx_b, |call, cx| {
1248 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1249 })
1250 .await
1251 .unwrap();
1252
1253 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1254 executor.run_until_parked();
1255 let call = incoming_call_c.borrow().clone().unwrap();
1256 assert_eq!(call.calling_user.github_login, "user_b");
1257 let initial_project = call.initial_project.unwrap();
1258 active_call_c
1259 .update(cx_c, |call, cx| call.accept_incoming(cx))
1260 .await
1261 .unwrap();
1262 let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1263
1264 // Client B closes the editor, and client A sees client B's selections removed.
1265 cx_b.update(move |_, _| drop(editor_b));
1266 executor.run_until_parked();
1267
1268 buffer_a.read_with(cx_a, |buffer, _| {
1269 buffer
1270 .snapshot()
1271 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1272 .count()
1273 == 0
1274 });
1275}
1276
1277#[gpui::test(iterations = 10)]
1278async fn test_on_input_format_from_host_to_guest(
1279 cx_a: &mut TestAppContext,
1280 cx_b: &mut TestAppContext,
1281) {
1282 let mut server = TestServer::start(cx_a.executor()).await;
1283 let executor = cx_a.executor();
1284 let client_a = server.create_client(cx_a, "user_a").await;
1285 let client_b = server.create_client(cx_b, "user_b").await;
1286 server
1287 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1288 .await;
1289 let active_call_a = cx_a.read(ActiveCall::global);
1290
1291 client_a.language_registry().add(rust_lang());
1292 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1293 "Rust",
1294 FakeLspAdapter {
1295 capabilities: lsp::ServerCapabilities {
1296 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1297 first_trigger_character: ":".to_string(),
1298 more_trigger_character: Some(vec![">".to_string()]),
1299 }),
1300 ..Default::default()
1301 },
1302 ..Default::default()
1303 },
1304 );
1305
1306 client_a
1307 .fs()
1308 .insert_tree(
1309 path!("/a"),
1310 json!({
1311 "main.rs": "fn main() { a }",
1312 "other.rs": "// Test file",
1313 }),
1314 )
1315 .await;
1316 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1317 let project_id = active_call_a
1318 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1319 .await
1320 .unwrap();
1321 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1322
1323 // Open a file in an editor as the host.
1324 let buffer_a = project_a
1325 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1326 .await
1327 .unwrap();
1328 let cx_a = cx_a.add_empty_window();
1329 let editor_a = cx_a.new_window_entity(|window, cx| {
1330 Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
1331 });
1332
1333 let fake_language_server = fake_language_servers.next().await.unwrap();
1334 executor.run_until_parked();
1335
1336 // Receive an OnTypeFormatting request as the host's language server.
1337 // Return some formatting from the host's language server.
1338 fake_language_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
1339 |params, _| async move {
1340 assert_eq!(
1341 params.text_document_position.text_document.uri,
1342 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
1343 );
1344 assert_eq!(
1345 params.text_document_position.position,
1346 lsp::Position::new(0, 14),
1347 );
1348
1349 Ok(Some(vec![lsp::TextEdit {
1350 new_text: "~<".to_string(),
1351 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1352 }]))
1353 },
1354 );
1355
1356 // Open the buffer on the guest and see that the formatting worked
1357 let buffer_b = project_b
1358 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1359 .await
1360 .unwrap();
1361
1362 // Type a on type formatting trigger character as the guest.
1363 cx_a.focus(&editor_a);
1364 editor_a.update_in(cx_a, |editor, window, cx| {
1365 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1366 editor.handle_input(">", window, cx);
1367 });
1368
1369 executor.run_until_parked();
1370
1371 buffer_b.read_with(cx_b, |buffer, _| {
1372 assert_eq!(buffer.text(), "fn main() { a>~< }")
1373 });
1374
1375 // Undo should remove LSP edits first
1376 editor_a.update_in(cx_a, |editor, window, cx| {
1377 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1378 editor.undo(&Undo, window, cx);
1379 assert_eq!(editor.text(cx), "fn main() { a> }");
1380 });
1381 executor.run_until_parked();
1382
1383 buffer_b.read_with(cx_b, |buffer, _| {
1384 assert_eq!(buffer.text(), "fn main() { a> }")
1385 });
1386
1387 editor_a.update_in(cx_a, |editor, window, cx| {
1388 assert_eq!(editor.text(cx), "fn main() { a> }");
1389 editor.undo(&Undo, window, cx);
1390 assert_eq!(editor.text(cx), "fn main() { a }");
1391 });
1392 executor.run_until_parked();
1393
1394 buffer_b.read_with(cx_b, |buffer, _| {
1395 assert_eq!(buffer.text(), "fn main() { a }")
1396 });
1397}
1398
1399#[gpui::test(iterations = 10)]
1400async fn test_on_input_format_from_guest_to_host(
1401 cx_a: &mut TestAppContext,
1402 cx_b: &mut TestAppContext,
1403) {
1404 let mut server = TestServer::start(cx_a.executor()).await;
1405 let executor = cx_a.executor();
1406 let client_a = server.create_client(cx_a, "user_a").await;
1407 let client_b = server.create_client(cx_b, "user_b").await;
1408 server
1409 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1410 .await;
1411 let active_call_a = cx_a.read(ActiveCall::global);
1412
1413 client_a.language_registry().add(rust_lang());
1414 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1415 "Rust",
1416 FakeLspAdapter {
1417 capabilities: lsp::ServerCapabilities {
1418 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1419 first_trigger_character: ":".to_string(),
1420 more_trigger_character: Some(vec![">".to_string()]),
1421 }),
1422 ..Default::default()
1423 },
1424 ..Default::default()
1425 },
1426 );
1427
1428 client_a
1429 .fs()
1430 .insert_tree(
1431 path!("/a"),
1432 json!({
1433 "main.rs": "fn main() { a }",
1434 "other.rs": "// Test file",
1435 }),
1436 )
1437 .await;
1438 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1439 let project_id = active_call_a
1440 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1441 .await
1442 .unwrap();
1443 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1444
1445 // Open a file in an editor as the guest.
1446 let buffer_b = project_b
1447 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1448 .await
1449 .unwrap();
1450 let cx_b = cx_b.add_empty_window();
1451 let editor_b = cx_b.new_window_entity(|window, cx| {
1452 Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
1453 });
1454
1455 let fake_language_server = fake_language_servers.next().await.unwrap();
1456 executor.run_until_parked();
1457
1458 // Type a on type formatting trigger character as the guest.
1459 cx_b.focus(&editor_b);
1460 editor_b.update_in(cx_b, |editor, window, cx| {
1461 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1462 editor.handle_input(":", window, cx);
1463 });
1464
1465 // Receive an OnTypeFormatting request as the host's language server.
1466 // Return some formatting from the host's language server.
1467 executor.start_waiting();
1468 fake_language_server
1469 .set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1470 assert_eq!(
1471 params.text_document_position.text_document.uri,
1472 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
1473 );
1474 assert_eq!(
1475 params.text_document_position.position,
1476 lsp::Position::new(0, 14),
1477 );
1478
1479 Ok(Some(vec![lsp::TextEdit {
1480 new_text: "~:".to_string(),
1481 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1482 }]))
1483 })
1484 .next()
1485 .await
1486 .unwrap();
1487 executor.finish_waiting();
1488
1489 // Open the buffer on the host and see that the formatting worked
1490 let buffer_a = project_a
1491 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1492 .await
1493 .unwrap();
1494 executor.run_until_parked();
1495
1496 buffer_a.read_with(cx_a, |buffer, _| {
1497 assert_eq!(buffer.text(), "fn main() { a:~: }")
1498 });
1499
1500 // Undo should remove LSP edits first
1501 editor_b.update_in(cx_b, |editor, window, cx| {
1502 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1503 editor.undo(&Undo, window, cx);
1504 assert_eq!(editor.text(cx), "fn main() { a: }");
1505 });
1506 executor.run_until_parked();
1507
1508 buffer_a.read_with(cx_a, |buffer, _| {
1509 assert_eq!(buffer.text(), "fn main() { a: }")
1510 });
1511
1512 editor_b.update_in(cx_b, |editor, window, cx| {
1513 assert_eq!(editor.text(cx), "fn main() { a: }");
1514 editor.undo(&Undo, window, cx);
1515 assert_eq!(editor.text(cx), "fn main() { a }");
1516 });
1517 executor.run_until_parked();
1518
1519 buffer_a.read_with(cx_a, |buffer, _| {
1520 assert_eq!(buffer.text(), "fn main() { a }")
1521 });
1522}
1523
1524#[gpui::test(iterations = 10)]
1525async fn test_mutual_editor_inlay_hint_cache_update(
1526 cx_a: &mut TestAppContext,
1527 cx_b: &mut TestAppContext,
1528) {
1529 let mut server = TestServer::start(cx_a.executor()).await;
1530 let executor = cx_a.executor();
1531 let client_a = server.create_client(cx_a, "user_a").await;
1532 let client_b = server.create_client(cx_b, "user_b").await;
1533 server
1534 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1535 .await;
1536 let active_call_a = cx_a.read(ActiveCall::global);
1537 let active_call_b = cx_b.read(ActiveCall::global);
1538
1539 cx_a.update(editor::init);
1540 cx_b.update(editor::init);
1541
1542 cx_a.update(|cx| {
1543 SettingsStore::update_global(cx, |store, cx| {
1544 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1545 settings.defaults.inlay_hints = Some(InlayHintSettings {
1546 enabled: true,
1547 show_value_hints: true,
1548 edit_debounce_ms: 0,
1549 scroll_debounce_ms: 0,
1550 show_type_hints: true,
1551 show_parameter_hints: false,
1552 show_other_hints: true,
1553 show_background: false,
1554 toggle_on_modifiers_press: None,
1555 })
1556 });
1557 });
1558 });
1559 cx_b.update(|cx| {
1560 SettingsStore::update_global(cx, |store, cx| {
1561 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1562 settings.defaults.inlay_hints = Some(InlayHintSettings {
1563 show_value_hints: true,
1564 enabled: true,
1565 edit_debounce_ms: 0,
1566 scroll_debounce_ms: 0,
1567 show_type_hints: true,
1568 show_parameter_hints: false,
1569 show_other_hints: true,
1570 show_background: false,
1571 toggle_on_modifiers_press: None,
1572 })
1573 });
1574 });
1575 });
1576
1577 client_a.language_registry().add(rust_lang());
1578 client_b.language_registry().add(rust_lang());
1579 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1580 "Rust",
1581 FakeLspAdapter {
1582 capabilities: lsp::ServerCapabilities {
1583 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1584 ..Default::default()
1585 },
1586 ..Default::default()
1587 },
1588 );
1589
1590 // Client A opens a project.
1591 client_a
1592 .fs()
1593 .insert_tree(
1594 path!("/a"),
1595 json!({
1596 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1597 "other.rs": "// Test file",
1598 }),
1599 )
1600 .await;
1601 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1602 active_call_a
1603 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1604 .await
1605 .unwrap();
1606 let project_id = active_call_a
1607 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1608 .await
1609 .unwrap();
1610
1611 // Client B joins the project
1612 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1613 active_call_b
1614 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1615 .await
1616 .unwrap();
1617
1618 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1619 executor.start_waiting();
1620
1621 // The host opens a rust file.
1622 let _buffer_a = project_a
1623 .update(cx_a, |project, cx| {
1624 project.open_local_buffer(path!("/a/main.rs"), cx)
1625 })
1626 .await
1627 .unwrap();
1628 let editor_a = workspace_a
1629 .update_in(cx_a, |workspace, window, cx| {
1630 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1631 })
1632 .await
1633 .unwrap()
1634 .downcast::<Editor>()
1635 .unwrap();
1636
1637 let fake_language_server = fake_language_servers.next().await.unwrap();
1638
1639 // Set up the language server to return an additional inlay hint on each request.
1640 let edits_made = Arc::new(AtomicUsize::new(0));
1641 let closure_edits_made = Arc::clone(&edits_made);
1642 fake_language_server
1643 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1644 let task_edits_made = Arc::clone(&closure_edits_made);
1645 async move {
1646 assert_eq!(
1647 params.text_document.uri,
1648 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
1649 );
1650 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1651 Ok(Some(vec![lsp::InlayHint {
1652 position: lsp::Position::new(0, edits_made as u32),
1653 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1654 kind: None,
1655 text_edits: None,
1656 tooltip: None,
1657 padding_left: None,
1658 padding_right: None,
1659 data: None,
1660 }]))
1661 }
1662 })
1663 .next()
1664 .await
1665 .unwrap();
1666
1667 executor.run_until_parked();
1668
1669 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1670 editor_a.update(cx_a, |editor, _| {
1671 assert_eq!(
1672 vec![initial_edit.to_string()],
1673 extract_hint_labels(editor),
1674 "Host should get its first hints when opens an editor"
1675 );
1676 });
1677 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1678 let editor_b = workspace_b
1679 .update_in(cx_b, |workspace, window, cx| {
1680 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1681 })
1682 .await
1683 .unwrap()
1684 .downcast::<Editor>()
1685 .unwrap();
1686
1687 executor.run_until_parked();
1688 editor_b.update(cx_b, |editor, _| {
1689 assert_eq!(
1690 vec![initial_edit.to_string()],
1691 extract_hint_labels(editor),
1692 "Client should get its first hints when opens an editor"
1693 );
1694 });
1695
1696 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1697 editor_b.update_in(cx_b, |editor, window, cx| {
1698 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
1699 editor.handle_input(":", window, cx);
1700 });
1701 cx_b.focus(&editor_b);
1702
1703 executor.run_until_parked();
1704 editor_a.update(cx_a, |editor, _| {
1705 assert_eq!(
1706 vec![after_client_edit.to_string()],
1707 extract_hint_labels(editor),
1708 );
1709 });
1710 editor_b.update(cx_b, |editor, _| {
1711 assert_eq!(
1712 vec![after_client_edit.to_string()],
1713 extract_hint_labels(editor),
1714 );
1715 });
1716
1717 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1718 editor_a.update_in(cx_a, |editor, window, cx| {
1719 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1720 editor.handle_input("a change to increment both buffers' versions", window, cx);
1721 });
1722 cx_a.focus(&editor_a);
1723
1724 executor.run_until_parked();
1725 editor_a.update(cx_a, |editor, _| {
1726 assert_eq!(
1727 vec![after_host_edit.to_string()],
1728 extract_hint_labels(editor),
1729 );
1730 });
1731 editor_b.update(cx_b, |editor, _| {
1732 assert_eq!(
1733 vec![after_host_edit.to_string()],
1734 extract_hint_labels(editor),
1735 );
1736 });
1737
1738 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1739 fake_language_server
1740 .request::<lsp::request::InlayHintRefreshRequest>(())
1741 .await
1742 .expect("inlay refresh request failed");
1743
1744 executor.run_until_parked();
1745 editor_a.update(cx_a, |editor, _| {
1746 assert_eq!(
1747 vec![after_special_edit_for_refresh.to_string()],
1748 extract_hint_labels(editor),
1749 "Host should react to /refresh LSP request"
1750 );
1751 });
1752 editor_b.update(cx_b, |editor, _| {
1753 assert_eq!(
1754 vec![after_special_edit_for_refresh.to_string()],
1755 extract_hint_labels(editor),
1756 "Guest should get a /refresh LSP request propagated by host"
1757 );
1758 });
1759}
1760
1761#[gpui::test(iterations = 10)]
1762async fn test_inlay_hint_refresh_is_forwarded(
1763 cx_a: &mut TestAppContext,
1764 cx_b: &mut TestAppContext,
1765) {
1766 let mut server = TestServer::start(cx_a.executor()).await;
1767 let executor = cx_a.executor();
1768 let client_a = server.create_client(cx_a, "user_a").await;
1769 let client_b = server.create_client(cx_b, "user_b").await;
1770 server
1771 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1772 .await;
1773 let active_call_a = cx_a.read(ActiveCall::global);
1774 let active_call_b = cx_b.read(ActiveCall::global);
1775
1776 cx_a.update(editor::init);
1777 cx_b.update(editor::init);
1778
1779 cx_a.update(|cx| {
1780 SettingsStore::update_global(cx, |store, cx| {
1781 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1782 settings.defaults.inlay_hints = Some(InlayHintSettings {
1783 show_value_hints: true,
1784 enabled: false,
1785 edit_debounce_ms: 0,
1786 scroll_debounce_ms: 0,
1787 show_type_hints: false,
1788 show_parameter_hints: false,
1789 show_other_hints: false,
1790 show_background: false,
1791 toggle_on_modifiers_press: None,
1792 })
1793 });
1794 });
1795 });
1796 cx_b.update(|cx| {
1797 SettingsStore::update_global(cx, |store, cx| {
1798 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1799 settings.defaults.inlay_hints = Some(InlayHintSettings {
1800 show_value_hints: true,
1801 enabled: true,
1802 edit_debounce_ms: 0,
1803 scroll_debounce_ms: 0,
1804 show_type_hints: true,
1805 show_parameter_hints: true,
1806 show_other_hints: true,
1807 show_background: false,
1808 toggle_on_modifiers_press: None,
1809 })
1810 });
1811 });
1812 });
1813
1814 client_a.language_registry().add(rust_lang());
1815 client_b.language_registry().add(rust_lang());
1816 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1817 "Rust",
1818 FakeLspAdapter {
1819 capabilities: lsp::ServerCapabilities {
1820 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1821 ..Default::default()
1822 },
1823 ..Default::default()
1824 },
1825 );
1826
1827 client_a
1828 .fs()
1829 .insert_tree(
1830 path!("/a"),
1831 json!({
1832 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1833 "other.rs": "// Test file",
1834 }),
1835 )
1836 .await;
1837 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1838 active_call_a
1839 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1840 .await
1841 .unwrap();
1842 let project_id = active_call_a
1843 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1844 .await
1845 .unwrap();
1846
1847 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1848 active_call_b
1849 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1850 .await
1851 .unwrap();
1852
1853 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1854 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1855
1856 cx_a.background_executor.start_waiting();
1857
1858 let editor_a = workspace_a
1859 .update_in(cx_a, |workspace, window, cx| {
1860 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1861 })
1862 .await
1863 .unwrap()
1864 .downcast::<Editor>()
1865 .unwrap();
1866
1867 let editor_b = workspace_b
1868 .update_in(cx_b, |workspace, window, cx| {
1869 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1870 })
1871 .await
1872 .unwrap()
1873 .downcast::<Editor>()
1874 .unwrap();
1875
1876 let other_hints = Arc::new(AtomicBool::new(false));
1877 let fake_language_server = fake_language_servers.next().await.unwrap();
1878 let closure_other_hints = Arc::clone(&other_hints);
1879 fake_language_server
1880 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1881 let task_other_hints = Arc::clone(&closure_other_hints);
1882 async move {
1883 assert_eq!(
1884 params.text_document.uri,
1885 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
1886 );
1887 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1888 let character = if other_hints { 0 } else { 2 };
1889 let label = if other_hints {
1890 "other hint"
1891 } else {
1892 "initial hint"
1893 };
1894 Ok(Some(vec![lsp::InlayHint {
1895 position: lsp::Position::new(0, character),
1896 label: lsp::InlayHintLabel::String(label.to_string()),
1897 kind: None,
1898 text_edits: None,
1899 tooltip: None,
1900 padding_left: None,
1901 padding_right: None,
1902 data: None,
1903 }]))
1904 }
1905 })
1906 .next()
1907 .await
1908 .unwrap();
1909 executor.finish_waiting();
1910
1911 executor.run_until_parked();
1912 editor_a.update(cx_a, |editor, _| {
1913 assert!(
1914 extract_hint_labels(editor).is_empty(),
1915 "Host should get no hints due to them turned off"
1916 );
1917 });
1918
1919 executor.run_until_parked();
1920 editor_b.update(cx_b, |editor, _| {
1921 assert_eq!(
1922 vec!["initial hint".to_string()],
1923 extract_hint_labels(editor),
1924 "Client should get its first hints when opens an editor"
1925 );
1926 });
1927
1928 other_hints.fetch_or(true, atomic::Ordering::Release);
1929 fake_language_server
1930 .request::<lsp::request::InlayHintRefreshRequest>(())
1931 .await
1932 .expect("inlay refresh request failed");
1933 executor.run_until_parked();
1934 editor_a.update(cx_a, |editor, _| {
1935 assert!(
1936 extract_hint_labels(editor).is_empty(),
1937 "Host should get no hints due to them turned off, even after the /refresh"
1938 );
1939 });
1940
1941 executor.run_until_parked();
1942 editor_b.update(cx_b, |editor, _| {
1943 assert_eq!(
1944 vec!["other hint".to_string()],
1945 extract_hint_labels(editor),
1946 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1947 );
1948 });
1949}
1950
1951#[gpui::test(iterations = 10)]
1952async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1953 let mut server = TestServer::start(cx_a.executor()).await;
1954 let client_a = server.create_client(cx_a, "user_a").await;
1955 let client_b = server.create_client(cx_b, "user_b").await;
1956 server
1957 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1958 .await;
1959 let active_call_a = cx_a.read(ActiveCall::global);
1960
1961 cx_a.update(editor::init);
1962 cx_b.update(editor::init);
1963 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1964 let inline_blame_off_settings = Some(InlineBlameSettings {
1965 enabled: false,
1966 delay_ms: None,
1967 min_column: None,
1968 show_commit_summary: false,
1969 });
1970 cx_a.update(|cx| {
1971 SettingsStore::update_global(cx, |store, cx| {
1972 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1973 settings.git.inline_blame = inline_blame_off_settings;
1974 });
1975 });
1976 });
1977 cx_b.update(|cx| {
1978 SettingsStore::update_global(cx, |store, cx| {
1979 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1980 settings.git.inline_blame = inline_blame_off_settings;
1981 });
1982 });
1983 });
1984
1985 client_a
1986 .fs()
1987 .insert_tree(
1988 path!("/my-repo"),
1989 json!({
1990 ".git": {},
1991 "file.txt": "line1\nline2\nline3\nline\n",
1992 }),
1993 )
1994 .await;
1995
1996 let blame = git::blame::Blame {
1997 entries: vec![
1998 blame_entry("1b1b1b", 0..1),
1999 blame_entry("0d0d0d", 1..2),
2000 blame_entry("3a3a3a", 2..3),
2001 blame_entry("4c4c4c", 3..4),
2002 ],
2003 messages: [
2004 ("1b1b1b", "message for idx-0"),
2005 ("0d0d0d", "message for idx-1"),
2006 ("3a3a3a", "message for idx-2"),
2007 ("4c4c4c", "message for idx-3"),
2008 ]
2009 .into_iter()
2010 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2011 .collect(),
2012 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2013 };
2014 client_a.fs().set_blame_for_repo(
2015 Path::new(path!("/my-repo/.git")),
2016 vec![("file.txt".into(), blame)],
2017 );
2018
2019 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
2020 let project_id = active_call_a
2021 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2022 .await
2023 .unwrap();
2024
2025 // Create editor_a
2026 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2027 let editor_a = workspace_a
2028 .update_in(cx_a, |workspace, window, cx| {
2029 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2030 })
2031 .await
2032 .unwrap()
2033 .downcast::<Editor>()
2034 .unwrap();
2035
2036 // Join the project as client B.
2037 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2038 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2039 let editor_b = workspace_b
2040 .update_in(cx_b, |workspace, window, cx| {
2041 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2042 })
2043 .await
2044 .unwrap()
2045 .downcast::<Editor>()
2046 .unwrap();
2047 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
2048 editor_b
2049 .buffer()
2050 .read(cx)
2051 .as_singleton()
2052 .unwrap()
2053 .read(cx)
2054 .remote_id()
2055 });
2056
2057 // client_b now requests git blame for the open buffer
2058 editor_b.update_in(cx_b, |editor_b, window, cx| {
2059 assert!(editor_b.blame().is_none());
2060 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
2061 });
2062
2063 cx_a.executor().run_until_parked();
2064 cx_b.executor().run_until_parked();
2065
2066 editor_b.update(cx_b, |editor_b, cx| {
2067 let blame = editor_b.blame().expect("editor_b should have blame now");
2068 let entries = blame.update(cx, |blame, cx| {
2069 blame
2070 .blame_for_rows(
2071 &(0..4)
2072 .map(|row| RowInfo {
2073 buffer_row: Some(row),
2074 buffer_id: Some(buffer_id_b),
2075 ..Default::default()
2076 })
2077 .collect::<Vec<_>>(),
2078 cx,
2079 )
2080 .collect::<Vec<_>>()
2081 });
2082
2083 assert_eq!(
2084 entries,
2085 vec![
2086 Some(blame_entry("1b1b1b", 0..1)),
2087 Some(blame_entry("0d0d0d", 1..2)),
2088 Some(blame_entry("3a3a3a", 2..3)),
2089 Some(blame_entry("4c4c4c", 3..4)),
2090 ]
2091 );
2092
2093 blame.update(cx, |blame, _| {
2094 for (idx, entry) in entries.iter().flatten().enumerate() {
2095 let details = blame.details_for_entry(entry).unwrap();
2096 assert_eq!(details.message, format!("message for idx-{}", idx));
2097 assert_eq!(
2098 details.permalink.unwrap().to_string(),
2099 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2100 );
2101 }
2102 });
2103 });
2104
2105 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2106 // which gets back to client_b.
2107 editor_b.update_in(cx_b, |editor_b, _, cx| {
2108 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2109 });
2110
2111 cx_a.executor().run_until_parked();
2112 cx_b.executor().run_until_parked();
2113
2114 editor_b.update(cx_b, |editor_b, cx| {
2115 let blame = editor_b.blame().expect("editor_b should have blame now");
2116 let entries = blame.update(cx, |blame, cx| {
2117 blame
2118 .blame_for_rows(
2119 &(0..4)
2120 .map(|row| RowInfo {
2121 buffer_row: Some(row),
2122 buffer_id: Some(buffer_id_b),
2123 ..Default::default()
2124 })
2125 .collect::<Vec<_>>(),
2126 cx,
2127 )
2128 .collect::<Vec<_>>()
2129 });
2130
2131 assert_eq!(
2132 entries,
2133 vec![
2134 None,
2135 Some(blame_entry("0d0d0d", 1..2)),
2136 Some(blame_entry("3a3a3a", 2..3)),
2137 Some(blame_entry("4c4c4c", 3..4)),
2138 ]
2139 );
2140 });
2141
2142 // Now editor_a also updates the file
2143 editor_a.update_in(cx_a, |editor_a, _, cx| {
2144 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2145 });
2146
2147 cx_a.executor().run_until_parked();
2148 cx_b.executor().run_until_parked();
2149
2150 editor_b.update(cx_b, |editor_b, cx| {
2151 let blame = editor_b.blame().expect("editor_b should have blame now");
2152 let entries = blame.update(cx, |blame, cx| {
2153 blame
2154 .blame_for_rows(
2155 &(0..4)
2156 .map(|row| RowInfo {
2157 buffer_row: Some(row),
2158 buffer_id: Some(buffer_id_b),
2159 ..Default::default()
2160 })
2161 .collect::<Vec<_>>(),
2162 cx,
2163 )
2164 .collect::<Vec<_>>()
2165 });
2166
2167 assert_eq!(
2168 entries,
2169 vec![
2170 None,
2171 None,
2172 Some(blame_entry("3a3a3a", 2..3)),
2173 Some(blame_entry("4c4c4c", 3..4)),
2174 ]
2175 );
2176 });
2177}
2178
2179#[gpui::test(iterations = 30)]
2180async fn test_collaborating_with_editorconfig(
2181 cx_a: &mut TestAppContext,
2182 cx_b: &mut TestAppContext,
2183) {
2184 let mut server = TestServer::start(cx_a.executor()).await;
2185 let client_a = server.create_client(cx_a, "user_a").await;
2186 let client_b = server.create_client(cx_b, "user_b").await;
2187 server
2188 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2189 .await;
2190 let active_call_a = cx_a.read(ActiveCall::global);
2191
2192 cx_b.update(editor::init);
2193
2194 // Set up a fake language server.
2195 client_a.language_registry().add(rust_lang());
2196 client_a
2197 .fs()
2198 .insert_tree(
2199 path!("/a"),
2200 json!({
2201 "src": {
2202 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2203 "other_mod": {
2204 "other.rs": "pub fn foo() -> usize {\n 4\n}",
2205 ".editorconfig": "",
2206 },
2207 },
2208 ".editorconfig": "[*]\ntab_width = 2\n",
2209 }),
2210 )
2211 .await;
2212 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2213 let project_id = active_call_a
2214 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2215 .await
2216 .unwrap();
2217 let main_buffer_a = project_a
2218 .update(cx_a, |p, cx| {
2219 p.open_buffer((worktree_id, "src/main.rs"), cx)
2220 })
2221 .await
2222 .unwrap();
2223 let other_buffer_a = project_a
2224 .update(cx_a, |p, cx| {
2225 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2226 })
2227 .await
2228 .unwrap();
2229 let cx_a = cx_a.add_empty_window();
2230 let main_editor_a = cx_a.new_window_entity(|window, cx| {
2231 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
2232 });
2233 let other_editor_a = cx_a.new_window_entity(|window, cx| {
2234 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
2235 });
2236 let mut main_editor_cx_a = EditorTestContext {
2237 cx: cx_a.clone(),
2238 window: cx_a.window_handle(),
2239 editor: main_editor_a,
2240 assertion_cx: AssertionContextManager::new(),
2241 };
2242 let mut other_editor_cx_a = EditorTestContext {
2243 cx: cx_a.clone(),
2244 window: cx_a.window_handle(),
2245 editor: other_editor_a,
2246 assertion_cx: AssertionContextManager::new(),
2247 };
2248
2249 // Join the project as client B.
2250 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2251 let main_buffer_b = project_b
2252 .update(cx_b, |p, cx| {
2253 p.open_buffer((worktree_id, "src/main.rs"), cx)
2254 })
2255 .await
2256 .unwrap();
2257 let other_buffer_b = project_b
2258 .update(cx_b, |p, cx| {
2259 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2260 })
2261 .await
2262 .unwrap();
2263 let cx_b = cx_b.add_empty_window();
2264 let main_editor_b = cx_b.new_window_entity(|window, cx| {
2265 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
2266 });
2267 let other_editor_b = cx_b.new_window_entity(|window, cx| {
2268 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
2269 });
2270 let mut main_editor_cx_b = EditorTestContext {
2271 cx: cx_b.clone(),
2272 window: cx_b.window_handle(),
2273 editor: main_editor_b,
2274 assertion_cx: AssertionContextManager::new(),
2275 };
2276 let mut other_editor_cx_b = EditorTestContext {
2277 cx: cx_b.clone(),
2278 window: cx_b.window_handle(),
2279 editor: other_editor_b,
2280 assertion_cx: AssertionContextManager::new(),
2281 };
2282
2283 let initial_main = indoc! {"
2284ˇmod other;
2285fn main() { let foo = other::foo(); }"};
2286 let initial_other = indoc! {"
2287ˇpub fn foo() -> usize {
2288 4
2289}"};
2290
2291 let first_tabbed_main = indoc! {"
2292 ˇmod other;
2293fn main() { let foo = other::foo(); }"};
2294 tab_undo_assert(
2295 &mut main_editor_cx_a,
2296 &mut main_editor_cx_b,
2297 initial_main,
2298 first_tabbed_main,
2299 true,
2300 );
2301 tab_undo_assert(
2302 &mut main_editor_cx_a,
2303 &mut main_editor_cx_b,
2304 initial_main,
2305 first_tabbed_main,
2306 false,
2307 );
2308
2309 let first_tabbed_other = indoc! {"
2310 ˇpub fn foo() -> usize {
2311 4
2312}"};
2313 tab_undo_assert(
2314 &mut other_editor_cx_a,
2315 &mut other_editor_cx_b,
2316 initial_other,
2317 first_tabbed_other,
2318 true,
2319 );
2320 tab_undo_assert(
2321 &mut other_editor_cx_a,
2322 &mut other_editor_cx_b,
2323 initial_other,
2324 first_tabbed_other,
2325 false,
2326 );
2327
2328 client_a
2329 .fs()
2330 .atomic_write(
2331 PathBuf::from(path!("/a/src/.editorconfig")),
2332 "[*]\ntab_width = 3\n".to_owned(),
2333 )
2334 .await
2335 .unwrap();
2336 cx_a.run_until_parked();
2337 cx_b.run_until_parked();
2338
2339 let second_tabbed_main = indoc! {"
2340 ˇmod other;
2341fn main() { let foo = other::foo(); }"};
2342 tab_undo_assert(
2343 &mut main_editor_cx_a,
2344 &mut main_editor_cx_b,
2345 initial_main,
2346 second_tabbed_main,
2347 true,
2348 );
2349 tab_undo_assert(
2350 &mut main_editor_cx_a,
2351 &mut main_editor_cx_b,
2352 initial_main,
2353 second_tabbed_main,
2354 false,
2355 );
2356
2357 let second_tabbed_other = indoc! {"
2358 ˇpub fn foo() -> usize {
2359 4
2360}"};
2361 tab_undo_assert(
2362 &mut other_editor_cx_a,
2363 &mut other_editor_cx_b,
2364 initial_other,
2365 second_tabbed_other,
2366 true,
2367 );
2368 tab_undo_assert(
2369 &mut other_editor_cx_a,
2370 &mut other_editor_cx_b,
2371 initial_other,
2372 second_tabbed_other,
2373 false,
2374 );
2375
2376 let editorconfig_buffer_b = project_b
2377 .update(cx_b, |p, cx| {
2378 p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2379 })
2380 .await
2381 .unwrap();
2382 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2383 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2384 });
2385 project_b
2386 .update(cx_b, |project, cx| {
2387 project.save_buffer(editorconfig_buffer_b.clone(), cx)
2388 })
2389 .await
2390 .unwrap();
2391 cx_a.run_until_parked();
2392 cx_b.run_until_parked();
2393
2394 tab_undo_assert(
2395 &mut main_editor_cx_a,
2396 &mut main_editor_cx_b,
2397 initial_main,
2398 second_tabbed_main,
2399 true,
2400 );
2401 tab_undo_assert(
2402 &mut main_editor_cx_a,
2403 &mut main_editor_cx_b,
2404 initial_main,
2405 second_tabbed_main,
2406 false,
2407 );
2408
2409 let third_tabbed_other = indoc! {"
2410 ˇpub fn foo() -> usize {
2411 4
2412}"};
2413 tab_undo_assert(
2414 &mut other_editor_cx_a,
2415 &mut other_editor_cx_b,
2416 initial_other,
2417 third_tabbed_other,
2418 true,
2419 );
2420
2421 tab_undo_assert(
2422 &mut other_editor_cx_a,
2423 &mut other_editor_cx_b,
2424 initial_other,
2425 third_tabbed_other,
2426 false,
2427 );
2428}
2429
2430#[gpui::test]
2431async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2432 let executor = cx_a.executor();
2433 let mut server = TestServer::start(executor.clone()).await;
2434 let client_a = server.create_client(cx_a, "user_a").await;
2435 let client_b = server.create_client(cx_b, "user_b").await;
2436 server
2437 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2438 .await;
2439 let active_call_a = cx_a.read(ActiveCall::global);
2440 let active_call_b = cx_b.read(ActiveCall::global);
2441 cx_a.update(editor::init);
2442 cx_b.update(editor::init);
2443 client_a
2444 .fs()
2445 .insert_tree(
2446 "/a",
2447 json!({
2448 "test.txt": "one\ntwo\nthree\nfour\nfive",
2449 }),
2450 )
2451 .await;
2452 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2453 let project_path = ProjectPath {
2454 worktree_id,
2455 path: Arc::from(Path::new(&"test.txt")),
2456 };
2457 let abs_path = project_a.read_with(cx_a, |project, cx| {
2458 project
2459 .absolute_path(&project_path, cx)
2460 .map(|path_buf| Arc::from(path_buf.to_owned()))
2461 .unwrap()
2462 });
2463
2464 active_call_a
2465 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2466 .await
2467 .unwrap();
2468 let project_id = active_call_a
2469 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2470 .await
2471 .unwrap();
2472 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2473 active_call_b
2474 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2475 .await
2476 .unwrap();
2477 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2478 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2479
2480 // Client A opens an editor.
2481 let editor_a = workspace_a
2482 .update_in(cx_a, |workspace, window, cx| {
2483 workspace.open_path(project_path.clone(), None, true, window, cx)
2484 })
2485 .await
2486 .unwrap()
2487 .downcast::<Editor>()
2488 .unwrap();
2489
2490 // Client B opens same editor as A.
2491 let editor_b = workspace_b
2492 .update_in(cx_b, |workspace, window, cx| {
2493 workspace.open_path(project_path.clone(), None, true, window, cx)
2494 })
2495 .await
2496 .unwrap()
2497 .downcast::<Editor>()
2498 .unwrap();
2499
2500 cx_a.run_until_parked();
2501 cx_b.run_until_parked();
2502
2503 // Client A adds breakpoint on line (1)
2504 editor_a.update_in(cx_a, |editor, window, cx| {
2505 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2506 });
2507
2508 cx_a.run_until_parked();
2509 cx_b.run_until_parked();
2510
2511 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2512 editor
2513 .breakpoint_store()
2514 .clone()
2515 .unwrap()
2516 .read(cx)
2517 .all_breakpoints(cx)
2518 .clone()
2519 });
2520 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2521 editor
2522 .breakpoint_store()
2523 .clone()
2524 .unwrap()
2525 .read(cx)
2526 .all_breakpoints(cx)
2527 .clone()
2528 });
2529
2530 assert_eq!(1, breakpoints_a.len());
2531 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
2532 assert_eq!(breakpoints_a, breakpoints_b);
2533
2534 // Client B adds breakpoint on line(2)
2535 editor_b.update_in(cx_b, |editor, window, cx| {
2536 editor.move_down(&editor::actions::MoveDown, window, cx);
2537 editor.move_down(&editor::actions::MoveDown, window, cx);
2538 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2539 });
2540
2541 cx_a.run_until_parked();
2542 cx_b.run_until_parked();
2543
2544 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2545 editor
2546 .breakpoint_store()
2547 .clone()
2548 .unwrap()
2549 .read(cx)
2550 .all_breakpoints(cx)
2551 .clone()
2552 });
2553 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2554 editor
2555 .breakpoint_store()
2556 .clone()
2557 .unwrap()
2558 .read(cx)
2559 .all_breakpoints(cx)
2560 .clone()
2561 });
2562
2563 assert_eq!(1, breakpoints_a.len());
2564 assert_eq!(breakpoints_a, breakpoints_b);
2565 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
2566
2567 // Client A removes last added breakpoint from client B
2568 editor_a.update_in(cx_a, |editor, window, cx| {
2569 editor.move_down(&editor::actions::MoveDown, window, cx);
2570 editor.move_down(&editor::actions::MoveDown, window, cx);
2571 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2572 });
2573
2574 cx_a.run_until_parked();
2575 cx_b.run_until_parked();
2576
2577 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2578 editor
2579 .breakpoint_store()
2580 .clone()
2581 .unwrap()
2582 .read(cx)
2583 .all_breakpoints(cx)
2584 .clone()
2585 });
2586 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2587 editor
2588 .breakpoint_store()
2589 .clone()
2590 .unwrap()
2591 .read(cx)
2592 .all_breakpoints(cx)
2593 .clone()
2594 });
2595
2596 assert_eq!(1, breakpoints_a.len());
2597 assert_eq!(breakpoints_a, breakpoints_b);
2598 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
2599
2600 // Client B removes first added breakpoint by client A
2601 editor_b.update_in(cx_b, |editor, window, cx| {
2602 editor.move_up(&editor::actions::MoveUp, window, cx);
2603 editor.move_up(&editor::actions::MoveUp, window, cx);
2604 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
2605 });
2606
2607 cx_a.run_until_parked();
2608 cx_b.run_until_parked();
2609
2610 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
2611 editor
2612 .breakpoint_store()
2613 .clone()
2614 .unwrap()
2615 .read(cx)
2616 .all_breakpoints(cx)
2617 .clone()
2618 });
2619 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
2620 editor
2621 .breakpoint_store()
2622 .clone()
2623 .unwrap()
2624 .read(cx)
2625 .all_breakpoints(cx)
2626 .clone()
2627 });
2628
2629 assert_eq!(0, breakpoints_a.len());
2630 assert_eq!(breakpoints_a, breakpoints_b);
2631}
2632
2633#[gpui::test]
2634async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2635 let mut server = TestServer::start(cx_a.executor()).await;
2636 let client_a = server.create_client(cx_a, "user_a").await;
2637 let client_b = server.create_client(cx_b, "user_b").await;
2638 server
2639 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2640 .await;
2641 let active_call_a = cx_a.read(ActiveCall::global);
2642 let active_call_b = cx_b.read(ActiveCall::global);
2643
2644 cx_a.update(editor::init);
2645 cx_b.update(editor::init);
2646
2647 client_a.language_registry().add(rust_lang());
2648 client_b.language_registry().add(rust_lang());
2649 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2650 "Rust",
2651 FakeLspAdapter {
2652 name: RUST_ANALYZER_NAME,
2653 ..FakeLspAdapter::default()
2654 },
2655 );
2656
2657 client_a
2658 .fs()
2659 .insert_tree(
2660 path!("/a"),
2661 json!({
2662 "main.rs": "fn main() {}",
2663 }),
2664 )
2665 .await;
2666 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2667 active_call_a
2668 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2669 .await
2670 .unwrap();
2671 let project_id = active_call_a
2672 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2673 .await
2674 .unwrap();
2675
2676 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2677 active_call_b
2678 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2679 .await
2680 .unwrap();
2681
2682 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2683 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2684
2685 let editor_a = workspace_a
2686 .update_in(cx_a, |workspace, window, cx| {
2687 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
2688 })
2689 .await
2690 .unwrap()
2691 .downcast::<Editor>()
2692 .unwrap();
2693
2694 let editor_b = workspace_b
2695 .update_in(cx_b, |workspace, window, cx| {
2696 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
2697 })
2698 .await
2699 .unwrap()
2700 .downcast::<Editor>()
2701 .unwrap();
2702
2703 let fake_language_server = fake_language_servers.next().await.unwrap();
2704
2705 // host
2706 let mut expand_request_a =
2707 fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
2708 assert_eq!(
2709 params.text_document.uri,
2710 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
2711 );
2712 assert_eq!(params.position, lsp::Position::new(0, 0),);
2713 Ok(Some(ExpandedMacro {
2714 name: "test_macro_name".to_string(),
2715 expansion: "test_macro_expansion on the host".to_string(),
2716 }))
2717 });
2718
2719 editor_a.update_in(cx_a, |editor, window, cx| {
2720 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
2721 });
2722 expand_request_a.next().await.unwrap();
2723 cx_a.run_until_parked();
2724
2725 workspace_a.update(cx_a, |workspace, cx| {
2726 workspace.active_pane().update(cx, |pane, cx| {
2727 assert_eq!(
2728 pane.items_len(),
2729 2,
2730 "Should have added a macro expansion to the host's pane"
2731 );
2732 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
2733 new_editor.update(cx, |editor, cx| {
2734 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
2735 });
2736 })
2737 });
2738
2739 // client
2740 let mut expand_request_b =
2741 fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
2742 assert_eq!(
2743 params.text_document.uri,
2744 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
2745 );
2746 assert_eq!(params.position, lsp::Position::new(0, 0),);
2747 Ok(Some(ExpandedMacro {
2748 name: "test_macro_name".to_string(),
2749 expansion: "test_macro_expansion on the client".to_string(),
2750 }))
2751 });
2752
2753 editor_b.update_in(cx_b, |editor, window, cx| {
2754 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
2755 });
2756 expand_request_b.next().await.unwrap();
2757 cx_b.run_until_parked();
2758
2759 workspace_b.update(cx_b, |workspace, cx| {
2760 workspace.active_pane().update(cx, |pane, cx| {
2761 assert_eq!(
2762 pane.items_len(),
2763 2,
2764 "Should have added a macro expansion to the client's pane"
2765 );
2766 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
2767 new_editor.update(cx, |editor, cx| {
2768 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
2769 });
2770 })
2771 });
2772}
2773
2774#[track_caller]
2775fn tab_undo_assert(
2776 cx_a: &mut EditorTestContext,
2777 cx_b: &mut EditorTestContext,
2778 expected_initial: &str,
2779 expected_tabbed: &str,
2780 a_tabs: bool,
2781) {
2782 cx_a.assert_editor_state(expected_initial);
2783 cx_b.assert_editor_state(expected_initial);
2784
2785 if a_tabs {
2786 cx_a.update_editor(|editor, window, cx| {
2787 editor.tab(&editor::actions::Tab, window, cx);
2788 });
2789 } else {
2790 cx_b.update_editor(|editor, window, cx| {
2791 editor.tab(&editor::actions::Tab, window, cx);
2792 });
2793 }
2794
2795 cx_a.run_until_parked();
2796 cx_b.run_until_parked();
2797
2798 cx_a.assert_editor_state(expected_tabbed);
2799 cx_b.assert_editor_state(expected_tabbed);
2800
2801 if a_tabs {
2802 cx_a.update_editor(|editor, window, cx| {
2803 editor.undo(&editor::actions::Undo, window, cx);
2804 });
2805 } else {
2806 cx_b.update_editor(|editor, window, cx| {
2807 editor.undo(&editor::actions::Undo, window, cx);
2808 });
2809 }
2810 cx_a.run_until_parked();
2811 cx_b.run_until_parked();
2812 cx_a.assert_editor_state(expected_initial);
2813 cx_b.assert_editor_state(expected_initial);
2814}
2815
2816fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2817 let mut labels = Vec::new();
2818 for hint in editor.inlay_hint_cache().hints() {
2819 match hint.label {
2820 project::InlayHintLabel::String(s) => labels.push(s),
2821 _ => unreachable!(),
2822 }
2823 }
2824 labels
2825}
2826
2827fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2828 git::blame::BlameEntry {
2829 sha: sha.parse().unwrap(),
2830 range,
2831 ..Default::default()
2832 }
2833}