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 let inlay_cache = editor.inlay_hint_cache();
1641 assert_eq!(
1642 inlay_cache.version(),
1643 1,
1644 "Host editor update the cache version after every cache/view change",
1645 );
1646 });
1647 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1648 let editor_b = workspace_b
1649 .update(cx_b, |workspace, cx| {
1650 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1651 })
1652 .await
1653 .unwrap()
1654 .downcast::<Editor>()
1655 .unwrap();
1656
1657 executor.run_until_parked();
1658 editor_b.update(cx_b, |editor, _| {
1659 assert_eq!(
1660 vec![initial_edit.to_string()],
1661 extract_hint_labels(editor),
1662 "Client should get its first hints when opens an editor"
1663 );
1664 let inlay_cache = editor.inlay_hint_cache();
1665 assert_eq!(
1666 inlay_cache.version(),
1667 1,
1668 "Guest editor update the cache version after every cache/view change"
1669 );
1670 });
1671
1672 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1673 editor_b.update(cx_b, |editor, cx| {
1674 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1675 editor.handle_input(":", cx);
1676 });
1677 cx_b.focus_view(&editor_b);
1678
1679 executor.run_until_parked();
1680 editor_a.update(cx_a, |editor, _| {
1681 assert_eq!(
1682 vec![after_client_edit.to_string()],
1683 extract_hint_labels(editor),
1684 );
1685 let inlay_cache = editor.inlay_hint_cache();
1686 assert_eq!(inlay_cache.version(), 2);
1687 });
1688 editor_b.update(cx_b, |editor, _| {
1689 assert_eq!(
1690 vec![after_client_edit.to_string()],
1691 extract_hint_labels(editor),
1692 );
1693 let inlay_cache = editor.inlay_hint_cache();
1694 assert_eq!(inlay_cache.version(), 2);
1695 });
1696
1697 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1698 editor_a.update(cx_a, |editor, cx| {
1699 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1700 editor.handle_input("a change to increment both buffers' versions", cx);
1701 });
1702 cx_a.focus_view(&editor_a);
1703
1704 executor.run_until_parked();
1705 editor_a.update(cx_a, |editor, _| {
1706 assert_eq!(
1707 vec![after_host_edit.to_string()],
1708 extract_hint_labels(editor),
1709 );
1710 let inlay_cache = editor.inlay_hint_cache();
1711 assert_eq!(inlay_cache.version(), 3);
1712 });
1713 editor_b.update(cx_b, |editor, _| {
1714 assert_eq!(
1715 vec![after_host_edit.to_string()],
1716 extract_hint_labels(editor),
1717 );
1718 let inlay_cache = editor.inlay_hint_cache();
1719 assert_eq!(inlay_cache.version(), 3);
1720 });
1721
1722 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1723 fake_language_server
1724 .request::<lsp::request::InlayHintRefreshRequest>(())
1725 .await
1726 .expect("inlay refresh request failed");
1727
1728 executor.run_until_parked();
1729 editor_a.update(cx_a, |editor, _| {
1730 assert_eq!(
1731 vec![after_special_edit_for_refresh.to_string()],
1732 extract_hint_labels(editor),
1733 "Host should react to /refresh LSP request"
1734 );
1735 let inlay_cache = editor.inlay_hint_cache();
1736 assert_eq!(
1737 inlay_cache.version(),
1738 4,
1739 "Host should accepted all edits and bump its cache version every time"
1740 );
1741 });
1742 editor_b.update(cx_b, |editor, _| {
1743 assert_eq!(
1744 vec![after_special_edit_for_refresh.to_string()],
1745 extract_hint_labels(editor),
1746 "Guest should get a /refresh LSP request propagated by host"
1747 );
1748 let inlay_cache = editor.inlay_hint_cache();
1749 assert_eq!(
1750 inlay_cache.version(),
1751 4,
1752 "Guest should accepted all edits and bump its cache version every time"
1753 );
1754 });
1755}
1756
1757#[gpui::test(iterations = 10)]
1758async fn test_inlay_hint_refresh_is_forwarded(
1759 cx_a: &mut TestAppContext,
1760 cx_b: &mut TestAppContext,
1761) {
1762 let mut server = TestServer::start(cx_a.executor()).await;
1763 let executor = cx_a.executor();
1764 let client_a = server.create_client(cx_a, "user_a").await;
1765 let client_b = server.create_client(cx_b, "user_b").await;
1766 server
1767 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1768 .await;
1769 let active_call_a = cx_a.read(ActiveCall::global);
1770 let active_call_b = cx_b.read(ActiveCall::global);
1771
1772 cx_a.update(editor::init);
1773 cx_b.update(editor::init);
1774
1775 cx_a.update(|cx| {
1776 SettingsStore::update_global(cx, |store, cx| {
1777 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1778 settings.defaults.inlay_hints = Some(InlayHintSettings {
1779 enabled: false,
1780 edit_debounce_ms: 0,
1781 scroll_debounce_ms: 0,
1782 show_type_hints: false,
1783 show_parameter_hints: false,
1784 show_other_hints: false,
1785 show_background: false,
1786 })
1787 });
1788 });
1789 });
1790 cx_b.update(|cx| {
1791 SettingsStore::update_global(cx, |store, cx| {
1792 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1793 settings.defaults.inlay_hints = Some(InlayHintSettings {
1794 enabled: true,
1795 edit_debounce_ms: 0,
1796 scroll_debounce_ms: 0,
1797 show_type_hints: true,
1798 show_parameter_hints: true,
1799 show_other_hints: true,
1800 show_background: false,
1801 })
1802 });
1803 });
1804 });
1805
1806 client_a.language_registry().add(rust_lang());
1807 client_b.language_registry().add(rust_lang());
1808 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1809 "Rust",
1810 FakeLspAdapter {
1811 capabilities: lsp::ServerCapabilities {
1812 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1813 ..Default::default()
1814 },
1815 ..Default::default()
1816 },
1817 );
1818
1819 client_a
1820 .fs()
1821 .insert_tree(
1822 "/a",
1823 json!({
1824 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1825 "other.rs": "// Test file",
1826 }),
1827 )
1828 .await;
1829 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1830 active_call_a
1831 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1832 .await
1833 .unwrap();
1834 let project_id = active_call_a
1835 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1836 .await
1837 .unwrap();
1838
1839 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1840 active_call_b
1841 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1842 .await
1843 .unwrap();
1844
1845 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1846 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1847
1848 cx_a.background_executor.start_waiting();
1849
1850 let editor_a = workspace_a
1851 .update(cx_a, |workspace, cx| {
1852 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1853 })
1854 .await
1855 .unwrap()
1856 .downcast::<Editor>()
1857 .unwrap();
1858
1859 let editor_b = workspace_b
1860 .update(cx_b, |workspace, cx| {
1861 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1862 })
1863 .await
1864 .unwrap()
1865 .downcast::<Editor>()
1866 .unwrap();
1867
1868 let other_hints = Arc::new(AtomicBool::new(false));
1869 let fake_language_server = fake_language_servers.next().await.unwrap();
1870 let closure_other_hints = Arc::clone(&other_hints);
1871 fake_language_server
1872 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1873 let task_other_hints = Arc::clone(&closure_other_hints);
1874 async move {
1875 assert_eq!(
1876 params.text_document.uri,
1877 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1878 );
1879 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1880 let character = if other_hints { 0 } else { 2 };
1881 let label = if other_hints {
1882 "other hint"
1883 } else {
1884 "initial hint"
1885 };
1886 Ok(Some(vec![lsp::InlayHint {
1887 position: lsp::Position::new(0, character),
1888 label: lsp::InlayHintLabel::String(label.to_string()),
1889 kind: None,
1890 text_edits: None,
1891 tooltip: None,
1892 padding_left: None,
1893 padding_right: None,
1894 data: None,
1895 }]))
1896 }
1897 })
1898 .next()
1899 .await
1900 .unwrap();
1901 executor.finish_waiting();
1902
1903 executor.run_until_parked();
1904 editor_a.update(cx_a, |editor, _| {
1905 assert!(
1906 extract_hint_labels(editor).is_empty(),
1907 "Host should get no hints due to them turned off"
1908 );
1909 let inlay_cache = editor.inlay_hint_cache();
1910 assert_eq!(
1911 inlay_cache.version(),
1912 0,
1913 "Turned off hints should not generate version updates"
1914 );
1915 });
1916
1917 executor.run_until_parked();
1918 editor_b.update(cx_b, |editor, _| {
1919 assert_eq!(
1920 vec!["initial hint".to_string()],
1921 extract_hint_labels(editor),
1922 "Client should get its first hints when opens an editor"
1923 );
1924 let inlay_cache = editor.inlay_hint_cache();
1925 assert_eq!(
1926 inlay_cache.version(),
1927 1,
1928 "Should update cache version after first hints"
1929 );
1930 });
1931
1932 other_hints.fetch_or(true, atomic::Ordering::Release);
1933 fake_language_server
1934 .request::<lsp::request::InlayHintRefreshRequest>(())
1935 .await
1936 .expect("inlay refresh request failed");
1937 executor.run_until_parked();
1938 editor_a.update(cx_a, |editor, _| {
1939 assert!(
1940 extract_hint_labels(editor).is_empty(),
1941 "Host should get nop hints due to them turned off, even after the /refresh"
1942 );
1943 let inlay_cache = editor.inlay_hint_cache();
1944 assert_eq!(
1945 inlay_cache.version(),
1946 0,
1947 "Turned off hints should not generate version updates, again"
1948 );
1949 });
1950
1951 executor.run_until_parked();
1952 editor_b.update(cx_b, |editor, _| {
1953 assert_eq!(
1954 vec!["other hint".to_string()],
1955 extract_hint_labels(editor),
1956 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1957 );
1958 let inlay_cache = editor.inlay_hint_cache();
1959 assert_eq!(
1960 inlay_cache.version(),
1961 2,
1962 "Guest should accepted all edits and bump its cache version every time"
1963 );
1964 });
1965}
1966
1967#[gpui::test(iterations = 10)]
1968async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1969 let mut server = TestServer::start(cx_a.executor()).await;
1970 let client_a = server.create_client(cx_a, "user_a").await;
1971 let client_b = server.create_client(cx_b, "user_b").await;
1972 server
1973 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1974 .await;
1975 let active_call_a = cx_a.read(ActiveCall::global);
1976
1977 cx_a.update(editor::init);
1978 cx_b.update(editor::init);
1979 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1980 let inline_blame_off_settings = Some(InlineBlameSettings {
1981 enabled: false,
1982 delay_ms: None,
1983 min_column: None,
1984 show_commit_summary: false,
1985 });
1986 cx_a.update(|cx| {
1987 SettingsStore::update_global(cx, |store, cx| {
1988 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1989 settings.git.inline_blame = inline_blame_off_settings;
1990 });
1991 });
1992 });
1993 cx_b.update(|cx| {
1994 SettingsStore::update_global(cx, |store, cx| {
1995 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1996 settings.git.inline_blame = inline_blame_off_settings;
1997 });
1998 });
1999 });
2000
2001 client_a
2002 .fs()
2003 .insert_tree(
2004 "/my-repo",
2005 json!({
2006 ".git": {},
2007 "file.txt": "line1\nline2\nline3\nline\n",
2008 }),
2009 )
2010 .await;
2011
2012 let blame = git::blame::Blame {
2013 entries: vec![
2014 blame_entry("1b1b1b", 0..1),
2015 blame_entry("0d0d0d", 1..2),
2016 blame_entry("3a3a3a", 2..3),
2017 blame_entry("4c4c4c", 3..4),
2018 ],
2019 permalinks: HashMap::default(), // This field is deprecrated
2020 messages: [
2021 ("1b1b1b", "message for idx-0"),
2022 ("0d0d0d", "message for idx-1"),
2023 ("3a3a3a", "message for idx-2"),
2024 ("4c4c4c", "message for idx-3"),
2025 ]
2026 .into_iter()
2027 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2028 .collect(),
2029 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2030 };
2031 client_a.fs().set_blame_for_repo(
2032 Path::new("/my-repo/.git"),
2033 vec![(Path::new("file.txt"), blame)],
2034 );
2035
2036 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2037 let project_id = active_call_a
2038 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2039 .await
2040 .unwrap();
2041
2042 // Create editor_a
2043 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2044 let editor_a = workspace_a
2045 .update(cx_a, |workspace, cx| {
2046 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2047 })
2048 .await
2049 .unwrap()
2050 .downcast::<Editor>()
2051 .unwrap();
2052
2053 // Join the project as client B.
2054 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2055 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2056 let editor_b = workspace_b
2057 .update(cx_b, |workspace, cx| {
2058 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2059 })
2060 .await
2061 .unwrap()
2062 .downcast::<Editor>()
2063 .unwrap();
2064
2065 // client_b now requests git blame for the open buffer
2066 editor_b.update(cx_b, |editor_b, cx| {
2067 assert!(editor_b.blame().is_none());
2068 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2069 });
2070
2071 cx_a.executor().run_until_parked();
2072 cx_b.executor().run_until_parked();
2073
2074 editor_b.update(cx_b, |editor_b, cx| {
2075 let blame = editor_b.blame().expect("editor_b should have blame now");
2076 let entries = blame.update(cx, |blame, cx| {
2077 blame
2078 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2079 .collect::<Vec<_>>()
2080 });
2081
2082 assert_eq!(
2083 entries,
2084 vec![
2085 Some(blame_entry("1b1b1b", 0..1)),
2086 Some(blame_entry("0d0d0d", 1..2)),
2087 Some(blame_entry("3a3a3a", 2..3)),
2088 Some(blame_entry("4c4c4c", 3..4)),
2089 ]
2090 );
2091
2092 blame.update(cx, |blame, _| {
2093 for (idx, entry) in entries.iter().flatten().enumerate() {
2094 let details = blame.details_for_entry(entry).unwrap();
2095 assert_eq!(details.message, format!("message for idx-{}", idx));
2096 assert_eq!(
2097 details.permalink.unwrap().to_string(),
2098 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2099 );
2100 }
2101 });
2102 });
2103
2104 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2105 // which gets back to client_b.
2106 editor_b.update(cx_b, |editor_b, cx| {
2107 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2108 });
2109
2110 cx_a.executor().run_until_parked();
2111 cx_b.executor().run_until_parked();
2112
2113 editor_b.update(cx_b, |editor_b, cx| {
2114 let blame = editor_b.blame().expect("editor_b should have blame now");
2115 let entries = blame.update(cx, |blame, cx| {
2116 blame
2117 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2118 .collect::<Vec<_>>()
2119 });
2120
2121 assert_eq!(
2122 entries,
2123 vec![
2124 None,
2125 Some(blame_entry("0d0d0d", 1..2)),
2126 Some(blame_entry("3a3a3a", 2..3)),
2127 Some(blame_entry("4c4c4c", 3..4)),
2128 ]
2129 );
2130 });
2131
2132 // Now editor_a also updates the file
2133 editor_a.update(cx_a, |editor_a, cx| {
2134 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2135 });
2136
2137 cx_a.executor().run_until_parked();
2138 cx_b.executor().run_until_parked();
2139
2140 editor_b.update(cx_b, |editor_b, cx| {
2141 let blame = editor_b.blame().expect("editor_b should have blame now");
2142 let entries = blame.update(cx, |blame, cx| {
2143 blame
2144 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
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 =
2212 cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
2213 let other_editor_a =
2214 cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
2215 let mut main_editor_cx_a = EditorTestContext {
2216 cx: cx_a.clone(),
2217 window: cx_a.handle(),
2218 editor: main_editor_a,
2219 assertion_cx: AssertionContextManager::new(),
2220 };
2221 let mut other_editor_cx_a = EditorTestContext {
2222 cx: cx_a.clone(),
2223 window: cx_a.handle(),
2224 editor: other_editor_a,
2225 assertion_cx: AssertionContextManager::new(),
2226 };
2227
2228 // Join the project as client B.
2229 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2230 let main_buffer_b = project_b
2231 .update(cx_b, |p, cx| {
2232 p.open_buffer((worktree_id, "src/main.rs"), cx)
2233 })
2234 .await
2235 .unwrap();
2236 let other_buffer_b = project_b
2237 .update(cx_b, |p, cx| {
2238 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2239 })
2240 .await
2241 .unwrap();
2242 let cx_b = cx_b.add_empty_window();
2243 let main_editor_b =
2244 cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
2245 let other_editor_b =
2246 cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
2247 let mut main_editor_cx_b = EditorTestContext {
2248 cx: cx_b.clone(),
2249 window: cx_b.handle(),
2250 editor: main_editor_b,
2251 assertion_cx: AssertionContextManager::new(),
2252 };
2253 let mut other_editor_cx_b = EditorTestContext {
2254 cx: cx_b.clone(),
2255 window: cx_b.handle(),
2256 editor: other_editor_b,
2257 assertion_cx: AssertionContextManager::new(),
2258 };
2259
2260 let initial_main = indoc! {"
2261ˇmod other;
2262fn main() { let foo = other::foo(); }"};
2263 let initial_other = indoc! {"
2264ˇpub fn foo() -> usize {
2265 4
2266}"};
2267
2268 let first_tabbed_main = indoc! {"
2269 ˇmod other;
2270fn main() { let foo = other::foo(); }"};
2271 tab_undo_assert(
2272 &mut main_editor_cx_a,
2273 &mut main_editor_cx_b,
2274 initial_main,
2275 first_tabbed_main,
2276 true,
2277 );
2278 tab_undo_assert(
2279 &mut main_editor_cx_a,
2280 &mut main_editor_cx_b,
2281 initial_main,
2282 first_tabbed_main,
2283 false,
2284 );
2285
2286 let first_tabbed_other = indoc! {"
2287 ˇpub fn foo() -> usize {
2288 4
2289}"};
2290 tab_undo_assert(
2291 &mut other_editor_cx_a,
2292 &mut other_editor_cx_b,
2293 initial_other,
2294 first_tabbed_other,
2295 true,
2296 );
2297 tab_undo_assert(
2298 &mut other_editor_cx_a,
2299 &mut other_editor_cx_b,
2300 initial_other,
2301 first_tabbed_other,
2302 false,
2303 );
2304
2305 client_a
2306 .fs()
2307 .atomic_write(
2308 PathBuf::from("/a/src/.editorconfig"),
2309 "[*]\ntab_width = 3\n".to_owned(),
2310 )
2311 .await
2312 .unwrap();
2313 cx_a.run_until_parked();
2314 cx_b.run_until_parked();
2315
2316 let second_tabbed_main = indoc! {"
2317 ˇmod other;
2318fn main() { let foo = other::foo(); }"};
2319 tab_undo_assert(
2320 &mut main_editor_cx_a,
2321 &mut main_editor_cx_b,
2322 initial_main,
2323 second_tabbed_main,
2324 true,
2325 );
2326 tab_undo_assert(
2327 &mut main_editor_cx_a,
2328 &mut main_editor_cx_b,
2329 initial_main,
2330 second_tabbed_main,
2331 false,
2332 );
2333
2334 let second_tabbed_other = indoc! {"
2335 ˇpub fn foo() -> usize {
2336 4
2337}"};
2338 tab_undo_assert(
2339 &mut other_editor_cx_a,
2340 &mut other_editor_cx_b,
2341 initial_other,
2342 second_tabbed_other,
2343 true,
2344 );
2345 tab_undo_assert(
2346 &mut other_editor_cx_a,
2347 &mut other_editor_cx_b,
2348 initial_other,
2349 second_tabbed_other,
2350 false,
2351 );
2352
2353 let editorconfig_buffer_b = project_b
2354 .update(cx_b, |p, cx| {
2355 p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2356 })
2357 .await
2358 .unwrap();
2359 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2360 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2361 });
2362 project_b
2363 .update(cx_b, |project, cx| {
2364 project.save_buffer(editorconfig_buffer_b.clone(), cx)
2365 })
2366 .await
2367 .unwrap();
2368 cx_a.run_until_parked();
2369 cx_b.run_until_parked();
2370
2371 tab_undo_assert(
2372 &mut main_editor_cx_a,
2373 &mut main_editor_cx_b,
2374 initial_main,
2375 second_tabbed_main,
2376 true,
2377 );
2378 tab_undo_assert(
2379 &mut main_editor_cx_a,
2380 &mut main_editor_cx_b,
2381 initial_main,
2382 second_tabbed_main,
2383 false,
2384 );
2385
2386 let third_tabbed_other = indoc! {"
2387 ˇpub fn foo() -> usize {
2388 4
2389}"};
2390 tab_undo_assert(
2391 &mut other_editor_cx_a,
2392 &mut other_editor_cx_b,
2393 initial_other,
2394 third_tabbed_other,
2395 true,
2396 );
2397
2398 tab_undo_assert(
2399 &mut other_editor_cx_a,
2400 &mut other_editor_cx_b,
2401 initial_other,
2402 third_tabbed_other,
2403 false,
2404 );
2405}
2406
2407#[track_caller]
2408fn tab_undo_assert(
2409 cx_a: &mut EditorTestContext,
2410 cx_b: &mut EditorTestContext,
2411 expected_initial: &str,
2412 expected_tabbed: &str,
2413 a_tabs: bool,
2414) {
2415 cx_a.assert_editor_state(expected_initial);
2416 cx_b.assert_editor_state(expected_initial);
2417
2418 if a_tabs {
2419 cx_a.update_editor(|editor, cx| {
2420 editor.tab(&editor::actions::Tab, cx);
2421 });
2422 } else {
2423 cx_b.update_editor(|editor, cx| {
2424 editor.tab(&editor::actions::Tab, cx);
2425 });
2426 }
2427
2428 cx_a.run_until_parked();
2429 cx_b.run_until_parked();
2430
2431 cx_a.assert_editor_state(expected_tabbed);
2432 cx_b.assert_editor_state(expected_tabbed);
2433
2434 if a_tabs {
2435 cx_a.update_editor(|editor, cx| {
2436 editor.undo(&editor::actions::Undo, cx);
2437 });
2438 } else {
2439 cx_b.update_editor(|editor, cx| {
2440 editor.undo(&editor::actions::Undo, cx);
2441 });
2442 }
2443 cx_a.run_until_parked();
2444 cx_b.run_until_parked();
2445 cx_a.assert_editor_state(expected_initial);
2446 cx_b.assert_editor_state(expected_initial);
2447}
2448
2449fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2450 let mut labels = Vec::new();
2451 for hint in editor.inlay_hint_cache().hints() {
2452 match hint.label {
2453 project::InlayHintLabel::String(s) => labels.push(s),
2454 _ => unreachable!(),
2455 }
2456 }
2457 labels
2458}
2459
2460fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2461 git::blame::BlameEntry {
2462 sha: sha.parse().unwrap(),
2463 range,
2464 ..Default::default()
2465 }
2466}