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