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