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