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