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 });
1982 cx_a.update(|cx| {
1983 SettingsStore::update_global(cx, |store, cx| {
1984 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1985 settings.git.inline_blame = inline_blame_off_settings;
1986 });
1987 });
1988 });
1989 cx_b.update(|cx| {
1990 SettingsStore::update_global(cx, |store, cx| {
1991 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1992 settings.git.inline_blame = inline_blame_off_settings;
1993 });
1994 });
1995 });
1996
1997 client_a
1998 .fs()
1999 .insert_tree(
2000 "/my-repo",
2001 json!({
2002 ".git": {},
2003 "file.txt": "line1\nline2\nline3\nline\n",
2004 }),
2005 )
2006 .await;
2007
2008 let blame = git::blame::Blame {
2009 entries: vec![
2010 blame_entry("1b1b1b", 0..1),
2011 blame_entry("0d0d0d", 1..2),
2012 blame_entry("3a3a3a", 2..3),
2013 blame_entry("4c4c4c", 3..4),
2014 ],
2015 permalinks: HashMap::default(), // This field is deprecrated
2016 messages: [
2017 ("1b1b1b", "message for idx-0"),
2018 ("0d0d0d", "message for idx-1"),
2019 ("3a3a3a", "message for idx-2"),
2020 ("4c4c4c", "message for idx-3"),
2021 ]
2022 .into_iter()
2023 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
2024 .collect(),
2025 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
2026 };
2027 client_a.fs().set_blame_for_repo(
2028 Path::new("/my-repo/.git"),
2029 vec![(Path::new("file.txt"), blame)],
2030 );
2031
2032 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2033 let project_id = active_call_a
2034 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2035 .await
2036 .unwrap();
2037
2038 // Create editor_a
2039 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2040 let editor_a = workspace_a
2041 .update(cx_a, |workspace, cx| {
2042 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2043 })
2044 .await
2045 .unwrap()
2046 .downcast::<Editor>()
2047 .unwrap();
2048
2049 // Join the project as client B.
2050 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2051 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2052 let editor_b = workspace_b
2053 .update(cx_b, |workspace, cx| {
2054 workspace.open_path((worktree_id, "file.txt"), None, true, cx)
2055 })
2056 .await
2057 .unwrap()
2058 .downcast::<Editor>()
2059 .unwrap();
2060
2061 // client_b now requests git blame for the open buffer
2062 editor_b.update(cx_b, |editor_b, cx| {
2063 assert!(editor_b.blame().is_none());
2064 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
2065 });
2066
2067 cx_a.executor().run_until_parked();
2068 cx_b.executor().run_until_parked();
2069
2070 editor_b.update(cx_b, |editor_b, cx| {
2071 let blame = editor_b.blame().expect("editor_b should have blame now");
2072 let entries = blame.update(cx, |blame, cx| {
2073 blame
2074 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2075 .collect::<Vec<_>>()
2076 });
2077
2078 assert_eq!(
2079 entries,
2080 vec![
2081 Some(blame_entry("1b1b1b", 0..1)),
2082 Some(blame_entry("0d0d0d", 1..2)),
2083 Some(blame_entry("3a3a3a", 2..3)),
2084 Some(blame_entry("4c4c4c", 3..4)),
2085 ]
2086 );
2087
2088 blame.update(cx, |blame, _| {
2089 for (idx, entry) in entries.iter().flatten().enumerate() {
2090 let details = blame.details_for_entry(entry).unwrap();
2091 assert_eq!(details.message, format!("message for idx-{}", idx));
2092 assert_eq!(
2093 details.permalink.unwrap().to_string(),
2094 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2095 );
2096 }
2097 });
2098 });
2099
2100 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2101 // which gets back to client_b.
2102 editor_b.update(cx_b, |editor_b, cx| {
2103 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2104 });
2105
2106 cx_a.executor().run_until_parked();
2107 cx_b.executor().run_until_parked();
2108
2109 editor_b.update(cx_b, |editor_b, cx| {
2110 let blame = editor_b.blame().expect("editor_b should have blame now");
2111 let entries = blame.update(cx, |blame, cx| {
2112 blame
2113 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2114 .collect::<Vec<_>>()
2115 });
2116
2117 assert_eq!(
2118 entries,
2119 vec![
2120 None,
2121 Some(blame_entry("0d0d0d", 1..2)),
2122 Some(blame_entry("3a3a3a", 2..3)),
2123 Some(blame_entry("4c4c4c", 3..4)),
2124 ]
2125 );
2126 });
2127
2128 // Now editor_a also updates the file
2129 editor_a.update(cx_a, |editor_a, cx| {
2130 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2131 });
2132
2133 cx_a.executor().run_until_parked();
2134 cx_b.executor().run_until_parked();
2135
2136 editor_b.update(cx_b, |editor_b, cx| {
2137 let blame = editor_b.blame().expect("editor_b should have blame now");
2138 let entries = blame.update(cx, |blame, cx| {
2139 blame
2140 .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
2141 .collect::<Vec<_>>()
2142 });
2143
2144 assert_eq!(
2145 entries,
2146 vec![
2147 None,
2148 None,
2149 Some(blame_entry("3a3a3a", 2..3)),
2150 Some(blame_entry("4c4c4c", 3..4)),
2151 ]
2152 );
2153 });
2154}
2155
2156#[gpui::test(iterations = 30)]
2157async fn test_collaborating_with_editorconfig(
2158 cx_a: &mut TestAppContext,
2159 cx_b: &mut TestAppContext,
2160) {
2161 let mut server = TestServer::start(cx_a.executor()).await;
2162 let client_a = server.create_client(cx_a, "user_a").await;
2163 let client_b = server.create_client(cx_b, "user_b").await;
2164 server
2165 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2166 .await;
2167 let active_call_a = cx_a.read(ActiveCall::global);
2168
2169 cx_b.update(editor::init);
2170
2171 // Set up a fake language server.
2172 client_a.language_registry().add(rust_lang());
2173 client_a
2174 .fs()
2175 .insert_tree(
2176 "/a",
2177 json!({
2178 "src": {
2179 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2180 "other_mod": {
2181 "other.rs": "pub fn foo() -> usize {\n 4\n}",
2182 ".editorconfig": "",
2183 },
2184 },
2185 ".editorconfig": "[*]\ntab_width = 2\n",
2186 }),
2187 )
2188 .await;
2189 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2190 let project_id = active_call_a
2191 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2192 .await
2193 .unwrap();
2194 let main_buffer_a = project_a
2195 .update(cx_a, |p, cx| {
2196 p.open_buffer((worktree_id, "src/main.rs"), cx)
2197 })
2198 .await
2199 .unwrap();
2200 let other_buffer_a = project_a
2201 .update(cx_a, |p, cx| {
2202 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2203 })
2204 .await
2205 .unwrap();
2206 let cx_a = cx_a.add_empty_window();
2207 let main_editor_a =
2208 cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
2209 let other_editor_a =
2210 cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
2211 let mut main_editor_cx_a = EditorTestContext {
2212 cx: cx_a.clone(),
2213 window: cx_a.handle(),
2214 editor: main_editor_a,
2215 assertion_cx: AssertionContextManager::new(),
2216 };
2217 let mut other_editor_cx_a = EditorTestContext {
2218 cx: cx_a.clone(),
2219 window: cx_a.handle(),
2220 editor: other_editor_a,
2221 assertion_cx: AssertionContextManager::new(),
2222 };
2223
2224 // Join the project as client B.
2225 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2226 let main_buffer_b = project_b
2227 .update(cx_b, |p, cx| {
2228 p.open_buffer((worktree_id, "src/main.rs"), cx)
2229 })
2230 .await
2231 .unwrap();
2232 let other_buffer_b = project_b
2233 .update(cx_b, |p, cx| {
2234 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2235 })
2236 .await
2237 .unwrap();
2238 let cx_b = cx_b.add_empty_window();
2239 let main_editor_b =
2240 cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
2241 let other_editor_b =
2242 cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
2243 let mut main_editor_cx_b = EditorTestContext {
2244 cx: cx_b.clone(),
2245 window: cx_b.handle(),
2246 editor: main_editor_b,
2247 assertion_cx: AssertionContextManager::new(),
2248 };
2249 let mut other_editor_cx_b = EditorTestContext {
2250 cx: cx_b.clone(),
2251 window: cx_b.handle(),
2252 editor: other_editor_b,
2253 assertion_cx: AssertionContextManager::new(),
2254 };
2255
2256 let initial_main = indoc! {"
2257ˇmod other;
2258fn main() { let foo = other::foo(); }"};
2259 let initial_other = indoc! {"
2260ˇpub fn foo() -> usize {
2261 4
2262}"};
2263
2264 let first_tabbed_main = indoc! {"
2265 ˇmod other;
2266fn main() { let foo = other::foo(); }"};
2267 tab_undo_assert(
2268 &mut main_editor_cx_a,
2269 &mut main_editor_cx_b,
2270 initial_main,
2271 first_tabbed_main,
2272 true,
2273 );
2274 tab_undo_assert(
2275 &mut main_editor_cx_a,
2276 &mut main_editor_cx_b,
2277 initial_main,
2278 first_tabbed_main,
2279 false,
2280 );
2281
2282 let first_tabbed_other = indoc! {"
2283 ˇpub fn foo() -> usize {
2284 4
2285}"};
2286 tab_undo_assert(
2287 &mut other_editor_cx_a,
2288 &mut other_editor_cx_b,
2289 initial_other,
2290 first_tabbed_other,
2291 true,
2292 );
2293 tab_undo_assert(
2294 &mut other_editor_cx_a,
2295 &mut other_editor_cx_b,
2296 initial_other,
2297 first_tabbed_other,
2298 false,
2299 );
2300
2301 client_a
2302 .fs()
2303 .atomic_write(
2304 PathBuf::from("/a/src/.editorconfig"),
2305 "[*]\ntab_width = 3\n".to_owned(),
2306 )
2307 .await
2308 .unwrap();
2309 cx_a.run_until_parked();
2310 cx_b.run_until_parked();
2311
2312 let second_tabbed_main = indoc! {"
2313 ˇmod other;
2314fn main() { let foo = other::foo(); }"};
2315 tab_undo_assert(
2316 &mut main_editor_cx_a,
2317 &mut main_editor_cx_b,
2318 initial_main,
2319 second_tabbed_main,
2320 true,
2321 );
2322 tab_undo_assert(
2323 &mut main_editor_cx_a,
2324 &mut main_editor_cx_b,
2325 initial_main,
2326 second_tabbed_main,
2327 false,
2328 );
2329
2330 let second_tabbed_other = indoc! {"
2331 ˇpub fn foo() -> usize {
2332 4
2333}"};
2334 tab_undo_assert(
2335 &mut other_editor_cx_a,
2336 &mut other_editor_cx_b,
2337 initial_other,
2338 second_tabbed_other,
2339 true,
2340 );
2341 tab_undo_assert(
2342 &mut other_editor_cx_a,
2343 &mut other_editor_cx_b,
2344 initial_other,
2345 second_tabbed_other,
2346 false,
2347 );
2348
2349 let editorconfig_buffer_b = project_b
2350 .update(cx_b, |p, cx| {
2351 p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2352 })
2353 .await
2354 .unwrap();
2355 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2356 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2357 });
2358 project_b
2359 .update(cx_b, |project, cx| {
2360 project.save_buffer(editorconfig_buffer_b.clone(), cx)
2361 })
2362 .await
2363 .unwrap();
2364 cx_a.run_until_parked();
2365 cx_b.run_until_parked();
2366
2367 tab_undo_assert(
2368 &mut main_editor_cx_a,
2369 &mut main_editor_cx_b,
2370 initial_main,
2371 second_tabbed_main,
2372 true,
2373 );
2374 tab_undo_assert(
2375 &mut main_editor_cx_a,
2376 &mut main_editor_cx_b,
2377 initial_main,
2378 second_tabbed_main,
2379 false,
2380 );
2381
2382 let third_tabbed_other = indoc! {"
2383 ˇpub fn foo() -> usize {
2384 4
2385}"};
2386 tab_undo_assert(
2387 &mut other_editor_cx_a,
2388 &mut other_editor_cx_b,
2389 initial_other,
2390 third_tabbed_other,
2391 true,
2392 );
2393
2394 tab_undo_assert(
2395 &mut other_editor_cx_a,
2396 &mut other_editor_cx_b,
2397 initial_other,
2398 third_tabbed_other,
2399 false,
2400 );
2401}
2402
2403#[track_caller]
2404fn tab_undo_assert(
2405 cx_a: &mut EditorTestContext,
2406 cx_b: &mut EditorTestContext,
2407 expected_initial: &str,
2408 expected_tabbed: &str,
2409 a_tabs: bool,
2410) {
2411 cx_a.assert_editor_state(expected_initial);
2412 cx_b.assert_editor_state(expected_initial);
2413
2414 if a_tabs {
2415 cx_a.update_editor(|editor, cx| {
2416 editor.tab(&editor::actions::Tab, cx);
2417 });
2418 } else {
2419 cx_b.update_editor(|editor, cx| {
2420 editor.tab(&editor::actions::Tab, cx);
2421 });
2422 }
2423
2424 cx_a.run_until_parked();
2425 cx_b.run_until_parked();
2426
2427 cx_a.assert_editor_state(expected_tabbed);
2428 cx_b.assert_editor_state(expected_tabbed);
2429
2430 if a_tabs {
2431 cx_a.update_editor(|editor, cx| {
2432 editor.undo(&editor::actions::Undo, cx);
2433 });
2434 } else {
2435 cx_b.update_editor(|editor, cx| {
2436 editor.undo(&editor::actions::Undo, cx);
2437 });
2438 }
2439 cx_a.run_until_parked();
2440 cx_b.run_until_parked();
2441 cx_a.assert_editor_state(expected_initial);
2442 cx_b.assert_editor_state(expected_initial);
2443}
2444
2445fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2446 let mut labels = Vec::new();
2447 for hint in editor.inlay_hint_cache().hints() {
2448 match hint.label {
2449 project::InlayHintLabel::String(s) => labels.push(s),
2450 _ => unreachable!(),
2451 }
2452 }
2453 labels
2454}
2455
2456fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2457 git::blame::BlameEntry {
2458 sha: sha.parse().unwrap(),
2459 range,
2460 ..Default::default()
2461 }
2462}