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