1use std::{
2 path::Path,
3 sync::{
4 atomic::{self, AtomicBool, AtomicUsize},
5 Arc,
6 },
7};
8
9use call::ActiveCall;
10use editor::{
11 actions::{
12 ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, ToggleCodeActions, Undo,
13 },
14 test::editor_test_context::{AssertionContextManager, EditorTestContext},
15 Editor,
16};
17use futures::StreamExt;
18use gpui::{TestAppContext, VisualContext, VisualTestContext};
19use indoc::indoc;
20use language::{
21 language_settings::{AllLanguageSettings, InlayHintSettings},
22 tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
23};
24use rpc::RECEIVE_TIMEOUT;
25use serde_json::json;
26use settings::SettingsStore;
27use text::Point;
28use workspace::Workspace;
29
30use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
31
32#[gpui::test(iterations = 10)]
33async fn test_host_disconnect(
34 cx_a: &mut TestAppContext,
35 cx_b: &mut TestAppContext,
36 cx_c: &mut TestAppContext,
37) {
38 let mut server = TestServer::start(cx_a.executor()).await;
39 let client_a = server.create_client(cx_a, "user_a").await;
40 let client_b = server.create_client(cx_b, "user_b").await;
41 let client_c = server.create_client(cx_c, "user_c").await;
42 server
43 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
44 .await;
45
46 cx_b.update(editor::init);
47
48 client_a
49 .fs()
50 .insert_tree(
51 "/a",
52 serde_json::json!({
53 "a.txt": "a-contents",
54 "b.txt": "b-contents",
55 }),
56 )
57 .await;
58
59 let active_call_a = cx_a.read(ActiveCall::global);
60 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
61
62 let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
63 let project_id = active_call_a
64 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
65 .await
66 .unwrap();
67
68 let project_b = client_b.build_remote_project(project_id, cx_b).await;
69 cx_a.background_executor.run_until_parked();
70
71 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
72
73 let workspace_b =
74 cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
75 let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
76 let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
77
78 let editor_b = workspace_b
79 .update(cx_b, |workspace, cx| {
80 workspace.open_path((worktree_id, "b.txt"), None, true, cx)
81 })
82 .unwrap()
83 .await
84 .unwrap()
85 .downcast::<Editor>()
86 .unwrap();
87
88 //TODO: focus
89 assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
90 editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
91
92 cx_b.update(|cx| {
93 assert!(workspace_b_view.read(cx).is_edited());
94 });
95
96 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
97 server.forbid_connections();
98 server.disconnect_client(client_a.peer_id().unwrap());
99 cx_a.background_executor
100 .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
101
102 project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
103
104 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
105
106 project_b.read_with(cx_b, |project, _| project.is_read_only());
107
108 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
109
110 // Ensure client B's edited state is reset and that the whole window is blurred.
111
112 workspace_b
113 .update(cx_b, |workspace, cx| {
114 assert_eq!(cx.focused(), None);
115 assert!(!workspace.is_edited())
116 })
117 .unwrap();
118
119 // Ensure client B is not prompted to save edits when closing window after disconnecting.
120 let can_close = workspace_b
121 .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx))
122 .unwrap()
123 .await
124 .unwrap();
125 assert!(can_close);
126
127 // Allow client A to reconnect to the server.
128 server.allow_connections();
129 cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);
130
131 // Client B calls client A again after they reconnected.
132 let active_call_b = cx_b.read(ActiveCall::global);
133 active_call_b
134 .update(cx_b, |call, cx| {
135 call.invite(client_a.user_id().unwrap(), None, cx)
136 })
137 .await
138 .unwrap();
139 cx_a.background_executor.run_until_parked();
140 active_call_a
141 .update(cx_a, |call, cx| call.accept_incoming(cx))
142 .await
143 .unwrap();
144
145 active_call_a
146 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
147 .await
148 .unwrap();
149
150 // Drop client A's connection again. We should still unshare it successfully.
151 server.forbid_connections();
152 server.disconnect_client(client_a.peer_id().unwrap());
153 cx_a.background_executor
154 .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
155
156 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
157}
158
159#[gpui::test]
160async fn test_newline_above_or_below_does_not_move_guest_cursor(
161 cx_a: &mut TestAppContext,
162 cx_b: &mut TestAppContext,
163) {
164 let mut server = TestServer::start(cx_a.executor()).await;
165 let client_a = server.create_client(cx_a, "user_a").await;
166 let client_b = server.create_client(cx_b, "user_b").await;
167 let executor = cx_a.executor();
168 server
169 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
170 .await;
171 let active_call_a = cx_a.read(ActiveCall::global);
172
173 client_a
174 .fs()
175 .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
176 .await;
177 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
178 let project_id = active_call_a
179 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
180 .await
181 .unwrap();
182
183 let project_b = client_b.build_remote_project(project_id, cx_b).await;
184
185 // Open a buffer as client A
186 let buffer_a = project_a
187 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
188 .await
189 .unwrap();
190 let cx_a = cx_a.add_empty_window();
191 let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
192
193 let mut editor_cx_a = EditorTestContext {
194 cx: cx_a.clone(),
195 window: cx_a.handle(),
196 editor: editor_a,
197 assertion_cx: AssertionContextManager::new(),
198 };
199
200 let cx_b = cx_b.add_empty_window();
201 // Open a buffer as client B
202 let buffer_b = project_b
203 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
204 .await
205 .unwrap();
206 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
207
208 let mut editor_cx_b = EditorTestContext {
209 cx: cx_b.clone(),
210 window: cx_b.handle(),
211 editor: editor_b,
212 assertion_cx: AssertionContextManager::new(),
213 };
214
215 // Test newline above
216 editor_cx_a.set_selections_state(indoc! {"
217 Some textˇ
218 "});
219 editor_cx_b.set_selections_state(indoc! {"
220 Some textˇ
221 "});
222 editor_cx_a
223 .update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx));
224 executor.run_until_parked();
225 editor_cx_a.assert_editor_state(indoc! {"
226 ˇ
227 Some text
228 "});
229 editor_cx_b.assert_editor_state(indoc! {"
230
231 Some textˇ
232 "});
233
234 // Test newline below
235 editor_cx_a.set_selections_state(indoc! {"
236
237 Some textˇ
238 "});
239 editor_cx_b.set_selections_state(indoc! {"
240
241 Some textˇ
242 "});
243 editor_cx_a
244 .update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx));
245 executor.run_until_parked();
246 editor_cx_a.assert_editor_state(indoc! {"
247
248 Some text
249 ˇ
250 "});
251 editor_cx_b.assert_editor_state(indoc! {"
252
253 Some textˇ
254
255 "});
256}
257
258#[gpui::test(iterations = 10)]
259async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
260 let mut server = TestServer::start(cx_a.executor()).await;
261 let client_a = server.create_client(cx_a, "user_a").await;
262 let client_b = server.create_client(cx_b, "user_b").await;
263 server
264 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
265 .await;
266 let active_call_a = cx_a.read(ActiveCall::global);
267
268 // Set up a fake language server.
269 let mut language = Language::new(
270 LanguageConfig {
271 name: "Rust".into(),
272 matcher: LanguageMatcher {
273 path_suffixes: vec!["rs".to_string()],
274 ..Default::default()
275 },
276 ..Default::default()
277 },
278 Some(tree_sitter_rust::language()),
279 );
280 let mut fake_language_servers = language
281 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
282 capabilities: lsp::ServerCapabilities {
283 completion_provider: Some(lsp::CompletionOptions {
284 trigger_characters: Some(vec![".".to_string()]),
285 resolve_provider: Some(true),
286 ..Default::default()
287 }),
288 ..Default::default()
289 },
290 ..Default::default()
291 }))
292 .await;
293 client_a.language_registry().add(Arc::new(language));
294
295 client_a
296 .fs()
297 .insert_tree(
298 "/a",
299 json!({
300 "main.rs": "fn main() { a }",
301 "other.rs": "",
302 }),
303 )
304 .await;
305 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
306 let project_id = active_call_a
307 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
308 .await
309 .unwrap();
310 let project_b = client_b.build_remote_project(project_id, cx_b).await;
311
312 // Open a file in an editor as the guest.
313 let buffer_b = project_b
314 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
315 .await
316 .unwrap();
317 let cx_b = cx_b.add_empty_window();
318 let editor_b =
319 cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
320
321 let fake_language_server = fake_language_servers.next().await.unwrap();
322 cx_a.background_executor.run_until_parked();
323
324 buffer_b.read_with(cx_b, |buffer, _| {
325 assert!(!buffer.completion_triggers().is_empty())
326 });
327
328 // Type a completion trigger character as the guest.
329 editor_b.update(cx_b, |editor, cx| {
330 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
331 editor.handle_input(".", cx);
332 });
333 cx_b.focus_view(&editor_b);
334
335 // Receive a completion request as the host's language server.
336 // Return some completions from the host's language server.
337 cx_a.executor().start_waiting();
338 fake_language_server
339 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
340 assert_eq!(
341 params.text_document_position.text_document.uri,
342 lsp::Url::from_file_path("/a/main.rs").unwrap(),
343 );
344 assert_eq!(
345 params.text_document_position.position,
346 lsp::Position::new(0, 14),
347 );
348
349 Ok(Some(lsp::CompletionResponse::Array(vec![
350 lsp::CompletionItem {
351 label: "first_method(…)".into(),
352 detail: Some("fn(&mut self, B) -> C".into()),
353 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
354 new_text: "first_method($1)".to_string(),
355 range: lsp::Range::new(
356 lsp::Position::new(0, 14),
357 lsp::Position::new(0, 14),
358 ),
359 })),
360 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
361 ..Default::default()
362 },
363 lsp::CompletionItem {
364 label: "second_method(…)".into(),
365 detail: Some("fn(&mut self, C) -> D<E>".into()),
366 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
367 new_text: "second_method()".to_string(),
368 range: lsp::Range::new(
369 lsp::Position::new(0, 14),
370 lsp::Position::new(0, 14),
371 ),
372 })),
373 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
374 ..Default::default()
375 },
376 ])))
377 })
378 .next()
379 .await
380 .unwrap();
381 cx_a.executor().finish_waiting();
382
383 // Open the buffer on the host.
384 let buffer_a = project_a
385 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
386 .await
387 .unwrap();
388 cx_a.executor().run_until_parked();
389
390 buffer_a.read_with(cx_a, |buffer, _| {
391 assert_eq!(buffer.text(), "fn main() { a. }")
392 });
393
394 // Confirm a completion on the guest.
395 editor_b.update(cx_b, |editor, cx| {
396 assert!(editor.context_menu_visible());
397 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
398 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
399 });
400
401 // Return a resolved completion from the host's language server.
402 // The resolved completion has an additional text edit.
403 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
404 |params, _| async move {
405 assert_eq!(params.label, "first_method(…)");
406 Ok(lsp::CompletionItem {
407 label: "first_method(…)".into(),
408 detail: Some("fn(&mut self, B) -> C".into()),
409 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
410 new_text: "first_method($1)".to_string(),
411 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
412 })),
413 additional_text_edits: Some(vec![lsp::TextEdit {
414 new_text: "use d::SomeTrait;\n".to_string(),
415 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
416 }]),
417 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
418 ..Default::default()
419 })
420 },
421 );
422
423 // The additional edit is applied.
424 cx_a.executor().run_until_parked();
425
426 buffer_a.read_with(cx_a, |buffer, _| {
427 assert_eq!(
428 buffer.text(),
429 "use d::SomeTrait;\nfn main() { a.first_method() }"
430 );
431 });
432
433 buffer_b.read_with(cx_b, |buffer, _| {
434 assert_eq!(
435 buffer.text(),
436 "use d::SomeTrait;\nfn main() { a.first_method() }"
437 );
438 });
439}
440
441#[gpui::test(iterations = 10)]
442async fn test_collaborating_with_code_actions(
443 cx_a: &mut TestAppContext,
444 cx_b: &mut TestAppContext,
445) {
446 let mut server = TestServer::start(cx_a.executor()).await;
447 let client_a = server.create_client(cx_a, "user_a").await;
448 //
449 let client_b = server.create_client(cx_b, "user_b").await;
450 server
451 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
452 .await;
453 let active_call_a = cx_a.read(ActiveCall::global);
454
455 cx_b.update(editor::init);
456
457 // Set up a fake language server.
458 let mut language = Language::new(
459 LanguageConfig {
460 name: "Rust".into(),
461 matcher: LanguageMatcher {
462 path_suffixes: vec!["rs".to_string()],
463 ..Default::default()
464 },
465 ..Default::default()
466 },
467 Some(tree_sitter_rust::language()),
468 );
469 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
470 client_a.language_registry().add(Arc::new(language));
471
472 client_a
473 .fs()
474 .insert_tree(
475 "/a",
476 json!({
477 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
478 "other.rs": "pub fn foo() -> usize { 4 }",
479 }),
480 )
481 .await;
482 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
483 let project_id = active_call_a
484 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
485 .await
486 .unwrap();
487
488 // Join the project as client B.
489 let project_b = client_b.build_remote_project(project_id, cx_b).await;
490 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
491 let editor_b = workspace_b
492 .update(cx_b, |workspace, cx| {
493 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
494 })
495 .await
496 .unwrap()
497 .downcast::<Editor>()
498 .unwrap();
499
500 let mut fake_language_server = fake_language_servers.next().await.unwrap();
501 let mut requests = fake_language_server
502 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
503 assert_eq!(
504 params.text_document.uri,
505 lsp::Url::from_file_path("/a/main.rs").unwrap(),
506 );
507 assert_eq!(params.range.start, lsp::Position::new(0, 0));
508 assert_eq!(params.range.end, lsp::Position::new(0, 0));
509 Ok(None)
510 });
511 cx_a.background_executor
512 .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
513 requests.next().await;
514
515 // Move cursor to a location that contains code actions.
516 editor_b.update(cx_b, |editor, cx| {
517 editor.change_selections(None, cx, |s| {
518 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
519 });
520 });
521 cx_b.focus_view(&editor_b);
522
523 let mut requests = fake_language_server
524 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
525 assert_eq!(
526 params.text_document.uri,
527 lsp::Url::from_file_path("/a/main.rs").unwrap(),
528 );
529 assert_eq!(params.range.start, lsp::Position::new(1, 31));
530 assert_eq!(params.range.end, lsp::Position::new(1, 31));
531
532 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
533 lsp::CodeAction {
534 title: "Inline into all callers".to_string(),
535 edit: Some(lsp::WorkspaceEdit {
536 changes: Some(
537 [
538 (
539 lsp::Url::from_file_path("/a/main.rs").unwrap(),
540 vec![lsp::TextEdit::new(
541 lsp::Range::new(
542 lsp::Position::new(1, 22),
543 lsp::Position::new(1, 34),
544 ),
545 "4".to_string(),
546 )],
547 ),
548 (
549 lsp::Url::from_file_path("/a/other.rs").unwrap(),
550 vec![lsp::TextEdit::new(
551 lsp::Range::new(
552 lsp::Position::new(0, 0),
553 lsp::Position::new(0, 27),
554 ),
555 "".to_string(),
556 )],
557 ),
558 ]
559 .into_iter()
560 .collect(),
561 ),
562 ..Default::default()
563 }),
564 data: Some(json!({
565 "codeActionParams": {
566 "range": {
567 "start": {"line": 1, "column": 31},
568 "end": {"line": 1, "column": 31},
569 }
570 }
571 })),
572 ..Default::default()
573 },
574 )]))
575 });
576 cx_a.background_executor
577 .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
578 requests.next().await;
579
580 // Toggle code actions and wait for them to display.
581 editor_b.update(cx_b, |editor, cx| {
582 editor.toggle_code_actions(
583 &ToggleCodeActions {
584 deployed_from_indicator: false,
585 },
586 cx,
587 );
588 });
589 cx_a.background_executor.run_until_parked();
590
591 editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible()));
592
593 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
594
595 // Confirming the code action will trigger a resolve request.
596 let confirm_action = editor_b
597 .update(cx_b, |editor, cx| {
598 Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
599 })
600 .unwrap();
601 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
602 |_, _| async move {
603 Ok(lsp::CodeAction {
604 title: "Inline into all callers".to_string(),
605 edit: Some(lsp::WorkspaceEdit {
606 changes: Some(
607 [
608 (
609 lsp::Url::from_file_path("/a/main.rs").unwrap(),
610 vec![lsp::TextEdit::new(
611 lsp::Range::new(
612 lsp::Position::new(1, 22),
613 lsp::Position::new(1, 34),
614 ),
615 "4".to_string(),
616 )],
617 ),
618 (
619 lsp::Url::from_file_path("/a/other.rs").unwrap(),
620 vec![lsp::TextEdit::new(
621 lsp::Range::new(
622 lsp::Position::new(0, 0),
623 lsp::Position::new(0, 27),
624 ),
625 "".to_string(),
626 )],
627 ),
628 ]
629 .into_iter()
630 .collect(),
631 ),
632 ..Default::default()
633 }),
634 ..Default::default()
635 })
636 },
637 );
638
639 // After the action is confirmed, an editor containing both modified files is opened.
640 confirm_action.await.unwrap();
641
642 let code_action_editor = workspace_b.update(cx_b, |workspace, cx| {
643 workspace
644 .active_item(cx)
645 .unwrap()
646 .downcast::<Editor>()
647 .unwrap()
648 });
649 code_action_editor.update(cx_b, |editor, cx| {
650 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
651 editor.undo(&Undo, cx);
652 assert_eq!(
653 editor.text(cx),
654 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
655 );
656 editor.redo(&Redo, cx);
657 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
658 });
659}
660
661#[gpui::test(iterations = 10)]
662async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
663 let mut server = TestServer::start(cx_a.executor()).await;
664 let client_a = server.create_client(cx_a, "user_a").await;
665 let client_b = server.create_client(cx_b, "user_b").await;
666 server
667 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
668 .await;
669 let active_call_a = cx_a.read(ActiveCall::global);
670
671 cx_b.update(editor::init);
672
673 // Set up a fake language server.
674 let mut language = Language::new(
675 LanguageConfig {
676 name: "Rust".into(),
677 matcher: LanguageMatcher {
678 path_suffixes: vec!["rs".to_string()],
679 ..Default::default()
680 },
681 ..Default::default()
682 },
683 Some(tree_sitter_rust::language()),
684 );
685 let mut fake_language_servers = language
686 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
687 capabilities: lsp::ServerCapabilities {
688 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
689 prepare_provider: Some(true),
690 work_done_progress_options: Default::default(),
691 })),
692 ..Default::default()
693 },
694 ..Default::default()
695 }))
696 .await;
697 client_a.language_registry().add(Arc::new(language));
698
699 client_a
700 .fs()
701 .insert_tree(
702 "/dir",
703 json!({
704 "one.rs": "const ONE: usize = 1;",
705 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
706 }),
707 )
708 .await;
709 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
710 let project_id = active_call_a
711 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
712 .await
713 .unwrap();
714 let project_b = client_b.build_remote_project(project_id, cx_b).await;
715
716 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
717 let editor_b = workspace_b
718 .update(cx_b, |workspace, cx| {
719 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
720 })
721 .await
722 .unwrap()
723 .downcast::<Editor>()
724 .unwrap();
725 let fake_language_server = fake_language_servers.next().await.unwrap();
726
727 // Move cursor to a location that can be renamed.
728 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
729 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
730 editor.rename(&Rename, cx).unwrap()
731 });
732
733 fake_language_server
734 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
735 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
736 assert_eq!(params.position, lsp::Position::new(0, 7));
737 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
738 lsp::Position::new(0, 6),
739 lsp::Position::new(0, 9),
740 ))))
741 })
742 .next()
743 .await
744 .unwrap();
745 prepare_rename.await.unwrap();
746 editor_b.update(cx_b, |editor, cx| {
747 use editor::ToOffset;
748 let rename = editor.pending_rename().unwrap();
749 let buffer = editor.buffer().read(cx).snapshot(cx);
750 assert_eq!(
751 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
752 6..9
753 );
754 rename.editor.update(cx, |rename_editor, cx| {
755 rename_editor.buffer().update(cx, |rename_buffer, cx| {
756 rename_buffer.edit([(0..3, "THREE")], None, cx);
757 });
758 });
759 });
760
761 let confirm_rename = editor_b.update(cx_b, |editor, cx| {
762 Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
763 });
764 fake_language_server
765 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
766 assert_eq!(
767 params.text_document_position.text_document.uri.as_str(),
768 "file:///dir/one.rs"
769 );
770 assert_eq!(
771 params.text_document_position.position,
772 lsp::Position::new(0, 6)
773 );
774 assert_eq!(params.new_name, "THREE");
775 Ok(Some(lsp::WorkspaceEdit {
776 changes: Some(
777 [
778 (
779 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
780 vec![lsp::TextEdit::new(
781 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
782 "THREE".to_string(),
783 )],
784 ),
785 (
786 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
787 vec![
788 lsp::TextEdit::new(
789 lsp::Range::new(
790 lsp::Position::new(0, 24),
791 lsp::Position::new(0, 27),
792 ),
793 "THREE".to_string(),
794 ),
795 lsp::TextEdit::new(
796 lsp::Range::new(
797 lsp::Position::new(0, 35),
798 lsp::Position::new(0, 38),
799 ),
800 "THREE".to_string(),
801 ),
802 ],
803 ),
804 ]
805 .into_iter()
806 .collect(),
807 ),
808 ..Default::default()
809 }))
810 })
811 .next()
812 .await
813 .unwrap();
814 confirm_rename.await.unwrap();
815
816 let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
817 workspace.active_item_as::<Editor>(cx).unwrap()
818 });
819
820 rename_editor.update(cx_b, |editor, cx| {
821 assert_eq!(
822 editor.text(cx),
823 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
824 );
825 editor.undo(&Undo, cx);
826 assert_eq!(
827 editor.text(cx),
828 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
829 );
830 editor.redo(&Redo, cx);
831 assert_eq!(
832 editor.text(cx),
833 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
834 );
835 });
836
837 // Ensure temporary rename edits cannot be undone/redone.
838 editor_b.update(cx_b, |editor, cx| {
839 editor.undo(&Undo, cx);
840 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
841 editor.undo(&Undo, cx);
842 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
843 editor.redo(&Redo, cx);
844 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
845 })
846}
847
848#[gpui::test(iterations = 10)]
849async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
850 let mut server = TestServer::start(cx_a.executor()).await;
851 let executor = cx_a.executor();
852 let client_a = server.create_client(cx_a, "user_a").await;
853 let client_b = server.create_client(cx_b, "user_b").await;
854 server
855 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
856 .await;
857 let active_call_a = cx_a.read(ActiveCall::global);
858
859 cx_b.update(editor::init);
860
861 // Set up a fake language server.
862 let mut language = Language::new(
863 LanguageConfig {
864 name: "Rust".into(),
865 matcher: LanguageMatcher {
866 path_suffixes: vec!["rs".to_string()],
867 ..Default::default()
868 },
869 ..Default::default()
870 },
871 Some(tree_sitter_rust::language()),
872 );
873 let mut fake_language_servers = language
874 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
875 name: "the-language-server",
876 ..Default::default()
877 }))
878 .await;
879 client_a.language_registry().add(Arc::new(language));
880
881 client_a
882 .fs()
883 .insert_tree(
884 "/dir",
885 json!({
886 "main.rs": "const ONE: usize = 1;",
887 }),
888 )
889 .await;
890 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
891
892 let _buffer_a = project_a
893 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
894 .await
895 .unwrap();
896
897 let fake_language_server = fake_language_servers.next().await.unwrap();
898 fake_language_server.start_progress("the-token").await;
899 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
900 token: lsp::NumberOrString::String("the-token".to_string()),
901 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
902 lsp::WorkDoneProgressReport {
903 message: Some("the-message".to_string()),
904 ..Default::default()
905 },
906 )),
907 });
908 executor.run_until_parked();
909
910 project_a.read_with(cx_a, |project, _| {
911 let status = project.language_server_statuses().next().unwrap();
912 assert_eq!(status.name, "the-language-server");
913 assert_eq!(status.pending_work.len(), 1);
914 assert_eq!(
915 status.pending_work["the-token"].message.as_ref().unwrap(),
916 "the-message"
917 );
918 });
919
920 let project_id = active_call_a
921 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
922 .await
923 .unwrap();
924 executor.run_until_parked();
925 let project_b = client_b.build_remote_project(project_id, cx_b).await;
926
927 project_b.read_with(cx_b, |project, _| {
928 let status = project.language_server_statuses().next().unwrap();
929 assert_eq!(status.name, "the-language-server");
930 });
931
932 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
933 token: lsp::NumberOrString::String("the-token".to_string()),
934 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
935 lsp::WorkDoneProgressReport {
936 message: Some("the-message-2".to_string()),
937 ..Default::default()
938 },
939 )),
940 });
941 executor.run_until_parked();
942
943 project_a.read_with(cx_a, |project, _| {
944 let status = project.language_server_statuses().next().unwrap();
945 assert_eq!(status.name, "the-language-server");
946 assert_eq!(status.pending_work.len(), 1);
947 assert_eq!(
948 status.pending_work["the-token"].message.as_ref().unwrap(),
949 "the-message-2"
950 );
951 });
952
953 project_b.read_with(cx_b, |project, _| {
954 let status = project.language_server_statuses().next().unwrap();
955 assert_eq!(status.name, "the-language-server");
956 assert_eq!(status.pending_work.len(), 1);
957 assert_eq!(
958 status.pending_work["the-token"].message.as_ref().unwrap(),
959 "the-message-2"
960 );
961 });
962}
963
964#[gpui::test(iterations = 10)]
965async fn test_share_project(
966 cx_a: &mut TestAppContext,
967 cx_b: &mut TestAppContext,
968 cx_c: &mut TestAppContext,
969) {
970 let executor = cx_a.executor();
971 let cx_b = cx_b.add_empty_window();
972 let mut server = TestServer::start(executor.clone()).await;
973 let client_a = server.create_client(cx_a, "user_a").await;
974 let client_b = server.create_client(cx_b, "user_b").await;
975 let client_c = server.create_client(cx_c, "user_c").await;
976 server
977 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
978 .await;
979 let active_call_a = cx_a.read(ActiveCall::global);
980 let active_call_b = cx_b.read(ActiveCall::global);
981 let active_call_c = cx_c.read(ActiveCall::global);
982
983 client_a
984 .fs()
985 .insert_tree(
986 "/a",
987 json!({
988 ".gitignore": "ignored-dir",
989 "a.txt": "a-contents",
990 "b.txt": "b-contents",
991 "ignored-dir": {
992 "c.txt": "",
993 "d.txt": "",
994 }
995 }),
996 )
997 .await;
998
999 // Invite client B to collaborate on a project
1000 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1001 active_call_a
1002 .update(cx_a, |call, cx| {
1003 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1004 })
1005 .await
1006 .unwrap();
1007
1008 // Join that project as client B
1009
1010 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1011 executor.run_until_parked();
1012 let call = incoming_call_b.borrow().clone().unwrap();
1013 assert_eq!(call.calling_user.github_login, "user_a");
1014 let initial_project = call.initial_project.unwrap();
1015 active_call_b
1016 .update(cx_b, |call, cx| call.accept_incoming(cx))
1017 .await
1018 .unwrap();
1019 let client_b_peer_id = client_b.peer_id().unwrap();
1020 let project_b = client_b
1021 .build_remote_project(initial_project.id, cx_b)
1022 .await;
1023
1024 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1025
1026 executor.run_until_parked();
1027
1028 project_a.read_with(cx_a, |project, _| {
1029 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1030 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1031 });
1032
1033 project_b.read_with(cx_b, |project, cx| {
1034 let worktree = project.worktrees().next().unwrap().read(cx);
1035 assert_eq!(
1036 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1037 [
1038 Path::new(".gitignore"),
1039 Path::new("a.txt"),
1040 Path::new("b.txt"),
1041 Path::new("ignored-dir"),
1042 ]
1043 );
1044 });
1045
1046 project_b
1047 .update(cx_b, |project, cx| {
1048 let worktree = project.worktrees().next().unwrap();
1049 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1050 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1051 })
1052 .await
1053 .unwrap();
1054
1055 project_b.read_with(cx_b, |project, cx| {
1056 let worktree = project.worktrees().next().unwrap().read(cx);
1057 assert_eq!(
1058 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1059 [
1060 Path::new(".gitignore"),
1061 Path::new("a.txt"),
1062 Path::new("b.txt"),
1063 Path::new("ignored-dir"),
1064 Path::new("ignored-dir/c.txt"),
1065 Path::new("ignored-dir/d.txt"),
1066 ]
1067 );
1068 });
1069
1070 // Open the same file as client B and client A.
1071 let buffer_b = project_b
1072 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1073 .await
1074 .unwrap();
1075
1076 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1077
1078 project_a.read_with(cx_a, |project, cx| {
1079 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1080 });
1081 let buffer_a = project_a
1082 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1083 .await
1084 .unwrap();
1085
1086 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
1087
1088 // Client A sees client B's selection
1089 executor.run_until_parked();
1090
1091 buffer_a.read_with(cx_a, |buffer, _| {
1092 buffer
1093 .snapshot()
1094 .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1095 .count()
1096 == 1
1097 });
1098
1099 // Edit the buffer as client B and see that edit as client A.
1100 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
1101 executor.run_until_parked();
1102
1103 buffer_a.read_with(cx_a, |buffer, _| {
1104 assert_eq!(buffer.text(), "ok, b-contents")
1105 });
1106
1107 // Client B can invite client C on a project shared by client A.
1108 active_call_b
1109 .update(cx_b, |call, cx| {
1110 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1111 })
1112 .await
1113 .unwrap();
1114
1115 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1116 executor.run_until_parked();
1117 let call = incoming_call_c.borrow().clone().unwrap();
1118 assert_eq!(call.calling_user.github_login, "user_b");
1119 let initial_project = call.initial_project.unwrap();
1120 active_call_c
1121 .update(cx_c, |call, cx| call.accept_incoming(cx))
1122 .await
1123 .unwrap();
1124 let _project_c = client_c
1125 .build_remote_project(initial_project.id, cx_c)
1126 .await;
1127
1128 // Client B closes the editor, and client A sees client B's selections removed.
1129 cx_b.update(move |_| drop(editor_b));
1130 executor.run_until_parked();
1131
1132 buffer_a.read_with(cx_a, |buffer, _| {
1133 buffer
1134 .snapshot()
1135 .remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
1136 .count()
1137 == 0
1138 });
1139}
1140
1141#[gpui::test(iterations = 10)]
1142async fn test_on_input_format_from_host_to_guest(
1143 cx_a: &mut TestAppContext,
1144 cx_b: &mut TestAppContext,
1145) {
1146 let mut server = TestServer::start(cx_a.executor()).await;
1147 let executor = cx_a.executor();
1148 let client_a = server.create_client(cx_a, "user_a").await;
1149 let client_b = server.create_client(cx_b, "user_b").await;
1150 server
1151 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1152 .await;
1153 let active_call_a = cx_a.read(ActiveCall::global);
1154
1155 // Set up a fake language server.
1156 let mut language = Language::new(
1157 LanguageConfig {
1158 name: "Rust".into(),
1159 matcher: LanguageMatcher {
1160 path_suffixes: vec!["rs".to_string()],
1161 ..Default::default()
1162 },
1163 ..Default::default()
1164 },
1165 Some(tree_sitter_rust::language()),
1166 );
1167 let mut fake_language_servers = language
1168 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1169 capabilities: lsp::ServerCapabilities {
1170 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1171 first_trigger_character: ":".to_string(),
1172 more_trigger_character: Some(vec![">".to_string()]),
1173 }),
1174 ..Default::default()
1175 },
1176 ..Default::default()
1177 }))
1178 .await;
1179 client_a.language_registry().add(Arc::new(language));
1180
1181 client_a
1182 .fs()
1183 .insert_tree(
1184 "/a",
1185 json!({
1186 "main.rs": "fn main() { a }",
1187 "other.rs": "// Test file",
1188 }),
1189 )
1190 .await;
1191 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1192 let project_id = active_call_a
1193 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1194 .await
1195 .unwrap();
1196 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1197
1198 // Open a file in an editor as the host.
1199 let buffer_a = project_a
1200 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1201 .await
1202 .unwrap();
1203 let cx_a = cx_a.add_empty_window();
1204 let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
1205
1206 let fake_language_server = fake_language_servers.next().await.unwrap();
1207 executor.run_until_parked();
1208
1209 // Receive an OnTypeFormatting request as the host's language server.
1210 // Return some formatting from the host's language server.
1211 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1212 |params, _| async move {
1213 assert_eq!(
1214 params.text_document_position.text_document.uri,
1215 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1216 );
1217 assert_eq!(
1218 params.text_document_position.position,
1219 lsp::Position::new(0, 14),
1220 );
1221
1222 Ok(Some(vec![lsp::TextEdit {
1223 new_text: "~<".to_string(),
1224 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1225 }]))
1226 },
1227 );
1228
1229 // Open the buffer on the guest and see that the formatting worked
1230 let buffer_b = project_b
1231 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1232 .await
1233 .unwrap();
1234
1235 // Type a on type formatting trigger character as the guest.
1236 cx_a.focus_view(&editor_a);
1237 editor_a.update(cx_a, |editor, cx| {
1238 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1239 editor.handle_input(">", cx);
1240 });
1241
1242 executor.run_until_parked();
1243
1244 buffer_b.read_with(cx_b, |buffer, _| {
1245 assert_eq!(buffer.text(), "fn main() { a>~< }")
1246 });
1247
1248 // Undo should remove LSP edits first
1249 editor_a.update(cx_a, |editor, cx| {
1250 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1251 editor.undo(&Undo, cx);
1252 assert_eq!(editor.text(cx), "fn main() { a> }");
1253 });
1254 executor.run_until_parked();
1255
1256 buffer_b.read_with(cx_b, |buffer, _| {
1257 assert_eq!(buffer.text(), "fn main() { a> }")
1258 });
1259
1260 editor_a.update(cx_a, |editor, cx| {
1261 assert_eq!(editor.text(cx), "fn main() { a> }");
1262 editor.undo(&Undo, cx);
1263 assert_eq!(editor.text(cx), "fn main() { a }");
1264 });
1265 executor.run_until_parked();
1266
1267 buffer_b.read_with(cx_b, |buffer, _| {
1268 assert_eq!(buffer.text(), "fn main() { a }")
1269 });
1270}
1271
1272#[gpui::test(iterations = 10)]
1273async fn test_on_input_format_from_guest_to_host(
1274 cx_a: &mut TestAppContext,
1275 cx_b: &mut TestAppContext,
1276) {
1277 let mut server = TestServer::start(cx_a.executor()).await;
1278 let executor = cx_a.executor();
1279 let client_a = server.create_client(cx_a, "user_a").await;
1280 let client_b = server.create_client(cx_b, "user_b").await;
1281 server
1282 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1283 .await;
1284 let active_call_a = cx_a.read(ActiveCall::global);
1285
1286 // Set up a fake language server.
1287 let mut language = Language::new(
1288 LanguageConfig {
1289 name: "Rust".into(),
1290 matcher: LanguageMatcher {
1291 path_suffixes: vec!["rs".to_string()],
1292 ..Default::default()
1293 },
1294 ..Default::default()
1295 },
1296 Some(tree_sitter_rust::language()),
1297 );
1298 let mut fake_language_servers = language
1299 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1300 capabilities: lsp::ServerCapabilities {
1301 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1302 first_trigger_character: ":".to_string(),
1303 more_trigger_character: Some(vec![">".to_string()]),
1304 }),
1305 ..Default::default()
1306 },
1307 ..Default::default()
1308 }))
1309 .await;
1310 client_a.language_registry().add(Arc::new(language));
1311
1312 client_a
1313 .fs()
1314 .insert_tree(
1315 "/a",
1316 json!({
1317 "main.rs": "fn main() { a }",
1318 "other.rs": "// Test file",
1319 }),
1320 )
1321 .await;
1322 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1323 let project_id = active_call_a
1324 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1325 .await
1326 .unwrap();
1327 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1328
1329 // Open a file in an editor as the guest.
1330 let buffer_b = project_b
1331 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1332 .await
1333 .unwrap();
1334 let cx_b = cx_b.add_empty_window();
1335 let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
1336
1337 let fake_language_server = fake_language_servers.next().await.unwrap();
1338 executor.run_until_parked();
1339
1340 // Type a on type formatting trigger character as the guest.
1341 cx_b.focus_view(&editor_b);
1342 editor_b.update(cx_b, |editor, cx| {
1343 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1344 editor.handle_input(":", cx);
1345 });
1346
1347 // Receive an OnTypeFormatting request as the host's language server.
1348 // Return some formatting from the host's language server.
1349 executor.start_waiting();
1350 fake_language_server
1351 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1352 assert_eq!(
1353 params.text_document_position.text_document.uri,
1354 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1355 );
1356 assert_eq!(
1357 params.text_document_position.position,
1358 lsp::Position::new(0, 14),
1359 );
1360
1361 Ok(Some(vec![lsp::TextEdit {
1362 new_text: "~:".to_string(),
1363 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1364 }]))
1365 })
1366 .next()
1367 .await
1368 .unwrap();
1369 executor.finish_waiting();
1370
1371 // Open the buffer on the host and see that the formatting worked
1372 let buffer_a = project_a
1373 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1374 .await
1375 .unwrap();
1376 executor.run_until_parked();
1377
1378 buffer_a.read_with(cx_a, |buffer, _| {
1379 assert_eq!(buffer.text(), "fn main() { a:~: }")
1380 });
1381
1382 // Undo should remove LSP edits first
1383 editor_b.update(cx_b, |editor, cx| {
1384 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1385 editor.undo(&Undo, cx);
1386 assert_eq!(editor.text(cx), "fn main() { a: }");
1387 });
1388 executor.run_until_parked();
1389
1390 buffer_a.read_with(cx_a, |buffer, _| {
1391 assert_eq!(buffer.text(), "fn main() { a: }")
1392 });
1393
1394 editor_b.update(cx_b, |editor, cx| {
1395 assert_eq!(editor.text(cx), "fn main() { a: }");
1396 editor.undo(&Undo, cx);
1397 assert_eq!(editor.text(cx), "fn main() { a }");
1398 });
1399 executor.run_until_parked();
1400
1401 buffer_a.read_with(cx_a, |buffer, _| {
1402 assert_eq!(buffer.text(), "fn main() { a }")
1403 });
1404}
1405
1406#[gpui::test(iterations = 10)]
1407async fn test_mutual_editor_inlay_hint_cache_update(
1408 cx_a: &mut TestAppContext,
1409 cx_b: &mut TestAppContext,
1410) {
1411 let mut server = TestServer::start(cx_a.executor()).await;
1412 let executor = cx_a.executor();
1413 let client_a = server.create_client(cx_a, "user_a").await;
1414 let client_b = server.create_client(cx_b, "user_b").await;
1415 server
1416 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1417 .await;
1418 let active_call_a = cx_a.read(ActiveCall::global);
1419 let active_call_b = cx_b.read(ActiveCall::global);
1420
1421 cx_a.update(editor::init);
1422 cx_b.update(editor::init);
1423
1424 cx_a.update(|cx| {
1425 cx.update_global(|store: &mut SettingsStore, cx| {
1426 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1427 settings.defaults.inlay_hints = Some(InlayHintSettings {
1428 enabled: true,
1429 show_type_hints: true,
1430 show_parameter_hints: false,
1431 show_other_hints: true,
1432 })
1433 });
1434 });
1435 });
1436 cx_b.update(|cx| {
1437 cx.update_global(|store: &mut SettingsStore, cx| {
1438 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1439 settings.defaults.inlay_hints = Some(InlayHintSettings {
1440 enabled: true,
1441 show_type_hints: true,
1442 show_parameter_hints: false,
1443 show_other_hints: true,
1444 })
1445 });
1446 });
1447 });
1448
1449 let mut language = Language::new(
1450 LanguageConfig {
1451 name: "Rust".into(),
1452 matcher: LanguageMatcher {
1453 path_suffixes: vec!["rs".to_string()],
1454 ..Default::default()
1455 },
1456 ..Default::default()
1457 },
1458 Some(tree_sitter_rust::language()),
1459 );
1460 let mut fake_language_servers = language
1461 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1462 capabilities: lsp::ServerCapabilities {
1463 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1464 ..Default::default()
1465 },
1466 ..Default::default()
1467 }))
1468 .await;
1469 let language = Arc::new(language);
1470 client_a.language_registry().add(Arc::clone(&language));
1471 client_b.language_registry().add(language);
1472
1473 // Client A opens a project.
1474 client_a
1475 .fs()
1476 .insert_tree(
1477 "/a",
1478 json!({
1479 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1480 "other.rs": "// Test file",
1481 }),
1482 )
1483 .await;
1484 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1485 active_call_a
1486 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1487 .await
1488 .unwrap();
1489 let project_id = active_call_a
1490 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1491 .await
1492 .unwrap();
1493
1494 // Client B joins the project
1495 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1496 active_call_b
1497 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1498 .await
1499 .unwrap();
1500
1501 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1502 executor.start_waiting();
1503
1504 // The host opens a rust file.
1505 let _buffer_a = project_a
1506 .update(cx_a, |project, cx| {
1507 project.open_local_buffer("/a/main.rs", cx)
1508 })
1509 .await
1510 .unwrap();
1511 let fake_language_server = fake_language_servers.next().await.unwrap();
1512 let editor_a = workspace_a
1513 .update(cx_a, |workspace, cx| {
1514 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1515 })
1516 .await
1517 .unwrap()
1518 .downcast::<Editor>()
1519 .unwrap();
1520
1521 // Set up the language server to return an additional inlay hint on each request.
1522 let edits_made = Arc::new(AtomicUsize::new(0));
1523 let closure_edits_made = Arc::clone(&edits_made);
1524 fake_language_server
1525 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1526 let task_edits_made = Arc::clone(&closure_edits_made);
1527 async move {
1528 assert_eq!(
1529 params.text_document.uri,
1530 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1531 );
1532 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1533 Ok(Some(vec![lsp::InlayHint {
1534 position: lsp::Position::new(0, edits_made as u32),
1535 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1536 kind: None,
1537 text_edits: None,
1538 tooltip: None,
1539 padding_left: None,
1540 padding_right: None,
1541 data: None,
1542 }]))
1543 }
1544 })
1545 .next()
1546 .await
1547 .unwrap();
1548
1549 executor.run_until_parked();
1550
1551 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1552 editor_a.update(cx_a, |editor, _| {
1553 assert_eq!(
1554 vec![initial_edit.to_string()],
1555 extract_hint_labels(editor),
1556 "Host should get its first hints when opens an editor"
1557 );
1558 let inlay_cache = editor.inlay_hint_cache();
1559 assert_eq!(
1560 inlay_cache.version(),
1561 1,
1562 "Host editor update the cache version after every cache/view change",
1563 );
1564 });
1565 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1566 let editor_b = workspace_b
1567 .update(cx_b, |workspace, cx| {
1568 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1569 })
1570 .await
1571 .unwrap()
1572 .downcast::<Editor>()
1573 .unwrap();
1574
1575 executor.run_until_parked();
1576 editor_b.update(cx_b, |editor, _| {
1577 assert_eq!(
1578 vec![initial_edit.to_string()],
1579 extract_hint_labels(editor),
1580 "Client should get its first hints when opens an editor"
1581 );
1582 let inlay_cache = editor.inlay_hint_cache();
1583 assert_eq!(
1584 inlay_cache.version(),
1585 1,
1586 "Guest editor update the cache version after every cache/view change"
1587 );
1588 });
1589
1590 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1591 editor_b.update(cx_b, |editor, cx| {
1592 editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
1593 editor.handle_input(":", cx);
1594 });
1595 cx_b.focus_view(&editor_b);
1596
1597 executor.run_until_parked();
1598 editor_a.update(cx_a, |editor, _| {
1599 assert_eq!(
1600 vec![after_client_edit.to_string()],
1601 extract_hint_labels(editor),
1602 );
1603 let inlay_cache = editor.inlay_hint_cache();
1604 assert_eq!(inlay_cache.version(), 2);
1605 });
1606 editor_b.update(cx_b, |editor, _| {
1607 assert_eq!(
1608 vec![after_client_edit.to_string()],
1609 extract_hint_labels(editor),
1610 );
1611 let inlay_cache = editor.inlay_hint_cache();
1612 assert_eq!(inlay_cache.version(), 2);
1613 });
1614
1615 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1616 editor_a.update(cx_a, |editor, cx| {
1617 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1618 editor.handle_input("a change to increment both buffers' versions", cx);
1619 });
1620 cx_a.focus_view(&editor_a);
1621
1622 executor.run_until_parked();
1623 editor_a.update(cx_a, |editor, _| {
1624 assert_eq!(
1625 vec![after_host_edit.to_string()],
1626 extract_hint_labels(editor),
1627 );
1628 let inlay_cache = editor.inlay_hint_cache();
1629 assert_eq!(inlay_cache.version(), 3);
1630 });
1631 editor_b.update(cx_b, |editor, _| {
1632 assert_eq!(
1633 vec![after_host_edit.to_string()],
1634 extract_hint_labels(editor),
1635 );
1636 let inlay_cache = editor.inlay_hint_cache();
1637 assert_eq!(inlay_cache.version(), 3);
1638 });
1639
1640 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1641 fake_language_server
1642 .request::<lsp::request::InlayHintRefreshRequest>(())
1643 .await
1644 .expect("inlay refresh request failed");
1645
1646 executor.run_until_parked();
1647 editor_a.update(cx_a, |editor, _| {
1648 assert_eq!(
1649 vec![after_special_edit_for_refresh.to_string()],
1650 extract_hint_labels(editor),
1651 "Host should react to /refresh LSP request"
1652 );
1653 let inlay_cache = editor.inlay_hint_cache();
1654 assert_eq!(
1655 inlay_cache.version(),
1656 4,
1657 "Host should accepted all edits and bump its cache version every time"
1658 );
1659 });
1660 editor_b.update(cx_b, |editor, _| {
1661 assert_eq!(
1662 vec![after_special_edit_for_refresh.to_string()],
1663 extract_hint_labels(editor),
1664 "Guest should get a /refresh LSP request propagated by host"
1665 );
1666 let inlay_cache = editor.inlay_hint_cache();
1667 assert_eq!(
1668 inlay_cache.version(),
1669 4,
1670 "Guest should accepted all edits and bump its cache version every time"
1671 );
1672 });
1673}
1674
1675#[gpui::test(iterations = 10)]
1676async fn test_inlay_hint_refresh_is_forwarded(
1677 cx_a: &mut TestAppContext,
1678 cx_b: &mut TestAppContext,
1679) {
1680 let mut server = TestServer::start(cx_a.executor()).await;
1681 let executor = cx_a.executor();
1682 let client_a = server.create_client(cx_a, "user_a").await;
1683 let client_b = server.create_client(cx_b, "user_b").await;
1684 server
1685 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1686 .await;
1687 let active_call_a = cx_a.read(ActiveCall::global);
1688 let active_call_b = cx_b.read(ActiveCall::global);
1689
1690 cx_a.update(editor::init);
1691 cx_b.update(editor::init);
1692
1693 cx_a.update(|cx| {
1694 cx.update_global(|store: &mut SettingsStore, cx| {
1695 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1696 settings.defaults.inlay_hints = Some(InlayHintSettings {
1697 enabled: false,
1698 show_type_hints: false,
1699 show_parameter_hints: false,
1700 show_other_hints: false,
1701 })
1702 });
1703 });
1704 });
1705 cx_b.update(|cx| {
1706 cx.update_global(|store: &mut SettingsStore, cx| {
1707 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1708 settings.defaults.inlay_hints = Some(InlayHintSettings {
1709 enabled: true,
1710 show_type_hints: true,
1711 show_parameter_hints: true,
1712 show_other_hints: true,
1713 })
1714 });
1715 });
1716 });
1717
1718 let mut language = Language::new(
1719 LanguageConfig {
1720 name: "Rust".into(),
1721 matcher: LanguageMatcher {
1722 path_suffixes: vec!["rs".to_string()],
1723 ..Default::default()
1724 },
1725 ..Default::default()
1726 },
1727 Some(tree_sitter_rust::language()),
1728 );
1729 let mut fake_language_servers = language
1730 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1731 capabilities: lsp::ServerCapabilities {
1732 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1733 ..Default::default()
1734 },
1735 ..Default::default()
1736 }))
1737 .await;
1738 let language = Arc::new(language);
1739 client_a.language_registry().add(Arc::clone(&language));
1740 client_b.language_registry().add(language);
1741
1742 client_a
1743 .fs()
1744 .insert_tree(
1745 "/a",
1746 json!({
1747 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1748 "other.rs": "// Test file",
1749 }),
1750 )
1751 .await;
1752 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1753 active_call_a
1754 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1755 .await
1756 .unwrap();
1757 let project_id = active_call_a
1758 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1759 .await
1760 .unwrap();
1761
1762 let project_b = client_b.build_remote_project(project_id, cx_b).await;
1763 active_call_b
1764 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1765 .await
1766 .unwrap();
1767
1768 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1769 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1770
1771 cx_a.background_executor.start_waiting();
1772
1773 let editor_a = workspace_a
1774 .update(cx_a, |workspace, cx| {
1775 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1776 })
1777 .await
1778 .unwrap()
1779 .downcast::<Editor>()
1780 .unwrap();
1781
1782 let editor_b = workspace_b
1783 .update(cx_b, |workspace, cx| {
1784 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
1785 })
1786 .await
1787 .unwrap()
1788 .downcast::<Editor>()
1789 .unwrap();
1790
1791 let other_hints = Arc::new(AtomicBool::new(false));
1792 let fake_language_server = fake_language_servers.next().await.unwrap();
1793 let closure_other_hints = Arc::clone(&other_hints);
1794 fake_language_server
1795 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1796 let task_other_hints = Arc::clone(&closure_other_hints);
1797 async move {
1798 assert_eq!(
1799 params.text_document.uri,
1800 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1801 );
1802 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1803 let character = if other_hints { 0 } else { 2 };
1804 let label = if other_hints {
1805 "other hint"
1806 } else {
1807 "initial hint"
1808 };
1809 Ok(Some(vec![lsp::InlayHint {
1810 position: lsp::Position::new(0, character),
1811 label: lsp::InlayHintLabel::String(label.to_string()),
1812 kind: None,
1813 text_edits: None,
1814 tooltip: None,
1815 padding_left: None,
1816 padding_right: None,
1817 data: None,
1818 }]))
1819 }
1820 })
1821 .next()
1822 .await
1823 .unwrap();
1824 executor.finish_waiting();
1825
1826 executor.run_until_parked();
1827 editor_a.update(cx_a, |editor, _| {
1828 assert!(
1829 extract_hint_labels(editor).is_empty(),
1830 "Host should get no hints due to them turned off"
1831 );
1832 let inlay_cache = editor.inlay_hint_cache();
1833 assert_eq!(
1834 inlay_cache.version(),
1835 0,
1836 "Turned off hints should not generate version updates"
1837 );
1838 });
1839
1840 executor.run_until_parked();
1841 editor_b.update(cx_b, |editor, _| {
1842 assert_eq!(
1843 vec!["initial hint".to_string()],
1844 extract_hint_labels(editor),
1845 "Client should get its first hints when opens an editor"
1846 );
1847 let inlay_cache = editor.inlay_hint_cache();
1848 assert_eq!(
1849 inlay_cache.version(),
1850 1,
1851 "Should update cache version after first hints"
1852 );
1853 });
1854
1855 other_hints.fetch_or(true, atomic::Ordering::Release);
1856 fake_language_server
1857 .request::<lsp::request::InlayHintRefreshRequest>(())
1858 .await
1859 .expect("inlay refresh request failed");
1860 executor.run_until_parked();
1861 editor_a.update(cx_a, |editor, _| {
1862 assert!(
1863 extract_hint_labels(editor).is_empty(),
1864 "Host should get nop hints due to them turned off, even after the /refresh"
1865 );
1866 let inlay_cache = editor.inlay_hint_cache();
1867 assert_eq!(
1868 inlay_cache.version(),
1869 0,
1870 "Turned off hints should not generate version updates, again"
1871 );
1872 });
1873
1874 executor.run_until_parked();
1875 editor_b.update(cx_b, |editor, _| {
1876 assert_eq!(
1877 vec!["other hint".to_string()],
1878 extract_hint_labels(editor),
1879 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1880 );
1881 let inlay_cache = editor.inlay_hint_cache();
1882 assert_eq!(
1883 inlay_cache.version(),
1884 2,
1885 "Guest should accepted all edits and bump its cache version every time"
1886 );
1887 });
1888}
1889
1890fn extract_hint_labels(editor: &Editor) -> Vec<String> {
1891 let mut labels = Vec::new();
1892 for hint in editor.inlay_hint_cache().hints() {
1893 match hint.label {
1894 project::InlayHintLabel::String(s) => labels.push(s),
1895 _ => unreachable!(),
1896 }
1897 }
1898 labels
1899}