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