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