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