1use crate::{
2 rpc::RECONNECT_TIMEOUT,
3 tests::{rust_lang, TestServer},
4};
5use call::ActiveCall;
6use collections::HashMap;
7use editor::{
8 actions::{
9 ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
10 ToggleCodeActions, Undo,
11 },
12 test::editor_test_context::{AssertionContextManager, EditorTestContext},
13 Editor, RowInfo,
14};
15use fs::Fs;
16use futures::StreamExt;
17use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
18use indoc::indoc;
19use language::{
20 language_settings::{AllLanguageSettings, InlayHintSettings},
21 FakeLspAdapter,
22};
23use project::{
24 project_settings::{InlineBlameSettings, ProjectSettings},
25 SERVER_PROGRESS_THROTTLE_TIMEOUT,
26};
27use recent_projects::disconnected_overlay::DisconnectedOverlay;
28use rpc::RECEIVE_TIMEOUT;
29use serde_json::json;
30use settings::SettingsStore;
31use std::{
32 ops::Range,
33 path::{Path, PathBuf},
34 sync::{
35 atomic::{self, AtomicBool, AtomicUsize},
36 Arc,
37 },
38};
39use text::Point;
40use workspace::{CloseIntent, Workspace};
41
42#[gpui::test(iterations = 10)]
43async fn test_host_disconnect(
44 cx_a: &mut TestAppContext,
45 cx_b: &mut TestAppContext,
46 cx_c: &mut TestAppContext,
47) {
48 let mut server = TestServer::start(cx_a.executor()).await;
49 let client_a = server.create_client(cx_a, "user_a").await;
50 let client_b = server.create_client(cx_b, "user_b").await;
51 let client_c = server.create_client(cx_c, "user_c").await;
52 server
53 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
54 .await;
55
56 cx_b.update(editor::init);
57 cx_b.update(recent_projects::init);
58
59 client_a
60 .fs()
61 .insert_tree(
62 "/a",
63 json!({
64 "a.txt": "a-contents",
65 "b.txt": "b-contents",
66 }),
67 )
68 .await;
69
70 let active_call_a = cx_a.read(ActiveCall::global);
71 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
72
73 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
74 let project_id = active_call_a
75 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
76 .await
77 .unwrap();
78
79 let project_b = client_b.join_remote_project(project_id, cx_b).await;
80 cx_a.background_executor.run_until_parked();
81
82 assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
83
84 let workspace_b = cx_b.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("/dir", json!({ "a.txt": "Some text\n" }))
195 .await;
196 let (project_a, worktree_id) = client_a.build_local_project("/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 "/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("/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 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
353 assert_eq!(
354 params.text_document_position.text_document.uri,
355 lsp::Url::from_file_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.handle_request::<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 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
470 assert_eq!(
471 params.text_document_position.text_document.uri,
472 lsp::Url::from_file_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 .handle_request::<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 "/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("/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 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
594 assert_eq!(
595 params.text_document.uri,
596 lsp::Url::from_file_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 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
616 assert_eq!(
617 params.text_document.uri,
618 lsp::Url::from_file_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("/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("/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.handle_request::<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("/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("/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 "/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("/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 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
817 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
818 assert_eq!(params.position, lsp::Position::new(0, 7));
819 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
820 lsp::Position::new(0, 6),
821 lsp::Position::new(0, 9),
822 ))))
823 })
824 .next()
825 .await
826 .unwrap();
827 prepare_rename.await.unwrap();
828 editor_b.update(cx_b, |editor, cx| {
829 use editor::ToOffset;
830 let rename = editor.pending_rename().unwrap();
831 let buffer = editor.buffer().read(cx).snapshot(cx);
832 assert_eq!(
833 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
834 6..9
835 );
836 rename.editor.update(cx, |rename_editor, cx| {
837 let rename_selection = rename_editor.selections.newest::<usize>(cx);
838 assert_eq!(
839 rename_selection.range(),
840 0..3,
841 "Rename that was triggered from zero selection caret, should propose the whole word."
842 );
843 rename_editor.buffer().update(cx, |rename_buffer, cx| {
844 rename_buffer.edit([(0..3, "THREE")], None, cx);
845 });
846 });
847 });
848
849 // Cancel the rename, and repeat the same, but use selections instead of cursor movement
850 editor_b.update_in(cx_b, |editor, window, cx| {
851 editor.cancel(&editor::actions::Cancel, window, cx);
852 });
853 let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
854 editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
855 editor.rename(&Rename, window, cx).unwrap()
856 });
857
858 fake_language_server
859 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
860 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
861 assert_eq!(params.position, lsp::Position::new(0, 8));
862 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
863 lsp::Position::new(0, 6),
864 lsp::Position::new(0, 9),
865 ))))
866 })
867 .next()
868 .await
869 .unwrap();
870 prepare_rename.await.unwrap();
871 editor_b.update(cx_b, |editor, cx| {
872 use editor::ToOffset;
873 let rename = editor.pending_rename().unwrap();
874 let buffer = editor.buffer().read(cx).snapshot(cx);
875 let lsp_rename_start = rename.range.start.to_offset(&buffer);
876 let lsp_rename_end = rename.range.end.to_offset(&buffer);
877 assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
878 rename.editor.update(cx, |rename_editor, cx| {
879 let rename_selection = rename_editor.selections.newest::<usize>(cx);
880 assert_eq!(
881 rename_selection.range(),
882 1..2,
883 "Rename that was triggered from a selection, should have the same selection range in the rename proposal"
884 );
885 rename_editor.buffer().update(cx, |rename_buffer, cx| {
886 rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
887 });
888 });
889 });
890
891 let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| {
892 Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap()
893 });
894 fake_language_server
895 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
896 assert_eq!(
897 params.text_document_position.text_document.uri.as_str(),
898 "file:///dir/one.rs"
899 );
900 assert_eq!(
901 params.text_document_position.position,
902 lsp::Position::new(0, 6)
903 );
904 assert_eq!(params.new_name, "THREE");
905 Ok(Some(lsp::WorkspaceEdit {
906 changes: Some(
907 [
908 (
909 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
910 vec![lsp::TextEdit::new(
911 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
912 "THREE".to_string(),
913 )],
914 ),
915 (
916 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
917 vec![
918 lsp::TextEdit::new(
919 lsp::Range::new(
920 lsp::Position::new(0, 24),
921 lsp::Position::new(0, 27),
922 ),
923 "THREE".to_string(),
924 ),
925 lsp::TextEdit::new(
926 lsp::Range::new(
927 lsp::Position::new(0, 35),
928 lsp::Position::new(0, 38),
929 ),
930 "THREE".to_string(),
931 ),
932 ],
933 ),
934 ]
935 .into_iter()
936 .collect(),
937 ),
938 ..Default::default()
939 }))
940 })
941 .next()
942 .await
943 .unwrap();
944 confirm_rename.await.unwrap();
945
946 let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
947 workspace.active_item_as::<Editor>(cx).unwrap()
948 });
949
950 rename_editor.update_in(cx_b, |editor, window, cx| {
951 assert_eq!(
952 editor.text(cx),
953 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
954 );
955 editor.undo(&Undo, window, cx);
956 assert_eq!(
957 editor.text(cx),
958 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
959 );
960 editor.redo(&Redo, window, cx);
961 assert_eq!(
962 editor.text(cx),
963 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
964 );
965 });
966
967 // Ensure temporary rename edits cannot be undone/redone.
968 editor_b.update_in(cx_b, |editor, window, cx| {
969 editor.undo(&Undo, window, cx);
970 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
971 editor.undo(&Undo, window, cx);
972 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
973 editor.redo(&Redo, window, cx);
974 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
975 })
976}
977
978#[gpui::test(iterations = 10)]
979async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
980 let mut server = TestServer::start(cx_a.executor()).await;
981 let executor = cx_a.executor();
982 let client_a = server.create_client(cx_a, "user_a").await;
983 let client_b = server.create_client(cx_b, "user_b").await;
984 server
985 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
986 .await;
987 let active_call_a = cx_a.read(ActiveCall::global);
988
989 cx_b.update(editor::init);
990
991 client_a.language_registry().add(rust_lang());
992 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
993 "Rust",
994 FakeLspAdapter {
995 name: "the-language-server",
996 ..Default::default()
997 },
998 );
999
1000 client_a
1001 .fs()
1002 .insert_tree(
1003 "/dir",
1004 json!({
1005 "main.rs": "const ONE: usize = 1;",
1006 }),
1007 )
1008 .await;
1009 let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
1010
1011 let _buffer_a = project_a
1012 .update(cx_a, |p, cx| {
1013 p.open_local_buffer_with_lsp("/dir/main.rs", cx)
1014 })
1015 .await
1016 .unwrap();
1017
1018 let fake_language_server = fake_language_servers.next().await.unwrap();
1019 fake_language_server.start_progress("the-token").await;
1020
1021 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1022 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1023 token: lsp::NumberOrString::String("the-token".to_string()),
1024 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1025 lsp::WorkDoneProgressReport {
1026 message: Some("the-message".to_string()),
1027 ..Default::default()
1028 },
1029 )),
1030 });
1031 executor.run_until_parked();
1032
1033 project_a.read_with(cx_a, |project, cx| {
1034 let status = project.language_server_statuses(cx).next().unwrap().1;
1035 assert_eq!(status.name, "the-language-server");
1036 assert_eq!(status.pending_work.len(), 1);
1037 assert_eq!(
1038 status.pending_work["the-token"].message.as_ref().unwrap(),
1039 "the-message"
1040 );
1041 });
1042
1043 let project_id = active_call_a
1044 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1045 .await
1046 .unwrap();
1047 executor.run_until_parked();
1048 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1049
1050 project_b.read_with(cx_b, |project, cx| {
1051 let status = project.language_server_statuses(cx).next().unwrap().1;
1052 assert_eq!(status.name, "the-language-server");
1053 });
1054
1055 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1056 fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
1057 token: lsp::NumberOrString::String("the-token".to_string()),
1058 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1059 lsp::WorkDoneProgressReport {
1060 message: Some("the-message-2".to_string()),
1061 ..Default::default()
1062 },
1063 )),
1064 });
1065 executor.run_until_parked();
1066
1067 project_a.read_with(cx_a, |project, cx| {
1068 let status = project.language_server_statuses(cx).next().unwrap().1;
1069 assert_eq!(status.name, "the-language-server");
1070 assert_eq!(status.pending_work.len(), 1);
1071 assert_eq!(
1072 status.pending_work["the-token"].message.as_ref().unwrap(),
1073 "the-message-2"
1074 );
1075 });
1076
1077 project_b.read_with(cx_b, |project, cx| {
1078 let status = project.language_server_statuses(cx).next().unwrap().1;
1079 assert_eq!(status.name, "the-language-server");
1080 assert_eq!(status.pending_work.len(), 1);
1081 assert_eq!(
1082 status.pending_work["the-token"].message.as_ref().unwrap(),
1083 "the-message-2"
1084 );
1085 });
1086}
1087
1088#[gpui::test(iterations = 10)]
1089async fn test_share_project(
1090 cx_a: &mut TestAppContext,
1091 cx_b: &mut TestAppContext,
1092 cx_c: &mut TestAppContext,
1093) {
1094 let executor = cx_a.executor();
1095 let cx_b = cx_b.add_empty_window();
1096 let mut server = TestServer::start(executor.clone()).await;
1097 let client_a = server.create_client(cx_a, "user_a").await;
1098 let client_b = server.create_client(cx_b, "user_b").await;
1099 let client_c = server.create_client(cx_c, "user_c").await;
1100 server
1101 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1102 .await;
1103 let active_call_a = cx_a.read(ActiveCall::global);
1104 let active_call_b = cx_b.read(ActiveCall::global);
1105 let active_call_c = cx_c.read(ActiveCall::global);
1106
1107 client_a
1108 .fs()
1109 .insert_tree(
1110 "/a",
1111 json!({
1112 ".gitignore": "ignored-dir",
1113 "a.txt": "a-contents",
1114 "b.txt": "b-contents",
1115 "ignored-dir": {
1116 "c.txt": "",
1117 "d.txt": "",
1118 }
1119 }),
1120 )
1121 .await;
1122
1123 // Invite client B to collaborate on a project
1124 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1125 active_call_a
1126 .update(cx_a, |call, cx| {
1127 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1128 })
1129 .await
1130 .unwrap();
1131
1132 // Join that project as client B
1133
1134 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1135 executor.run_until_parked();
1136 let call = incoming_call_b.borrow().clone().unwrap();
1137 assert_eq!(call.calling_user.github_login, "user_a");
1138 let initial_project = call.initial_project.unwrap();
1139 active_call_b
1140 .update(cx_b, |call, cx| call.accept_incoming(cx))
1141 .await
1142 .unwrap();
1143 let client_b_peer_id = client_b.peer_id().unwrap();
1144 let project_b = client_b.join_remote_project(initial_project.id, cx_b).await;
1145
1146 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1147
1148 executor.run_until_parked();
1149
1150 project_a.read_with(cx_a, |project, _| {
1151 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1152 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1153 });
1154
1155 project_b.read_with(cx_b, |project, cx| {
1156 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1157 assert_eq!(
1158 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1159 [
1160 Path::new(".gitignore"),
1161 Path::new("a.txt"),
1162 Path::new("b.txt"),
1163 Path::new("ignored-dir"),
1164 ]
1165 );
1166 });
1167
1168 project_b
1169 .update(cx_b, |project, cx| {
1170 let worktree = project.worktrees(cx).next().unwrap();
1171 let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
1172 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1173 })
1174 .await
1175 .unwrap();
1176
1177 project_b.read_with(cx_b, |project, cx| {
1178 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1179 assert_eq!(
1180 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
1181 [
1182 Path::new(".gitignore"),
1183 Path::new("a.txt"),
1184 Path::new("b.txt"),
1185 Path::new("ignored-dir"),
1186 Path::new("ignored-dir/c.txt"),
1187 Path::new("ignored-dir/d.txt"),
1188 ]
1189 );
1190 });
1191
1192 // Open the same file as client B and client A.
1193 let buffer_b = project_b
1194 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1195 .await
1196 .unwrap();
1197
1198 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1199
1200 project_a.read_with(cx_a, |project, cx| {
1201 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
1202 });
1203 let buffer_a = project_a
1204 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
1205 .await
1206 .unwrap();
1207
1208 let editor_b =
1209 cx_b.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
1210
1211 // Client A sees client B's selection
1212 executor.run_until_parked();
1213
1214 buffer_a.read_with(cx_a, |buffer, _| {
1215 buffer
1216 .snapshot()
1217 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1218 .count()
1219 == 1
1220 });
1221
1222 // Edit the buffer as client B and see that edit as client A.
1223 editor_b.update_in(cx_b, |editor, window, cx| {
1224 editor.handle_input("ok, ", window, cx)
1225 });
1226 executor.run_until_parked();
1227
1228 buffer_a.read_with(cx_a, |buffer, _| {
1229 assert_eq!(buffer.text(), "ok, b-contents")
1230 });
1231
1232 // Client B can invite client C on a project shared by client A.
1233 active_call_b
1234 .update(cx_b, |call, cx| {
1235 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1236 })
1237 .await
1238 .unwrap();
1239
1240 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1241 executor.run_until_parked();
1242 let call = incoming_call_c.borrow().clone().unwrap();
1243 assert_eq!(call.calling_user.github_login, "user_b");
1244 let initial_project = call.initial_project.unwrap();
1245 active_call_c
1246 .update(cx_c, |call, cx| call.accept_incoming(cx))
1247 .await
1248 .unwrap();
1249 let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1250
1251 // Client B closes the editor, and client A sees client B's selections removed.
1252 cx_b.update(move |_, _| drop(editor_b));
1253 executor.run_until_parked();
1254
1255 buffer_a.read_with(cx_a, |buffer, _| {
1256 buffer
1257 .snapshot()
1258 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1259 .count()
1260 == 0
1261 });
1262}
1263
1264#[gpui::test(iterations = 10)]
1265async fn test_on_input_format_from_host_to_guest(
1266 cx_a: &mut TestAppContext,
1267 cx_b: &mut TestAppContext,
1268) {
1269 let mut server = TestServer::start(cx_a.executor()).await;
1270 let executor = cx_a.executor();
1271 let client_a = server.create_client(cx_a, "user_a").await;
1272 let client_b = server.create_client(cx_b, "user_b").await;
1273 server
1274 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1275 .await;
1276 let active_call_a = cx_a.read(ActiveCall::global);
1277
1278 client_a.language_registry().add(rust_lang());
1279 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1280 "Rust",
1281 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 );
1292
1293 client_a
1294 .fs()
1295 .insert_tree(
1296 "/a",
1297 json!({
1298 "main.rs": "fn main() { a }",
1299 "other.rs": "// Test file",
1300 }),
1301 )
1302 .await;
1303 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1304 let project_id = active_call_a
1305 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1306 .await
1307 .unwrap();
1308 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1309
1310 // Open a file in an editor as the host.
1311 let buffer_a = project_a
1312 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1313 .await
1314 .unwrap();
1315 let cx_a = cx_a.add_empty_window();
1316 let editor_a = cx_a.new_window_entity(|window, cx| {
1317 Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
1318 });
1319
1320 let fake_language_server = fake_language_servers.next().await.unwrap();
1321 executor.run_until_parked();
1322
1323 // Receive an OnTypeFormatting request as the host's language server.
1324 // Return some formatting from the host's language server.
1325 fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
1326 |params, _| async move {
1327 assert_eq!(
1328 params.text_document_position.text_document.uri,
1329 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1330 );
1331 assert_eq!(
1332 params.text_document_position.position,
1333 lsp::Position::new(0, 14),
1334 );
1335
1336 Ok(Some(vec![lsp::TextEdit {
1337 new_text: "~<".to_string(),
1338 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1339 }]))
1340 },
1341 );
1342
1343 // Open the buffer on the guest and see that the formatting worked
1344 let buffer_b = project_b
1345 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1346 .await
1347 .unwrap();
1348
1349 // Type a on type formatting trigger character as the guest.
1350 cx_a.focus(&editor_a);
1351 editor_a.update_in(cx_a, |editor, window, cx| {
1352 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1353 editor.handle_input(">", window, cx);
1354 });
1355
1356 executor.run_until_parked();
1357
1358 buffer_b.read_with(cx_b, |buffer, _| {
1359 assert_eq!(buffer.text(), "fn main() { a>~< }")
1360 });
1361
1362 // Undo should remove LSP edits first
1363 editor_a.update_in(cx_a, |editor, window, cx| {
1364 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1365 editor.undo(&Undo, window, cx);
1366 assert_eq!(editor.text(cx), "fn main() { a> }");
1367 });
1368 executor.run_until_parked();
1369
1370 buffer_b.read_with(cx_b, |buffer, _| {
1371 assert_eq!(buffer.text(), "fn main() { a> }")
1372 });
1373
1374 editor_a.update_in(cx_a, |editor, window, cx| {
1375 assert_eq!(editor.text(cx), "fn main() { a> }");
1376 editor.undo(&Undo, window, cx);
1377 assert_eq!(editor.text(cx), "fn main() { a }");
1378 });
1379 executor.run_until_parked();
1380
1381 buffer_b.read_with(cx_b, |buffer, _| {
1382 assert_eq!(buffer.text(), "fn main() { a }")
1383 });
1384}
1385
1386#[gpui::test(iterations = 10)]
1387async fn test_on_input_format_from_guest_to_host(
1388 cx_a: &mut TestAppContext,
1389 cx_b: &mut TestAppContext,
1390) {
1391 let mut server = TestServer::start(cx_a.executor()).await;
1392 let executor = cx_a.executor();
1393 let client_a = server.create_client(cx_a, "user_a").await;
1394 let client_b = server.create_client(cx_b, "user_b").await;
1395 server
1396 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1397 .await;
1398 let active_call_a = cx_a.read(ActiveCall::global);
1399
1400 client_a.language_registry().add(rust_lang());
1401 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1402 "Rust",
1403 FakeLspAdapter {
1404 capabilities: lsp::ServerCapabilities {
1405 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1406 first_trigger_character: ":".to_string(),
1407 more_trigger_character: Some(vec![">".to_string()]),
1408 }),
1409 ..Default::default()
1410 },
1411 ..Default::default()
1412 },
1413 );
1414
1415 client_a
1416 .fs()
1417 .insert_tree(
1418 "/a",
1419 json!({
1420 "main.rs": "fn main() { a }",
1421 "other.rs": "// Test file",
1422 }),
1423 )
1424 .await;
1425 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1426 let project_id = active_call_a
1427 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1428 .await
1429 .unwrap();
1430 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1431
1432 // Open a file in an editor as the guest.
1433 let buffer_b = project_b
1434 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1435 .await
1436 .unwrap();
1437 let cx_b = cx_b.add_empty_window();
1438 let editor_b = cx_b.new_window_entity(|window, cx| {
1439 Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
1440 });
1441
1442 let fake_language_server = fake_language_servers.next().await.unwrap();
1443 executor.run_until_parked();
1444
1445 // Type a on type formatting trigger character as the guest.
1446 cx_b.focus(&editor_b);
1447 editor_b.update_in(cx_b, |editor, window, cx| {
1448 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1449 editor.handle_input(":", window, cx);
1450 });
1451
1452 // Receive an OnTypeFormatting request as the host's language server.
1453 // Return some formatting from the host's language server.
1454 executor.start_waiting();
1455 fake_language_server
1456 .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1457 assert_eq!(
1458 params.text_document_position.text_document.uri,
1459 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1460 );
1461 assert_eq!(
1462 params.text_document_position.position,
1463 lsp::Position::new(0, 14),
1464 );
1465
1466 Ok(Some(vec![lsp::TextEdit {
1467 new_text: "~:".to_string(),
1468 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1469 }]))
1470 })
1471 .next()
1472 .await
1473 .unwrap();
1474 executor.finish_waiting();
1475
1476 // Open the buffer on the host and see that the formatting worked
1477 let buffer_a = project_a
1478 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1479 .await
1480 .unwrap();
1481 executor.run_until_parked();
1482
1483 buffer_a.read_with(cx_a, |buffer, _| {
1484 assert_eq!(buffer.text(), "fn main() { a:~: }")
1485 });
1486
1487 // Undo should remove LSP edits first
1488 editor_b.update_in(cx_b, |editor, window, cx| {
1489 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1490 editor.undo(&Undo, window, cx);
1491 assert_eq!(editor.text(cx), "fn main() { a: }");
1492 });
1493 executor.run_until_parked();
1494
1495 buffer_a.read_with(cx_a, |buffer, _| {
1496 assert_eq!(buffer.text(), "fn main() { a: }")
1497 });
1498
1499 editor_b.update_in(cx_b, |editor, window, cx| {
1500 assert_eq!(editor.text(cx), "fn main() { a: }");
1501 editor.undo(&Undo, window, cx);
1502 assert_eq!(editor.text(cx), "fn main() { a }");
1503 });
1504 executor.run_until_parked();
1505
1506 buffer_a.read_with(cx_a, |buffer, _| {
1507 assert_eq!(buffer.text(), "fn main() { a }")
1508 });
1509}
1510
1511#[gpui::test(iterations = 10)]
1512async fn test_mutual_editor_inlay_hint_cache_update(
1513 cx_a: &mut TestAppContext,
1514 cx_b: &mut TestAppContext,
1515) {
1516 let mut server = TestServer::start(cx_a.executor()).await;
1517 let executor = cx_a.executor();
1518 let client_a = server.create_client(cx_a, "user_a").await;
1519 let client_b = server.create_client(cx_b, "user_b").await;
1520 server
1521 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1522 .await;
1523 let active_call_a = cx_a.read(ActiveCall::global);
1524 let active_call_b = cx_b.read(ActiveCall::global);
1525
1526 cx_a.update(editor::init);
1527 cx_b.update(editor::init);
1528
1529 cx_a.update(|cx| {
1530 SettingsStore::update_global(cx, |store, cx| {
1531 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1532 settings.defaults.inlay_hints = Some(InlayHintSettings {
1533 enabled: true,
1534 edit_debounce_ms: 0,
1535 scroll_debounce_ms: 0,
1536 show_type_hints: true,
1537 show_parameter_hints: false,
1538 show_other_hints: true,
1539 show_background: false,
1540 toggle_on_modifiers_press: None,
1541 })
1542 });
1543 });
1544 });
1545 cx_b.update(|cx| {
1546 SettingsStore::update_global(cx, |store, cx| {
1547 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1548 settings.defaults.inlay_hints = Some(InlayHintSettings {
1549 enabled: true,
1550 edit_debounce_ms: 0,
1551 scroll_debounce_ms: 0,
1552 show_type_hints: true,
1553 show_parameter_hints: false,
1554 show_other_hints: true,
1555 show_background: false,
1556 toggle_on_modifiers_press: None,
1557 })
1558 });
1559 });
1560 });
1561
1562 client_a.language_registry().add(rust_lang());
1563 client_b.language_registry().add(rust_lang());
1564 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1565 "Rust",
1566 FakeLspAdapter {
1567 capabilities: lsp::ServerCapabilities {
1568 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1569 ..Default::default()
1570 },
1571 ..Default::default()
1572 },
1573 );
1574
1575 // Client A opens a project.
1576 client_a
1577 .fs()
1578 .insert_tree(
1579 "/a",
1580 json!({
1581 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1582 "other.rs": "// Test file",
1583 }),
1584 )
1585 .await;
1586 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1587 active_call_a
1588 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1589 .await
1590 .unwrap();
1591 let project_id = active_call_a
1592 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1593 .await
1594 .unwrap();
1595
1596 // Client B joins the project
1597 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1598 active_call_b
1599 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1600 .await
1601 .unwrap();
1602
1603 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1604 executor.start_waiting();
1605
1606 // The host opens a rust file.
1607 let _buffer_a = project_a
1608 .update(cx_a, |project, cx| {
1609 project.open_local_buffer("/a/main.rs", cx)
1610 })
1611 .await
1612 .unwrap();
1613 let editor_a = workspace_a
1614 .update_in(cx_a, |workspace, window, cx| {
1615 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1616 })
1617 .await
1618 .unwrap()
1619 .downcast::<Editor>()
1620 .unwrap();
1621
1622 let fake_language_server = fake_language_servers.next().await.unwrap();
1623
1624 // Set up the language server to return an additional inlay hint on each request.
1625 let edits_made = Arc::new(AtomicUsize::new(0));
1626 let closure_edits_made = Arc::clone(&edits_made);
1627 fake_language_server
1628 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1629 let task_edits_made = Arc::clone(&closure_edits_made);
1630 async move {
1631 assert_eq!(
1632 params.text_document.uri,
1633 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1634 );
1635 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1636 Ok(Some(vec![lsp::InlayHint {
1637 position: lsp::Position::new(0, edits_made as u32),
1638 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1639 kind: None,
1640 text_edits: None,
1641 tooltip: None,
1642 padding_left: None,
1643 padding_right: None,
1644 data: None,
1645 }]))
1646 }
1647 })
1648 .next()
1649 .await
1650 .unwrap();
1651
1652 executor.run_until_parked();
1653
1654 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1655 editor_a.update(cx_a, |editor, _| {
1656 assert_eq!(
1657 vec![initial_edit.to_string()],
1658 extract_hint_labels(editor),
1659 "Host should get its first hints when opens an editor"
1660 );
1661 });
1662 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1663 let editor_b = workspace_b
1664 .update_in(cx_b, |workspace, window, cx| {
1665 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1666 })
1667 .await
1668 .unwrap()
1669 .downcast::<Editor>()
1670 .unwrap();
1671
1672 executor.run_until_parked();
1673 editor_b.update(cx_b, |editor, _| {
1674 assert_eq!(
1675 vec![initial_edit.to_string()],
1676 extract_hint_labels(editor),
1677 "Client should get its first hints when opens an editor"
1678 );
1679 });
1680
1681 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1682 editor_b.update_in(cx_b, |editor, window, cx| {
1683 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
1684 editor.handle_input(":", window, cx);
1685 });
1686 cx_b.focus(&editor_b);
1687
1688 executor.run_until_parked();
1689 editor_a.update(cx_a, |editor, _| {
1690 assert_eq!(
1691 vec![after_client_edit.to_string()],
1692 extract_hint_labels(editor),
1693 );
1694 });
1695 editor_b.update(cx_b, |editor, _| {
1696 assert_eq!(
1697 vec![after_client_edit.to_string()],
1698 extract_hint_labels(editor),
1699 );
1700 });
1701
1702 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1703 editor_a.update_in(cx_a, |editor, window, cx| {
1704 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1705 editor.handle_input("a change to increment both buffers' versions", window, cx);
1706 });
1707 cx_a.focus(&editor_a);
1708
1709 executor.run_until_parked();
1710 editor_a.update(cx_a, |editor, _| {
1711 assert_eq!(
1712 vec![after_host_edit.to_string()],
1713 extract_hint_labels(editor),
1714 );
1715 });
1716 editor_b.update(cx_b, |editor, _| {
1717 assert_eq!(
1718 vec![after_host_edit.to_string()],
1719 extract_hint_labels(editor),
1720 );
1721 });
1722
1723 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1724 fake_language_server
1725 .request::<lsp::request::InlayHintRefreshRequest>(())
1726 .await
1727 .expect("inlay refresh request failed");
1728
1729 executor.run_until_parked();
1730 editor_a.update(cx_a, |editor, _| {
1731 assert_eq!(
1732 vec![after_special_edit_for_refresh.to_string()],
1733 extract_hint_labels(editor),
1734 "Host should react to /refresh LSP request"
1735 );
1736 });
1737 editor_b.update(cx_b, |editor, _| {
1738 assert_eq!(
1739 vec![after_special_edit_for_refresh.to_string()],
1740 extract_hint_labels(editor),
1741 "Guest should get a /refresh LSP request propagated by host"
1742 );
1743 });
1744}
1745
1746#[gpui::test(iterations = 10)]
1747async fn test_inlay_hint_refresh_is_forwarded(
1748 cx_a: &mut TestAppContext,
1749 cx_b: &mut TestAppContext,
1750) {
1751 let mut server = TestServer::start(cx_a.executor()).await;
1752 let executor = cx_a.executor();
1753 let client_a = server.create_client(cx_a, "user_a").await;
1754 let client_b = server.create_client(cx_b, "user_b").await;
1755 server
1756 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1757 .await;
1758 let active_call_a = cx_a.read(ActiveCall::global);
1759 let active_call_b = cx_b.read(ActiveCall::global);
1760
1761 cx_a.update(editor::init);
1762 cx_b.update(editor::init);
1763
1764 cx_a.update(|cx| {
1765 SettingsStore::update_global(cx, |store, cx| {
1766 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1767 settings.defaults.inlay_hints = Some(InlayHintSettings {
1768 enabled: false,
1769 edit_debounce_ms: 0,
1770 scroll_debounce_ms: 0,
1771 show_type_hints: false,
1772 show_parameter_hints: false,
1773 show_other_hints: false,
1774 show_background: false,
1775 toggle_on_modifiers_press: None,
1776 })
1777 });
1778 });
1779 });
1780 cx_b.update(|cx| {
1781 SettingsStore::update_global(cx, |store, cx| {
1782 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1783 settings.defaults.inlay_hints = Some(InlayHintSettings {
1784 enabled: true,
1785 edit_debounce_ms: 0,
1786 scroll_debounce_ms: 0,
1787 show_type_hints: true,
1788 show_parameter_hints: true,
1789 show_other_hints: true,
1790 show_background: false,
1791 toggle_on_modifiers_press: None,
1792 })
1793 });
1794 });
1795 });
1796
1797 client_a.language_registry().add(rust_lang());
1798 client_b.language_registry().add(rust_lang());
1799 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1800 "Rust",
1801 FakeLspAdapter {
1802 capabilities: lsp::ServerCapabilities {
1803 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1804 ..Default::default()
1805 },
1806 ..Default::default()
1807 },
1808 );
1809
1810 client_a
1811 .fs()
1812 .insert_tree(
1813 "/a",
1814 json!({
1815 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1816 "other.rs": "// Test file",
1817 }),
1818 )
1819 .await;
1820 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1821 active_call_a
1822 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1823 .await
1824 .unwrap();
1825 let project_id = active_call_a
1826 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1827 .await
1828 .unwrap();
1829
1830 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1831 active_call_b
1832 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1833 .await
1834 .unwrap();
1835
1836 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1837 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1838
1839 cx_a.background_executor.start_waiting();
1840
1841 let editor_a = workspace_a
1842 .update_in(cx_a, |workspace, window, cx| {
1843 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1844 })
1845 .await
1846 .unwrap()
1847 .downcast::<Editor>()
1848 .unwrap();
1849
1850 let editor_b = workspace_b
1851 .update_in(cx_b, |workspace, window, cx| {
1852 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1853 })
1854 .await
1855 .unwrap()
1856 .downcast::<Editor>()
1857 .unwrap();
1858
1859 let other_hints = Arc::new(AtomicBool::new(false));
1860 let fake_language_server = fake_language_servers.next().await.unwrap();
1861 let closure_other_hints = Arc::clone(&other_hints);
1862 fake_language_server
1863 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1864 let task_other_hints = Arc::clone(&closure_other_hints);
1865 async move {
1866 assert_eq!(
1867 params.text_document.uri,
1868 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1869 );
1870 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1871 let character = if other_hints { 0 } else { 2 };
1872 let label = if other_hints {
1873 "other hint"
1874 } else {
1875 "initial hint"
1876 };
1877 Ok(Some(vec![lsp::InlayHint {
1878 position: lsp::Position::new(0, character),
1879 label: lsp::InlayHintLabel::String(label.to_string()),
1880 kind: None,
1881 text_edits: None,
1882 tooltip: None,
1883 padding_left: None,
1884 padding_right: None,
1885 data: None,
1886 }]))
1887 }
1888 })
1889 .next()
1890 .await
1891 .unwrap();
1892 executor.finish_waiting();
1893
1894 executor.run_until_parked();
1895 editor_a.update(cx_a, |editor, _| {
1896 assert!(
1897 extract_hint_labels(editor).is_empty(),
1898 "Host should get no hints due to them turned off"
1899 );
1900 });
1901
1902 executor.run_until_parked();
1903 editor_b.update(cx_b, |editor, _| {
1904 assert_eq!(
1905 vec!["initial hint".to_string()],
1906 extract_hint_labels(editor),
1907 "Client should get its first hints when opens an editor"
1908 );
1909 });
1910
1911 other_hints.fetch_or(true, atomic::Ordering::Release);
1912 fake_language_server
1913 .request::<lsp::request::InlayHintRefreshRequest>(())
1914 .await
1915 .expect("inlay refresh request failed");
1916 executor.run_until_parked();
1917 editor_a.update(cx_a, |editor, _| {
1918 assert!(
1919 extract_hint_labels(editor).is_empty(),
1920 "Host should get no hints due to them turned off, even after the /refresh"
1921 );
1922 });
1923
1924 executor.run_until_parked();
1925 editor_b.update(cx_b, |editor, _| {
1926 assert_eq!(
1927 vec!["other hint".to_string()],
1928 extract_hint_labels(editor),
1929 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1930 );
1931 });
1932}
1933
1934#[gpui::test(iterations = 10)]
1935async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1936 let mut server = TestServer::start(cx_a.executor()).await;
1937 let client_a = server.create_client(cx_a, "user_a").await;
1938 let client_b = server.create_client(cx_b, "user_b").await;
1939 server
1940 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1941 .await;
1942 let active_call_a = cx_a.read(ActiveCall::global);
1943
1944 cx_a.update(editor::init);
1945 cx_b.update(editor::init);
1946 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1947 let inline_blame_off_settings = Some(InlineBlameSettings {
1948 enabled: false,
1949 delay_ms: None,
1950 min_column: None,
1951 show_commit_summary: false,
1952 });
1953 cx_a.update(|cx| {
1954 SettingsStore::update_global(cx, |store, cx| {
1955 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1956 settings.git.inline_blame = inline_blame_off_settings;
1957 });
1958 });
1959 });
1960 cx_b.update(|cx| {
1961 SettingsStore::update_global(cx, |store, cx| {
1962 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1963 settings.git.inline_blame = inline_blame_off_settings;
1964 });
1965 });
1966 });
1967
1968 client_a
1969 .fs()
1970 .insert_tree(
1971 "/my-repo",
1972 json!({
1973 ".git": {},
1974 "file.txt": "line1\nline2\nline3\nline\n",
1975 }),
1976 )
1977 .await;
1978
1979 let blame = git::blame::Blame {
1980 entries: vec![
1981 blame_entry("1b1b1b", 0..1),
1982 blame_entry("0d0d0d", 1..2),
1983 blame_entry("3a3a3a", 2..3),
1984 blame_entry("4c4c4c", 3..4),
1985 ],
1986 permalinks: HashMap::default(), // This field is deprecrated
1987 messages: [
1988 ("1b1b1b", "message for idx-0"),
1989 ("0d0d0d", "message for idx-1"),
1990 ("3a3a3a", "message for idx-2"),
1991 ("4c4c4c", "message for idx-3"),
1992 ]
1993 .into_iter()
1994 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
1995 .collect(),
1996 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
1997 };
1998 client_a
1999 .fs()
2000 .set_blame_for_repo(Path::new("/my-repo/.git"), vec![("file.txt".into(), blame)]);
2001
2002 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
2003 let project_id = active_call_a
2004 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2005 .await
2006 .unwrap();
2007
2008 // Create editor_a
2009 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2010 let editor_a = workspace_a
2011 .update_in(cx_a, |workspace, window, cx| {
2012 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2013 })
2014 .await
2015 .unwrap()
2016 .downcast::<Editor>()
2017 .unwrap();
2018
2019 // Join the project as client B.
2020 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2021 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2022 let editor_b = workspace_b
2023 .update_in(cx_b, |workspace, window, cx| {
2024 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2025 })
2026 .await
2027 .unwrap()
2028 .downcast::<Editor>()
2029 .unwrap();
2030 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
2031 editor_b
2032 .buffer()
2033 .read(cx)
2034 .as_singleton()
2035 .unwrap()
2036 .read(cx)
2037 .remote_id()
2038 });
2039
2040 // client_b now requests git blame for the open buffer
2041 editor_b.update_in(cx_b, |editor_b, window, cx| {
2042 assert!(editor_b.blame().is_none());
2043 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx);
2044 });
2045
2046 cx_a.executor().run_until_parked();
2047 cx_b.executor().run_until_parked();
2048
2049 editor_b.update(cx_b, |editor_b, cx| {
2050 let blame = editor_b.blame().expect("editor_b should have blame now");
2051 let entries = blame.update(cx, |blame, cx| {
2052 blame
2053 .blame_for_rows(
2054 &(0..4)
2055 .map(|row| RowInfo {
2056 buffer_row: Some(row),
2057 buffer_id: Some(buffer_id_b),
2058 ..Default::default()
2059 })
2060 .collect::<Vec<_>>(),
2061 cx,
2062 )
2063 .collect::<Vec<_>>()
2064 });
2065
2066 assert_eq!(
2067 entries,
2068 vec![
2069 Some(blame_entry("1b1b1b", 0..1)),
2070 Some(blame_entry("0d0d0d", 1..2)),
2071 Some(blame_entry("3a3a3a", 2..3)),
2072 Some(blame_entry("4c4c4c", 3..4)),
2073 ]
2074 );
2075
2076 blame.update(cx, |blame, _| {
2077 for (idx, entry) in entries.iter().flatten().enumerate() {
2078 let details = blame.details_for_entry(entry).unwrap();
2079 assert_eq!(details.message, format!("message for idx-{}", idx));
2080 assert_eq!(
2081 details.permalink.unwrap().to_string(),
2082 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2083 );
2084 }
2085 });
2086 });
2087
2088 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2089 // which gets back to client_b.
2090 editor_b.update_in(cx_b, |editor_b, _, cx| {
2091 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2092 });
2093
2094 cx_a.executor().run_until_parked();
2095 cx_b.executor().run_until_parked();
2096
2097 editor_b.update(cx_b, |editor_b, cx| {
2098 let blame = editor_b.blame().expect("editor_b should have blame now");
2099 let entries = blame.update(cx, |blame, cx| {
2100 blame
2101 .blame_for_rows(
2102 &(0..4)
2103 .map(|row| RowInfo {
2104 buffer_row: Some(row),
2105 buffer_id: Some(buffer_id_b),
2106 ..Default::default()
2107 })
2108 .collect::<Vec<_>>(),
2109 cx,
2110 )
2111 .collect::<Vec<_>>()
2112 });
2113
2114 assert_eq!(
2115 entries,
2116 vec![
2117 None,
2118 Some(blame_entry("0d0d0d", 1..2)),
2119 Some(blame_entry("3a3a3a", 2..3)),
2120 Some(blame_entry("4c4c4c", 3..4)),
2121 ]
2122 );
2123 });
2124
2125 // Now editor_a also updates the file
2126 editor_a.update_in(cx_a, |editor_a, _, cx| {
2127 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2128 });
2129
2130 cx_a.executor().run_until_parked();
2131 cx_b.executor().run_until_parked();
2132
2133 editor_b.update(cx_b, |editor_b, cx| {
2134 let blame = editor_b.blame().expect("editor_b should have blame now");
2135 let entries = blame.update(cx, |blame, cx| {
2136 blame
2137 .blame_for_rows(
2138 &(0..4)
2139 .map(|row| RowInfo {
2140 buffer_row: Some(row),
2141 buffer_id: Some(buffer_id_b),
2142 ..Default::default()
2143 })
2144 .collect::<Vec<_>>(),
2145 cx,
2146 )
2147 .collect::<Vec<_>>()
2148 });
2149
2150 assert_eq!(
2151 entries,
2152 vec![
2153 None,
2154 None,
2155 Some(blame_entry("3a3a3a", 2..3)),
2156 Some(blame_entry("4c4c4c", 3..4)),
2157 ]
2158 );
2159 });
2160}
2161
2162#[gpui::test(iterations = 30)]
2163async fn test_collaborating_with_editorconfig(
2164 cx_a: &mut TestAppContext,
2165 cx_b: &mut TestAppContext,
2166) {
2167 let mut server = TestServer::start(cx_a.executor()).await;
2168 let client_a = server.create_client(cx_a, "user_a").await;
2169 let client_b = server.create_client(cx_b, "user_b").await;
2170 server
2171 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2172 .await;
2173 let active_call_a = cx_a.read(ActiveCall::global);
2174
2175 cx_b.update(editor::init);
2176
2177 // Set up a fake language server.
2178 client_a.language_registry().add(rust_lang());
2179 client_a
2180 .fs()
2181 .insert_tree(
2182 "/a",
2183 json!({
2184 "src": {
2185 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2186 "other_mod": {
2187 "other.rs": "pub fn foo() -> usize {\n 4\n}",
2188 ".editorconfig": "",
2189 },
2190 },
2191 ".editorconfig": "[*]\ntab_width = 2\n",
2192 }),
2193 )
2194 .await;
2195 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2196 let project_id = active_call_a
2197 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2198 .await
2199 .unwrap();
2200 let main_buffer_a = project_a
2201 .update(cx_a, |p, cx| {
2202 p.open_buffer((worktree_id, "src/main.rs"), cx)
2203 })
2204 .await
2205 .unwrap();
2206 let other_buffer_a = project_a
2207 .update(cx_a, |p, cx| {
2208 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2209 })
2210 .await
2211 .unwrap();
2212 let cx_a = cx_a.add_empty_window();
2213 let main_editor_a = cx_a.new_window_entity(|window, cx| {
2214 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
2215 });
2216 let other_editor_a = cx_a.new_window_entity(|window, cx| {
2217 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
2218 });
2219 let mut main_editor_cx_a = EditorTestContext {
2220 cx: cx_a.clone(),
2221 window: cx_a.window_handle(),
2222 editor: main_editor_a,
2223 assertion_cx: AssertionContextManager::new(),
2224 };
2225 let mut other_editor_cx_a = EditorTestContext {
2226 cx: cx_a.clone(),
2227 window: cx_a.window_handle(),
2228 editor: other_editor_a,
2229 assertion_cx: AssertionContextManager::new(),
2230 };
2231
2232 // Join the project as client B.
2233 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2234 let main_buffer_b = project_b
2235 .update(cx_b, |p, cx| {
2236 p.open_buffer((worktree_id, "src/main.rs"), cx)
2237 })
2238 .await
2239 .unwrap();
2240 let other_buffer_b = project_b
2241 .update(cx_b, |p, cx| {
2242 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2243 })
2244 .await
2245 .unwrap();
2246 let cx_b = cx_b.add_empty_window();
2247 let main_editor_b = cx_b.new_window_entity(|window, cx| {
2248 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
2249 });
2250 let other_editor_b = cx_b.new_window_entity(|window, cx| {
2251 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
2252 });
2253 let mut main_editor_cx_b = EditorTestContext {
2254 cx: cx_b.clone(),
2255 window: cx_b.window_handle(),
2256 editor: main_editor_b,
2257 assertion_cx: AssertionContextManager::new(),
2258 };
2259 let mut other_editor_cx_b = EditorTestContext {
2260 cx: cx_b.clone(),
2261 window: cx_b.window_handle(),
2262 editor: other_editor_b,
2263 assertion_cx: AssertionContextManager::new(),
2264 };
2265
2266 let initial_main = indoc! {"
2267ˇmod other;
2268fn main() { let foo = other::foo(); }"};
2269 let initial_other = indoc! {"
2270ˇpub fn foo() -> usize {
2271 4
2272}"};
2273
2274 let first_tabbed_main = indoc! {"
2275 ˇmod other;
2276fn main() { let foo = other::foo(); }"};
2277 tab_undo_assert(
2278 &mut main_editor_cx_a,
2279 &mut main_editor_cx_b,
2280 initial_main,
2281 first_tabbed_main,
2282 true,
2283 );
2284 tab_undo_assert(
2285 &mut main_editor_cx_a,
2286 &mut main_editor_cx_b,
2287 initial_main,
2288 first_tabbed_main,
2289 false,
2290 );
2291
2292 let first_tabbed_other = indoc! {"
2293 ˇpub fn foo() -> usize {
2294 4
2295}"};
2296 tab_undo_assert(
2297 &mut other_editor_cx_a,
2298 &mut other_editor_cx_b,
2299 initial_other,
2300 first_tabbed_other,
2301 true,
2302 );
2303 tab_undo_assert(
2304 &mut other_editor_cx_a,
2305 &mut other_editor_cx_b,
2306 initial_other,
2307 first_tabbed_other,
2308 false,
2309 );
2310
2311 client_a
2312 .fs()
2313 .atomic_write(
2314 PathBuf::from("/a/src/.editorconfig"),
2315 "[*]\ntab_width = 3\n".to_owned(),
2316 )
2317 .await
2318 .unwrap();
2319 cx_a.run_until_parked();
2320 cx_b.run_until_parked();
2321
2322 let second_tabbed_main = indoc! {"
2323 ˇmod other;
2324fn main() { let foo = other::foo(); }"};
2325 tab_undo_assert(
2326 &mut main_editor_cx_a,
2327 &mut main_editor_cx_b,
2328 initial_main,
2329 second_tabbed_main,
2330 true,
2331 );
2332 tab_undo_assert(
2333 &mut main_editor_cx_a,
2334 &mut main_editor_cx_b,
2335 initial_main,
2336 second_tabbed_main,
2337 false,
2338 );
2339
2340 let second_tabbed_other = indoc! {"
2341 ˇpub fn foo() -> usize {
2342 4
2343}"};
2344 tab_undo_assert(
2345 &mut other_editor_cx_a,
2346 &mut other_editor_cx_b,
2347 initial_other,
2348 second_tabbed_other,
2349 true,
2350 );
2351 tab_undo_assert(
2352 &mut other_editor_cx_a,
2353 &mut other_editor_cx_b,
2354 initial_other,
2355 second_tabbed_other,
2356 false,
2357 );
2358
2359 let editorconfig_buffer_b = project_b
2360 .update(cx_b, |p, cx| {
2361 p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2362 })
2363 .await
2364 .unwrap();
2365 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2366 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2367 });
2368 project_b
2369 .update(cx_b, |project, cx| {
2370 project.save_buffer(editorconfig_buffer_b.clone(), cx)
2371 })
2372 .await
2373 .unwrap();
2374 cx_a.run_until_parked();
2375 cx_b.run_until_parked();
2376
2377 tab_undo_assert(
2378 &mut main_editor_cx_a,
2379 &mut main_editor_cx_b,
2380 initial_main,
2381 second_tabbed_main,
2382 true,
2383 );
2384 tab_undo_assert(
2385 &mut main_editor_cx_a,
2386 &mut main_editor_cx_b,
2387 initial_main,
2388 second_tabbed_main,
2389 false,
2390 );
2391
2392 let third_tabbed_other = indoc! {"
2393 ˇpub fn foo() -> usize {
2394 4
2395}"};
2396 tab_undo_assert(
2397 &mut other_editor_cx_a,
2398 &mut other_editor_cx_b,
2399 initial_other,
2400 third_tabbed_other,
2401 true,
2402 );
2403
2404 tab_undo_assert(
2405 &mut other_editor_cx_a,
2406 &mut other_editor_cx_b,
2407 initial_other,
2408 third_tabbed_other,
2409 false,
2410 );
2411}
2412
2413#[track_caller]
2414fn tab_undo_assert(
2415 cx_a: &mut EditorTestContext,
2416 cx_b: &mut EditorTestContext,
2417 expected_initial: &str,
2418 expected_tabbed: &str,
2419 a_tabs: bool,
2420) {
2421 cx_a.assert_editor_state(expected_initial);
2422 cx_b.assert_editor_state(expected_initial);
2423
2424 if a_tabs {
2425 cx_a.update_editor(|editor, window, cx| {
2426 editor.tab(&editor::actions::Tab, window, cx);
2427 });
2428 } else {
2429 cx_b.update_editor(|editor, window, cx| {
2430 editor.tab(&editor::actions::Tab, window, cx);
2431 });
2432 }
2433
2434 cx_a.run_until_parked();
2435 cx_b.run_until_parked();
2436
2437 cx_a.assert_editor_state(expected_tabbed);
2438 cx_b.assert_editor_state(expected_tabbed);
2439
2440 if a_tabs {
2441 cx_a.update_editor(|editor, window, cx| {
2442 editor.undo(&editor::actions::Undo, window, cx);
2443 });
2444 } else {
2445 cx_b.update_editor(|editor, window, cx| {
2446 editor.undo(&editor::actions::Undo, window, cx);
2447 });
2448 }
2449 cx_a.run_until_parked();
2450 cx_b.run_until_parked();
2451 cx_a.assert_editor_state(expected_initial);
2452 cx_b.assert_editor_state(expected_initial);
2453}
2454
2455fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2456 let mut labels = Vec::new();
2457 for hint in editor.inlay_hint_cache().hints() {
2458 match hint.label {
2459 project::InlayHintLabel::String(s) => labels.push(s),
2460 _ => unreachable!(),
2461 }
2462 }
2463 labels
2464}
2465
2466fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2467 git::blame::BlameEntry {
2468 sha: sha.parse().unwrap(),
2469 range,
2470 ..Default::default()
2471 }
2472}