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(text::Anchor::MIN..text::Anchor::MAX, false)
1583 .count()
1584 == 1
1585 });
1586
1587 // Edit the buffer as client B and see that edit as client A.
1588 editor_b.update_in(cx_b, |editor, window, cx| {
1589 editor.handle_input("ok, ", window, cx)
1590 });
1591 executor.run_until_parked();
1592
1593 buffer_a.read_with(cx_a, |buffer, _| {
1594 assert_eq!(buffer.text(), "ok, b-contents")
1595 });
1596
1597 // Client B can invite client C on a project shared by client A.
1598 active_call_b
1599 .update(cx_b, |call, cx| {
1600 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1601 })
1602 .await
1603 .unwrap();
1604
1605 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1606 executor.run_until_parked();
1607 let call = incoming_call_c.borrow().clone().unwrap();
1608 assert_eq!(call.calling_user.github_login, "user_b");
1609 let initial_project = call.initial_project.unwrap();
1610 active_call_c
1611 .update(cx_c, |call, cx| call.accept_incoming(cx))
1612 .await
1613 .unwrap();
1614 let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1615
1616 // Client B closes the editor, and client A sees client B's selections removed.
1617 cx_b.update(move |_, _| drop(editor_b));
1618 executor.run_until_parked();
1619
1620 buffer_a.read_with(cx_a, |buffer, _| {
1621 buffer
1622 .snapshot()
1623 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1624 .count()
1625 == 0
1626 });
1627}
1628
1629#[gpui::test(iterations = 10)]
1630async fn test_on_input_format_from_host_to_guest(
1631 cx_a: &mut TestAppContext,
1632 cx_b: &mut TestAppContext,
1633) {
1634 let mut server = TestServer::start(cx_a.executor()).await;
1635 let executor = cx_a.executor();
1636 let client_a = server.create_client(cx_a, "user_a").await;
1637 let client_b = server.create_client(cx_b, "user_b").await;
1638 server
1639 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1640 .await;
1641 let active_call_a = cx_a.read(ActiveCall::global);
1642
1643 client_a.language_registry().add(rust_lang());
1644 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1645 "Rust",
1646 FakeLspAdapter {
1647 capabilities: lsp::ServerCapabilities {
1648 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1649 first_trigger_character: ":".to_string(),
1650 more_trigger_character: Some(vec![">".to_string()]),
1651 }),
1652 ..Default::default()
1653 },
1654 ..Default::default()
1655 },
1656 );
1657
1658 client_a
1659 .fs()
1660 .insert_tree(
1661 path!("/a"),
1662 json!({
1663 "main.rs": "fn main() { a }",
1664 "other.rs": "// Test file",
1665 }),
1666 )
1667 .await;
1668 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1669 let project_id = active_call_a
1670 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1671 .await
1672 .unwrap();
1673 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1674
1675 // Open a file in an editor as the host.
1676 let buffer_a = project_a
1677 .update(cx_a, |p, cx| {
1678 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1679 })
1680 .await
1681 .unwrap();
1682 let cx_a = cx_a.add_empty_window();
1683 let editor_a = cx_a.new_window_entity(|window, cx| {
1684 Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
1685 });
1686
1687 let fake_language_server = fake_language_servers.next().await.unwrap();
1688 executor.run_until_parked();
1689
1690 // Receive an OnTypeFormatting request as the host's language server.
1691 // Return some formatting from the host's language server.
1692 fake_language_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
1693 |params, _| async move {
1694 assert_eq!(
1695 params.text_document_position.text_document.uri,
1696 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1697 );
1698 assert_eq!(
1699 params.text_document_position.position,
1700 lsp::Position::new(0, 14),
1701 );
1702
1703 Ok(Some(vec![lsp::TextEdit {
1704 new_text: "~<".to_string(),
1705 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1706 }]))
1707 },
1708 );
1709
1710 // Open the buffer on the guest and see that the formatting worked
1711 let buffer_b = project_b
1712 .update(cx_b, |p, cx| {
1713 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1714 })
1715 .await
1716 .unwrap();
1717
1718 // Type a on type formatting trigger character as the guest.
1719 cx_a.focus(&editor_a);
1720 editor_a.update_in(cx_a, |editor, window, cx| {
1721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1722 s.select_ranges([13..13])
1723 });
1724 editor.handle_input(">", window, cx);
1725 });
1726
1727 executor.run_until_parked();
1728
1729 buffer_b.read_with(cx_b, |buffer, _| {
1730 assert_eq!(buffer.text(), "fn main() { a>~< }")
1731 });
1732
1733 // Undo should remove LSP edits first
1734 editor_a.update_in(cx_a, |editor, window, cx| {
1735 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1736 editor.undo(&Undo, window, cx);
1737 assert_eq!(editor.text(cx), "fn main() { a> }");
1738 });
1739 executor.run_until_parked();
1740
1741 buffer_b.read_with(cx_b, |buffer, _| {
1742 assert_eq!(buffer.text(), "fn main() { a> }")
1743 });
1744
1745 editor_a.update_in(cx_a, |editor, window, cx| {
1746 assert_eq!(editor.text(cx), "fn main() { a> }");
1747 editor.undo(&Undo, window, cx);
1748 assert_eq!(editor.text(cx), "fn main() { a }");
1749 });
1750 executor.run_until_parked();
1751
1752 buffer_b.read_with(cx_b, |buffer, _| {
1753 assert_eq!(buffer.text(), "fn main() { a }")
1754 });
1755}
1756
1757#[gpui::test(iterations = 10)]
1758async fn test_on_input_format_from_guest_to_host(
1759 cx_a: &mut TestAppContext,
1760 cx_b: &mut TestAppContext,
1761) {
1762 let mut server = TestServer::start(cx_a.executor()).await;
1763 let executor = cx_a.executor();
1764 let client_a = server.create_client(cx_a, "user_a").await;
1765 let client_b = server.create_client(cx_b, "user_b").await;
1766 server
1767 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1768 .await;
1769 let active_call_a = cx_a.read(ActiveCall::global);
1770
1771 let capabilities = lsp::ServerCapabilities {
1772 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1773 first_trigger_character: ":".to_string(),
1774 more_trigger_character: Some(vec![">".to_string()]),
1775 }),
1776 ..lsp::ServerCapabilities::default()
1777 };
1778 client_a.language_registry().add(rust_lang());
1779 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1780 "Rust",
1781 FakeLspAdapter {
1782 capabilities: capabilities.clone(),
1783 ..FakeLspAdapter::default()
1784 },
1785 );
1786 client_b.language_registry().add(rust_lang());
1787 client_b.language_registry().register_fake_lsp_adapter(
1788 "Rust",
1789 FakeLspAdapter {
1790 capabilities,
1791 ..FakeLspAdapter::default()
1792 },
1793 );
1794
1795 client_a
1796 .fs()
1797 .insert_tree(
1798 path!("/a"),
1799 json!({
1800 "main.rs": "fn main() { a }",
1801 "other.rs": "// Test file",
1802 }),
1803 )
1804 .await;
1805 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1806 let project_id = active_call_a
1807 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1808 .await
1809 .unwrap();
1810 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1811
1812 // Open a file in an editor as the guest.
1813 let buffer_b = project_b
1814 .update(cx_b, |p, cx| {
1815 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1816 })
1817 .await
1818 .unwrap();
1819 let cx_b = cx_b.add_empty_window();
1820 let editor_b = cx_b.new_window_entity(|window, cx| {
1821 Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
1822 });
1823
1824 let fake_language_server = fake_language_servers.next().await.unwrap();
1825 executor.run_until_parked();
1826
1827 // Type a on type formatting trigger character as the guest.
1828 cx_b.focus(&editor_b);
1829 editor_b.update_in(cx_b, |editor, window, cx| {
1830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1831 s.select_ranges([13..13])
1832 });
1833 editor.handle_input(":", window, cx);
1834 });
1835
1836 // Receive an OnTypeFormatting request as the host's language server.
1837 // Return some formatting from the host's language server.
1838 executor.start_waiting();
1839 fake_language_server
1840 .set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1841 assert_eq!(
1842 params.text_document_position.text_document.uri,
1843 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1844 );
1845 assert_eq!(
1846 params.text_document_position.position,
1847 lsp::Position::new(0, 14),
1848 );
1849
1850 Ok(Some(vec![lsp::TextEdit {
1851 new_text: "~:".to_string(),
1852 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1853 }]))
1854 })
1855 .next()
1856 .await
1857 .unwrap();
1858 executor.finish_waiting();
1859
1860 // Open the buffer on the host and see that the formatting worked
1861 let buffer_a = project_a
1862 .update(cx_a, |p, cx| {
1863 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1864 })
1865 .await
1866 .unwrap();
1867 executor.run_until_parked();
1868
1869 buffer_a.read_with(cx_a, |buffer, _| {
1870 assert_eq!(buffer.text(), "fn main() { a:~: }")
1871 });
1872
1873 // Undo should remove LSP edits first
1874 editor_b.update_in(cx_b, |editor, window, cx| {
1875 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1876 editor.undo(&Undo, window, cx);
1877 assert_eq!(editor.text(cx), "fn main() { a: }");
1878 });
1879 executor.run_until_parked();
1880
1881 buffer_a.read_with(cx_a, |buffer, _| {
1882 assert_eq!(buffer.text(), "fn main() { a: }")
1883 });
1884
1885 editor_b.update_in(cx_b, |editor, window, cx| {
1886 assert_eq!(editor.text(cx), "fn main() { a: }");
1887 editor.undo(&Undo, window, cx);
1888 assert_eq!(editor.text(cx), "fn main() { a }");
1889 });
1890 executor.run_until_parked();
1891
1892 buffer_a.read_with(cx_a, |buffer, _| {
1893 assert_eq!(buffer.text(), "fn main() { a }")
1894 });
1895}
1896
1897#[gpui::test(iterations = 10)]
1898async fn test_mutual_editor_inlay_hint_cache_update(
1899 cx_a: &mut TestAppContext,
1900 cx_b: &mut TestAppContext,
1901) {
1902 let mut server = TestServer::start(cx_a.executor()).await;
1903 let executor = cx_a.executor();
1904 let client_a = server.create_client(cx_a, "user_a").await;
1905 let client_b = server.create_client(cx_b, "user_b").await;
1906 server
1907 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1908 .await;
1909 let active_call_a = cx_a.read(ActiveCall::global);
1910 let active_call_b = cx_b.read(ActiveCall::global);
1911
1912 cx_a.update(editor::init);
1913 cx_b.update(editor::init);
1914
1915 cx_a.update(|cx| {
1916 SettingsStore::update_global(cx, |store, cx| {
1917 store.update_user_settings(cx, |settings| {
1918 settings.project.all_languages.defaults.inlay_hints =
1919 Some(InlayHintSettingsContent {
1920 enabled: Some(true),
1921 ..InlayHintSettingsContent::default()
1922 })
1923 });
1924 });
1925 });
1926 cx_b.update(|cx| {
1927 SettingsStore::update_global(cx, |store, cx| {
1928 store.update_user_settings(cx, |settings| {
1929 settings.project.all_languages.defaults.inlay_hints =
1930 Some(InlayHintSettingsContent {
1931 enabled: Some(true),
1932 ..InlayHintSettingsContent::default()
1933 })
1934 });
1935 });
1936 });
1937
1938 let capabilities = lsp::ServerCapabilities {
1939 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1940 ..lsp::ServerCapabilities::default()
1941 };
1942 client_a.language_registry().add(rust_lang());
1943
1944 // Set up the language server to return an additional inlay hint on each request.
1945 let edits_made = Arc::new(AtomicUsize::new(0));
1946 let closure_edits_made = Arc::clone(&edits_made);
1947 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1948 "Rust",
1949 FakeLspAdapter {
1950 capabilities: capabilities.clone(),
1951 initializer: Some(Box::new(move |fake_language_server| {
1952 let closure_edits_made = closure_edits_made.clone();
1953 fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1954 move |params, _| {
1955 let edits_made_2 = Arc::clone(&closure_edits_made);
1956 async move {
1957 assert_eq!(
1958 params.text_document.uri,
1959 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1960 );
1961 let edits_made =
1962 AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
1963 Ok(Some(vec![lsp::InlayHint {
1964 position: lsp::Position::new(0, edits_made as u32),
1965 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1966 kind: None,
1967 text_edits: None,
1968 tooltip: None,
1969 padding_left: None,
1970 padding_right: None,
1971 data: None,
1972 }]))
1973 }
1974 },
1975 );
1976 })),
1977 ..FakeLspAdapter::default()
1978 },
1979 );
1980 client_b.language_registry().add(rust_lang());
1981 client_b.language_registry().register_fake_lsp_adapter(
1982 "Rust",
1983 FakeLspAdapter {
1984 capabilities,
1985 ..FakeLspAdapter::default()
1986 },
1987 );
1988
1989 // Client A opens a project.
1990 client_a
1991 .fs()
1992 .insert_tree(
1993 path!("/a"),
1994 json!({
1995 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1996 "other.rs": "// Test file",
1997 }),
1998 )
1999 .await;
2000 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2001 active_call_a
2002 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2003 .await
2004 .unwrap();
2005 let project_id = active_call_a
2006 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2007 .await
2008 .unwrap();
2009
2010 // Client B joins the project
2011 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2012 active_call_b
2013 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2014 .await
2015 .unwrap();
2016
2017 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2018
2019 // The host opens a rust file.
2020 let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
2021 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2022 });
2023 let fake_language_server = fake_language_servers.next().await.unwrap();
2024 let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
2025 executor.advance_clock(Duration::from_millis(100));
2026 executor.run_until_parked();
2027
2028 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
2029 editor_a.update(cx_a, |editor, cx| {
2030 assert_eq!(
2031 vec![initial_edit.to_string()],
2032 extract_hint_labels(editor, cx),
2033 "Host should get its first hints when opens an editor"
2034 );
2035 });
2036 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2037 let editor_b = workspace_b
2038 .update_in(cx_b, |workspace, window, cx| {
2039 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2040 })
2041 .await
2042 .unwrap()
2043 .downcast::<Editor>()
2044 .unwrap();
2045
2046 executor.advance_clock(Duration::from_millis(100));
2047 executor.run_until_parked();
2048 editor_b.update(cx_b, |editor, cx| {
2049 assert_eq!(
2050 vec![initial_edit.to_string()],
2051 extract_hint_labels(editor, cx),
2052 "Client should get its first hints when opens an editor"
2053 );
2054 });
2055
2056 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2057 editor_b.update_in(cx_b, |editor, window, cx| {
2058 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2059 s.select_ranges([13..13].clone())
2060 });
2061 editor.handle_input(":", window, cx);
2062 });
2063 cx_b.focus(&editor_b);
2064
2065 executor.advance_clock(Duration::from_secs(1));
2066 executor.run_until_parked();
2067 editor_a.update(cx_a, |editor, cx| {
2068 assert_eq!(
2069 vec![after_client_edit.to_string()],
2070 extract_hint_labels(editor, cx),
2071 );
2072 });
2073 editor_b.update(cx_b, |editor, cx| {
2074 assert_eq!(
2075 vec![after_client_edit.to_string()],
2076 extract_hint_labels(editor, cx),
2077 );
2078 });
2079
2080 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2081 editor_a.update_in(cx_a, |editor, window, cx| {
2082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2083 s.select_ranges([13..13])
2084 });
2085 editor.handle_input("a change to increment both buffers' versions", window, cx);
2086 });
2087 cx_a.focus(&editor_a);
2088
2089 executor.advance_clock(Duration::from_secs(1));
2090 executor.run_until_parked();
2091 editor_a.update(cx_a, |editor, cx| {
2092 assert_eq!(
2093 vec![after_host_edit.to_string()],
2094 extract_hint_labels(editor, cx),
2095 );
2096 });
2097 editor_b.update(cx_b, |editor, cx| {
2098 assert_eq!(
2099 vec![after_host_edit.to_string()],
2100 extract_hint_labels(editor, cx),
2101 );
2102 });
2103
2104 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2105 fake_language_server
2106 .request::<lsp::request::InlayHintRefreshRequest>(())
2107 .await
2108 .into_response()
2109 .expect("inlay refresh request failed");
2110
2111 executor.advance_clock(Duration::from_secs(1));
2112 executor.run_until_parked();
2113 editor_a.update(cx_a, |editor, cx| {
2114 assert_eq!(
2115 vec![after_special_edit_for_refresh.to_string()],
2116 extract_hint_labels(editor, cx),
2117 "Host should react to /refresh LSP request"
2118 );
2119 });
2120 editor_b.update(cx_b, |editor, cx| {
2121 assert_eq!(
2122 vec![after_special_edit_for_refresh.to_string()],
2123 extract_hint_labels(editor, cx),
2124 "Guest should get a /refresh LSP request propagated by host"
2125 );
2126 });
2127}
2128
2129#[gpui::test(iterations = 10)]
2130async fn test_inlay_hint_refresh_is_forwarded(
2131 cx_a: &mut TestAppContext,
2132 cx_b: &mut TestAppContext,
2133) {
2134 let mut server = TestServer::start(cx_a.executor()).await;
2135 let executor = cx_a.executor();
2136 let client_a = server.create_client(cx_a, "user_a").await;
2137 let client_b = server.create_client(cx_b, "user_b").await;
2138 server
2139 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2140 .await;
2141 let active_call_a = cx_a.read(ActiveCall::global);
2142 let active_call_b = cx_b.read(ActiveCall::global);
2143
2144 cx_a.update(editor::init);
2145 cx_b.update(editor::init);
2146
2147 cx_a.update(|cx| {
2148 SettingsStore::update_global(cx, |store, cx| {
2149 store.update_user_settings(cx, |settings| {
2150 settings.project.all_languages.defaults.inlay_hints =
2151 Some(InlayHintSettingsContent {
2152 show_value_hints: Some(true),
2153 enabled: Some(false),
2154 edit_debounce_ms: Some(0),
2155 scroll_debounce_ms: Some(0),
2156 show_type_hints: Some(false),
2157 show_parameter_hints: Some(false),
2158 show_other_hints: Some(false),
2159 show_background: Some(false),
2160 toggle_on_modifiers_press: None,
2161 })
2162 });
2163 });
2164 });
2165 cx_b.update(|cx| {
2166 SettingsStore::update_global(cx, |store, cx| {
2167 store.update_user_settings(cx, |settings| {
2168 settings.project.all_languages.defaults.inlay_hints =
2169 Some(InlayHintSettingsContent {
2170 show_value_hints: Some(true),
2171 enabled: Some(true),
2172 edit_debounce_ms: Some(0),
2173 scroll_debounce_ms: Some(0),
2174 show_type_hints: Some(true),
2175 show_parameter_hints: Some(true),
2176 show_other_hints: Some(true),
2177 show_background: Some(false),
2178 toggle_on_modifiers_press: None,
2179 })
2180 });
2181 });
2182 });
2183
2184 let capabilities = lsp::ServerCapabilities {
2185 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2186 ..lsp::ServerCapabilities::default()
2187 };
2188 client_a.language_registry().add(rust_lang());
2189 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2190 "Rust",
2191 FakeLspAdapter {
2192 capabilities: capabilities.clone(),
2193 ..FakeLspAdapter::default()
2194 },
2195 );
2196 client_b.language_registry().add(rust_lang());
2197 client_b.language_registry().register_fake_lsp_adapter(
2198 "Rust",
2199 FakeLspAdapter {
2200 capabilities,
2201 ..FakeLspAdapter::default()
2202 },
2203 );
2204
2205 client_a
2206 .fs()
2207 .insert_tree(
2208 path!("/a"),
2209 json!({
2210 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
2211 "other.rs": "// Test file",
2212 }),
2213 )
2214 .await;
2215 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2216 active_call_a
2217 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2218 .await
2219 .unwrap();
2220 let project_id = active_call_a
2221 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2222 .await
2223 .unwrap();
2224
2225 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2226 active_call_b
2227 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2228 .await
2229 .unwrap();
2230
2231 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2232 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2233
2234 cx_a.background_executor.start_waiting();
2235
2236 let editor_a = workspace_a
2237 .update_in(cx_a, |workspace, window, cx| {
2238 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2239 })
2240 .await
2241 .unwrap()
2242 .downcast::<Editor>()
2243 .unwrap();
2244
2245 let editor_b = workspace_b
2246 .update_in(cx_b, |workspace, window, cx| {
2247 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2248 })
2249 .await
2250 .unwrap()
2251 .downcast::<Editor>()
2252 .unwrap();
2253
2254 let other_hints = Arc::new(AtomicBool::new(false));
2255 let fake_language_server = fake_language_servers.next().await.unwrap();
2256 let closure_other_hints = Arc::clone(&other_hints);
2257 fake_language_server
2258 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2259 let task_other_hints = Arc::clone(&closure_other_hints);
2260 async move {
2261 assert_eq!(
2262 params.text_document.uri,
2263 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2264 );
2265 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
2266 let character = if other_hints { 0 } else { 2 };
2267 let label = if other_hints {
2268 "other hint"
2269 } else {
2270 "initial hint"
2271 };
2272 Ok(Some(vec![
2273 lsp::InlayHint {
2274 position: lsp::Position::new(0, character),
2275 label: lsp::InlayHintLabel::String(label.to_string()),
2276 kind: None,
2277 text_edits: None,
2278 tooltip: None,
2279 padding_left: None,
2280 padding_right: None,
2281 data: None,
2282 },
2283 lsp::InlayHint {
2284 position: lsp::Position::new(1090, 1090),
2285 label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
2286 kind: None,
2287 text_edits: None,
2288 tooltip: None,
2289 padding_left: None,
2290 padding_right: None,
2291 data: None,
2292 },
2293 ]))
2294 }
2295 })
2296 .next()
2297 .await
2298 .unwrap();
2299 executor.finish_waiting();
2300
2301 executor.run_until_parked();
2302 editor_a.update(cx_a, |editor, cx| {
2303 assert!(
2304 extract_hint_labels(editor, cx).is_empty(),
2305 "Host should get no hints due to them turned off"
2306 );
2307 });
2308
2309 executor.run_until_parked();
2310 editor_b.update(cx_b, |editor, cx| {
2311 assert_eq!(
2312 vec!["initial hint".to_string()],
2313 extract_hint_labels(editor, cx),
2314 "Client should get its first hints when opens an editor"
2315 );
2316 });
2317
2318 other_hints.fetch_or(true, atomic::Ordering::Release);
2319 fake_language_server
2320 .request::<lsp::request::InlayHintRefreshRequest>(())
2321 .await
2322 .into_response()
2323 .expect("inlay refresh request failed");
2324 executor.run_until_parked();
2325 editor_a.update(cx_a, |editor, cx| {
2326 assert!(
2327 extract_hint_labels(editor, cx).is_empty(),
2328 "Host should get no hints due to them turned off, even after the /refresh"
2329 );
2330 });
2331
2332 executor.run_until_parked();
2333 editor_b.update(cx_b, |editor, cx| {
2334 assert_eq!(
2335 vec!["other hint".to_string()],
2336 extract_hint_labels(editor, cx),
2337 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
2338 );
2339 });
2340}
2341
2342#[gpui::test(iterations = 10)]
2343async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2344 let expected_color = Rgba {
2345 r: 0.33,
2346 g: 0.33,
2347 b: 0.33,
2348 a: 0.33,
2349 };
2350 let mut server = TestServer::start(cx_a.executor()).await;
2351 let executor = cx_a.executor();
2352 let client_a = server.create_client(cx_a, "user_a").await;
2353 let client_b = server.create_client(cx_b, "user_b").await;
2354 server
2355 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2356 .await;
2357 let active_call_a = cx_a.read(ActiveCall::global);
2358 let active_call_b = cx_b.read(ActiveCall::global);
2359
2360 cx_a.update(editor::init);
2361 cx_b.update(editor::init);
2362
2363 cx_a.update(|cx| {
2364 SettingsStore::update_global(cx, |store, cx| {
2365 store.update_user_settings(cx, |settings| {
2366 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None);
2367 });
2368 });
2369 });
2370 cx_b.update(|cx| {
2371 SettingsStore::update_global(cx, |store, cx| {
2372 store.update_user_settings(cx, |settings| {
2373 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2374 });
2375 });
2376 });
2377
2378 let capabilities = lsp::ServerCapabilities {
2379 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
2380 ..lsp::ServerCapabilities::default()
2381 };
2382 client_a.language_registry().add(rust_lang());
2383 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2384 "Rust",
2385 FakeLspAdapter {
2386 capabilities: capabilities.clone(),
2387 ..FakeLspAdapter::default()
2388 },
2389 );
2390 client_b.language_registry().add(rust_lang());
2391 client_b.language_registry().register_fake_lsp_adapter(
2392 "Rust",
2393 FakeLspAdapter {
2394 capabilities,
2395 ..FakeLspAdapter::default()
2396 },
2397 );
2398
2399 // Client A opens a project.
2400 client_a
2401 .fs()
2402 .insert_tree(
2403 path!("/a"),
2404 json!({
2405 "main.rs": "fn main() { a }",
2406 }),
2407 )
2408 .await;
2409 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2410 active_call_a
2411 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2412 .await
2413 .unwrap();
2414 let project_id = active_call_a
2415 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2416 .await
2417 .unwrap();
2418
2419 // Client B joins the project
2420 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2421 active_call_b
2422 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2423 .await
2424 .unwrap();
2425
2426 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2427
2428 // The host opens a rust file.
2429 let _buffer_a = project_a
2430 .update(cx_a, |project, cx| {
2431 project.open_local_buffer(path!("/a/main.rs"), cx)
2432 })
2433 .await
2434 .unwrap();
2435 let editor_a = workspace_a
2436 .update_in(cx_a, |workspace, window, cx| {
2437 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2438 })
2439 .await
2440 .unwrap()
2441 .downcast::<Editor>()
2442 .unwrap();
2443
2444 let fake_language_server = fake_language_servers.next().await.unwrap();
2445 cx_a.run_until_parked();
2446 cx_b.run_until_parked();
2447
2448 let requests_made = Arc::new(AtomicUsize::new(0));
2449 let closure_requests_made = Arc::clone(&requests_made);
2450 let mut color_request_handle = fake_language_server
2451 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
2452 let requests_made = Arc::clone(&closure_requests_made);
2453 async move {
2454 assert_eq!(
2455 params.text_document.uri,
2456 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2457 );
2458 requests_made.fetch_add(1, atomic::Ordering::Release);
2459 Ok(vec![lsp::ColorInformation {
2460 range: lsp::Range {
2461 start: lsp::Position {
2462 line: 0,
2463 character: 0,
2464 },
2465 end: lsp::Position {
2466 line: 0,
2467 character: 1,
2468 },
2469 },
2470 color: lsp::Color {
2471 red: 0.33,
2472 green: 0.33,
2473 blue: 0.33,
2474 alpha: 0.33,
2475 },
2476 }])
2477 }
2478 });
2479 executor.run_until_parked();
2480
2481 assert_eq!(
2482 0,
2483 requests_made.load(atomic::Ordering::Acquire),
2484 "Host did not enable document colors, hence should query for none"
2485 );
2486 editor_a.update(cx_a, |editor, cx| {
2487 assert_eq!(
2488 Vec::<Rgba>::new(),
2489 extract_color_inlays(editor, cx),
2490 "No query colors should result in no hints"
2491 );
2492 });
2493
2494 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2495 let editor_b = workspace_b
2496 .update_in(cx_b, |workspace, window, cx| {
2497 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2498 })
2499 .await
2500 .unwrap()
2501 .downcast::<Editor>()
2502 .unwrap();
2503
2504 color_request_handle.next().await.unwrap();
2505 executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
2506 executor.run_until_parked();
2507
2508 assert_eq!(
2509 1,
2510 requests_made.load(atomic::Ordering::Acquire),
2511 "The client opened the file and got its first colors back"
2512 );
2513 editor_b.update(cx_b, |editor, cx| {
2514 assert_eq!(
2515 vec![expected_color],
2516 extract_color_inlays(editor, cx),
2517 "With document colors as inlays, color inlays should be pushed"
2518 );
2519 });
2520
2521 editor_a.update_in(cx_a, |editor, window, cx| {
2522 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2523 s.select_ranges([13..13].clone())
2524 });
2525 editor.handle_input(":", window, cx);
2526 });
2527 color_request_handle.next().await.unwrap();
2528 executor.run_until_parked();
2529 assert_eq!(
2530 2,
2531 requests_made.load(atomic::Ordering::Acquire),
2532 "After the host edits his file, the client should request the colors again"
2533 );
2534 editor_a.update(cx_a, |editor, cx| {
2535 assert_eq!(
2536 Vec::<Rgba>::new(),
2537 extract_color_inlays(editor, cx),
2538 "Host has no colors still"
2539 );
2540 });
2541 editor_b.update(cx_b, |editor, cx| {
2542 assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
2543 });
2544
2545 cx_b.update(|_, cx| {
2546 SettingsStore::update_global(cx, |store, cx| {
2547 store.update_user_settings(cx, |settings| {
2548 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
2549 });
2550 });
2551 });
2552 executor.run_until_parked();
2553 assert_eq!(
2554 2,
2555 requests_made.load(atomic::Ordering::Acquire),
2556 "After the client have changed the colors settings, no extra queries should happen"
2557 );
2558 editor_a.update(cx_a, |editor, cx| {
2559 assert_eq!(
2560 Vec::<Rgba>::new(),
2561 extract_color_inlays(editor, cx),
2562 "Host is unaffected by the client's settings changes"
2563 );
2564 });
2565 editor_b.update(cx_b, |editor, cx| {
2566 assert_eq!(
2567 Vec::<Rgba>::new(),
2568 extract_color_inlays(editor, cx),
2569 "Client should have no colors hints, as in the settings"
2570 );
2571 });
2572
2573 cx_b.update(|_, cx| {
2574 SettingsStore::update_global(cx, |store, cx| {
2575 store.update_user_settings(cx, |settings| {
2576 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2577 });
2578 });
2579 });
2580 executor.run_until_parked();
2581 assert_eq!(
2582 2,
2583 requests_made.load(atomic::Ordering::Acquire),
2584 "After falling back to colors as inlays, no extra LSP queries are made"
2585 );
2586 editor_a.update(cx_a, |editor, cx| {
2587 assert_eq!(
2588 Vec::<Rgba>::new(),
2589 extract_color_inlays(editor, cx),
2590 "Host is unaffected by the client's settings changes, again"
2591 );
2592 });
2593 editor_b.update(cx_b, |editor, cx| {
2594 assert_eq!(
2595 vec![expected_color],
2596 extract_color_inlays(editor, cx),
2597 "Client should have its color hints back"
2598 );
2599 });
2600
2601 cx_a.update(|_, cx| {
2602 SettingsStore::update_global(cx, |store, cx| {
2603 store.update_user_settings(cx, |settings| {
2604 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
2605 });
2606 });
2607 });
2608 color_request_handle.next().await.unwrap();
2609 executor.run_until_parked();
2610 assert_eq!(
2611 3,
2612 requests_made.load(atomic::Ordering::Acquire),
2613 "After the host enables document colors, another LSP query should be made"
2614 );
2615 editor_a.update(cx_a, |editor, cx| {
2616 assert_eq!(
2617 Vec::<Rgba>::new(),
2618 extract_color_inlays(editor, cx),
2619 "Host did not configure document colors as hints hence gets nothing"
2620 );
2621 });
2622 editor_b.update(cx_b, |editor, cx| {
2623 assert_eq!(
2624 vec![expected_color],
2625 extract_color_inlays(editor, cx),
2626 "Client should be unaffected by the host's settings changes"
2627 );
2628 });
2629}
2630
2631async fn test_lsp_pull_diagnostics(
2632 should_stream_workspace_diagnostic: bool,
2633 cx_a: &mut TestAppContext,
2634 cx_b: &mut TestAppContext,
2635) {
2636 let mut server = TestServer::start(cx_a.executor()).await;
2637 let executor = cx_a.executor();
2638 let client_a = server.create_client(cx_a, "user_a").await;
2639 let client_b = server.create_client(cx_b, "user_b").await;
2640 server
2641 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2642 .await;
2643 let active_call_a = cx_a.read(ActiveCall::global);
2644 let active_call_b = cx_b.read(ActiveCall::global);
2645
2646 cx_a.update(editor::init);
2647 cx_b.update(editor::init);
2648
2649 let expected_push_diagnostic_main_message = "pushed main diagnostic";
2650 let expected_push_diagnostic_lib_message = "pushed lib diagnostic";
2651 let expected_pull_diagnostic_main_message = "pulled main diagnostic";
2652 let expected_pull_diagnostic_lib_message = "pulled lib diagnostic";
2653 let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic";
2654 let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic";
2655
2656 let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<Option<String>>::new()));
2657 let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<String>::new()));
2658 let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2659 let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone();
2660 let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone();
2661 let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2662 let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone();
2663 let closure_workspace_diagnostics_pulls_result_ids =
2664 workspace_diagnostics_pulls_result_ids.clone();
2665 let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) =
2666 smol::channel::bounded::<()>(1);
2667 let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
2668 smol::channel::bounded::<()>(1);
2669
2670 let capabilities = lsp::ServerCapabilities {
2671 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
2672 lsp::DiagnosticOptions {
2673 identifier: Some("test-pulls".to_string()),
2674 inter_file_dependencies: true,
2675 workspace_diagnostics: true,
2676 work_done_progress_options: lsp::WorkDoneProgressOptions {
2677 work_done_progress: None,
2678 },
2679 },
2680 )),
2681 ..lsp::ServerCapabilities::default()
2682 };
2683 client_a.language_registry().add(rust_lang());
2684
2685 let pull_diagnostics_handle = Arc::new(parking_lot::Mutex::new(None));
2686 let workspace_diagnostics_pulls_handle = Arc::new(parking_lot::Mutex::new(None));
2687
2688 let closure_pull_diagnostics_handle = pull_diagnostics_handle.clone();
2689 let closure_workspace_diagnostics_pulls_handle = workspace_diagnostics_pulls_handle.clone();
2690 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2691 "Rust",
2692 FakeLspAdapter {
2693 capabilities: capabilities.clone(),
2694 initializer: Some(Box::new(move |fake_language_server| {
2695 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2696 "workspace/diagnostic/{}/1",
2697 fake_language_server.server.server_id()
2698 ));
2699 let closure_workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone();
2700 let diagnostics_pulls_made = closure_diagnostics_pulls_made.clone();
2701 let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
2702 let closure_pull_diagnostics_handle = closure_pull_diagnostics_handle.clone();
2703 let closure_workspace_diagnostics_pulls_handle = closure_workspace_diagnostics_pulls_handle.clone();
2704 let closure_workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2705 let closure_workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2706 let pull_diagnostics_handle = fake_language_server
2707 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(
2708 move |params, _| {
2709 let requests_made = diagnostics_pulls_made.clone();
2710 let diagnostics_pulls_result_ids =
2711 diagnostics_pulls_result_ids.clone();
2712 async move {
2713 let message = if lsp::Uri::from_file_path(path!("/a/main.rs"))
2714 .unwrap()
2715 == params.text_document.uri
2716 {
2717 expected_pull_diagnostic_main_message.to_string()
2718 } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2719 == params.text_document.uri
2720 {
2721 expected_pull_diagnostic_lib_message.to_string()
2722 } else {
2723 panic!("Unexpected document: {}", params.text_document.uri)
2724 };
2725 {
2726 diagnostics_pulls_result_ids
2727 .lock()
2728 .await
2729 .insert(params.previous_result_id);
2730 }
2731 let new_requests_count =
2732 requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2733 Ok(lsp::DocumentDiagnosticReportResult::Report(
2734 lsp::DocumentDiagnosticReport::Full(
2735 lsp::RelatedFullDocumentDiagnosticReport {
2736 related_documents: None,
2737 full_document_diagnostic_report:
2738 lsp::FullDocumentDiagnosticReport {
2739 result_id: Some(format!(
2740 "pull-{new_requests_count}"
2741 )),
2742 items: vec![lsp::Diagnostic {
2743 range: lsp::Range {
2744 start: lsp::Position {
2745 line: 0,
2746 character: 0,
2747 },
2748 end: lsp::Position {
2749 line: 0,
2750 character: 2,
2751 },
2752 },
2753 severity: Some(
2754 lsp::DiagnosticSeverity::ERROR,
2755 ),
2756 message,
2757 ..lsp::Diagnostic::default()
2758 }],
2759 },
2760 },
2761 ),
2762 ))
2763 }
2764 },
2765 );
2766 let _ = closure_pull_diagnostics_handle.lock().insert(pull_diagnostics_handle);
2767
2768 let closure_workspace_diagnostics_pulls_made = closure_workspace_diagnostics_pulls_made.clone();
2769 let workspace_diagnostics_pulls_handle = fake_language_server.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
2770 move |params, _| {
2771 let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone();
2772 let workspace_diagnostics_pulls_result_ids =
2773 closure_workspace_diagnostics_pulls_result_ids.clone();
2774 let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2775 let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2776 let expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
2777 async move {
2778 let workspace_request_count =
2779 workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2780 {
2781 workspace_diagnostics_pulls_result_ids
2782 .lock()
2783 .await
2784 .extend(params.previous_result_ids.into_iter().map(|id| id.value));
2785 }
2786 if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed()
2787 {
2788 assert_eq!(
2789 params.partial_result_params.partial_result_token,
2790 Some(expected_workspace_diagnostic_token)
2791 );
2792 workspace_diagnostic_received_tx.send(()).await.unwrap();
2793 workspace_diagnostic_cancel_rx.recv().await.unwrap();
2794 workspace_diagnostic_cancel_rx.close();
2795 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
2796 // > The final response has to be empty in terms of result values.
2797 return Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2798 lsp::WorkspaceDiagnosticReport { items: Vec::new() },
2799 ));
2800 }
2801 Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2802 lsp::WorkspaceDiagnosticReport {
2803 items: vec![
2804 lsp::WorkspaceDocumentDiagnosticReport::Full(
2805 lsp::WorkspaceFullDocumentDiagnosticReport {
2806 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2807 version: None,
2808 full_document_diagnostic_report:
2809 lsp::FullDocumentDiagnosticReport {
2810 result_id: Some(format!(
2811 "workspace_{workspace_request_count}"
2812 )),
2813 items: vec![lsp::Diagnostic {
2814 range: lsp::Range {
2815 start: lsp::Position {
2816 line: 0,
2817 character: 1,
2818 },
2819 end: lsp::Position {
2820 line: 0,
2821 character: 3,
2822 },
2823 },
2824 severity: Some(lsp::DiagnosticSeverity::WARNING),
2825 message:
2826 expected_workspace_pull_diagnostics_main_message
2827 .to_string(),
2828 ..lsp::Diagnostic::default()
2829 }],
2830 },
2831 },
2832 ),
2833 lsp::WorkspaceDocumentDiagnosticReport::Full(
2834 lsp::WorkspaceFullDocumentDiagnosticReport {
2835 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
2836 version: None,
2837 full_document_diagnostic_report:
2838 lsp::FullDocumentDiagnosticReport {
2839 result_id: Some(format!(
2840 "workspace_{workspace_request_count}"
2841 )),
2842 items: vec![lsp::Diagnostic {
2843 range: lsp::Range {
2844 start: lsp::Position {
2845 line: 0,
2846 character: 1,
2847 },
2848 end: lsp::Position {
2849 line: 0,
2850 character: 3,
2851 },
2852 },
2853 severity: Some(lsp::DiagnosticSeverity::WARNING),
2854 message:
2855 expected_workspace_pull_diagnostics_lib_message
2856 .to_string(),
2857 ..lsp::Diagnostic::default()
2858 }],
2859 },
2860 },
2861 ),
2862 ],
2863 },
2864 ))
2865 }
2866 });
2867 let _ = closure_workspace_diagnostics_pulls_handle.lock().insert(workspace_diagnostics_pulls_handle);
2868 })),
2869 ..FakeLspAdapter::default()
2870 },
2871 );
2872
2873 client_b.language_registry().add(rust_lang());
2874 client_b.language_registry().register_fake_lsp_adapter(
2875 "Rust",
2876 FakeLspAdapter {
2877 capabilities,
2878 ..FakeLspAdapter::default()
2879 },
2880 );
2881
2882 // Client A opens a project.
2883 client_a
2884 .fs()
2885 .insert_tree(
2886 path!("/a"),
2887 json!({
2888 "main.rs": "fn main() { a }",
2889 "lib.rs": "fn other() {}",
2890 }),
2891 )
2892 .await;
2893 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2894 active_call_a
2895 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2896 .await
2897 .unwrap();
2898 let project_id = active_call_a
2899 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2900 .await
2901 .unwrap();
2902
2903 // Client B joins the project
2904 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2905 active_call_b
2906 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2907 .await
2908 .unwrap();
2909
2910 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2911 executor.start_waiting();
2912
2913 // The host opens a rust file.
2914 let _buffer_a = project_a
2915 .update(cx_a, |project, cx| {
2916 project.open_local_buffer(path!("/a/main.rs"), cx)
2917 })
2918 .await
2919 .unwrap();
2920 let editor_a_main = workspace_a
2921 .update_in(cx_a, |workspace, window, cx| {
2922 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2923 })
2924 .await
2925 .unwrap()
2926 .downcast::<Editor>()
2927 .unwrap();
2928
2929 let fake_language_server = fake_language_servers.next().await.unwrap();
2930 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2931 "workspace/diagnostic-{}-1",
2932 fake_language_server.server.server_id()
2933 ));
2934 cx_a.run_until_parked();
2935 cx_b.run_until_parked();
2936 let mut pull_diagnostics_handle = pull_diagnostics_handle.lock().take().unwrap();
2937 let mut workspace_diagnostics_pulls_handle =
2938 workspace_diagnostics_pulls_handle.lock().take().unwrap();
2939
2940 if should_stream_workspace_diagnostic {
2941 workspace_diagnostic_received_rx.recv().await.unwrap();
2942 } else {
2943 workspace_diagnostics_pulls_handle.next().await.unwrap();
2944 }
2945 assert_eq!(
2946 1,
2947 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
2948 "Workspace diagnostics should be pulled initially on a server startup"
2949 );
2950 pull_diagnostics_handle.next().await.unwrap();
2951 assert_eq!(
2952 1,
2953 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
2954 "Host should query pull diagnostics when the editor is opened"
2955 );
2956 executor.run_until_parked();
2957 editor_a_main.update(cx_a, |editor, cx| {
2958 let snapshot = editor.buffer().read(cx).snapshot(cx);
2959 let all_diagnostics = snapshot
2960 .diagnostics_in_range(0..snapshot.len())
2961 .collect::<Vec<_>>();
2962 assert_eq!(
2963 all_diagnostics.len(),
2964 1,
2965 "Expected single diagnostic, but got: {all_diagnostics:?}"
2966 );
2967 let diagnostic = &all_diagnostics[0];
2968 let mut expected_messages = vec![expected_pull_diagnostic_main_message];
2969 if !should_stream_workspace_diagnostic {
2970 expected_messages.push(expected_workspace_pull_diagnostics_main_message);
2971 }
2972 assert!(
2973 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
2974 "Expected {expected_messages:?} on the host, but got: {}",
2975 diagnostic.diagnostic.message
2976 );
2977 });
2978
2979 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
2980 lsp::PublishDiagnosticsParams {
2981 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2982 diagnostics: vec![lsp::Diagnostic {
2983 range: lsp::Range {
2984 start: lsp::Position {
2985 line: 0,
2986 character: 3,
2987 },
2988 end: lsp::Position {
2989 line: 0,
2990 character: 4,
2991 },
2992 },
2993 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
2994 message: expected_push_diagnostic_main_message.to_string(),
2995 ..lsp::Diagnostic::default()
2996 }],
2997 version: None,
2998 },
2999 );
3000 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3001 lsp::PublishDiagnosticsParams {
3002 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3003 diagnostics: vec![lsp::Diagnostic {
3004 range: lsp::Range {
3005 start: lsp::Position {
3006 line: 0,
3007 character: 3,
3008 },
3009 end: lsp::Position {
3010 line: 0,
3011 character: 4,
3012 },
3013 },
3014 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
3015 message: expected_push_diagnostic_lib_message.to_string(),
3016 ..lsp::Diagnostic::default()
3017 }],
3018 version: None,
3019 },
3020 );
3021
3022 if should_stream_workspace_diagnostic {
3023 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3024 token: expected_workspace_diagnostic_token.clone(),
3025 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3026 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3027 items: vec![
3028 lsp::WorkspaceDocumentDiagnosticReport::Full(
3029 lsp::WorkspaceFullDocumentDiagnosticReport {
3030 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3031 version: None,
3032 full_document_diagnostic_report:
3033 lsp::FullDocumentDiagnosticReport {
3034 result_id: Some(format!(
3035 "workspace_{}",
3036 workspace_diagnostics_pulls_made
3037 .fetch_add(1, atomic::Ordering::Release)
3038 + 1
3039 )),
3040 items: vec![lsp::Diagnostic {
3041 range: lsp::Range {
3042 start: lsp::Position {
3043 line: 0,
3044 character: 1,
3045 },
3046 end: lsp::Position {
3047 line: 0,
3048 character: 2,
3049 },
3050 },
3051 severity: Some(lsp::DiagnosticSeverity::ERROR),
3052 message:
3053 expected_workspace_pull_diagnostics_main_message
3054 .to_string(),
3055 ..lsp::Diagnostic::default()
3056 }],
3057 },
3058 },
3059 ),
3060 lsp::WorkspaceDocumentDiagnosticReport::Full(
3061 lsp::WorkspaceFullDocumentDiagnosticReport {
3062 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3063 version: None,
3064 full_document_diagnostic_report:
3065 lsp::FullDocumentDiagnosticReport {
3066 result_id: Some(format!(
3067 "workspace_{}",
3068 workspace_diagnostics_pulls_made
3069 .fetch_add(1, atomic::Ordering::Release)
3070 + 1
3071 )),
3072 items: Vec::new(),
3073 },
3074 },
3075 ),
3076 ],
3077 }),
3078 ),
3079 });
3080 };
3081
3082 let mut workspace_diagnostic_start_count =
3083 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3084
3085 executor.run_until_parked();
3086 editor_a_main.update(cx_a, |editor, cx| {
3087 let snapshot = editor.buffer().read(cx).snapshot(cx);
3088 let all_diagnostics = snapshot
3089 .diagnostics_in_range(0..snapshot.len())
3090 .collect::<Vec<_>>();
3091 assert_eq!(
3092 all_diagnostics.len(),
3093 2,
3094 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3095 );
3096 let expected_messages = [
3097 expected_workspace_pull_diagnostics_main_message,
3098 expected_pull_diagnostic_main_message,
3099 expected_push_diagnostic_main_message,
3100 ];
3101 for diagnostic in all_diagnostics {
3102 assert!(
3103 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3104 "Expected push and pull messages on the host: {expected_messages:?}, but got: {}",
3105 diagnostic.diagnostic.message
3106 );
3107 }
3108 });
3109
3110 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3111 let editor_b_main = workspace_b
3112 .update_in(cx_b, |workspace, window, cx| {
3113 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
3114 })
3115 .await
3116 .unwrap()
3117 .downcast::<Editor>()
3118 .unwrap();
3119 cx_b.run_until_parked();
3120
3121 pull_diagnostics_handle.next().await.unwrap();
3122 assert_eq!(
3123 2,
3124 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3125 "Client should query pull diagnostics when its editor is opened"
3126 );
3127 executor.run_until_parked();
3128 assert_eq!(
3129 workspace_diagnostic_start_count,
3130 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3131 "Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull"
3132 );
3133 editor_b_main.update(cx_b, |editor, cx| {
3134 let snapshot = editor.buffer().read(cx).snapshot(cx);
3135 let all_diagnostics = snapshot
3136 .diagnostics_in_range(0..snapshot.len())
3137 .collect::<Vec<_>>();
3138 assert_eq!(
3139 all_diagnostics.len(),
3140 2,
3141 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3142 );
3143
3144 // Despite the workspace diagnostics not re-initialized for the remote client, we can still expect its message synced from the host.
3145 let expected_messages = [
3146 expected_workspace_pull_diagnostics_main_message,
3147 expected_pull_diagnostic_main_message,
3148 expected_push_diagnostic_main_message,
3149 ];
3150 for diagnostic in all_diagnostics {
3151 assert!(
3152 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3153 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3154 diagnostic.diagnostic.message
3155 );
3156 }
3157 });
3158
3159 let editor_b_lib = workspace_b
3160 .update_in(cx_b, |workspace, window, cx| {
3161 workspace.open_path((worktree_id, rel_path("lib.rs")), None, true, window, cx)
3162 })
3163 .await
3164 .unwrap()
3165 .downcast::<Editor>()
3166 .unwrap();
3167
3168 pull_diagnostics_handle.next().await.unwrap();
3169 assert_eq!(
3170 3,
3171 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3172 "Client should query pull diagnostics when its another editor is opened"
3173 );
3174 executor.run_until_parked();
3175 assert_eq!(
3176 workspace_diagnostic_start_count,
3177 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3178 "The remote client still did not anything to trigger the workspace diagnostics pull"
3179 );
3180 editor_b_lib.update(cx_b, |editor, cx| {
3181 let snapshot = editor.buffer().read(cx).snapshot(cx);
3182 let all_diagnostics = snapshot
3183 .diagnostics_in_range(0..snapshot.len())
3184 .collect::<Vec<_>>();
3185 let expected_messages = [
3186 expected_pull_diagnostic_lib_message,
3187 // TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
3188 // expected_push_diagnostic_lib_message,
3189 ];
3190 assert_eq!(
3191 all_diagnostics.len(),
3192 1,
3193 "Expected pull diagnostics, but got: {all_diagnostics:?}"
3194 );
3195 for diagnostic in all_diagnostics {
3196 assert!(
3197 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3198 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3199 diagnostic.diagnostic.message
3200 );
3201 }
3202 });
3203
3204 if should_stream_workspace_diagnostic {
3205 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3206 token: expected_workspace_diagnostic_token.clone(),
3207 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3208 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3209 items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
3210 lsp::WorkspaceFullDocumentDiagnosticReport {
3211 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3212 version: None,
3213 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
3214 result_id: Some(format!(
3215 "workspace_{}",
3216 workspace_diagnostics_pulls_made
3217 .fetch_add(1, atomic::Ordering::Release)
3218 + 1
3219 )),
3220 items: vec![lsp::Diagnostic {
3221 range: lsp::Range {
3222 start: lsp::Position {
3223 line: 0,
3224 character: 1,
3225 },
3226 end: lsp::Position {
3227 line: 0,
3228 character: 2,
3229 },
3230 },
3231 severity: Some(lsp::DiagnosticSeverity::ERROR),
3232 message: expected_workspace_pull_diagnostics_lib_message
3233 .to_string(),
3234 ..lsp::Diagnostic::default()
3235 }],
3236 },
3237 },
3238 )],
3239 }),
3240 ),
3241 });
3242 workspace_diagnostic_start_count =
3243 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3244 workspace_diagnostic_cancel_tx.send(()).await.unwrap();
3245 workspace_diagnostics_pulls_handle.next().await.unwrap();
3246 executor.run_until_parked();
3247 editor_b_lib.update(cx_b, |editor, cx| {
3248 let snapshot = editor.buffer().read(cx).snapshot(cx);
3249 let all_diagnostics = snapshot
3250 .diagnostics_in_range(0..snapshot.len())
3251 .collect::<Vec<_>>();
3252 let expected_messages = [
3253 expected_workspace_pull_diagnostics_lib_message,
3254 // TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
3255 // expected_push_diagnostic_lib_message,
3256 ];
3257 assert_eq!(
3258 all_diagnostics.len(),
3259 1,
3260 "Expected pull diagnostics, but got: {all_diagnostics:?}"
3261 );
3262 for diagnostic in all_diagnostics {
3263 assert!(
3264 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3265 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3266 diagnostic.diagnostic.message
3267 );
3268 }
3269 });
3270 };
3271
3272 {
3273 assert!(
3274 !diagnostics_pulls_result_ids.lock().await.is_empty(),
3275 "Initial diagnostics pulls should report None at least"
3276 );
3277 assert_eq!(
3278 0,
3279 workspace_diagnostics_pulls_result_ids
3280 .lock()
3281 .await
3282 .deref()
3283 .len(),
3284 "After the initial workspace request, opening files should not reuse any result ids"
3285 );
3286 }
3287
3288 editor_b_lib.update_in(cx_b, |editor, window, cx| {
3289 editor.move_to_end(&MoveToEnd, window, cx);
3290 editor.handle_input(":", window, cx);
3291 });
3292 pull_diagnostics_handle.next().await.unwrap();
3293 assert_eq!(
3294 4,
3295 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3296 "Client lib.rs edits should trigger another diagnostics pull for a buffer"
3297 );
3298 workspace_diagnostics_pulls_handle.next().await.unwrap();
3299 assert_eq!(
3300 workspace_diagnostic_start_count + 1,
3301 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3302 "After client lib.rs edits, the workspace diagnostics request should follow"
3303 );
3304 executor.run_until_parked();
3305
3306 editor_b_main.update_in(cx_b, |editor, window, cx| {
3307 editor.move_to_end(&MoveToEnd, window, cx);
3308 editor.handle_input(":", window, cx);
3309 });
3310 pull_diagnostics_handle.next().await.unwrap();
3311 pull_diagnostics_handle.next().await.unwrap();
3312 assert_eq!(
3313 6,
3314 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3315 "Client main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
3316 );
3317 workspace_diagnostics_pulls_handle.next().await.unwrap();
3318 assert_eq!(
3319 workspace_diagnostic_start_count + 2,
3320 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3321 "After client main.rs edits, the workspace diagnostics pull should follow"
3322 );
3323 executor.run_until_parked();
3324
3325 editor_a_main.update_in(cx_a, |editor, window, cx| {
3326 editor.move_to_end(&MoveToEnd, window, cx);
3327 editor.handle_input(":", window, cx);
3328 });
3329 pull_diagnostics_handle.next().await.unwrap();
3330 pull_diagnostics_handle.next().await.unwrap();
3331 assert_eq!(
3332 8,
3333 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3334 "Host main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
3335 );
3336 workspace_diagnostics_pulls_handle.next().await.unwrap();
3337 assert_eq!(
3338 workspace_diagnostic_start_count + 3,
3339 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3340 "After host main.rs edits, the workspace diagnostics pull should follow"
3341 );
3342 executor.run_until_parked();
3343 let diagnostic_pulls_result_ids = diagnostics_pulls_result_ids.lock().await.len();
3344 let workspace_pulls_result_ids = workspace_diagnostics_pulls_result_ids.lock().await.len();
3345 {
3346 assert!(
3347 diagnostic_pulls_result_ids > 1,
3348 "Should have sent result ids when pulling diagnostics"
3349 );
3350 assert!(
3351 workspace_pulls_result_ids > 1,
3352 "Should have sent result ids when pulling workspace diagnostics"
3353 );
3354 }
3355
3356 fake_language_server
3357 .request::<lsp::request::WorkspaceDiagnosticRefresh>(())
3358 .await
3359 .into_response()
3360 .expect("workspace diagnostics refresh request failed");
3361 assert_eq!(
3362 8,
3363 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3364 "No single file pulls should happen after the diagnostics refresh server request"
3365 );
3366 workspace_diagnostics_pulls_handle.next().await.unwrap();
3367 assert_eq!(
3368 workspace_diagnostic_start_count + 4,
3369 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3370 "Another workspace diagnostics pull should happen after the diagnostics refresh server request"
3371 );
3372 {
3373 assert!(
3374 diagnostics_pulls_result_ids.lock().await.len() == diagnostic_pulls_result_ids,
3375 "Pulls should not happen hence no extra ids should appear"
3376 );
3377 assert!(
3378 workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
3379 "More workspace diagnostics should be pulled"
3380 );
3381 }
3382 editor_b_lib.update(cx_b, |editor, cx| {
3383 let snapshot = editor.buffer().read(cx).snapshot(cx);
3384 let all_diagnostics = snapshot
3385 .diagnostics_in_range(0..snapshot.len())
3386 .collect::<Vec<_>>();
3387 let expected_messages = [
3388 expected_workspace_pull_diagnostics_lib_message,
3389 expected_pull_diagnostic_lib_message,
3390 expected_push_diagnostic_lib_message,
3391 ];
3392 assert_eq!(all_diagnostics.len(), 1);
3393 for diagnostic in &all_diagnostics {
3394 assert!(
3395 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3396 "Unexpected diagnostics: {all_diagnostics:?}"
3397 );
3398 }
3399 });
3400 editor_b_main.update(cx_b, |editor, cx| {
3401 let snapshot = editor.buffer().read(cx).snapshot(cx);
3402 let all_diagnostics = snapshot
3403 .diagnostics_in_range(0..snapshot.len())
3404 .collect::<Vec<_>>();
3405 assert_eq!(all_diagnostics.len(), 2);
3406
3407 let expected_messages = [
3408 expected_workspace_pull_diagnostics_main_message,
3409 expected_pull_diagnostic_main_message,
3410 expected_push_diagnostic_main_message,
3411 ];
3412 for diagnostic in &all_diagnostics {
3413 assert!(
3414 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3415 "Unexpected diagnostics: {all_diagnostics:?}"
3416 );
3417 }
3418 });
3419 editor_a_main.update(cx_a, |editor, cx| {
3420 let snapshot = editor.buffer().read(cx).snapshot(cx);
3421 let all_diagnostics = snapshot
3422 .diagnostics_in_range(0..snapshot.len())
3423 .collect::<Vec<_>>();
3424 assert_eq!(all_diagnostics.len(), 2);
3425 let expected_messages = [
3426 expected_workspace_pull_diagnostics_main_message,
3427 expected_pull_diagnostic_main_message,
3428 expected_push_diagnostic_main_message,
3429 ];
3430 for diagnostic in &all_diagnostics {
3431 assert!(
3432 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3433 "Unexpected diagnostics: {all_diagnostics:?}"
3434 );
3435 }
3436 });
3437}
3438
3439#[gpui::test(iterations = 10)]
3440async fn test_non_streamed_lsp_pull_diagnostics(
3441 cx_a: &mut TestAppContext,
3442 cx_b: &mut TestAppContext,
3443) {
3444 test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
3445}
3446
3447#[gpui::test(iterations = 10)]
3448async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3449 test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
3450}
3451
3452#[gpui::test(iterations = 10)]
3453async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3454 let mut server = TestServer::start(cx_a.executor()).await;
3455 let client_a = server.create_client(cx_a, "user_a").await;
3456 let client_b = server.create_client(cx_b, "user_b").await;
3457 server
3458 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3459 .await;
3460 let active_call_a = cx_a.read(ActiveCall::global);
3461
3462 cx_a.update(editor::init);
3463 cx_b.update(editor::init);
3464 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
3465 let inline_blame_off_settings = Some(InlineBlameSettings {
3466 enabled: Some(false),
3467 ..Default::default()
3468 });
3469 cx_a.update(|cx| {
3470 SettingsStore::update_global(cx, |store, cx| {
3471 store.update_user_settings(cx, |settings| {
3472 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3473 });
3474 });
3475 });
3476 cx_b.update(|cx| {
3477 SettingsStore::update_global(cx, |store, cx| {
3478 store.update_user_settings(cx, |settings| {
3479 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3480 });
3481 });
3482 });
3483
3484 client_a
3485 .fs()
3486 .insert_tree(
3487 path!("/my-repo"),
3488 json!({
3489 ".git": {},
3490 "file.txt": "line1\nline2\nline3\nline\n",
3491 }),
3492 )
3493 .await;
3494
3495 let blame = git::blame::Blame {
3496 entries: vec![
3497 blame_entry("1b1b1b", 0..1),
3498 blame_entry("0d0d0d", 1..2),
3499 blame_entry("3a3a3a", 2..3),
3500 blame_entry("4c4c4c", 3..4),
3501 ],
3502 messages: [
3503 ("1b1b1b", "message for idx-0"),
3504 ("0d0d0d", "message for idx-1"),
3505 ("3a3a3a", "message for idx-2"),
3506 ("4c4c4c", "message for idx-3"),
3507 ]
3508 .into_iter()
3509 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3510 .collect(),
3511 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
3512 };
3513 client_a.fs().set_blame_for_repo(
3514 Path::new(path!("/my-repo/.git")),
3515 vec![(repo_path("file.txt"), blame)],
3516 );
3517
3518 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3519 let project_id = active_call_a
3520 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3521 .await
3522 .unwrap();
3523
3524 // Create editor_a
3525 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3526 let editor_a = workspace_a
3527 .update_in(cx_a, |workspace, window, cx| {
3528 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3529 })
3530 .await
3531 .unwrap()
3532 .downcast::<Editor>()
3533 .unwrap();
3534
3535 // Join the project as client B.
3536 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3537 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3538 let editor_b = workspace_b
3539 .update_in(cx_b, |workspace, window, cx| {
3540 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3541 })
3542 .await
3543 .unwrap()
3544 .downcast::<Editor>()
3545 .unwrap();
3546 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3547 editor_b
3548 .buffer()
3549 .read(cx)
3550 .as_singleton()
3551 .unwrap()
3552 .read(cx)
3553 .remote_id()
3554 });
3555
3556 // client_b now requests git blame for the open buffer
3557 editor_b.update_in(cx_b, |editor_b, window, cx| {
3558 assert!(editor_b.blame().is_none());
3559 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3560 });
3561
3562 cx_a.executor().run_until_parked();
3563 cx_b.executor().run_until_parked();
3564
3565 editor_b.update(cx_b, |editor_b, cx| {
3566 let blame = editor_b.blame().expect("editor_b should have blame now");
3567 let entries = blame.update(cx, |blame, cx| {
3568 blame
3569 .blame_for_rows(
3570 &(0..4)
3571 .map(|row| RowInfo {
3572 buffer_row: Some(row),
3573 buffer_id: Some(buffer_id_b),
3574 ..Default::default()
3575 })
3576 .collect::<Vec<_>>(),
3577 cx,
3578 )
3579 .collect::<Vec<_>>()
3580 });
3581
3582 assert_eq!(
3583 entries,
3584 vec![
3585 Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
3586 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3587 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3588 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3589 ]
3590 );
3591
3592 blame.update(cx, |blame, _| {
3593 for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
3594 let details = blame.details_for_entry(*buffer, entry).unwrap();
3595 assert_eq!(details.message, format!("message for idx-{}", idx));
3596 assert_eq!(
3597 details.permalink.unwrap().to_string(),
3598 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
3599 );
3600 }
3601 });
3602 });
3603
3604 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3605 // which gets back to client_b.
3606 editor_b.update_in(cx_b, |editor_b, _, cx| {
3607 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3608 });
3609
3610 cx_a.executor().run_until_parked();
3611 cx_b.executor().run_until_parked();
3612
3613 editor_b.update(cx_b, |editor_b, cx| {
3614 let blame = editor_b.blame().expect("editor_b should have blame now");
3615 let entries = blame.update(cx, |blame, cx| {
3616 blame
3617 .blame_for_rows(
3618 &(0..4)
3619 .map(|row| RowInfo {
3620 buffer_row: Some(row),
3621 buffer_id: Some(buffer_id_b),
3622 ..Default::default()
3623 })
3624 .collect::<Vec<_>>(),
3625 cx,
3626 )
3627 .collect::<Vec<_>>()
3628 });
3629
3630 assert_eq!(
3631 entries,
3632 vec![
3633 None,
3634 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3635 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3636 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3637 ]
3638 );
3639 });
3640
3641 // Now editor_a also updates the file
3642 editor_a.update_in(cx_a, |editor_a, _, cx| {
3643 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3644 });
3645
3646 cx_a.executor().run_until_parked();
3647 cx_b.executor().run_until_parked();
3648
3649 editor_b.update(cx_b, |editor_b, cx| {
3650 let blame = editor_b.blame().expect("editor_b should have blame now");
3651 let entries = blame.update(cx, |blame, cx| {
3652 blame
3653 .blame_for_rows(
3654 &(0..4)
3655 .map(|row| RowInfo {
3656 buffer_row: Some(row),
3657 buffer_id: Some(buffer_id_b),
3658 ..Default::default()
3659 })
3660 .collect::<Vec<_>>(),
3661 cx,
3662 )
3663 .collect::<Vec<_>>()
3664 });
3665
3666 assert_eq!(
3667 entries,
3668 vec![
3669 None,
3670 None,
3671 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3672 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3673 ]
3674 );
3675 });
3676}
3677
3678#[gpui::test(iterations = 30)]
3679async fn test_collaborating_with_editorconfig(
3680 cx_a: &mut TestAppContext,
3681 cx_b: &mut TestAppContext,
3682) {
3683 let mut server = TestServer::start(cx_a.executor()).await;
3684 let client_a = server.create_client(cx_a, "user_a").await;
3685 let client_b = server.create_client(cx_b, "user_b").await;
3686 server
3687 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3688 .await;
3689 let active_call_a = cx_a.read(ActiveCall::global);
3690
3691 cx_b.update(editor::init);
3692
3693 // Set up a fake language server.
3694 client_a.language_registry().add(rust_lang());
3695 client_a
3696 .fs()
3697 .insert_tree(
3698 path!("/a"),
3699 json!({
3700 "src": {
3701 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3702 "other_mod": {
3703 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3704 ".editorconfig": "",
3705 },
3706 },
3707 ".editorconfig": "[*]\ntab_width = 2\n",
3708 }),
3709 )
3710 .await;
3711 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3712 let project_id = active_call_a
3713 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3714 .await
3715 .unwrap();
3716 let main_buffer_a = project_a
3717 .update(cx_a, |p, cx| {
3718 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3719 })
3720 .await
3721 .unwrap();
3722 let other_buffer_a = project_a
3723 .update(cx_a, |p, cx| {
3724 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3725 })
3726 .await
3727 .unwrap();
3728 let cx_a = cx_a.add_empty_window();
3729 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3730 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3731 });
3732 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3733 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3734 });
3735 let mut main_editor_cx_a = EditorTestContext {
3736 cx: cx_a.clone(),
3737 window: cx_a.window_handle(),
3738 editor: main_editor_a,
3739 assertion_cx: AssertionContextManager::new(),
3740 };
3741 let mut other_editor_cx_a = EditorTestContext {
3742 cx: cx_a.clone(),
3743 window: cx_a.window_handle(),
3744 editor: other_editor_a,
3745 assertion_cx: AssertionContextManager::new(),
3746 };
3747
3748 // Join the project as client B.
3749 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3750 let main_buffer_b = project_b
3751 .update(cx_b, |p, cx| {
3752 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3753 })
3754 .await
3755 .unwrap();
3756 let other_buffer_b = project_b
3757 .update(cx_b, |p, cx| {
3758 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3759 })
3760 .await
3761 .unwrap();
3762 let cx_b = cx_b.add_empty_window();
3763 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3764 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3765 });
3766 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3767 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3768 });
3769 let mut main_editor_cx_b = EditorTestContext {
3770 cx: cx_b.clone(),
3771 window: cx_b.window_handle(),
3772 editor: main_editor_b,
3773 assertion_cx: AssertionContextManager::new(),
3774 };
3775 let mut other_editor_cx_b = EditorTestContext {
3776 cx: cx_b.clone(),
3777 window: cx_b.window_handle(),
3778 editor: other_editor_b,
3779 assertion_cx: AssertionContextManager::new(),
3780 };
3781
3782 let initial_main = indoc! {"
3783ˇmod other;
3784fn main() { let foo = other::foo(); }"};
3785 let initial_other = indoc! {"
3786ˇpub fn foo() -> usize {
3787 4
3788}"};
3789
3790 let first_tabbed_main = indoc! {"
3791 ˇmod other;
3792fn main() { let foo = other::foo(); }"};
3793 tab_undo_assert(
3794 &mut main_editor_cx_a,
3795 &mut main_editor_cx_b,
3796 initial_main,
3797 first_tabbed_main,
3798 true,
3799 );
3800 tab_undo_assert(
3801 &mut main_editor_cx_a,
3802 &mut main_editor_cx_b,
3803 initial_main,
3804 first_tabbed_main,
3805 false,
3806 );
3807
3808 let first_tabbed_other = indoc! {"
3809 ˇpub fn foo() -> usize {
3810 4
3811}"};
3812 tab_undo_assert(
3813 &mut other_editor_cx_a,
3814 &mut other_editor_cx_b,
3815 initial_other,
3816 first_tabbed_other,
3817 true,
3818 );
3819 tab_undo_assert(
3820 &mut other_editor_cx_a,
3821 &mut other_editor_cx_b,
3822 initial_other,
3823 first_tabbed_other,
3824 false,
3825 );
3826
3827 client_a
3828 .fs()
3829 .atomic_write(
3830 PathBuf::from(path!("/a/src/.editorconfig")),
3831 "[*]\ntab_width = 3\n".to_owned(),
3832 )
3833 .await
3834 .unwrap();
3835 cx_a.run_until_parked();
3836 cx_b.run_until_parked();
3837
3838 let second_tabbed_main = indoc! {"
3839 ˇmod other;
3840fn main() { let foo = other::foo(); }"};
3841 tab_undo_assert(
3842 &mut main_editor_cx_a,
3843 &mut main_editor_cx_b,
3844 initial_main,
3845 second_tabbed_main,
3846 true,
3847 );
3848 tab_undo_assert(
3849 &mut main_editor_cx_a,
3850 &mut main_editor_cx_b,
3851 initial_main,
3852 second_tabbed_main,
3853 false,
3854 );
3855
3856 let second_tabbed_other = indoc! {"
3857 ˇpub fn foo() -> usize {
3858 4
3859}"};
3860 tab_undo_assert(
3861 &mut other_editor_cx_a,
3862 &mut other_editor_cx_b,
3863 initial_other,
3864 second_tabbed_other,
3865 true,
3866 );
3867 tab_undo_assert(
3868 &mut other_editor_cx_a,
3869 &mut other_editor_cx_b,
3870 initial_other,
3871 second_tabbed_other,
3872 false,
3873 );
3874
3875 let editorconfig_buffer_b = project_b
3876 .update(cx_b, |p, cx| {
3877 p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
3878 })
3879 .await
3880 .unwrap();
3881 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3882 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3883 });
3884 project_b
3885 .update(cx_b, |project, cx| {
3886 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3887 })
3888 .await
3889 .unwrap();
3890 cx_a.run_until_parked();
3891 cx_b.run_until_parked();
3892
3893 tab_undo_assert(
3894 &mut main_editor_cx_a,
3895 &mut main_editor_cx_b,
3896 initial_main,
3897 second_tabbed_main,
3898 true,
3899 );
3900 tab_undo_assert(
3901 &mut main_editor_cx_a,
3902 &mut main_editor_cx_b,
3903 initial_main,
3904 second_tabbed_main,
3905 false,
3906 );
3907
3908 let third_tabbed_other = indoc! {"
3909 ˇpub fn foo() -> usize {
3910 4
3911}"};
3912 tab_undo_assert(
3913 &mut other_editor_cx_a,
3914 &mut other_editor_cx_b,
3915 initial_other,
3916 third_tabbed_other,
3917 true,
3918 );
3919
3920 tab_undo_assert(
3921 &mut other_editor_cx_a,
3922 &mut other_editor_cx_b,
3923 initial_other,
3924 third_tabbed_other,
3925 false,
3926 );
3927}
3928
3929#[gpui::test]
3930async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3931 let executor = cx_a.executor();
3932 let mut server = TestServer::start(executor.clone()).await;
3933 let client_a = server.create_client(cx_a, "user_a").await;
3934 let client_b = server.create_client(cx_b, "user_b").await;
3935 server
3936 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3937 .await;
3938 let active_call_a = cx_a.read(ActiveCall::global);
3939 let active_call_b = cx_b.read(ActiveCall::global);
3940 cx_a.update(editor::init);
3941 cx_b.update(editor::init);
3942 client_a
3943 .fs()
3944 .insert_tree(
3945 "/a",
3946 json!({
3947 "test.txt": "one\ntwo\nthree\nfour\nfive",
3948 }),
3949 )
3950 .await;
3951 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3952 let project_path = ProjectPath {
3953 worktree_id,
3954 path: rel_path(&"test.txt").into(),
3955 };
3956 let abs_path = project_a.read_with(cx_a, |project, cx| {
3957 project
3958 .absolute_path(&project_path, cx)
3959 .map(Arc::from)
3960 .unwrap()
3961 });
3962
3963 active_call_a
3964 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
3965 .await
3966 .unwrap();
3967 let project_id = active_call_a
3968 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3969 .await
3970 .unwrap();
3971 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3972 active_call_b
3973 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
3974 .await
3975 .unwrap();
3976 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3977 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3978
3979 // Client A opens an editor.
3980 let editor_a = workspace_a
3981 .update_in(cx_a, |workspace, window, cx| {
3982 workspace.open_path(project_path.clone(), None, true, window, cx)
3983 })
3984 .await
3985 .unwrap()
3986 .downcast::<Editor>()
3987 .unwrap();
3988
3989 // Client B opens same editor as A.
3990 let editor_b = workspace_b
3991 .update_in(cx_b, |workspace, window, cx| {
3992 workspace.open_path(project_path.clone(), None, true, window, cx)
3993 })
3994 .await
3995 .unwrap()
3996 .downcast::<Editor>()
3997 .unwrap();
3998
3999 cx_a.run_until_parked();
4000 cx_b.run_until_parked();
4001
4002 // Client A adds breakpoint on line (1)
4003 editor_a.update_in(cx_a, |editor, window, cx| {
4004 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4005 });
4006
4007 cx_a.run_until_parked();
4008 cx_b.run_until_parked();
4009
4010 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4011 editor
4012 .breakpoint_store()
4013 .unwrap()
4014 .read(cx)
4015 .all_source_breakpoints(cx)
4016 });
4017 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4018 editor
4019 .breakpoint_store()
4020 .unwrap()
4021 .read(cx)
4022 .all_source_breakpoints(cx)
4023 });
4024
4025 assert_eq!(1, breakpoints_a.len());
4026 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4027 assert_eq!(breakpoints_a, breakpoints_b);
4028
4029 // Client B adds breakpoint on line(2)
4030 editor_b.update_in(cx_b, |editor, window, cx| {
4031 editor.move_down(&editor::actions::MoveDown, window, cx);
4032 editor.move_down(&editor::actions::MoveDown, window, cx);
4033 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4034 });
4035
4036 cx_a.run_until_parked();
4037 cx_b.run_until_parked();
4038
4039 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4040 editor
4041 .breakpoint_store()
4042 .unwrap()
4043 .read(cx)
4044 .all_source_breakpoints(cx)
4045 });
4046 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4047 editor
4048 .breakpoint_store()
4049 .unwrap()
4050 .read(cx)
4051 .all_source_breakpoints(cx)
4052 });
4053
4054 assert_eq!(1, breakpoints_a.len());
4055 assert_eq!(breakpoints_a, breakpoints_b);
4056 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
4057
4058 // Client A removes last added breakpoint from client B
4059 editor_a.update_in(cx_a, |editor, window, cx| {
4060 editor.move_down(&editor::actions::MoveDown, window, cx);
4061 editor.move_down(&editor::actions::MoveDown, window, cx);
4062 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4063 });
4064
4065 cx_a.run_until_parked();
4066 cx_b.run_until_parked();
4067
4068 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4069 editor
4070 .breakpoint_store()
4071 .unwrap()
4072 .read(cx)
4073 .all_source_breakpoints(cx)
4074 });
4075 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4076 editor
4077 .breakpoint_store()
4078 .unwrap()
4079 .read(cx)
4080 .all_source_breakpoints(cx)
4081 });
4082
4083 assert_eq!(1, breakpoints_a.len());
4084 assert_eq!(breakpoints_a, breakpoints_b);
4085 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4086
4087 // Client B removes first added breakpoint by client A
4088 editor_b.update_in(cx_b, |editor, window, cx| {
4089 editor.move_up(&editor::actions::MoveUp, window, cx);
4090 editor.move_up(&editor::actions::MoveUp, window, cx);
4091 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4092 });
4093
4094 cx_a.run_until_parked();
4095 cx_b.run_until_parked();
4096
4097 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4098 editor
4099 .breakpoint_store()
4100 .unwrap()
4101 .read(cx)
4102 .all_source_breakpoints(cx)
4103 });
4104 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4105 editor
4106 .breakpoint_store()
4107 .unwrap()
4108 .read(cx)
4109 .all_source_breakpoints(cx)
4110 });
4111
4112 assert_eq!(0, breakpoints_a.len());
4113 assert_eq!(breakpoints_a, breakpoints_b);
4114}
4115
4116#[gpui::test]
4117async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4118 let mut server = TestServer::start(cx_a.executor()).await;
4119 let client_a = server.create_client(cx_a, "user_a").await;
4120 let client_b = server.create_client(cx_b, "user_b").await;
4121 server
4122 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4123 .await;
4124 let active_call_a = cx_a.read(ActiveCall::global);
4125 let active_call_b = cx_b.read(ActiveCall::global);
4126
4127 cx_a.update(editor::init);
4128 cx_b.update(editor::init);
4129
4130 client_a.language_registry().add(rust_lang());
4131 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4132 "Rust",
4133 FakeLspAdapter {
4134 name: "rust-analyzer",
4135 ..FakeLspAdapter::default()
4136 },
4137 );
4138 client_b.language_registry().add(rust_lang());
4139 client_b.language_registry().register_fake_lsp_adapter(
4140 "Rust",
4141 FakeLspAdapter {
4142 name: "rust-analyzer",
4143 ..FakeLspAdapter::default()
4144 },
4145 );
4146
4147 client_a
4148 .fs()
4149 .insert_tree(
4150 path!("/a"),
4151 json!({
4152 "main.rs": "fn main() {}",
4153 }),
4154 )
4155 .await;
4156 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4157 active_call_a
4158 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4159 .await
4160 .unwrap();
4161 let project_id = active_call_a
4162 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4163 .await
4164 .unwrap();
4165
4166 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4167 active_call_b
4168 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4169 .await
4170 .unwrap();
4171
4172 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4173 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4174
4175 let editor_a = workspace_a
4176 .update_in(cx_a, |workspace, window, cx| {
4177 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4178 })
4179 .await
4180 .unwrap()
4181 .downcast::<Editor>()
4182 .unwrap();
4183
4184 let editor_b = workspace_b
4185 .update_in(cx_b, |workspace, window, cx| {
4186 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4187 })
4188 .await
4189 .unwrap()
4190 .downcast::<Editor>()
4191 .unwrap();
4192
4193 let fake_language_server = fake_language_servers.next().await.unwrap();
4194
4195 // host
4196 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4197 |params, _| async move {
4198 assert_eq!(
4199 params.text_document.uri,
4200 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4201 );
4202 assert_eq!(params.position, lsp::Position::new(0, 0));
4203 Ok(Some(ExpandedMacro {
4204 name: "test_macro_name".to_string(),
4205 expansion: "test_macro_expansion on the host".to_string(),
4206 }))
4207 },
4208 );
4209
4210 editor_a.update_in(cx_a, |editor, window, cx| {
4211 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4212 });
4213 expand_request_a.next().await.unwrap();
4214 cx_a.run_until_parked();
4215
4216 workspace_a.update(cx_a, |workspace, cx| {
4217 workspace.active_pane().update(cx, |pane, cx| {
4218 assert_eq!(
4219 pane.items_len(),
4220 2,
4221 "Should have added a macro expansion to the host's pane"
4222 );
4223 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4224 new_editor.update(cx, |editor, cx| {
4225 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
4226 });
4227 })
4228 });
4229
4230 // client
4231 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4232 |params, _| async move {
4233 assert_eq!(
4234 params.text_document.uri,
4235 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4236 );
4237 assert_eq!(
4238 params.position,
4239 lsp::Position::new(0, 12),
4240 "editor_b has selected the entire text and should query for a different position"
4241 );
4242 Ok(Some(ExpandedMacro {
4243 name: "test_macro_name".to_string(),
4244 expansion: "test_macro_expansion on the client".to_string(),
4245 }))
4246 },
4247 );
4248
4249 editor_b.update_in(cx_b, |editor, window, cx| {
4250 editor.select_all(&SelectAll, window, cx);
4251 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4252 });
4253 expand_request_b.next().await.unwrap();
4254 cx_b.run_until_parked();
4255
4256 workspace_b.update(cx_b, |workspace, cx| {
4257 workspace.active_pane().update(cx, |pane, cx| {
4258 assert_eq!(
4259 pane.items_len(),
4260 2,
4261 "Should have added a macro expansion to the client's pane"
4262 );
4263 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4264 new_editor.update(cx, |editor, cx| {
4265 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
4266 });
4267 })
4268 });
4269}
4270
4271#[track_caller]
4272fn tab_undo_assert(
4273 cx_a: &mut EditorTestContext,
4274 cx_b: &mut EditorTestContext,
4275 expected_initial: &str,
4276 expected_tabbed: &str,
4277 a_tabs: bool,
4278) {
4279 cx_a.assert_editor_state(expected_initial);
4280 cx_b.assert_editor_state(expected_initial);
4281
4282 if a_tabs {
4283 cx_a.update_editor(|editor, window, cx| {
4284 editor.tab(&editor::actions::Tab, window, cx);
4285 });
4286 } else {
4287 cx_b.update_editor(|editor, window, cx| {
4288 editor.tab(&editor::actions::Tab, window, cx);
4289 });
4290 }
4291
4292 cx_a.run_until_parked();
4293 cx_b.run_until_parked();
4294
4295 cx_a.assert_editor_state(expected_tabbed);
4296 cx_b.assert_editor_state(expected_tabbed);
4297
4298 if a_tabs {
4299 cx_a.update_editor(|editor, window, cx| {
4300 editor.undo(&editor::actions::Undo, window, cx);
4301 });
4302 } else {
4303 cx_b.update_editor(|editor, window, cx| {
4304 editor.undo(&editor::actions::Undo, window, cx);
4305 });
4306 }
4307 cx_a.run_until_parked();
4308 cx_b.run_until_parked();
4309 cx_a.assert_editor_state(expected_initial);
4310 cx_b.assert_editor_state(expected_initial);
4311}
4312
4313fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4314 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4315
4316 let mut all_cached_labels = Vec::new();
4317 let mut all_fetched_hints = Vec::new();
4318 for buffer in editor.buffer().read(cx).all_buffers() {
4319 lsp_store.update(cx, |lsp_store, cx| {
4320 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4321 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4322 let mut label = hint.text().to_string();
4323 if hint.padding_left {
4324 label.insert(0, ' ');
4325 }
4326 if hint.padding_right {
4327 label.push_str(" ");
4328 }
4329 label
4330 }));
4331 all_fetched_hints.extend(hints.all_fetched_hints());
4332 });
4333 }
4334
4335 assert!(
4336 all_fetched_hints.is_empty(),
4337 "Did not expect background hints fetch tasks, but got {} of them",
4338 all_fetched_hints.len()
4339 );
4340
4341 all_cached_labels
4342}
4343
4344#[track_caller]
4345fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
4346 editor
4347 .all_inlays(cx)
4348 .into_iter()
4349 .filter_map(|inlay| inlay.get_color())
4350 .map(Rgba::from)
4351 .collect()
4352}
4353
4354fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
4355 git::blame::BlameEntry {
4356 sha: sha.parse().unwrap(),
4357 range,
4358 ..Default::default()
4359 }
4360}