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 })
1541 });
1542 });
1543 });
1544 cx_b.update(|cx| {
1545 SettingsStore::update_global(cx, |store, cx| {
1546 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1547 settings.defaults.inlay_hints = Some(InlayHintSettings {
1548 enabled: true,
1549 edit_debounce_ms: 0,
1550 scroll_debounce_ms: 0,
1551 show_type_hints: true,
1552 show_parameter_hints: false,
1553 show_other_hints: true,
1554 show_background: false,
1555 })
1556 });
1557 });
1558 });
1559
1560 client_a.language_registry().add(rust_lang());
1561 client_b.language_registry().add(rust_lang());
1562 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1563 "Rust",
1564 FakeLspAdapter {
1565 capabilities: lsp::ServerCapabilities {
1566 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1567 ..Default::default()
1568 },
1569 ..Default::default()
1570 },
1571 );
1572
1573 // Client A opens a project.
1574 client_a
1575 .fs()
1576 .insert_tree(
1577 "/a",
1578 json!({
1579 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1580 "other.rs": "// Test file",
1581 }),
1582 )
1583 .await;
1584 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1585 active_call_a
1586 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1587 .await
1588 .unwrap();
1589 let project_id = active_call_a
1590 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1591 .await
1592 .unwrap();
1593
1594 // Client B joins the project
1595 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1596 active_call_b
1597 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1598 .await
1599 .unwrap();
1600
1601 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1602 executor.start_waiting();
1603
1604 // The host opens a rust file.
1605 let _buffer_a = project_a
1606 .update(cx_a, |project, cx| {
1607 project.open_local_buffer("/a/main.rs", cx)
1608 })
1609 .await
1610 .unwrap();
1611 let editor_a = workspace_a
1612 .update_in(cx_a, |workspace, window, cx| {
1613 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1614 })
1615 .await
1616 .unwrap()
1617 .downcast::<Editor>()
1618 .unwrap();
1619
1620 let fake_language_server = fake_language_servers.next().await.unwrap();
1621
1622 // Set up the language server to return an additional inlay hint on each request.
1623 let edits_made = Arc::new(AtomicUsize::new(0));
1624 let closure_edits_made = Arc::clone(&edits_made);
1625 fake_language_server
1626 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1627 let task_edits_made = Arc::clone(&closure_edits_made);
1628 async move {
1629 assert_eq!(
1630 params.text_document.uri,
1631 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1632 );
1633 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
1634 Ok(Some(vec![lsp::InlayHint {
1635 position: lsp::Position::new(0, edits_made as u32),
1636 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1637 kind: None,
1638 text_edits: None,
1639 tooltip: None,
1640 padding_left: None,
1641 padding_right: None,
1642 data: None,
1643 }]))
1644 }
1645 })
1646 .next()
1647 .await
1648 .unwrap();
1649
1650 executor.run_until_parked();
1651
1652 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1653 editor_a.update(cx_a, |editor, _| {
1654 assert_eq!(
1655 vec![initial_edit.to_string()],
1656 extract_hint_labels(editor),
1657 "Host should get its first hints when opens an editor"
1658 );
1659 });
1660 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1661 let editor_b = workspace_b
1662 .update_in(cx_b, |workspace, window, cx| {
1663 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1664 })
1665 .await
1666 .unwrap()
1667 .downcast::<Editor>()
1668 .unwrap();
1669
1670 executor.run_until_parked();
1671 editor_b.update(cx_b, |editor, _| {
1672 assert_eq!(
1673 vec![initial_edit.to_string()],
1674 extract_hint_labels(editor),
1675 "Client should get its first hints when opens an editor"
1676 );
1677 });
1678
1679 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1680 editor_b.update_in(cx_b, |editor, window, cx| {
1681 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
1682 editor.handle_input(":", window, cx);
1683 });
1684 cx_b.focus(&editor_b);
1685
1686 executor.run_until_parked();
1687 editor_a.update(cx_a, |editor, _| {
1688 assert_eq!(
1689 vec![after_client_edit.to_string()],
1690 extract_hint_labels(editor),
1691 );
1692 });
1693 editor_b.update(cx_b, |editor, _| {
1694 assert_eq!(
1695 vec![after_client_edit.to_string()],
1696 extract_hint_labels(editor),
1697 );
1698 });
1699
1700 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1701 editor_a.update_in(cx_a, |editor, window, cx| {
1702 editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
1703 editor.handle_input("a change to increment both buffers' versions", window, cx);
1704 });
1705 cx_a.focus(&editor_a);
1706
1707 executor.run_until_parked();
1708 editor_a.update(cx_a, |editor, _| {
1709 assert_eq!(
1710 vec![after_host_edit.to_string()],
1711 extract_hint_labels(editor),
1712 );
1713 });
1714 editor_b.update(cx_b, |editor, _| {
1715 assert_eq!(
1716 vec![after_host_edit.to_string()],
1717 extract_hint_labels(editor),
1718 );
1719 });
1720
1721 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
1722 fake_language_server
1723 .request::<lsp::request::InlayHintRefreshRequest>(())
1724 .await
1725 .expect("inlay refresh request failed");
1726
1727 executor.run_until_parked();
1728 editor_a.update(cx_a, |editor, _| {
1729 assert_eq!(
1730 vec![after_special_edit_for_refresh.to_string()],
1731 extract_hint_labels(editor),
1732 "Host should react to /refresh LSP request"
1733 );
1734 });
1735 editor_b.update(cx_b, |editor, _| {
1736 assert_eq!(
1737 vec![after_special_edit_for_refresh.to_string()],
1738 extract_hint_labels(editor),
1739 "Guest should get a /refresh LSP request propagated by host"
1740 );
1741 });
1742}
1743
1744#[gpui::test(iterations = 10)]
1745async fn test_inlay_hint_refresh_is_forwarded(
1746 cx_a: &mut TestAppContext,
1747 cx_b: &mut TestAppContext,
1748) {
1749 let mut server = TestServer::start(cx_a.executor()).await;
1750 let executor = cx_a.executor();
1751 let client_a = server.create_client(cx_a, "user_a").await;
1752 let client_b = server.create_client(cx_b, "user_b").await;
1753 server
1754 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1755 .await;
1756 let active_call_a = cx_a.read(ActiveCall::global);
1757 let active_call_b = cx_b.read(ActiveCall::global);
1758
1759 cx_a.update(editor::init);
1760 cx_b.update(editor::init);
1761
1762 cx_a.update(|cx| {
1763 SettingsStore::update_global(cx, |store, cx| {
1764 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1765 settings.defaults.inlay_hints = Some(InlayHintSettings {
1766 enabled: false,
1767 edit_debounce_ms: 0,
1768 scroll_debounce_ms: 0,
1769 show_type_hints: false,
1770 show_parameter_hints: false,
1771 show_other_hints: false,
1772 show_background: false,
1773 })
1774 });
1775 });
1776 });
1777 cx_b.update(|cx| {
1778 SettingsStore::update_global(cx, |store, cx| {
1779 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1780 settings.defaults.inlay_hints = Some(InlayHintSettings {
1781 enabled: true,
1782 edit_debounce_ms: 0,
1783 scroll_debounce_ms: 0,
1784 show_type_hints: true,
1785 show_parameter_hints: true,
1786 show_other_hints: true,
1787 show_background: false,
1788 })
1789 });
1790 });
1791 });
1792
1793 client_a.language_registry().add(rust_lang());
1794 client_b.language_registry().add(rust_lang());
1795 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1796 "Rust",
1797 FakeLspAdapter {
1798 capabilities: lsp::ServerCapabilities {
1799 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1800 ..Default::default()
1801 },
1802 ..Default::default()
1803 },
1804 );
1805
1806 client_a
1807 .fs()
1808 .insert_tree(
1809 "/a",
1810 json!({
1811 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1812 "other.rs": "// Test file",
1813 }),
1814 )
1815 .await;
1816 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1817 active_call_a
1818 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
1819 .await
1820 .unwrap();
1821 let project_id = active_call_a
1822 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1823 .await
1824 .unwrap();
1825
1826 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1827 active_call_b
1828 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
1829 .await
1830 .unwrap();
1831
1832 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1833 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1834
1835 cx_a.background_executor.start_waiting();
1836
1837 let editor_a = workspace_a
1838 .update_in(cx_a, |workspace, window, cx| {
1839 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1840 })
1841 .await
1842 .unwrap()
1843 .downcast::<Editor>()
1844 .unwrap();
1845
1846 let editor_b = workspace_b
1847 .update_in(cx_b, |workspace, window, cx| {
1848 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
1849 })
1850 .await
1851 .unwrap()
1852 .downcast::<Editor>()
1853 .unwrap();
1854
1855 let other_hints = Arc::new(AtomicBool::new(false));
1856 let fake_language_server = fake_language_servers.next().await.unwrap();
1857 let closure_other_hints = Arc::clone(&other_hints);
1858 fake_language_server
1859 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1860 let task_other_hints = Arc::clone(&closure_other_hints);
1861 async move {
1862 assert_eq!(
1863 params.text_document.uri,
1864 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1865 );
1866 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
1867 let character = if other_hints { 0 } else { 2 };
1868 let label = if other_hints {
1869 "other hint"
1870 } else {
1871 "initial hint"
1872 };
1873 Ok(Some(vec![lsp::InlayHint {
1874 position: lsp::Position::new(0, character),
1875 label: lsp::InlayHintLabel::String(label.to_string()),
1876 kind: None,
1877 text_edits: None,
1878 tooltip: None,
1879 padding_left: None,
1880 padding_right: None,
1881 data: None,
1882 }]))
1883 }
1884 })
1885 .next()
1886 .await
1887 .unwrap();
1888 executor.finish_waiting();
1889
1890 executor.run_until_parked();
1891 editor_a.update(cx_a, |editor, _| {
1892 assert!(
1893 extract_hint_labels(editor).is_empty(),
1894 "Host should get no hints due to them turned off"
1895 );
1896 });
1897
1898 executor.run_until_parked();
1899 editor_b.update(cx_b, |editor, _| {
1900 assert_eq!(
1901 vec!["initial hint".to_string()],
1902 extract_hint_labels(editor),
1903 "Client should get its first hints when opens an editor"
1904 );
1905 });
1906
1907 other_hints.fetch_or(true, atomic::Ordering::Release);
1908 fake_language_server
1909 .request::<lsp::request::InlayHintRefreshRequest>(())
1910 .await
1911 .expect("inlay refresh request failed");
1912 executor.run_until_parked();
1913 editor_a.update(cx_a, |editor, _| {
1914 assert!(
1915 extract_hint_labels(editor).is_empty(),
1916 "Host should get no hints due to them turned off, even after the /refresh"
1917 );
1918 });
1919
1920 executor.run_until_parked();
1921 editor_b.update(cx_b, |editor, _| {
1922 assert_eq!(
1923 vec!["other hint".to_string()],
1924 extract_hint_labels(editor),
1925 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
1926 );
1927 });
1928}
1929
1930#[gpui::test(iterations = 10)]
1931async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1932 let mut server = TestServer::start(cx_a.executor()).await;
1933 let client_a = server.create_client(cx_a, "user_a").await;
1934 let client_b = server.create_client(cx_b, "user_b").await;
1935 server
1936 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1937 .await;
1938 let active_call_a = cx_a.read(ActiveCall::global);
1939
1940 cx_a.update(editor::init);
1941 cx_b.update(editor::init);
1942 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
1943 let inline_blame_off_settings = Some(InlineBlameSettings {
1944 enabled: false,
1945 delay_ms: None,
1946 min_column: None,
1947 show_commit_summary: false,
1948 });
1949 cx_a.update(|cx| {
1950 SettingsStore::update_global(cx, |store, cx| {
1951 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1952 settings.git.inline_blame = inline_blame_off_settings;
1953 });
1954 });
1955 });
1956 cx_b.update(|cx| {
1957 SettingsStore::update_global(cx, |store, cx| {
1958 store.update_user_settings::<ProjectSettings>(cx, |settings| {
1959 settings.git.inline_blame = inline_blame_off_settings;
1960 });
1961 });
1962 });
1963
1964 client_a
1965 .fs()
1966 .insert_tree(
1967 "/my-repo",
1968 json!({
1969 ".git": {},
1970 "file.txt": "line1\nline2\nline3\nline\n",
1971 }),
1972 )
1973 .await;
1974
1975 let blame = git::blame::Blame {
1976 entries: vec![
1977 blame_entry("1b1b1b", 0..1),
1978 blame_entry("0d0d0d", 1..2),
1979 blame_entry("3a3a3a", 2..3),
1980 blame_entry("4c4c4c", 3..4),
1981 ],
1982 permalinks: HashMap::default(), // This field is deprecrated
1983 messages: [
1984 ("1b1b1b", "message for idx-0"),
1985 ("0d0d0d", "message for idx-1"),
1986 ("3a3a3a", "message for idx-2"),
1987 ("4c4c4c", "message for idx-3"),
1988 ]
1989 .into_iter()
1990 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
1991 .collect(),
1992 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
1993 };
1994 client_a
1995 .fs()
1996 .set_blame_for_repo(Path::new("/my-repo/.git"), vec![("file.txt".into(), blame)]);
1997
1998 let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
1999 let project_id = active_call_a
2000 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2001 .await
2002 .unwrap();
2003
2004 // Create editor_a
2005 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2006 let editor_a = workspace_a
2007 .update_in(cx_a, |workspace, window, cx| {
2008 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2009 })
2010 .await
2011 .unwrap()
2012 .downcast::<Editor>()
2013 .unwrap();
2014
2015 // Join the project as client B.
2016 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2017 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2018 let editor_b = workspace_b
2019 .update_in(cx_b, |workspace, window, cx| {
2020 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
2021 })
2022 .await
2023 .unwrap()
2024 .downcast::<Editor>()
2025 .unwrap();
2026
2027 // client_b now requests git blame for the open buffer
2028 editor_b.update_in(cx_b, |editor_b, window, cx| {
2029 assert!(editor_b.blame().is_none());
2030 editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx);
2031 });
2032
2033 cx_a.executor().run_until_parked();
2034 cx_b.executor().run_until_parked();
2035
2036 editor_b.update(cx_b, |editor_b, cx| {
2037 let blame = editor_b.blame().expect("editor_b should have blame now");
2038 let entries = blame.update(cx, |blame, cx| {
2039 blame
2040 .blame_for_rows(
2041 &(0..4)
2042 .map(|row| RowInfo {
2043 buffer_row: Some(row),
2044 ..Default::default()
2045 })
2046 .collect::<Vec<_>>(),
2047 cx,
2048 )
2049 .collect::<Vec<_>>()
2050 });
2051
2052 assert_eq!(
2053 entries,
2054 vec![
2055 Some(blame_entry("1b1b1b", 0..1)),
2056 Some(blame_entry("0d0d0d", 1..2)),
2057 Some(blame_entry("3a3a3a", 2..3)),
2058 Some(blame_entry("4c4c4c", 3..4)),
2059 ]
2060 );
2061
2062 blame.update(cx, |blame, _| {
2063 for (idx, entry) in entries.iter().flatten().enumerate() {
2064 let details = blame.details_for_entry(entry).unwrap();
2065 assert_eq!(details.message, format!("message for idx-{}", idx));
2066 assert_eq!(
2067 details.permalink.unwrap().to_string(),
2068 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
2069 );
2070 }
2071 });
2072 });
2073
2074 // editor_b updates the file, which gets sent to client_a, which updates git blame,
2075 // which gets back to client_b.
2076 editor_b.update_in(cx_b, |editor_b, _, cx| {
2077 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
2078 });
2079
2080 cx_a.executor().run_until_parked();
2081 cx_b.executor().run_until_parked();
2082
2083 editor_b.update(cx_b, |editor_b, cx| {
2084 let blame = editor_b.blame().expect("editor_b should have blame now");
2085 let entries = blame.update(cx, |blame, cx| {
2086 blame
2087 .blame_for_rows(
2088 &(0..4)
2089 .map(|row| RowInfo {
2090 buffer_row: Some(row),
2091 ..Default::default()
2092 })
2093 .collect::<Vec<_>>(),
2094 cx,
2095 )
2096 .collect::<Vec<_>>()
2097 });
2098
2099 assert_eq!(
2100 entries,
2101 vec![
2102 None,
2103 Some(blame_entry("0d0d0d", 1..2)),
2104 Some(blame_entry("3a3a3a", 2..3)),
2105 Some(blame_entry("4c4c4c", 3..4)),
2106 ]
2107 );
2108 });
2109
2110 // Now editor_a also updates the file
2111 editor_a.update_in(cx_a, |editor_a, _, cx| {
2112 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
2113 });
2114
2115 cx_a.executor().run_until_parked();
2116 cx_b.executor().run_until_parked();
2117
2118 editor_b.update(cx_b, |editor_b, cx| {
2119 let blame = editor_b.blame().expect("editor_b should have blame now");
2120 let entries = blame.update(cx, |blame, cx| {
2121 blame
2122 .blame_for_rows(
2123 &(0..4)
2124 .map(|row| RowInfo {
2125 buffer_row: Some(row),
2126 ..Default::default()
2127 })
2128 .collect::<Vec<_>>(),
2129 cx,
2130 )
2131 .collect::<Vec<_>>()
2132 });
2133
2134 assert_eq!(
2135 entries,
2136 vec![
2137 None,
2138 None,
2139 Some(blame_entry("3a3a3a", 2..3)),
2140 Some(blame_entry("4c4c4c", 3..4)),
2141 ]
2142 );
2143 });
2144}
2145
2146#[gpui::test(iterations = 30)]
2147async fn test_collaborating_with_editorconfig(
2148 cx_a: &mut TestAppContext,
2149 cx_b: &mut TestAppContext,
2150) {
2151 let mut server = TestServer::start(cx_a.executor()).await;
2152 let client_a = server.create_client(cx_a, "user_a").await;
2153 let client_b = server.create_client(cx_b, "user_b").await;
2154 server
2155 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2156 .await;
2157 let active_call_a = cx_a.read(ActiveCall::global);
2158
2159 cx_b.update(editor::init);
2160
2161 // Set up a fake language server.
2162 client_a.language_registry().add(rust_lang());
2163 client_a
2164 .fs()
2165 .insert_tree(
2166 "/a",
2167 json!({
2168 "src": {
2169 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2170 "other_mod": {
2171 "other.rs": "pub fn foo() -> usize {\n 4\n}",
2172 ".editorconfig": "",
2173 },
2174 },
2175 ".editorconfig": "[*]\ntab_width = 2\n",
2176 }),
2177 )
2178 .await;
2179 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2180 let project_id = active_call_a
2181 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2182 .await
2183 .unwrap();
2184 let main_buffer_a = project_a
2185 .update(cx_a, |p, cx| {
2186 p.open_buffer((worktree_id, "src/main.rs"), cx)
2187 })
2188 .await
2189 .unwrap();
2190 let other_buffer_a = project_a
2191 .update(cx_a, |p, cx| {
2192 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2193 })
2194 .await
2195 .unwrap();
2196 let cx_a = cx_a.add_empty_window();
2197 let main_editor_a = cx_a.new_window_entity(|window, cx| {
2198 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
2199 });
2200 let other_editor_a = cx_a.new_window_entity(|window, cx| {
2201 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
2202 });
2203 let mut main_editor_cx_a = EditorTestContext {
2204 cx: cx_a.clone(),
2205 window: cx_a.window_handle(),
2206 editor: main_editor_a,
2207 assertion_cx: AssertionContextManager::new(),
2208 };
2209 let mut other_editor_cx_a = EditorTestContext {
2210 cx: cx_a.clone(),
2211 window: cx_a.window_handle(),
2212 editor: other_editor_a,
2213 assertion_cx: AssertionContextManager::new(),
2214 };
2215
2216 // Join the project as client B.
2217 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2218 let main_buffer_b = project_b
2219 .update(cx_b, |p, cx| {
2220 p.open_buffer((worktree_id, "src/main.rs"), cx)
2221 })
2222 .await
2223 .unwrap();
2224 let other_buffer_b = project_b
2225 .update(cx_b, |p, cx| {
2226 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
2227 })
2228 .await
2229 .unwrap();
2230 let cx_b = cx_b.add_empty_window();
2231 let main_editor_b = cx_b.new_window_entity(|window, cx| {
2232 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
2233 });
2234 let other_editor_b = cx_b.new_window_entity(|window, cx| {
2235 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
2236 });
2237 let mut main_editor_cx_b = EditorTestContext {
2238 cx: cx_b.clone(),
2239 window: cx_b.window_handle(),
2240 editor: main_editor_b,
2241 assertion_cx: AssertionContextManager::new(),
2242 };
2243 let mut other_editor_cx_b = EditorTestContext {
2244 cx: cx_b.clone(),
2245 window: cx_b.window_handle(),
2246 editor: other_editor_b,
2247 assertion_cx: AssertionContextManager::new(),
2248 };
2249
2250 let initial_main = indoc! {"
2251ˇmod other;
2252fn main() { let foo = other::foo(); }"};
2253 let initial_other = indoc! {"
2254ˇpub fn foo() -> usize {
2255 4
2256}"};
2257
2258 let first_tabbed_main = indoc! {"
2259 ˇmod other;
2260fn main() { let foo = other::foo(); }"};
2261 tab_undo_assert(
2262 &mut main_editor_cx_a,
2263 &mut main_editor_cx_b,
2264 initial_main,
2265 first_tabbed_main,
2266 true,
2267 );
2268 tab_undo_assert(
2269 &mut main_editor_cx_a,
2270 &mut main_editor_cx_b,
2271 initial_main,
2272 first_tabbed_main,
2273 false,
2274 );
2275
2276 let first_tabbed_other = indoc! {"
2277 ˇpub fn foo() -> usize {
2278 4
2279}"};
2280 tab_undo_assert(
2281 &mut other_editor_cx_a,
2282 &mut other_editor_cx_b,
2283 initial_other,
2284 first_tabbed_other,
2285 true,
2286 );
2287 tab_undo_assert(
2288 &mut other_editor_cx_a,
2289 &mut other_editor_cx_b,
2290 initial_other,
2291 first_tabbed_other,
2292 false,
2293 );
2294
2295 client_a
2296 .fs()
2297 .atomic_write(
2298 PathBuf::from("/a/src/.editorconfig"),
2299 "[*]\ntab_width = 3\n".to_owned(),
2300 )
2301 .await
2302 .unwrap();
2303 cx_a.run_until_parked();
2304 cx_b.run_until_parked();
2305
2306 let second_tabbed_main = indoc! {"
2307 ˇmod other;
2308fn main() { let foo = other::foo(); }"};
2309 tab_undo_assert(
2310 &mut main_editor_cx_a,
2311 &mut main_editor_cx_b,
2312 initial_main,
2313 second_tabbed_main,
2314 true,
2315 );
2316 tab_undo_assert(
2317 &mut main_editor_cx_a,
2318 &mut main_editor_cx_b,
2319 initial_main,
2320 second_tabbed_main,
2321 false,
2322 );
2323
2324 let second_tabbed_other = indoc! {"
2325 ˇpub fn foo() -> usize {
2326 4
2327}"};
2328 tab_undo_assert(
2329 &mut other_editor_cx_a,
2330 &mut other_editor_cx_b,
2331 initial_other,
2332 second_tabbed_other,
2333 true,
2334 );
2335 tab_undo_assert(
2336 &mut other_editor_cx_a,
2337 &mut other_editor_cx_b,
2338 initial_other,
2339 second_tabbed_other,
2340 false,
2341 );
2342
2343 let editorconfig_buffer_b = project_b
2344 .update(cx_b, |p, cx| {
2345 p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
2346 })
2347 .await
2348 .unwrap();
2349 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
2350 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
2351 });
2352 project_b
2353 .update(cx_b, |project, cx| {
2354 project.save_buffer(editorconfig_buffer_b.clone(), cx)
2355 })
2356 .await
2357 .unwrap();
2358 cx_a.run_until_parked();
2359 cx_b.run_until_parked();
2360
2361 tab_undo_assert(
2362 &mut main_editor_cx_a,
2363 &mut main_editor_cx_b,
2364 initial_main,
2365 second_tabbed_main,
2366 true,
2367 );
2368 tab_undo_assert(
2369 &mut main_editor_cx_a,
2370 &mut main_editor_cx_b,
2371 initial_main,
2372 second_tabbed_main,
2373 false,
2374 );
2375
2376 let third_tabbed_other = indoc! {"
2377 ˇpub fn foo() -> usize {
2378 4
2379}"};
2380 tab_undo_assert(
2381 &mut other_editor_cx_a,
2382 &mut other_editor_cx_b,
2383 initial_other,
2384 third_tabbed_other,
2385 true,
2386 );
2387
2388 tab_undo_assert(
2389 &mut other_editor_cx_a,
2390 &mut other_editor_cx_b,
2391 initial_other,
2392 third_tabbed_other,
2393 false,
2394 );
2395}
2396
2397#[track_caller]
2398fn tab_undo_assert(
2399 cx_a: &mut EditorTestContext,
2400 cx_b: &mut EditorTestContext,
2401 expected_initial: &str,
2402 expected_tabbed: &str,
2403 a_tabs: bool,
2404) {
2405 cx_a.assert_editor_state(expected_initial);
2406 cx_b.assert_editor_state(expected_initial);
2407
2408 if a_tabs {
2409 cx_a.update_editor(|editor, window, cx| {
2410 editor.tab(&editor::actions::Tab, window, cx);
2411 });
2412 } else {
2413 cx_b.update_editor(|editor, window, cx| {
2414 editor.tab(&editor::actions::Tab, window, cx);
2415 });
2416 }
2417
2418 cx_a.run_until_parked();
2419 cx_b.run_until_parked();
2420
2421 cx_a.assert_editor_state(expected_tabbed);
2422 cx_b.assert_editor_state(expected_tabbed);
2423
2424 if a_tabs {
2425 cx_a.update_editor(|editor, window, cx| {
2426 editor.undo(&editor::actions::Undo, window, cx);
2427 });
2428 } else {
2429 cx_b.update_editor(|editor, window, cx| {
2430 editor.undo(&editor::actions::Undo, window, cx);
2431 });
2432 }
2433 cx_a.run_until_parked();
2434 cx_b.run_until_parked();
2435 cx_a.assert_editor_state(expected_initial);
2436 cx_b.assert_editor_state(expected_initial);
2437}
2438
2439fn extract_hint_labels(editor: &Editor) -> Vec<String> {
2440 let mut labels = Vec::new();
2441 for hint in editor.inlay_hint_cache().hints() {
2442 match hint.label {
2443 project::InlayHintLabel::String(s) => labels.push(s),
2444 _ => unreachable!(),
2445 }
2446 }
2447 labels
2448}
2449
2450fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
2451 git::blame::BlameEntry {
2452 sha: sha.parse().unwrap(),
2453 range,
2454 ..Default::default()
2455 }
2456}