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, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
998
999 let _buffer_a = project_a
1000 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1001 .await
1002 .unwrap();
1003
1004 let fake_language_server = fake_language_servers.next().await.unwrap();
1005 fake_language_server.start_progress("the-token").await;
1006
1007 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1008 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1009 token: lsp::NumberOrString::String("the-token".to_string()),
1010 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1011 lsp::WorkDoneProgressReport {
1012 message: Some("the-message".to_string()),
1013 ..Default::default()
1014 },
1015 )),
1016 });
1017 executor.run_until_parked();
1018
1019 project_a.read_with(cx_a, |project, cx| {
1020 let status = project.language_server_statuses(cx).next().unwrap().1;
1021 assert_eq!(status.name, "the-language-server");
1022 assert_eq!(status.pending_work.len(), 1);
1023 assert_eq!(
1024 status.pending_work["the-token"].message.as_ref().unwrap(),
1025 "the-message"
1026 );
1027 });
1028
1029 let project_id = active_call_a
1030 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1031 .await
1032 .unwrap();
1033 executor.run_until_parked();
1034 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1035
1036 project_b.read_with(cx_b, |project, cx| {
1037 let status = project.language_server_statuses(cx).next().unwrap().1;
1038 assert_eq!(status.name, "the-language-server");
1039 });
1040
1041 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1042 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1043 token: lsp::NumberOrString::String("the-token".to_string()),
1044 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1045 lsp::WorkDoneProgressReport {
1046 message: Some("the-message-2".to_string()),
1047 ..Default::default()
1048 },
1049 )),
1050 });
1051 executor.run_until_parked();
1052
1053 project_a.read_with(cx_a, |project, cx| {
1054 let status = project.language_server_statuses(cx).next().unwrap().1;
1055 assert_eq!(status.name, "the-language-server");
1056 assert_eq!(status.pending_work.len(), 1);
1057 assert_eq!(
1058 status.pending_work["the-token"].message.as_ref().unwrap(),
1059 "the-message-2"
1060 );
1061 });
1062
1063 project_b.read_with(cx_b, |project, cx| {
1064 let status = project.language_server_statuses(cx).next().unwrap().1;
1065 assert_eq!(status.name, "the-language-server");
1066 assert_eq!(status.pending_work.len(), 1);
1067 assert_eq!(
1068 status.pending_work["the-token"].message.as_ref().unwrap(),
1069 "the-message-2"
1070 );
1071 });
1072}
1073
1074#[gpui::test(iterations = 10)]
1075async fn test_share_project(
1076 cx_a: &mut TestAppContext,
1077 cx_b: &mut TestAppContext,
1078 cx_c: &mut TestAppContext,
1079) {
1080 let executor = cx_a.executor();
1081 let cx_b = cx_b.add_empty_window();
1082 let mut server = TestServer::start(executor.clone()).await;
1083 let client_a = server.create_client(cx_a, "user_a").await;
1084 let client_b = server.create_client(cx_b, "user_b").await;
1085 let client_c = server.create_client(cx_c, "user_c").await;
1086 server
1087 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1088 .await;
1089 let active_call_a = cx_a.read(ActiveCall::global);
1090 let active_call_b = cx_b.read(ActiveCall::global);
1091 let active_call_c = cx_c.read(ActiveCall::global);
1092
1093 client_a
1094 .fs()
1095 .insert_tree(
1096 "/a",
1097 json!({
1098 ".gitignore": "ignored-dir",
1099 "a.txt": "a-contents",
1100 "b.txt": "b-contents",
1101 "ignored-dir": {
1102 "c.txt": "",
1103 "d.txt": "",
1104 }
1105 }),
1106 )
1107 .await;
1108
1109 // Invite client B to collaborate on a project
1110 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1111 active_call_a
1112 .update(cx_a, |call, cx| {
1113 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1114 })
1115 .await
1116 .unwrap();
1117
1118 // Join that project as client B
1119
1120 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1121 executor.run_until_parked();
1122 let call = incoming_call_b.borrow().clone().unwrap();
1123 assert_eq!(call.calling_user.github_login, "user_a");
1124 let initial_project = call.initial_project.unwrap();
1125 active_call_b
1126 .update(cx_b, |call, cx| call.accept_incoming(cx))
1127 .await
1128 .unwrap();
1129 let client_b_peer_id = client_b.peer_id().unwrap();
1130 let project_b = client_b.join_remote_project(initial_project.id, cx_b).await;
1131
1132 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1133
1134 executor.run_until_parked();
1135
1136 project_a.read_with(cx_a, |project, _| {
1137 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1138 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1139 });
1140
1141 project_b.read_with(cx_b, |project, cx| {
1142 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1143 assert_eq!(
1144 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1145 [
1146 Path::new(".gitignore"),
1147 Path::new("a.txt"),
1148 Path::new("b.txt"),
1149 Path::new("ignored-dir"),
1150 ]
1151 );
1152 });
1153
1154 project_b
1155 .update(cx_b, |project, cx| {
1156 let worktree = project.worktrees(cx).next().unwrap();
1157 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1158 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1159 })
1160 .await
1161 .unwrap();
1162
1163 project_b.read_with(cx_b, |project, cx| {
1164 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1165 assert_eq!(
1166 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1167 [
1168 Path::new(".gitignore"),
1169 Path::new("a.txt"),
1170 Path::new("b.txt"),
1171 Path::new("ignored-dir"),
1172 Path::new("ignored-dir/c.txt"),
1173 Path::new("ignored-dir/d.txt"),
1174 ]
1175 );
1176 });
1177
1178 // Open the same file as client B and client A.
1179 let buffer_b = project_b
1180 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1181 .await
1182 .unwrap();
1183
1184 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1185
1186 project_a.read_with(cx_a, |project, cx| {
1187 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1188 });
1189 let buffer_a = project_a
1190 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1191 .await
1192 .unwrap();
1193
1194 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1195
1196 // Client A sees client B's selection
1197 executor.run_until_parked();
1198
1199 buffer_a.read_with(cx_a, |buffer, _| {
1200 buffer
1201 .snapshot()
1202 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1203 .count()
1204 == 1
1205 });
1206
1207 // Edit the buffer as client B and see that edit as client A.
1208 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1209 executor.run_until_parked();
1210
1211 buffer_a.read_with(cx_a, |buffer, _| {
1212 assert_eq!(buffer.text(), "ok, b-contents")
1213 });
1214
1215 // Client B can invite client C on a project shared by client A.
1216 active_call_b
1217 .update(cx_b, |call, cx| {
1218 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1219 })
1220 .await
1221 .unwrap();
1222
1223 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1224 executor.run_until_parked();
1225 let call = incoming_call_c.borrow().clone().unwrap();
1226 assert_eq!(call.calling_user.github_login, "user_b");
1227 let initial_project = call.initial_project.unwrap();
1228 active_call_c
1229 .update(cx_c, |call, cx| call.accept_incoming(cx))
1230 .await
1231 .unwrap();
1232 let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1233
1234 // Client B closes the editor, and client A sees client B's selections removed.
1235 cx_b.update(move |_| drop(editor_b));
1236 executor.run_until_parked();
1237
1238 buffer_a.read_with(cx_a, |buffer, _| {
1239 buffer
1240 .snapshot()
1241 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1242 .count()
1243 == 0
1244 });
1245}
1246
1247#[gpui::test(iterations = 10)]
1248async fn test_on_input_format_from_host_to_guest(
1249 cx_a: &mut TestAppContext,
1250 cx_b: &mut TestAppContext,
1251) {
1252 let mut server = TestServer::start(cx_a.executor()).await;
1253 let executor = cx_a.executor();
1254 let client_a = server.create_client(cx_a, "user_a").await;
1255 let client_b = server.create_client(cx_b, "user_b").await;
1256 server
1257 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1258 .await;
1259 let active_call_a = cx_a.read(ActiveCall::global);
1260
1261 client_a.language_registry().add(rust_lang());
1262 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1263 "Rust",
1264 FakeLspAdapter {
1265 capabilities: lsp::ServerCapabilities {
1266 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1267 first_trigger_character: ":".to_string(),
1268 more_trigger_character: Some(vec![">".to_string()]),
1269 }),
1270 ..Default::default()
1271 },
1272 ..Default::default()
1273 },
1274 );
1275
1276 client_a
1277 .fs()
1278 .insert_tree(
1279 "/a",
1280 json!({
1281 "main.rs": "fn main() { a }",
1282 "other.rs": "// Test file",
1283 }),
1284 )
1285 .await;
1286 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1287 let project_id = active_call_a
1288 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1289 .await
1290 .unwrap();
1291 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1292
1293 // Open a file in an editor as the host.
1294 let buffer_a = project_a
1295 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1296 .await
1297 .unwrap();
1298 let cx_a = cx_a.add_empty_window();
1299 let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1300
1301 let fake_language_server = fake_language_servers.next().await.unwrap();
1302 executor.run_until_parked();
1303
1304 // Receive an OnTypeFormatting request as the host's language server.
1305 // Return some formatting from the host's language server.
1306 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1307 |params, _| async move {
1308 assert_eq!(
1309 params.text_document_position.text_document.uri,
1310 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1311 );
1312 assert_eq!(
1313 params.text_document_position.position,
1314 lsp::Position::new(0, 14),
1315 );
1316
1317 Ok(Some(vec![lsp::TextEdit {
1318 new_text: "~<".to_string(),
1319 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1320 }]))
1321 },
1322 );
1323
1324 // Open the buffer on the guest and see that the formatting worked
1325 let buffer_b = project_b
1326 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1327 .await
1328 .unwrap();
1329
1330 // Type a on type formatting trigger character as the guest.
1331 cx_a.focus_view(&editor_a);
1332 editor_a.update(cx_a, |editor, cx| {
1333 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1334 editor.handle_input(">", cx);
1335 });
1336
1337 executor.run_until_parked();
1338
1339 buffer_b.read_with(cx_b, |buffer, _| {
1340 assert_eq!(buffer.text(), "fn main() { a>~< }")
1341 });
1342
1343 // Undo should remove LSP edits first
1344 editor_a.update(cx_a, |editor, cx| {
1345 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1346 editor.undo(&Undo, cx);
1347 assert_eq!(editor.text(cx), "fn main() { a> }");
1348 });
1349 executor.run_until_parked();
1350
1351 buffer_b.read_with(cx_b, |buffer, _| {
1352 assert_eq!(buffer.text(), "fn main() { a> }")
1353 });
1354
1355 editor_a.update(cx_a, |editor, cx| {
1356 assert_eq!(editor.text(cx), "fn main() { a> }");
1357 editor.undo(&Undo, cx);
1358 assert_eq!(editor.text(cx), "fn main() { a }");
1359 });
1360 executor.run_until_parked();
1361
1362 buffer_b.read_with(cx_b, |buffer, _| {
1363 assert_eq!(buffer.text(), "fn main() { a }")
1364 });
1365}
1366
1367#[gpui::test(iterations = 10)]
1368async fn test_on_input_format_from_guest_to_host(
1369 cx_a: &mut TestAppContext,
1370 cx_b: &mut TestAppContext,
1371) {
1372 let mut server = TestServer::start(cx_a.executor()).await;
1373 let executor = cx_a.executor();
1374 let client_a = server.create_client(cx_a, "user_a").await;
1375 let client_b = server.create_client(cx_b, "user_b").await;
1376 server
1377 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1378 .await;
1379 let active_call_a = cx_a.read(ActiveCall::global);
1380
1381 client_a.language_registry().add(rust_lang());
1382 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1383 "Rust",
1384 FakeLspAdapter {
1385 capabilities: lsp::ServerCapabilities {
1386 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1387 first_trigger_character: ":".to_string(),
1388 more_trigger_character: Some(vec![">".to_string()]),
1389 }),
1390 ..Default::default()
1391 },
1392 ..Default::default()
1393 },
1394 );
1395
1396 client_a
1397 .fs()
1398 .insert_tree(
1399 "/a",
1400 json!({
1401 "main.rs": "fn main() { a }",
1402 "other.rs": "// Test file",
1403 }),
1404 )
1405 .await;
1406 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1407 let project_id = active_call_a
1408 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1409 .await
1410 .unwrap();
1411 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1412
1413 // Open a file in an editor as the guest.
1414 let buffer_b = project_b
1415 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1416 .await
1417 .unwrap();
1418 let cx_b = cx_b.add_empty_window();
1419 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1420
1421 let fake_language_server = fake_language_servers.next().await.unwrap();
1422 executor.run_until_parked();
1423
1424 // Type a on type formatting trigger character as the guest.
1425 cx_b.focus_view(&editor_b);
1426 editor_b.update(cx_b, |editor, cx| {
1427 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1428 editor.handle_input(":", cx);
1429 });
1430
1431 // Receive an OnTypeFormatting request as the host's language server.
1432 // Return some formatting from the host's language server.
1433 executor.start_waiting();
1434 fake_language_server
1435 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1436 assert_eq!(
1437 params.text_document_position.text_document.uri,
1438 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1439 );
1440 assert_eq!(
1441 params.text_document_position.position,
1442 lsp::Position::new(0, 14),
1443 );
1444
1445 Ok(Some(vec![lsp::TextEdit {
1446 new_text: "~:".to_string(),
1447 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1448 }]))
1449 })
1450 .next()
1451 .await
1452 .unwrap();
1453 executor.finish_waiting();
1454
1455 // Open the buffer on the host and see that the formatting worked
1456 let buffer_a = project_a
1457 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1458 .await
1459 .unwrap();
1460 executor.run_until_parked();
1461
1462 buffer_a.read_with(cx_a, |buffer, _| {
1463 assert_eq!(buffer.text(), "fn main() { a:~: }")
1464 });
1465
1466 // Undo should remove LSP edits first
1467 editor_b.update(cx_b, |editor, cx| {
1468 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1469 editor.undo(&Undo, cx);
1470 assert_eq!(editor.text(cx), "fn main() { a: }");
1471 });
1472 executor.run_until_parked();
1473
1474 buffer_a.read_with(cx_a, |buffer, _| {
1475 assert_eq!(buffer.text(), "fn main() { a: }")
1476 });
1477
1478 editor_b.update(cx_b, |editor, cx| {
1479 assert_eq!(editor.text(cx), "fn main() { a: }");
1480 editor.undo(&Undo, cx);
1481 assert_eq!(editor.text(cx), "fn main() { a }");
1482 });
1483 executor.run_until_parked();
1484
1485 buffer_a.read_with(cx_a, |buffer, _| {
1486 assert_eq!(buffer.text(), "fn main() { a }")
1487 });
1488}
1489
1490#[gpui::test(iterations = 10)]
1491async fn test_mutual_editor_inlay_hint_cache_update(
1492 cx_a: &mut TestAppContext,
1493 cx_b: &mut TestAppContext,
1494) {
1495 let mut server = TestServer::start(cx_a.executor()).await;
1496 let executor = cx_a.executor();
1497 let client_a = server.create_client(cx_a, "user_a").await;
1498 let client_b = server.create_client(cx_b, "user_b").await;
1499 server
1500 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1501 .await;
1502 let active_call_a = cx_a.read(ActiveCall::global);
1503 let active_call_b = cx_b.read(ActiveCall::global);
1504
1505 cx_a.update(editor::init);
1506 cx_b.update(editor::init);
1507
1508 cx_a.update(|cx| {
1509 SettingsStore::update_global(cx, |store, cx| {
1510 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1511 settings.defaults.inlay_hints = Some(InlayHintSettings {
1512 enabled: true,
1513 edit_debounce_ms: 0,
1514 scroll_debounce_ms: 0,
1515 show_type_hints: true,
1516 show_parameter_hints: false,
1517 show_other_hints: true,
1518 show_background: false,
1519 })
1520 });
1521 });
1522 });
1523 cx_b.update(|cx| {
1524 SettingsStore::update_global(cx, |store, cx| {
1525 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1526 settings.defaults.inlay_hints = Some(InlayHintSettings {
1527 enabled: true,
1528 edit_debounce_ms: 0,
1529 scroll_debounce_ms: 0,
1530 show_type_hints: true,
1531 show_parameter_hints: false,
1532 show_other_hints: true,
1533 show_background: false,
1534 })
1535 });
1536 });
1537 });
1538
1539 client_a.language_registry().add(rust_lang());
1540 client_b.language_registry().add(rust_lang());
1541 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1542 "Rust",
1543 FakeLspAdapter {
1544 capabilities: lsp::ServerCapabilities {
1545 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1546 ..Default::default()
1547 },
1548 ..Default::default()
1549 },
1550 );
1551
1552 // Client A opens a project.
1553 client_a
1554 .fs()
1555 .insert_tree(
1556 "/a",
1557 json!({
1558 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1559 "other.rs": "// Test file",
1560 }),
1561 )
1562 .await;
1563 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1564 active_call_a
1565 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1566 .await
1567 .unwrap();
1568 let project_id = active_call_a
1569 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1570 .await
1571 .unwrap();
1572
1573 // Client B joins the project
1574 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1575 active_call_b
1576 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1577 .await
1578 .unwrap();
1579
1580 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1581 executor.start_waiting();
1582
1583 // The host opens a rust file.
1584 let _buffer_a = project_a
1585 .update(cx_a, |project, cx| {
1586 project.open_local_buffer("/a/main.rs", cx)
1587 })
1588 .await
1589 .unwrap();
1590 let fake_language_server = fake_language_servers.next().await.unwrap();
1591 let editor_a = workspace_a
1592 .update(cx_a, |workspace, cx| {
1593 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1594 })
1595 .await
1596 .unwrap()
1597 .downcast::<Editor>()
1598 .unwrap();
1599
1600 // Set up the language server to return an additional inlay hint on each request.
1601 let edits_made = Arc::new(AtomicUsize::new(0));
1602 let closure_edits_made = Arc::clone(&edits_made);
1603 fake_language_server
1604 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1605 let task_edits_made = Arc::clone(&closure_edits_made);
1606 async move {
1607 assert_eq!(
1608 params.text_document.uri,
1609 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1610 );
1611 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1612 Ok(Some(vec![lsp::InlayHint {
1613 position: lsp::Position::new(0, edits_made as u32),
1614 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1615 kind: None,
1616 text_edits: None,
1617 tooltip: None,
1618 padding_left: None,
1619 padding_right: None,
1620 data: None,
1621 }]))
1622 }
1623 })
1624 .next()
1625 .await
1626 .unwrap();
1627
1628 executor.run_until_parked();
1629
1630 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1631 editor_a.update(cx_a, |editor, _| {
1632 assert_eq!(
1633 vec![initial_edit.to_string()],
1634 extract_hint_labels(editor),
1635 "Host should get its first hints when opens an editor"
1636 );
1637 let inlay_cache = editor.inlay_hint_cache();
1638 assert_eq!(
1639 inlay_cache.version(),
1640 1,
1641 "Host editor update the cache version after every cache/view change",
1642 );
1643 });
1644 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1645 let editor_b = workspace_b
1646 .update(cx_b, |workspace, cx| {
1647 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1648 })
1649 .await
1650 .unwrap()
1651 .downcast::<Editor>()
1652 .unwrap();
1653
1654 executor.run_until_parked();
1655 editor_b.update(cx_b, |editor, _| {
1656 assert_eq!(
1657 vec![initial_edit.to_string()],
1658 extract_hint_labels(editor),
1659 "Client should get its first hints when opens an editor"
1660 );
1661 let inlay_cache = editor.inlay_hint_cache();
1662 assert_eq!(
1663 inlay_cache.version(),
1664 1,
1665 "Guest editor update the cache version after every cache/view change"
1666 );
1667 });
1668
1669 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1670 editor_b.update(cx_b, |editor, cx| {
1671 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1672 editor.handle_input(":", cx);
1673 });
1674 cx_b.focus_view(&editor_b);
1675
1676 executor.run_until_parked();
1677 editor_a.update(cx_a, |editor, _| {
1678 assert_eq!(
1679 vec![after_client_edit.to_string()],
1680 extract_hint_labels(editor),
1681 );
1682 let inlay_cache = editor.inlay_hint_cache();
1683 assert_eq!(inlay_cache.version(), 2);
1684 });
1685 editor_b.update(cx_b, |editor, _| {
1686 assert_eq!(
1687 vec![after_client_edit.to_string()],
1688 extract_hint_labels(editor),
1689 );
1690 let inlay_cache = editor.inlay_hint_cache();
1691 assert_eq!(inlay_cache.version(), 2);
1692 });
1693
1694 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1695 editor_a.update(cx_a, |editor, cx| {
1696 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1697 editor.handle_input("a change to increment both buffers' versions", cx);
1698 });
1699 cx_a.focus_view(&editor_a);
1700
1701 executor.run_until_parked();
1702 editor_a.update(cx_a, |editor, _| {
1703 assert_eq!(
1704 vec![after_host_edit.to_string()],
1705 extract_hint_labels(editor),
1706 );
1707 let inlay_cache = editor.inlay_hint_cache();
1708 assert_eq!(inlay_cache.version(), 3);
1709 });
1710 editor_b.update(cx_b, |editor, _| {
1711 assert_eq!(
1712 vec![after_host_edit.to_string()],
1713 extract_hint_labels(editor),
1714 );
1715 let inlay_cache = editor.inlay_hint_cache();
1716 assert_eq!(inlay_cache.version(), 3);
1717 });
1718
1719 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1720 fake_language_server
1721 .request::<lsp::request::InlayHintRefreshRequest>(())
1722 .await
1723 .expect("inlay refresh request failed");
1724
1725 executor.run_until_parked();
1726 editor_a.update(cx_a, |editor, _| {
1727 assert_eq!(
1728 vec![after_special_edit_for_refresh.to_string()],
1729 extract_hint_labels(editor),
1730 "Host should react to /refresh LSP request"
1731 );
1732 let inlay_cache = editor.inlay_hint_cache();
1733 assert_eq!(
1734 inlay_cache.version(),
1735 4,
1736 "Host should accepted all edits and bump its cache version every time"
1737 );
1738 });
1739 editor_b.update(cx_b, |editor, _| {
1740 assert_eq!(
1741 vec![after_special_edit_for_refresh.to_string()],
1742 extract_hint_labels(editor),
1743 "Guest should get a /refresh LSP request propagated by host"
1744 );
1745 let inlay_cache = editor.inlay_hint_cache();
1746 assert_eq!(
1747 inlay_cache.version(),
1748 4,
1749 "Guest should accepted all edits and bump its cache version every time"
1750 );
1751 });
1752}
1753
1754#[gpui::test(iterations = 10)]
1755async fn test_inlay_hint_refresh_is_forwarded(
1756 cx_a: &mut TestAppContext,
1757 cx_b: &mut TestAppContext,
1758) {
1759 let mut server = TestServer::start(cx_a.executor()).await;
1760 let executor = cx_a.executor();
1761 let client_a = server.create_client(cx_a, "user_a").await;
1762 let client_b = server.create_client(cx_b, "user_b").await;
1763 server
1764 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1765 .await;
1766 let active_call_a = cx_a.read(ActiveCall::global);
1767 let active_call_b = cx_b.read(ActiveCall::global);
1768
1769 cx_a.update(editor::init);
1770 cx_b.update(editor::init);
1771
1772 cx_a.update(|cx| {
1773 SettingsStore::update_global(cx, |store, cx| {
1774 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1775 settings.defaults.inlay_hints = Some(InlayHintSettings {
1776 enabled: false,
1777 edit_debounce_ms: 0,
1778 scroll_debounce_ms: 0,
1779 show_type_hints: false,
1780 show_parameter_hints: false,
1781 show_other_hints: false,
1782 show_background: false,
1783 })
1784 });
1785 });
1786 });
1787 cx_b.update(|cx| {
1788 SettingsStore::update_global(cx, |store, cx| {
1789 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1790 settings.defaults.inlay_hints = Some(InlayHintSettings {
1791 enabled: true,
1792 edit_debounce_ms: 0,
1793 scroll_debounce_ms: 0,
1794 show_type_hints: true,
1795 show_parameter_hints: true,
1796 show_other_hints: true,
1797 show_background: false,
1798 })
1799 });
1800 });
1801 });
1802
1803 client_a.language_registry().add(rust_lang());
1804 client_b.language_registry().add(rust_lang());
1805 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1806 "Rust",
1807 FakeLspAdapter {
1808 capabilities: lsp::ServerCapabilities {
1809 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1810 ..Default::default()
1811 },
1812 ..Default::default()
1813 },
1814 );
1815
1816 client_a
1817 .fs()
1818 .insert_tree(
1819 "/a",
1820 json!({
1821 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1822 "other.rs": "// Test file",
1823 }),
1824 )
1825 .await;
1826 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1827 active_call_a
1828 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1829 .await
1830 .unwrap();
1831 let project_id = active_call_a
1832 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1833 .await
1834 .unwrap();
1835
1836 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1837 active_call_b
1838 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1839 .await
1840 .unwrap();
1841
1842 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1843 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1844
1845 cx_a.background_executor.start_waiting();
1846
1847 let editor_a = workspace_a
1848 .update(cx_a, |workspace, cx| {
1849 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1850 })
1851 .await
1852 .unwrap()
1853 .downcast::<Editor>()
1854 .unwrap();
1855
1856 let editor_b = workspace_b
1857 .update(cx_b, |workspace, cx| {
1858 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1859 })
1860 .await
1861 .unwrap()
1862 .downcast::<Editor>()
1863 .unwrap();
1864
1865 let other_hints = Arc::new(AtomicBool::new(false));
1866 let fake_language_server = fake_language_servers.next().await.unwrap();
1867 let closure_other_hints = Arc::clone(&other_hints);
1868 fake_language_server
1869 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1870 let task_other_hints = Arc::clone(&closure_other_hints);
1871 async move {
1872 assert_eq!(
1873 params.text_document.uri,
1874 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1875 );
1876 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1877 let character = if other_hints { 0 } else { 2 };
1878 let label = if other_hints {
1879 "other hint"
1880 } else {
1881 "initial hint"
1882 };
1883 Ok(Some(vec![lsp::InlayHint {
1884 position: lsp::Position::new(0, character),
1885 label: lsp::InlayHintLabel::String(label.to_string()),
1886 kind: None,
1887 text_edits: None,
1888 tooltip: None,
1889 padding_left: None,
1890 padding_right: None,
1891 data: None,
1892 }]))
1893 }
1894 })
1895 .next()
1896 .await
1897 .unwrap();
1898 executor.finish_waiting();
1899
1900 executor.run_until_parked();
1901 editor_a.update(cx_a, |editor, _| {
1902 assert!(
1903 extract_hint_labels(editor).is_empty(),
1904 "Host should get no hints due to them turned off"
1905 );
1906 let inlay_cache = editor.inlay_hint_cache();
1907 assert_eq!(
1908 inlay_cache.version(),
1909 0,
1910 "Turned off hints should not generate version updates"
1911 );
1912 });
1913
1914 executor.run_until_parked();
1915 editor_b.update(cx_b, |editor, _| {
1916 assert_eq!(
1917 vec!["initial hint".to_string()],
1918 extract_hint_labels(editor),
1919 "Client should get its first hints when opens an editor"
1920 );
1921 let inlay_cache = editor.inlay_hint_cache();
1922 assert_eq!(
1923 inlay_cache.version(),
1924 1,
1925 "Should update cache version after first hints"
1926 );
1927 });
1928
1929 other_hints.fetch_or(true, atomic::Ordering::Release);
1930 fake_language_server
1931 .request::<lsp::request::InlayHintRefreshRequest>(())
1932 .await
1933 .expect("inlay refresh request failed");
1934 executor.run_until_parked();
1935 editor_a.update(cx_a, |editor, _| {
1936 assert!(
1937 extract_hint_labels(editor).is_empty(),
1938 "Host should get nop hints due to them turned off, even after the /refresh"
1939 );
1940 let inlay_cache = editor.inlay_hint_cache();
1941 assert_eq!(
1942 inlay_cache.version(),
1943 0,
1944 "Turned off hints should not generate version updates, again"
1945 );
1946 });
1947
1948 executor.run_until_parked();
1949 editor_b.update(cx_b, |editor, _| {
1950 assert_eq!(
1951 vec!["other hint".to_string()],
1952 extract_hint_labels(editor),
1953 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1954 );
1955 let inlay_cache = editor.inlay_hint_cache();
1956 assert_eq!(
1957 inlay_cache.version(),
1958 2,
1959 "Guest should accepted all edits and bump its cache version every time"
1960 );
1961 });
1962}
1963
1964#[gpui::test(iterations = 10)]
1965async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1966 let mut server = TestServer::start(cx_a.executor()).await;
1967 let client_a = server.create_client(cx_a, "user_a").await;
1968 let client_b = server.create_client(cx_b, "user_b").await;
1969 server
1970 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1971 .await;
1972 let active_call_a = cx_a.read(ActiveCall::global);
1973
1974 cx_a.update(editor::init);
1975 cx_b.update(editor::init);
1976 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1977 let inline_blame_off_settings = Some(InlineBlameSettings {
1978 enabled: false,
1979 delay_ms: None,
1980 min_column: None,
1981 show_commit_summary: false,
1982 });
1983 cx_a.update(|cx| {
1984 SettingsStore::update_global(cx, |store, cx| {
1985 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1986 settings.git.inline_blame = inline_blame_off_settings;
1987 });
1988 });
1989 });
1990 cx_b.update(|cx| {
1991 SettingsStore::update_global(cx, |store, cx| {
1992 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1993 settings.git.inline_blame = inline_blame_off_settings;
1994 });
1995 });
1996 });
1997
1998 client_a
1999 .fs()
2000 .insert_tree(
2001 "/my-repo",
2002 json!({
2003 ".git": {},
2004 "file.txt": "line1\nline2\nline3\nline\n",
2005 }),
2006 )
2007 .await;
2008
2009 let blame = git::blame::Blame {
2010 entries: vec![
2011 blame_entry("1b1b1b", 0..1),
2012 blame_entry("0d0d0d", 1..2),
2013 blame_entry("3a3a3a", 2..3),
2014 blame_entry("4c4c4c", 3..4),
2015 ],
2016 permalinks: HashMap::default(), // This field is deprecrated
2017 messages: [
2018 ("1b1b1b", "message for idx-0"),
2019 ("0d0d0d", "message for idx-1"),
2020 ("3a3a3a", "message for idx-2"),
2021 ("4c4c4c", "message for idx-3"),
2022 ]
2023 .into_iter()
2024 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2025 .collect(),
2026 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2027 };
2028 client_a.fs().set_blame_for_repo(
2029 Path::new("/my-repo/.git"),
2030 vec![(Path::new("file.txt"), blame)],
2031 );
2032
2033 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2034 let project_id = active_call_a
2035 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2036 .await
2037 .unwrap();
2038
2039 // Create editor_a
2040 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2041 let editor_a = workspace_a
2042 .update(cx_a, |workspace, cx| {
2043 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2044 })
2045 .await
2046 .unwrap()
2047 .downcast::<Editor>()
2048 .unwrap();
2049
2050 // Join the project as client B.
2051 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2052 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2053 let editor_b = workspace_b
2054 .update(cx_b, |workspace, cx| {
2055 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2056 })
2057 .await
2058 .unwrap()
2059 .downcast::<Editor>()
2060 .unwrap();
2061
2062 // client_b now requests git blame for the open buffer
2063 editor_b.update(cx_b, |editor_b, cx| {
2064 assert!(editor_b.blame().is_none());
2065 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2066 });
2067
2068 cx_a.executor().run_until_parked();
2069 cx_b.executor().run_until_parked();
2070
2071 editor_b.update(cx_b, |editor_b, cx| {
2072 let blame = editor_b.blame().expect("editor_b should have blame now");
2073 let entries = blame.update(cx, |blame, cx| {
2074 blame
2075 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2076 .collect::<Vec<_>>()
2077 });
2078
2079 assert_eq!(
2080 entries,
2081 vec![
2082 Some(blame_entry("1b1b1b", 0..1)),
2083 Some(blame_entry("0d0d0d", 1..2)),
2084 Some(blame_entry("3a3a3a", 2..3)),
2085 Some(blame_entry("4c4c4c", 3..4)),
2086 ]
2087 );
2088
2089 blame.update(cx, |blame, _| {
2090 for (idx, entry) in entries.iter().flatten().enumerate() {
2091 let details = blame.details_for_entry(entry).unwrap();
2092 assert_eq!(details.message, format!("message for idx-{}", idx));
2093 assert_eq!(
2094 details.permalink.unwrap().to_string(),
2095 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2096 );
2097 }
2098 });
2099 });
2100
2101 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2102 // which gets back to client_b.
2103 editor_b.update(cx_b, |editor_b, cx| {
2104 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2105 });
2106
2107 cx_a.executor().run_until_parked();
2108 cx_b.executor().run_until_parked();
2109
2110 editor_b.update(cx_b, |editor_b, cx| {
2111 let blame = editor_b.blame().expect("editor_b should have blame now");
2112 let entries = blame.update(cx, |blame, cx| {
2113 blame
2114 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2115 .collect::<Vec<_>>()
2116 });
2117
2118 assert_eq!(
2119 entries,
2120 vec![
2121 None,
2122 Some(blame_entry("0d0d0d", 1..2)),
2123 Some(blame_entry("3a3a3a", 2..3)),
2124 Some(blame_entry("4c4c4c", 3..4)),
2125 ]
2126 );
2127 });
2128
2129 // Now editor_a also updates the file
2130 editor_a.update(cx_a, |editor_a, cx| {
2131 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2132 });
2133
2134 cx_a.executor().run_until_parked();
2135 cx_b.executor().run_until_parked();
2136
2137 editor_b.update(cx_b, |editor_b, cx| {
2138 let blame = editor_b.blame().expect("editor_b should have blame now");
2139 let entries = blame.update(cx, |blame, cx| {
2140 blame
2141 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2142 .collect::<Vec<_>>()
2143 });
2144
2145 assert_eq!(
2146 entries,
2147 vec![
2148 None,
2149 None,
2150 Some(blame_entry("3a3a3a", 2..3)),
2151 Some(blame_entry("4c4c4c", 3..4)),
2152 ]
2153 );
2154 });
2155}
2156
2157#[gpui::test(iterations = 30)]
2158async fn test_collaborating_with_editorconfig(
2159 cx_a: &mut TestAppContext,
2160 cx_b: &mut TestAppContext,
2161) {
2162 let mut server = TestServer::start(cx_a.executor()).await;
2163 let client_a = server.create_client(cx_a, "user_a").await;
2164 let client_b = server.create_client(cx_b, "user_b").await;
2165 server
2166 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2167 .await;
2168 let active_call_a = cx_a.read(ActiveCall::global);
2169
2170 cx_b.update(editor::init);
2171
2172 // Set up a fake language server.
2173 client_a.language_registry().add(rust_lang());
2174 client_a
2175 .fs()
2176 .insert_tree(
2177 "/a",
2178 json!({
2179 "src": {
2180 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2181 "other_mod": {
2182 "other.rs": "pub fn foo() -> usize {\n 4\n}",
2183 ".editorconfig": "",
2184 },
2185 },
2186 ".editorconfig": "[*]\ntab_width = 2\n",
2187 }),
2188 )
2189 .await;
2190 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2191 let project_id = active_call_a
2192 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2193 .await
2194 .unwrap();
2195 let main_buffer_a = project_a
2196 .update(cx_a, |p, cx| {
2197 p.open_buffer((worktree_id, "src/main.rs"), cx)
2198 })
2199 .await
2200 .unwrap();
2201 let other_buffer_a = project_a
2202 .update(cx_a, |p, cx| {
2203 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2204 })
2205 .await
2206 .unwrap();
2207 let cx_a = cx_a.add_empty_window();
2208 let main_editor_a =
2209 cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
2210 let other_editor_a =
2211 cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
2212 let mut main_editor_cx_a = EditorTestContext {
2213 cx: cx_a.clone(),
2214 window: cx_a.handle(),
2215 editor: main_editor_a,
2216 assertion_cx: AssertionContextManager::new(),
2217 };
2218 let mut other_editor_cx_a = EditorTestContext {
2219 cx: cx_a.clone(),
2220 window: cx_a.handle(),
2221 editor: other_editor_a,
2222 assertion_cx: AssertionContextManager::new(),
2223 };
2224
2225 // Join the project as client B.
2226 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2227 let main_buffer_b = project_b
2228 .update(cx_b, |p, cx| {
2229 p.open_buffer((worktree_id, "src/main.rs"), cx)
2230 })
2231 .await
2232 .unwrap();
2233 let other_buffer_b = project_b
2234 .update(cx_b, |p, cx| {
2235 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2236 })
2237 .await
2238 .unwrap();
2239 let cx_b = cx_b.add_empty_window();
2240 let main_editor_b =
2241 cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
2242 let other_editor_b =
2243 cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
2244 let mut main_editor_cx_b = EditorTestContext {
2245 cx: cx_b.clone(),
2246 window: cx_b.handle(),
2247 editor: main_editor_b,
2248 assertion_cx: AssertionContextManager::new(),
2249 };
2250 let mut other_editor_cx_b = EditorTestContext {
2251 cx: cx_b.clone(),
2252 window: cx_b.handle(),
2253 editor: other_editor_b,
2254 assertion_cx: AssertionContextManager::new(),
2255 };
2256
2257 let initial_main = indoc! {"
2258ˇmod other;
2259fn main() { let foo = other::foo(); }"};
2260 let initial_other = indoc! {"
2261ˇpub fn foo() -> usize {
2262 4
2263}"};
2264
2265 let first_tabbed_main = indoc! {"
2266 ˇmod other;
2267fn main() { let foo = other::foo(); }"};
2268 tab_undo_assert(
2269 &mut main_editor_cx_a,
2270 &mut main_editor_cx_b,
2271 initial_main,
2272 first_tabbed_main,
2273 true,
2274 );
2275 tab_undo_assert(
2276 &mut main_editor_cx_a,
2277 &mut main_editor_cx_b,
2278 initial_main,
2279 first_tabbed_main,
2280 false,
2281 );
2282
2283 let first_tabbed_other = indoc! {"
2284 ˇpub fn foo() -> usize {
2285 4
2286}"};
2287 tab_undo_assert(
2288 &mut other_editor_cx_a,
2289 &mut other_editor_cx_b,
2290 initial_other,
2291 first_tabbed_other,
2292 true,
2293 );
2294 tab_undo_assert(
2295 &mut other_editor_cx_a,
2296 &mut other_editor_cx_b,
2297 initial_other,
2298 first_tabbed_other,
2299 false,
2300 );
2301
2302 client_a
2303 .fs()
2304 .atomic_write(
2305 PathBuf::from("/a/src/.editorconfig"),
2306 "[*]\ntab_width = 3\n".to_owned(),
2307 )
2308 .await
2309 .unwrap();
2310 cx_a.run_until_parked();
2311 cx_b.run_until_parked();
2312
2313 let second_tabbed_main = indoc! {"
2314 ˇmod other;
2315fn main() { let foo = other::foo(); }"};
2316 tab_undo_assert(
2317 &mut main_editor_cx_a,
2318 &mut main_editor_cx_b,
2319 initial_main,
2320 second_tabbed_main,
2321 true,
2322 );
2323 tab_undo_assert(
2324 &mut main_editor_cx_a,
2325 &mut main_editor_cx_b,
2326 initial_main,
2327 second_tabbed_main,
2328 false,
2329 );
2330
2331 let second_tabbed_other = indoc! {"
2332 ˇpub fn foo() -> usize {
2333 4
2334}"};
2335 tab_undo_assert(
2336 &mut other_editor_cx_a,
2337 &mut other_editor_cx_b,
2338 initial_other,
2339 second_tabbed_other,
2340 true,
2341 );
2342 tab_undo_assert(
2343 &mut other_editor_cx_a,
2344 &mut other_editor_cx_b,
2345 initial_other,
2346 second_tabbed_other,
2347 false,
2348 );
2349
2350 let editorconfig_buffer_b = project_b
2351 .update(cx_b, |p, cx| {
2352 p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2353 })
2354 .await
2355 .unwrap();
2356 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2357 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2358 });
2359 project_b
2360 .update(cx_b, |project, cx| {
2361 project.save_buffer(editorconfig_buffer_b.clone(), cx)
2362 })
2363 .await
2364 .unwrap();
2365 cx_a.run_until_parked();
2366 cx_b.run_until_parked();
2367
2368 tab_undo_assert(
2369 &mut main_editor_cx_a,
2370 &mut main_editor_cx_b,
2371 initial_main,
2372 second_tabbed_main,
2373 true,
2374 );
2375 tab_undo_assert(
2376 &mut main_editor_cx_a,
2377 &mut main_editor_cx_b,
2378 initial_main,
2379 second_tabbed_main,
2380 false,
2381 );
2382
2383 let third_tabbed_other = indoc! {"
2384 ˇpub fn foo() -> usize {
2385 4
2386}"};
2387 tab_undo_assert(
2388 &mut other_editor_cx_a,
2389 &mut other_editor_cx_b,
2390 initial_other,
2391 third_tabbed_other,
2392 true,
2393 );
2394
2395 tab_undo_assert(
2396 &mut other_editor_cx_a,
2397 &mut other_editor_cx_b,
2398 initial_other,
2399 third_tabbed_other,
2400 false,
2401 );
2402}
2403
2404#[track_caller]
2405fn tab_undo_assert(
2406 cx_a: &mut EditorTestContext,
2407 cx_b: &mut EditorTestContext,
2408 expected_initial: &str,
2409 expected_tabbed: &str,
2410 a_tabs: bool,
2411) {
2412 cx_a.assert_editor_state(expected_initial);
2413 cx_b.assert_editor_state(expected_initial);
2414
2415 if a_tabs {
2416 cx_a.update_editor(|editor, cx| {
2417 editor.tab(&editor::actions::Tab, cx);
2418 });
2419 } else {
2420 cx_b.update_editor(|editor, cx| {
2421 editor.tab(&editor::actions::Tab, cx);
2422 });
2423 }
2424
2425 cx_a.run_until_parked();
2426 cx_b.run_until_parked();
2427
2428 cx_a.assert_editor_state(expected_tabbed);
2429 cx_b.assert_editor_state(expected_tabbed);
2430
2431 if a_tabs {
2432 cx_a.update_editor(|editor, cx| {
2433 editor.undo(&editor::actions::Undo, cx);
2434 });
2435 } else {
2436 cx_b.update_editor(|editor, cx| {
2437 editor.undo(&editor::actions::Undo, cx);
2438 });
2439 }
2440 cx_a.run_until_parked();
2441 cx_b.run_until_parked();
2442 cx_a.assert_editor_state(expected_initial);
2443 cx_b.assert_editor_state(expected_initial);
2444}
2445
2446fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2447 let mut labels = Vec::new();
2448 for hint in editor.inlay_hint_cache().hints() {
2449 match hint.label {
2450 project::InlayHintLabel::String(s) => labels.push(s),
2451 _ => unreachable!(),
2452 }
2453 }
2454 labels
2455}
2456
2457fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2458 git::blame::BlameEntry {
2459 sha: sha.parse().unwrap(),
2460 range,
2461 ..Default::default()
2462 }
2463}