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