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