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