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