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 pretty_assertions::assert_eq;
26use project::{
27 ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
28 lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
29};
30use recent_projects::disconnected_overlay::DisconnectedOverlay;
31use rpc::RECEIVE_TIMEOUT;
32use serde_json::json;
33use settings::{InlayHintSettingsContent, InlineBlameSettings, SettingsStore};
34use std::{
35 collections::BTreeSet,
36 ops::{Deref as _, Range},
37 path::{Path, PathBuf},
38 sync::{
39 Arc,
40 atomic::{self, AtomicBool, AtomicUsize},
41 },
42 time::Duration,
43};
44use text::Point;
45use util::{path, rel_path::rel_path, uri};
46use workspace::{CloseIntent, Workspace};
47
48#[gpui::test(iterations = 10)]
49async fn test_host_disconnect(
50 cx_a: &mut TestAppContext,
51 cx_b: &mut TestAppContext,
52 cx_c: &mut TestAppContext,
53) {
54 let mut server = TestServer::start(cx_a.executor()).await;
55 let client_a = server.create_client(cx_a, "user_a").await;
56 let client_b = server.create_client(cx_b, "user_b").await;
57 let client_c = server.create_client(cx_c, "user_c").await;
58 server
59 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
60 .await;
61
62 cx_b.update(editor::init);
63 cx_b.update(recent_projects::init);
64
65 client_a
66 .fs()
67 .insert_tree(
68 "/a",
69 json!({
70 "a.txt": "a-contents",
71 "b.txt": "b-contents",
72 }),
73 )
74 .await;
75
76 let active_call_a = cx_a.read(ActiveCall::global);
77 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
78
79 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
80 let project_id = active_call_a
81 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
82 .await
83 .unwrap();
84
85 let project_b = client_b.join_remote_project(project_id, cx_b).await;
86 cx_a.background_executor.run_until_parked();
87
88 assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
89
90 let workspace_b = cx_b.add_window(|window, cx| {
91 Workspace::new(
92 None,
93 project_b.clone(),
94 client_b.app_state.clone(),
95 window,
96 cx,
97 )
98 });
99 let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
100 let workspace_b_view = workspace_b.root(cx_b).unwrap();
101
102 let editor_b = workspace_b
103 .update(cx_b, |workspace, window, cx| {
104 workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx)
105 })
106 .unwrap()
107 .await
108 .unwrap()
109 .downcast::<Editor>()
110 .unwrap();
111
112 //TODO: focus
113 assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window)));
114 editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
115
116 cx_b.update(|_, cx| {
117 assert!(workspace_b_view.read(cx).is_edited());
118 });
119
120 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
121 server.forbid_connections();
122 server.disconnect_client(client_a.peer_id().unwrap());
123 cx_a.background_executor
124 .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
125
126 project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
127
128 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
129
130 project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));
131
132 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
133
134 // Ensure client B's edited state is reset and that the whole window is blurred.
135 workspace_b
136 .update(cx_b, |workspace, _, cx| {
137 assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
138 assert!(!workspace.is_edited());
139 })
140 .unwrap();
141
142 // Ensure client B is not prompted to save edits when closing window after disconnecting.
143 let can_close = workspace_b
144 .update(cx_b, |workspace, window, cx| {
145 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
146 })
147 .unwrap()
148 .await
149 .unwrap();
150 assert!(can_close);
151
152 // Allow client A to reconnect to the server.
153 server.allow_connections();
154 cx_a.background_executor.advance_clock(RECEIVE_TIMEOUT);
155
156 // Client B calls client A again after they reconnected.
157 let active_call_b = cx_b.read(ActiveCall::global);
158 active_call_b
159 .update(cx_b, |call, cx| {
160 call.invite(client_a.user_id().unwrap(), None, cx)
161 })
162 .await
163 .unwrap();
164 cx_a.background_executor.run_until_parked();
165 active_call_a
166 .update(cx_a, |call, cx| call.accept_incoming(cx))
167 .await
168 .unwrap();
169
170 active_call_a
171 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
172 .await
173 .unwrap();
174
175 // Drop client A's connection again. We should still unshare it successfully.
176 server.forbid_connections();
177 server.disconnect_client(client_a.peer_id().unwrap());
178 cx_a.background_executor
179 .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
180
181 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
182}
183
184#[gpui::test]
185async fn test_newline_above_or_below_does_not_move_guest_cursor(
186 cx_a: &mut TestAppContext,
187 cx_b: &mut TestAppContext,
188) {
189 let mut server = TestServer::start(cx_a.executor()).await;
190 let client_a = server.create_client(cx_a, "user_a").await;
191 let client_b = server.create_client(cx_b, "user_b").await;
192 let executor = cx_a.executor();
193 server
194 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
195 .await;
196 let active_call_a = cx_a.read(ActiveCall::global);
197
198 client_a
199 .fs()
200 .insert_tree(path!("/dir"), json!({ "a.txt": "Some text\n" }))
201 .await;
202 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
203 let project_id = active_call_a
204 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
205 .await
206 .unwrap();
207
208 let project_b = client_b.join_remote_project(project_id, cx_b).await;
209
210 // Open a buffer as client A
211 let buffer_a = project_a
212 .update(cx_a, |p, cx| {
213 p.open_buffer((worktree_id, rel_path("a.txt")), cx)
214 })
215 .await
216 .unwrap();
217 let cx_a = cx_a.add_empty_window();
218 let editor_a = cx_a
219 .new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx));
220
221 let mut editor_cx_a = EditorTestContext {
222 cx: cx_a.clone(),
223 window: cx_a.window_handle(),
224 editor: editor_a,
225 assertion_cx: AssertionContextManager::new(),
226 };
227
228 let cx_b = cx_b.add_empty_window();
229 // Open a buffer as client B
230 let buffer_b = project_b
231 .update(cx_b, |p, cx| {
232 p.open_buffer((worktree_id, rel_path("a.txt")), cx)
233 })
234 .await
235 .unwrap();
236 let editor_b = cx_b
237 .new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx));
238
239 let mut editor_cx_b = EditorTestContext {
240 cx: cx_b.clone(),
241 window: cx_b.window_handle(),
242 editor: editor_b,
243 assertion_cx: AssertionContextManager::new(),
244 };
245
246 // Test newline above
247 editor_cx_a.set_selections_state(indoc! {"
248 Some textˇ
249 "});
250 editor_cx_b.set_selections_state(indoc! {"
251 Some textˇ
252 "});
253 editor_cx_a.update_editor(|editor, window, cx| {
254 editor.newline_above(&editor::actions::NewlineAbove, window, cx)
255 });
256 executor.run_until_parked();
257 editor_cx_a.assert_editor_state(indoc! {"
258 ˇ
259 Some text
260 "});
261 editor_cx_b.assert_editor_state(indoc! {"
262
263 Some textˇ
264 "});
265
266 // Test newline below
267 editor_cx_a.set_selections_state(indoc! {"
268
269 Some textˇ
270 "});
271 editor_cx_b.set_selections_state(indoc! {"
272
273 Some textˇ
274 "});
275 editor_cx_a.update_editor(|editor, window, cx| {
276 editor.newline_below(&editor::actions::NewlineBelow, window, cx)
277 });
278 executor.run_until_parked();
279 editor_cx_a.assert_editor_state(indoc! {"
280
281 Some text
282 ˇ
283 "});
284 editor_cx_b.assert_editor_state(indoc! {"
285
286 Some textˇ
287
288 "});
289}
290
291#[gpui::test]
292async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
293 let mut server = TestServer::start(cx_a.executor()).await;
294 let client_a = server.create_client(cx_a, "user_a").await;
295 let client_b = server.create_client(cx_b, "user_b").await;
296 server
297 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
298 .await;
299 let active_call_a = cx_a.read(ActiveCall::global);
300
301 let capabilities = lsp::ServerCapabilities {
302 completion_provider: Some(lsp::CompletionOptions {
303 trigger_characters: Some(vec![".".to_string()]),
304 resolve_provider: Some(true),
305 ..lsp::CompletionOptions::default()
306 }),
307 ..lsp::ServerCapabilities::default()
308 };
309 client_a.language_registry().add(rust_lang());
310 let mut fake_language_servers = [
311 client_a.language_registry().register_fake_lsp(
312 "Rust",
313 FakeLspAdapter {
314 capabilities: capabilities.clone(),
315 initializer: Some(Box::new(|fake_server| {
316 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
317 |params, _| async move {
318 assert_eq!(
319 params.text_document_position.text_document.uri,
320 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
321 );
322 assert_eq!(
323 params.text_document_position.position,
324 lsp::Position::new(0, 14),
325 );
326
327 Ok(Some(lsp::CompletionResponse::Array(vec![
328 lsp::CompletionItem {
329 label: "first_method(…)".into(),
330 detail: Some("fn(&mut self, B) -> C".into()),
331 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
332 new_text: "first_method($1)".to_string(),
333 range: lsp::Range::new(
334 lsp::Position::new(0, 14),
335 lsp::Position::new(0, 14),
336 ),
337 })),
338 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
339 ..Default::default()
340 },
341 lsp::CompletionItem {
342 label: "second_method(…)".into(),
343 detail: Some("fn(&mut self, C) -> D<E>".into()),
344 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
345 new_text: "second_method()".to_string(),
346 range: lsp::Range::new(
347 lsp::Position::new(0, 14),
348 lsp::Position::new(0, 14),
349 ),
350 })),
351 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
352 ..Default::default()
353 },
354 ])))
355 },
356 );
357 })),
358 ..FakeLspAdapter::default()
359 },
360 ),
361 client_a.language_registry().register_fake_lsp(
362 "Rust",
363 FakeLspAdapter {
364 name: "fake-analyzer",
365 capabilities: capabilities.clone(),
366 initializer: Some(Box::new(|fake_server| {
367 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
368 |_, _| async move { Ok(None) },
369 );
370 })),
371 ..FakeLspAdapter::default()
372 },
373 ),
374 ];
375 client_b.language_registry().add(rust_lang());
376 client_b.language_registry().register_fake_lsp_adapter(
377 "Rust",
378 FakeLspAdapter {
379 capabilities: capabilities.clone(),
380 ..FakeLspAdapter::default()
381 },
382 );
383 client_b.language_registry().register_fake_lsp_adapter(
384 "Rust",
385 FakeLspAdapter {
386 name: "fake-analyzer",
387 capabilities,
388 ..FakeLspAdapter::default()
389 },
390 );
391
392 client_a
393 .fs()
394 .insert_tree(
395 path!("/a"),
396 json!({
397 "main.rs": "fn main() { a }",
398 "other.rs": "",
399 }),
400 )
401 .await;
402 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
403 let project_id = active_call_a
404 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
405 .await
406 .unwrap();
407 let project_b = client_b.join_remote_project(project_id, cx_b).await;
408
409 // Open a file in an editor as the guest.
410 let buffer_b = project_b
411 .update(cx_b, |p, cx| {
412 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
413 })
414 .await
415 .unwrap();
416 let cx_b = cx_b.add_empty_window();
417 let editor_b = cx_b.new_window_entity(|window, cx| {
418 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
419 });
420
421 let fake_language_server = fake_language_servers[0].next().await.unwrap();
422 let second_fake_language_server = fake_language_servers[1].next().await.unwrap();
423 cx_a.background_executor.run_until_parked();
424 cx_b.background_executor.run_until_parked();
425
426 buffer_b.read_with(cx_b, |buffer, _| {
427 assert!(!buffer.completion_triggers().is_empty())
428 });
429
430 // Type a completion trigger character as the guest.
431 editor_b.update_in(cx_b, |editor, window, cx| {
432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
433 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
434 });
435 editor.handle_input(".", window, cx);
436 });
437 cx_b.focus(&editor_b);
438
439 // Allow the completion request to propagate from guest to host to LSP.
440 cx_b.background_executor.run_until_parked();
441 cx_a.background_executor.run_until_parked();
442
443 // Open the buffer on the host.
444 let buffer_a = project_a
445 .update(cx_a, |p, cx| {
446 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
447 })
448 .await
449 .unwrap();
450 cx_a.executor().run_until_parked();
451
452 buffer_a.read_with(cx_a, |buffer, _| {
453 assert_eq!(buffer.text(), "fn main() { a. }")
454 });
455
456 // Confirm a completion on the guest.
457 editor_b.update_in(cx_b, |editor, window, cx| {
458 assert!(editor.context_menu_visible());
459 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
460 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
461 });
462
463 // Return a resolved completion from the host's language server.
464 // The resolved completion has an additional text edit.
465 fake_language_server.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
466 |params, _| async move {
467 assert_eq!(params.label, "first_method(…)");
468 Ok(lsp::CompletionItem {
469 label: "first_method(…)".into(),
470 detail: Some("fn(&mut self, B) -> C".into()),
471 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
472 new_text: "first_method($1)".to_string(),
473 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
474 })),
475 additional_text_edits: Some(vec![lsp::TextEdit {
476 new_text: "use d::SomeTrait;\n".to_string(),
477 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
478 }]),
479 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
480 ..Default::default()
481 })
482 },
483 );
484
485 // The additional edit is applied.
486 cx_a.executor().run_until_parked();
487 cx_b.executor().run_until_parked();
488
489 buffer_a.read_with(cx_a, |buffer, _| {
490 assert_eq!(
491 buffer.text(),
492 "use d::SomeTrait;\nfn main() { a.first_method() }"
493 );
494 });
495
496 buffer_b.read_with(cx_b, |buffer, _| {
497 assert_eq!(
498 buffer.text(),
499 "use d::SomeTrait;\nfn main() { a.first_method() }"
500 );
501 });
502
503 // Now we do a second completion, this time to ensure that documentation/snippets are
504 // resolved
505 editor_b.update_in(cx_b, |editor, window, cx| {
506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
507 s.select_ranges([MultiBufferOffset(46)..MultiBufferOffset(46)])
508 });
509 editor.handle_input("; a", window, cx);
510 editor.handle_input(".", window, cx);
511 });
512
513 buffer_b.read_with(cx_b, |buffer, _| {
514 assert_eq!(
515 buffer.text(),
516 "use d::SomeTrait;\nfn main() { a.first_method(); a. }"
517 );
518 });
519
520 let mut completion_response = fake_language_server
521 .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
522 assert_eq!(
523 params.text_document_position.text_document.uri,
524 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
525 );
526 assert_eq!(
527 params.text_document_position.position,
528 lsp::Position::new(1, 32),
529 );
530
531 Ok(Some(lsp::CompletionResponse::Array(vec![
532 lsp::CompletionItem {
533 label: "third_method(…)".into(),
534 detail: Some("fn(&mut self, B, C, D) -> E".into()),
535 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
536 // no snippet placeholders
537 new_text: "third_method".to_string(),
538 range: lsp::Range::new(
539 lsp::Position::new(1, 32),
540 lsp::Position::new(1, 32),
541 ),
542 })),
543 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
544 documentation: None,
545 ..Default::default()
546 },
547 ])))
548 });
549
550 // Second language server also needs to handle the request (returns None)
551 let mut second_completion_response = second_fake_language_server
552 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
553
554 // The completion now gets a new `text_edit.new_text` when resolving the completion item
555 let mut resolve_completion_response = fake_language_server
556 .set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
557 assert_eq!(params.label, "third_method(…)");
558 Ok(lsp::CompletionItem {
559 label: "third_method(…)".into(),
560 detail: Some("fn(&mut self, B, C, D) -> E".into()),
561 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
562 // Now it's a snippet
563 new_text: "third_method($1, $2, $3)".to_string(),
564 range: lsp::Range::new(lsp::Position::new(1, 32), lsp::Position::new(1, 32)),
565 })),
566 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
567 documentation: Some(lsp::Documentation::String(
568 "this is the documentation".into(),
569 )),
570 ..Default::default()
571 })
572 });
573
574 cx_b.executor().run_until_parked();
575
576 completion_response.next().await.unwrap();
577 second_completion_response.next().await.unwrap();
578
579 editor_b.update_in(cx_b, |editor, window, cx| {
580 assert!(editor.context_menu_visible());
581 editor.context_menu_first(&ContextMenuFirst {}, window, cx);
582 });
583
584 resolve_completion_response.next().await.unwrap();
585 cx_b.executor().run_until_parked();
586
587 // When accepting the completion, the snippet is insert.
588 editor_b.update_in(cx_b, |editor, window, cx| {
589 assert!(editor.context_menu_visible());
590 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
591 assert_eq!(
592 editor.text(cx),
593 "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
594 );
595 });
596
597 // Ensure buffer is synced before proceeding with the next test
598 cx_a.executor().run_until_parked();
599 cx_b.executor().run_until_parked();
600
601 // Test completions from the second fake language server
602 // Add another completion trigger to test the second language server
603 editor_b.update_in(cx_b, |editor, window, cx| {
604 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
605 s.select_ranges([MultiBufferOffset(68)..MultiBufferOffset(68)])
606 });
607 editor.handle_input("; b", window, cx);
608 editor.handle_input(".", window, cx);
609 });
610
611 buffer_b.read_with(cx_b, |buffer, _| {
612 assert_eq!(
613 buffer.text(),
614 "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b. }"
615 );
616 });
617
618 // Set up completion handlers for both language servers
619 let mut first_lsp_completion = fake_language_server
620 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
621
622 let mut second_lsp_completion = second_fake_language_server
623 .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
624 assert_eq!(
625 params.text_document_position.text_document.uri,
626 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
627 );
628 assert_eq!(
629 params.text_document_position.position,
630 lsp::Position::new(1, 54),
631 );
632
633 Ok(Some(lsp::CompletionResponse::Array(vec![
634 lsp::CompletionItem {
635 label: "analyzer_method(…)".into(),
636 detail: Some("fn(&self) -> Result<T>".into()),
637 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
638 new_text: "analyzer_method()".to_string(),
639 range: lsp::Range::new(
640 lsp::Position::new(1, 54),
641 lsp::Position::new(1, 54),
642 ),
643 })),
644 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
645 ..lsp::CompletionItem::default()
646 },
647 ])))
648 });
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 expected_push_diagnostic_lib_message,
3193 ];
3194 assert_eq!(
3195 all_diagnostics.len(),
3196 2,
3197 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3198 );
3199 for diagnostic in all_diagnostics {
3200 assert!(
3201 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3202 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3203 diagnostic.diagnostic.message
3204 );
3205 }
3206 });
3207
3208 if should_stream_workspace_diagnostic {
3209 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3210 token: expected_workspace_diagnostic_token.clone(),
3211 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3212 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3213 items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
3214 lsp::WorkspaceFullDocumentDiagnosticReport {
3215 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3216 version: None,
3217 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
3218 result_id: Some(format!(
3219 "workspace_{}",
3220 workspace_diagnostics_pulls_made
3221 .fetch_add(1, atomic::Ordering::Release)
3222 + 1
3223 )),
3224 items: vec![lsp::Diagnostic {
3225 range: lsp::Range {
3226 start: lsp::Position {
3227 line: 0,
3228 character: 1,
3229 },
3230 end: lsp::Position {
3231 line: 0,
3232 character: 2,
3233 },
3234 },
3235 severity: Some(lsp::DiagnosticSeverity::ERROR),
3236 message: expected_workspace_pull_diagnostics_lib_message
3237 .to_string(),
3238 ..lsp::Diagnostic::default()
3239 }],
3240 },
3241 },
3242 )],
3243 }),
3244 ),
3245 });
3246 workspace_diagnostic_start_count =
3247 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3248 workspace_diagnostic_cancel_tx.send(()).await.unwrap();
3249 workspace_diagnostics_pulls_handle.next().await.unwrap();
3250 executor.run_until_parked();
3251 editor_b_lib.update(cx_b, |editor, cx| {
3252 let snapshot = editor.buffer().read(cx).snapshot(cx);
3253 let all_diagnostics = snapshot
3254 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3255 .collect::<Vec<_>>();
3256 let expected_messages = [
3257 // Despite workspace diagnostics provided,
3258 // the currently open file's diagnostics should be preferred, as LSP suggests.
3259 expected_pull_diagnostic_lib_message,
3260 expected_push_diagnostic_lib_message,
3261 ];
3262 assert_eq!(
3263 all_diagnostics.len(),
3264 2,
3265 "Expected pull and push 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_eq!(
3379 diagnostics_pulls_result_ids.lock().await.len(),
3380 diagnostic_pulls_result_ids,
3381 "Pulls should not happen hence no extra ids should appear"
3382 );
3383 assert!(
3384 workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
3385 "More workspace diagnostics should be pulled"
3386 );
3387 }
3388 editor_b_lib.update(cx_b, |editor, cx| {
3389 let snapshot = editor.buffer().read(cx).snapshot(cx);
3390 let all_diagnostics = snapshot
3391 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3392 .collect::<Vec<_>>();
3393 let expected_messages = [
3394 expected_workspace_pull_diagnostics_lib_message,
3395 expected_pull_diagnostic_lib_message,
3396 expected_push_diagnostic_lib_message,
3397 ];
3398 assert_eq!(all_diagnostics.len(), 2);
3399 for diagnostic in &all_diagnostics {
3400 assert!(
3401 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3402 "Unexpected diagnostics: {all_diagnostics:?}"
3403 );
3404 }
3405 });
3406 editor_b_main.update(cx_b, |editor, cx| {
3407 let snapshot = editor.buffer().read(cx).snapshot(cx);
3408 let all_diagnostics = snapshot
3409 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3410 .collect::<Vec<_>>();
3411 assert_eq!(all_diagnostics.len(), 2);
3412
3413 let expected_messages = [
3414 expected_workspace_pull_diagnostics_main_message,
3415 expected_pull_diagnostic_main_message,
3416 expected_push_diagnostic_main_message,
3417 ];
3418 for diagnostic in &all_diagnostics {
3419 assert!(
3420 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3421 "Unexpected diagnostics: {all_diagnostics:?}"
3422 );
3423 }
3424 });
3425 editor_a_main.update(cx_a, |editor, cx| {
3426 let snapshot = editor.buffer().read(cx).snapshot(cx);
3427 let all_diagnostics = snapshot
3428 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3429 .collect::<Vec<_>>();
3430 assert_eq!(all_diagnostics.len(), 2);
3431 let expected_messages = [
3432 expected_workspace_pull_diagnostics_main_message,
3433 expected_pull_diagnostic_main_message,
3434 expected_push_diagnostic_main_message,
3435 ];
3436 for diagnostic in &all_diagnostics {
3437 assert!(
3438 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3439 "Unexpected diagnostics: {all_diagnostics:?}"
3440 );
3441 }
3442 });
3443}
3444
3445#[gpui::test(iterations = 10)]
3446async fn test_non_streamed_lsp_pull_diagnostics(
3447 cx_a: &mut TestAppContext,
3448 cx_b: &mut TestAppContext,
3449) {
3450 test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
3451}
3452
3453#[gpui::test(iterations = 10)]
3454async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3455 test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
3456}
3457
3458#[gpui::test(iterations = 10)]
3459async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3460 let mut server = TestServer::start(cx_a.executor()).await;
3461 let client_a = server.create_client(cx_a, "user_a").await;
3462 let client_b = server.create_client(cx_b, "user_b").await;
3463 server
3464 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3465 .await;
3466 let active_call_a = cx_a.read(ActiveCall::global);
3467
3468 cx_a.update(editor::init);
3469 cx_b.update(editor::init);
3470 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
3471 let inline_blame_off_settings = Some(InlineBlameSettings {
3472 enabled: Some(false),
3473 ..Default::default()
3474 });
3475 cx_a.update(|cx| {
3476 SettingsStore::update_global(cx, |store, cx| {
3477 store.update_user_settings(cx, |settings| {
3478 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3479 });
3480 });
3481 });
3482 cx_b.update(|cx| {
3483 SettingsStore::update_global(cx, |store, cx| {
3484 store.update_user_settings(cx, |settings| {
3485 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3486 });
3487 });
3488 });
3489
3490 client_a
3491 .fs()
3492 .insert_tree(
3493 path!("/my-repo"),
3494 json!({
3495 ".git": {},
3496 "file.txt": "line1\nline2\nline3\nline\n",
3497 }),
3498 )
3499 .await;
3500
3501 let blame = git::blame::Blame {
3502 entries: vec![
3503 blame_entry("1b1b1b", 0..1),
3504 blame_entry("0d0d0d", 1..2),
3505 blame_entry("3a3a3a", 2..3),
3506 blame_entry("4c4c4c", 3..4),
3507 ],
3508 messages: [
3509 ("1b1b1b", "message for idx-0"),
3510 ("0d0d0d", "message for idx-1"),
3511 ("3a3a3a", "message for idx-2"),
3512 ("4c4c4c", "message for idx-3"),
3513 ]
3514 .into_iter()
3515 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3516 .collect(),
3517 };
3518 client_a.fs().set_blame_for_repo(
3519 Path::new(path!("/my-repo/.git")),
3520 vec![(repo_path("file.txt"), blame)],
3521 );
3522
3523 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3524 let project_id = active_call_a
3525 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3526 .await
3527 .unwrap();
3528
3529 // Create editor_a
3530 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3531 let editor_a = workspace_a
3532 .update_in(cx_a, |workspace, window, cx| {
3533 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3534 })
3535 .await
3536 .unwrap()
3537 .downcast::<Editor>()
3538 .unwrap();
3539
3540 // Join the project as client B.
3541 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3542 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3543 let editor_b = workspace_b
3544 .update_in(cx_b, |workspace, window, cx| {
3545 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3546 })
3547 .await
3548 .unwrap()
3549 .downcast::<Editor>()
3550 .unwrap();
3551 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3552 editor_b
3553 .buffer()
3554 .read(cx)
3555 .as_singleton()
3556 .unwrap()
3557 .read(cx)
3558 .remote_id()
3559 });
3560
3561 // client_b now requests git blame for the open buffer
3562 editor_b.update_in(cx_b, |editor_b, window, cx| {
3563 assert!(editor_b.blame().is_none());
3564 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3565 });
3566
3567 cx_a.executor().run_until_parked();
3568 cx_b.executor().run_until_parked();
3569
3570 editor_b.update(cx_b, |editor_b, cx| {
3571 let blame = editor_b.blame().expect("editor_b should have blame now");
3572 let entries = blame.update(cx, |blame, cx| {
3573 blame
3574 .blame_for_rows(
3575 &(0..4)
3576 .map(|row| RowInfo {
3577 buffer_row: Some(row),
3578 buffer_id: Some(buffer_id_b),
3579 ..Default::default()
3580 })
3581 .collect::<Vec<_>>(),
3582 cx,
3583 )
3584 .collect::<Vec<_>>()
3585 });
3586
3587 assert_eq!(
3588 entries,
3589 vec![
3590 Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
3591 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3592 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3593 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3594 ]
3595 );
3596
3597 blame.update(cx, |blame, _| {
3598 for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
3599 let details = blame.details_for_entry(*buffer, entry).unwrap();
3600 assert_eq!(details.message, format!("message for idx-{}", idx));
3601 }
3602 });
3603 });
3604
3605 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3606 // which gets back to client_b.
3607 editor_b.update_in(cx_b, |editor_b, _, cx| {
3608 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3609 });
3610
3611 cx_a.executor().run_until_parked();
3612 cx_b.executor().run_until_parked();
3613
3614 editor_b.update(cx_b, |editor_b, cx| {
3615 let blame = editor_b.blame().expect("editor_b should have blame now");
3616 let entries = blame.update(cx, |blame, cx| {
3617 blame
3618 .blame_for_rows(
3619 &(0..4)
3620 .map(|row| RowInfo {
3621 buffer_row: Some(row),
3622 buffer_id: Some(buffer_id_b),
3623 ..Default::default()
3624 })
3625 .collect::<Vec<_>>(),
3626 cx,
3627 )
3628 .collect::<Vec<_>>()
3629 });
3630
3631 assert_eq!(
3632 entries,
3633 vec![
3634 None,
3635 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3636 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3637 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3638 ]
3639 );
3640 });
3641
3642 // Now editor_a also updates the file
3643 editor_a.update_in(cx_a, |editor_a, _, cx| {
3644 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3645 });
3646
3647 cx_a.executor().run_until_parked();
3648 cx_b.executor().run_until_parked();
3649
3650 editor_b.update(cx_b, |editor_b, cx| {
3651 let blame = editor_b.blame().expect("editor_b should have blame now");
3652 let entries = blame.update(cx, |blame, cx| {
3653 blame
3654 .blame_for_rows(
3655 &(0..4)
3656 .map(|row| RowInfo {
3657 buffer_row: Some(row),
3658 buffer_id: Some(buffer_id_b),
3659 ..Default::default()
3660 })
3661 .collect::<Vec<_>>(),
3662 cx,
3663 )
3664 .collect::<Vec<_>>()
3665 });
3666
3667 assert_eq!(
3668 entries,
3669 vec![
3670 None,
3671 None,
3672 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3673 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3674 ]
3675 );
3676 });
3677}
3678
3679#[gpui::test(iterations = 30)]
3680async fn test_collaborating_with_editorconfig(
3681 cx_a: &mut TestAppContext,
3682 cx_b: &mut TestAppContext,
3683) {
3684 let mut server = TestServer::start(cx_a.executor()).await;
3685 let client_a = server.create_client(cx_a, "user_a").await;
3686 let client_b = server.create_client(cx_b, "user_b").await;
3687 server
3688 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3689 .await;
3690 let active_call_a = cx_a.read(ActiveCall::global);
3691
3692 cx_b.update(editor::init);
3693
3694 // Set up a fake language server.
3695 client_a.language_registry().add(rust_lang());
3696 client_a
3697 .fs()
3698 .insert_tree(
3699 path!("/a"),
3700 json!({
3701 "src": {
3702 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3703 "other_mod": {
3704 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3705 ".editorconfig": "",
3706 },
3707 },
3708 ".editorconfig": "[*]\ntab_width = 2\n",
3709 }),
3710 )
3711 .await;
3712 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3713 let project_id = active_call_a
3714 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3715 .await
3716 .unwrap();
3717 let main_buffer_a = project_a
3718 .update(cx_a, |p, cx| {
3719 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3720 })
3721 .await
3722 .unwrap();
3723 let other_buffer_a = project_a
3724 .update(cx_a, |p, cx| {
3725 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3726 })
3727 .await
3728 .unwrap();
3729 let cx_a = cx_a.add_empty_window();
3730 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3731 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3732 });
3733 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3734 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3735 });
3736 let mut main_editor_cx_a = EditorTestContext {
3737 cx: cx_a.clone(),
3738 window: cx_a.window_handle(),
3739 editor: main_editor_a,
3740 assertion_cx: AssertionContextManager::new(),
3741 };
3742 let mut other_editor_cx_a = EditorTestContext {
3743 cx: cx_a.clone(),
3744 window: cx_a.window_handle(),
3745 editor: other_editor_a,
3746 assertion_cx: AssertionContextManager::new(),
3747 };
3748
3749 // Join the project as client B.
3750 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3751 let main_buffer_b = project_b
3752 .update(cx_b, |p, cx| {
3753 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3754 })
3755 .await
3756 .unwrap();
3757 let other_buffer_b = project_b
3758 .update(cx_b, |p, cx| {
3759 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3760 })
3761 .await
3762 .unwrap();
3763 let cx_b = cx_b.add_empty_window();
3764 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3765 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3766 });
3767 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3768 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3769 });
3770 let mut main_editor_cx_b = EditorTestContext {
3771 cx: cx_b.clone(),
3772 window: cx_b.window_handle(),
3773 editor: main_editor_b,
3774 assertion_cx: AssertionContextManager::new(),
3775 };
3776 let mut other_editor_cx_b = EditorTestContext {
3777 cx: cx_b.clone(),
3778 window: cx_b.window_handle(),
3779 editor: other_editor_b,
3780 assertion_cx: AssertionContextManager::new(),
3781 };
3782
3783 let initial_main = indoc! {"
3784ˇmod other;
3785fn main() { let foo = other::foo(); }"};
3786 let initial_other = indoc! {"
3787ˇpub fn foo() -> usize {
3788 4
3789}"};
3790
3791 let first_tabbed_main = indoc! {"
3792 ˇmod other;
3793fn main() { let foo = other::foo(); }"};
3794 tab_undo_assert(
3795 &mut main_editor_cx_a,
3796 &mut main_editor_cx_b,
3797 initial_main,
3798 first_tabbed_main,
3799 true,
3800 );
3801 tab_undo_assert(
3802 &mut main_editor_cx_a,
3803 &mut main_editor_cx_b,
3804 initial_main,
3805 first_tabbed_main,
3806 false,
3807 );
3808
3809 let first_tabbed_other = indoc! {"
3810 ˇpub fn foo() -> usize {
3811 4
3812}"};
3813 tab_undo_assert(
3814 &mut other_editor_cx_a,
3815 &mut other_editor_cx_b,
3816 initial_other,
3817 first_tabbed_other,
3818 true,
3819 );
3820 tab_undo_assert(
3821 &mut other_editor_cx_a,
3822 &mut other_editor_cx_b,
3823 initial_other,
3824 first_tabbed_other,
3825 false,
3826 );
3827
3828 client_a
3829 .fs()
3830 .atomic_write(
3831 PathBuf::from(path!("/a/src/.editorconfig")),
3832 "[*]\ntab_width = 3\n".to_owned(),
3833 )
3834 .await
3835 .unwrap();
3836 cx_a.run_until_parked();
3837 cx_b.run_until_parked();
3838
3839 let second_tabbed_main = indoc! {"
3840 ˇmod other;
3841fn main() { let foo = other::foo(); }"};
3842 tab_undo_assert(
3843 &mut main_editor_cx_a,
3844 &mut main_editor_cx_b,
3845 initial_main,
3846 second_tabbed_main,
3847 true,
3848 );
3849 tab_undo_assert(
3850 &mut main_editor_cx_a,
3851 &mut main_editor_cx_b,
3852 initial_main,
3853 second_tabbed_main,
3854 false,
3855 );
3856
3857 let second_tabbed_other = indoc! {"
3858 ˇpub fn foo() -> usize {
3859 4
3860}"};
3861 tab_undo_assert(
3862 &mut other_editor_cx_a,
3863 &mut other_editor_cx_b,
3864 initial_other,
3865 second_tabbed_other,
3866 true,
3867 );
3868 tab_undo_assert(
3869 &mut other_editor_cx_a,
3870 &mut other_editor_cx_b,
3871 initial_other,
3872 second_tabbed_other,
3873 false,
3874 );
3875
3876 let editorconfig_buffer_b = project_b
3877 .update(cx_b, |p, cx| {
3878 p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
3879 })
3880 .await
3881 .unwrap();
3882 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3883 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3884 });
3885 project_b
3886 .update(cx_b, |project, cx| {
3887 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3888 })
3889 .await
3890 .unwrap();
3891 cx_a.run_until_parked();
3892 cx_b.run_until_parked();
3893
3894 tab_undo_assert(
3895 &mut main_editor_cx_a,
3896 &mut main_editor_cx_b,
3897 initial_main,
3898 second_tabbed_main,
3899 true,
3900 );
3901 tab_undo_assert(
3902 &mut main_editor_cx_a,
3903 &mut main_editor_cx_b,
3904 initial_main,
3905 second_tabbed_main,
3906 false,
3907 );
3908
3909 let third_tabbed_other = indoc! {"
3910 ˇpub fn foo() -> usize {
3911 4
3912}"};
3913 tab_undo_assert(
3914 &mut other_editor_cx_a,
3915 &mut other_editor_cx_b,
3916 initial_other,
3917 third_tabbed_other,
3918 true,
3919 );
3920
3921 tab_undo_assert(
3922 &mut other_editor_cx_a,
3923 &mut other_editor_cx_b,
3924 initial_other,
3925 third_tabbed_other,
3926 false,
3927 );
3928}
3929
3930#[gpui::test]
3931async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3932 let executor = cx_a.executor();
3933 let mut server = TestServer::start(executor.clone()).await;
3934 let client_a = server.create_client(cx_a, "user_a").await;
3935 let client_b = server.create_client(cx_b, "user_b").await;
3936 server
3937 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3938 .await;
3939 let active_call_a = cx_a.read(ActiveCall::global);
3940 let active_call_b = cx_b.read(ActiveCall::global);
3941 cx_a.update(editor::init);
3942 cx_b.update(editor::init);
3943 client_a
3944 .fs()
3945 .insert_tree(
3946 "/a",
3947 json!({
3948 "test.txt": "one\ntwo\nthree\nfour\nfive",
3949 }),
3950 )
3951 .await;
3952 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3953 let project_path = ProjectPath {
3954 worktree_id,
3955 path: rel_path(&"test.txt").into(),
3956 };
3957 let abs_path = project_a.read_with(cx_a, |project, cx| {
3958 project
3959 .absolute_path(&project_path, cx)
3960 .map(Arc::from)
3961 .unwrap()
3962 });
3963
3964 active_call_a
3965 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
3966 .await
3967 .unwrap();
3968 let project_id = active_call_a
3969 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3970 .await
3971 .unwrap();
3972 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3973 active_call_b
3974 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
3975 .await
3976 .unwrap();
3977 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3978 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3979
3980 // Client A opens an editor.
3981 let editor_a = workspace_a
3982 .update_in(cx_a, |workspace, window, cx| {
3983 workspace.open_path(project_path.clone(), None, true, window, cx)
3984 })
3985 .await
3986 .unwrap()
3987 .downcast::<Editor>()
3988 .unwrap();
3989
3990 // Client B opens same editor as A.
3991 let editor_b = workspace_b
3992 .update_in(cx_b, |workspace, window, cx| {
3993 workspace.open_path(project_path.clone(), None, true, window, cx)
3994 })
3995 .await
3996 .unwrap()
3997 .downcast::<Editor>()
3998 .unwrap();
3999
4000 cx_a.run_until_parked();
4001 cx_b.run_until_parked();
4002
4003 // Client A adds breakpoint on line (1)
4004 editor_a.update_in(cx_a, |editor, window, cx| {
4005 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4006 });
4007
4008 cx_a.run_until_parked();
4009 cx_b.run_until_parked();
4010
4011 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4012 editor
4013 .breakpoint_store()
4014 .unwrap()
4015 .read(cx)
4016 .all_source_breakpoints(cx)
4017 });
4018 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4019 editor
4020 .breakpoint_store()
4021 .unwrap()
4022 .read(cx)
4023 .all_source_breakpoints(cx)
4024 });
4025
4026 assert_eq!(1, breakpoints_a.len());
4027 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4028 assert_eq!(breakpoints_a, breakpoints_b);
4029
4030 // Client B adds breakpoint on line(2)
4031 editor_b.update_in(cx_b, |editor, window, cx| {
4032 editor.move_down(&editor::actions::MoveDown, window, cx);
4033 editor.move_down(&editor::actions::MoveDown, window, cx);
4034 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4035 });
4036
4037 cx_a.run_until_parked();
4038 cx_b.run_until_parked();
4039
4040 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4041 editor
4042 .breakpoint_store()
4043 .unwrap()
4044 .read(cx)
4045 .all_source_breakpoints(cx)
4046 });
4047 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4048 editor
4049 .breakpoint_store()
4050 .unwrap()
4051 .read(cx)
4052 .all_source_breakpoints(cx)
4053 });
4054
4055 assert_eq!(1, breakpoints_a.len());
4056 assert_eq!(breakpoints_a, breakpoints_b);
4057 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
4058
4059 // Client A removes last added breakpoint from client B
4060 editor_a.update_in(cx_a, |editor, window, cx| {
4061 editor.move_down(&editor::actions::MoveDown, window, cx);
4062 editor.move_down(&editor::actions::MoveDown, window, cx);
4063 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4064 });
4065
4066 cx_a.run_until_parked();
4067 cx_b.run_until_parked();
4068
4069 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4070 editor
4071 .breakpoint_store()
4072 .unwrap()
4073 .read(cx)
4074 .all_source_breakpoints(cx)
4075 });
4076 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4077 editor
4078 .breakpoint_store()
4079 .unwrap()
4080 .read(cx)
4081 .all_source_breakpoints(cx)
4082 });
4083
4084 assert_eq!(1, breakpoints_a.len());
4085 assert_eq!(breakpoints_a, breakpoints_b);
4086 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4087
4088 // Client B removes first added breakpoint by client A
4089 editor_b.update_in(cx_b, |editor, window, cx| {
4090 editor.move_up(&editor::actions::MoveUp, window, cx);
4091 editor.move_up(&editor::actions::MoveUp, window, cx);
4092 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4093 });
4094
4095 cx_a.run_until_parked();
4096 cx_b.run_until_parked();
4097
4098 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4099 editor
4100 .breakpoint_store()
4101 .unwrap()
4102 .read(cx)
4103 .all_source_breakpoints(cx)
4104 });
4105 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4106 editor
4107 .breakpoint_store()
4108 .unwrap()
4109 .read(cx)
4110 .all_source_breakpoints(cx)
4111 });
4112
4113 assert_eq!(0, breakpoints_a.len());
4114 assert_eq!(breakpoints_a, breakpoints_b);
4115}
4116
4117#[gpui::test]
4118async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4119 let mut server = TestServer::start(cx_a.executor()).await;
4120 let client_a = server.create_client(cx_a, "user_a").await;
4121 let client_b = server.create_client(cx_b, "user_b").await;
4122 server
4123 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4124 .await;
4125 let active_call_a = cx_a.read(ActiveCall::global);
4126 let active_call_b = cx_b.read(ActiveCall::global);
4127
4128 cx_a.update(editor::init);
4129 cx_b.update(editor::init);
4130
4131 client_a.language_registry().add(rust_lang());
4132 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4133 "Rust",
4134 FakeLspAdapter {
4135 name: "rust-analyzer",
4136 ..FakeLspAdapter::default()
4137 },
4138 );
4139 client_b.language_registry().add(rust_lang());
4140 client_b.language_registry().register_fake_lsp_adapter(
4141 "Rust",
4142 FakeLspAdapter {
4143 name: "rust-analyzer",
4144 ..FakeLspAdapter::default()
4145 },
4146 );
4147
4148 client_a
4149 .fs()
4150 .insert_tree(
4151 path!("/a"),
4152 json!({
4153 "main.rs": "fn main() {}",
4154 }),
4155 )
4156 .await;
4157 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4158 active_call_a
4159 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4160 .await
4161 .unwrap();
4162 let project_id = active_call_a
4163 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4164 .await
4165 .unwrap();
4166
4167 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4168 active_call_b
4169 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4170 .await
4171 .unwrap();
4172
4173 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4174 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4175
4176 let editor_a = workspace_a
4177 .update_in(cx_a, |workspace, window, cx| {
4178 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4179 })
4180 .await
4181 .unwrap()
4182 .downcast::<Editor>()
4183 .unwrap();
4184
4185 let editor_b = workspace_b
4186 .update_in(cx_b, |workspace, window, cx| {
4187 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4188 })
4189 .await
4190 .unwrap()
4191 .downcast::<Editor>()
4192 .unwrap();
4193
4194 let fake_language_server = fake_language_servers.next().await.unwrap();
4195
4196 // host
4197 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4198 |params, _| async move {
4199 assert_eq!(
4200 params.text_document.uri,
4201 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4202 );
4203 assert_eq!(params.position, lsp::Position::new(0, 0));
4204 Ok(Some(ExpandedMacro {
4205 name: "test_macro_name".to_string(),
4206 expansion: "test_macro_expansion on the host".to_string(),
4207 }))
4208 },
4209 );
4210
4211 editor_a.update_in(cx_a, |editor, window, cx| {
4212 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4213 });
4214 expand_request_a.next().await.unwrap();
4215 cx_a.run_until_parked();
4216
4217 workspace_a.update(cx_a, |workspace, cx| {
4218 workspace.active_pane().update(cx, |pane, cx| {
4219 assert_eq!(
4220 pane.items_len(),
4221 2,
4222 "Should have added a macro expansion to the host's pane"
4223 );
4224 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4225 new_editor.update(cx, |editor, cx| {
4226 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
4227 });
4228 })
4229 });
4230
4231 // client
4232 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4233 |params, _| async move {
4234 assert_eq!(
4235 params.text_document.uri,
4236 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4237 );
4238 assert_eq!(
4239 params.position,
4240 lsp::Position::new(0, 12),
4241 "editor_b has selected the entire text and should query for a different position"
4242 );
4243 Ok(Some(ExpandedMacro {
4244 name: "test_macro_name".to_string(),
4245 expansion: "test_macro_expansion on the client".to_string(),
4246 }))
4247 },
4248 );
4249
4250 editor_b.update_in(cx_b, |editor, window, cx| {
4251 editor.select_all(&SelectAll, window, cx);
4252 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4253 });
4254 expand_request_b.next().await.unwrap();
4255 cx_b.run_until_parked();
4256
4257 workspace_b.update(cx_b, |workspace, cx| {
4258 workspace.active_pane().update(cx, |pane, cx| {
4259 assert_eq!(
4260 pane.items_len(),
4261 2,
4262 "Should have added a macro expansion to the client's pane"
4263 );
4264 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4265 new_editor.update(cx, |editor, cx| {
4266 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
4267 });
4268 })
4269 });
4270}
4271
4272#[gpui::test]
4273async fn test_copy_file_name_without_extension(
4274 cx_a: &mut TestAppContext,
4275 cx_b: &mut TestAppContext,
4276) {
4277 let mut server = TestServer::start(cx_a.executor()).await;
4278 let client_a = server.create_client(cx_a, "user_a").await;
4279 let client_b = server.create_client(cx_b, "user_b").await;
4280 server
4281 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4282 .await;
4283
4284 cx_b.update(editor::init);
4285
4286 client_a
4287 .fs()
4288 .insert_tree(
4289 path!("/root"),
4290 json!({
4291 "src": {
4292 "main.rs": indoc! {"
4293 fn main() {
4294 println!(\"Hello, world!\");
4295 }
4296 "},
4297 }
4298 }),
4299 )
4300 .await;
4301
4302 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4303 let active_call_a = cx_a.read(ActiveCall::global);
4304 let project_id = active_call_a
4305 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4306 .await
4307 .unwrap();
4308
4309 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4310
4311 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4312 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4313
4314 let editor_a = workspace_a
4315 .update_in(cx_a, |workspace, window, cx| {
4316 workspace.open_path(
4317 (worktree_id, rel_path("src/main.rs")),
4318 None,
4319 true,
4320 window,
4321 cx,
4322 )
4323 })
4324 .await
4325 .unwrap()
4326 .downcast::<Editor>()
4327 .unwrap();
4328
4329 let editor_b = workspace_b
4330 .update_in(cx_b, |workspace, window, cx| {
4331 workspace.open_path(
4332 (worktree_id, rel_path("src/main.rs")),
4333 None,
4334 true,
4335 window,
4336 cx,
4337 )
4338 })
4339 .await
4340 .unwrap()
4341 .downcast::<Editor>()
4342 .unwrap();
4343
4344 cx_a.run_until_parked();
4345 cx_b.run_until_parked();
4346
4347 editor_a.update_in(cx_a, |editor, window, cx| {
4348 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4349 });
4350
4351 assert_eq!(
4352 cx_a.read_from_clipboard().and_then(|item| item.text()),
4353 Some("main".to_string())
4354 );
4355
4356 editor_b.update_in(cx_b, |editor, window, cx| {
4357 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4358 });
4359
4360 assert_eq!(
4361 cx_b.read_from_clipboard().and_then(|item| item.text()),
4362 Some("main".to_string())
4363 );
4364}
4365
4366#[gpui::test]
4367async fn test_copy_file_name(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4368 let mut server = TestServer::start(cx_a.executor()).await;
4369 let client_a = server.create_client(cx_a, "user_a").await;
4370 let client_b = server.create_client(cx_b, "user_b").await;
4371 server
4372 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4373 .await;
4374
4375 cx_b.update(editor::init);
4376
4377 client_a
4378 .fs()
4379 .insert_tree(
4380 path!("/root"),
4381 json!({
4382 "src": {
4383 "main.rs": indoc! {"
4384 fn main() {
4385 println!(\"Hello, world!\");
4386 }
4387 "},
4388 }
4389 }),
4390 )
4391 .await;
4392
4393 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4394 let active_call_a = cx_a.read(ActiveCall::global);
4395 let project_id = active_call_a
4396 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4397 .await
4398 .unwrap();
4399
4400 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4401
4402 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4403 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4404
4405 let editor_a = workspace_a
4406 .update_in(cx_a, |workspace, window, cx| {
4407 workspace.open_path(
4408 (worktree_id, rel_path("src/main.rs")),
4409 None,
4410 true,
4411 window,
4412 cx,
4413 )
4414 })
4415 .await
4416 .unwrap()
4417 .downcast::<Editor>()
4418 .unwrap();
4419
4420 let editor_b = workspace_b
4421 .update_in(cx_b, |workspace, window, cx| {
4422 workspace.open_path(
4423 (worktree_id, rel_path("src/main.rs")),
4424 None,
4425 true,
4426 window,
4427 cx,
4428 )
4429 })
4430 .await
4431 .unwrap()
4432 .downcast::<Editor>()
4433 .unwrap();
4434
4435 cx_a.run_until_parked();
4436 cx_b.run_until_parked();
4437
4438 editor_a.update_in(cx_a, |editor, window, cx| {
4439 editor.copy_file_name(&CopyFileName, window, cx);
4440 });
4441
4442 assert_eq!(
4443 cx_a.read_from_clipboard().and_then(|item| item.text()),
4444 Some("main.rs".to_string())
4445 );
4446
4447 editor_b.update_in(cx_b, |editor, window, cx| {
4448 editor.copy_file_name(&CopyFileName, window, cx);
4449 });
4450
4451 assert_eq!(
4452 cx_b.read_from_clipboard().and_then(|item| item.text()),
4453 Some("main.rs".to_string())
4454 );
4455}
4456
4457#[gpui::test]
4458async fn test_copy_file_location(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4459 let mut server = TestServer::start(cx_a.executor()).await;
4460 let client_a = server.create_client(cx_a, "user_a").await;
4461 let client_b = server.create_client(cx_b, "user_b").await;
4462 server
4463 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4464 .await;
4465
4466 cx_b.update(editor::init);
4467
4468 client_a
4469 .fs()
4470 .insert_tree(
4471 path!("/root"),
4472 json!({
4473 "src": {
4474 "main.rs": indoc! {"
4475 fn main() {
4476 println!(\"Hello, world!\");
4477 }
4478 "},
4479 }
4480 }),
4481 )
4482 .await;
4483
4484 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4485 let active_call_a = cx_a.read(ActiveCall::global);
4486 let project_id = active_call_a
4487 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4488 .await
4489 .unwrap();
4490
4491 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4492
4493 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4494 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4495
4496 let editor_a = workspace_a
4497 .update_in(cx_a, |workspace, window, cx| {
4498 workspace.open_path(
4499 (worktree_id, rel_path("src/main.rs")),
4500 None,
4501 true,
4502 window,
4503 cx,
4504 )
4505 })
4506 .await
4507 .unwrap()
4508 .downcast::<Editor>()
4509 .unwrap();
4510
4511 let editor_b = workspace_b
4512 .update_in(cx_b, |workspace, window, cx| {
4513 workspace.open_path(
4514 (worktree_id, rel_path("src/main.rs")),
4515 None,
4516 true,
4517 window,
4518 cx,
4519 )
4520 })
4521 .await
4522 .unwrap()
4523 .downcast::<Editor>()
4524 .unwrap();
4525
4526 cx_a.run_until_parked();
4527 cx_b.run_until_parked();
4528
4529 editor_a.update_in(cx_a, |editor, window, cx| {
4530 editor.change_selections(Default::default(), window, cx, |s| {
4531 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4532 });
4533 editor.copy_file_location(&CopyFileLocation, window, cx);
4534 });
4535
4536 assert_eq!(
4537 cx_a.read_from_clipboard().and_then(|item| item.text()),
4538 Some(format!("{}:2", path!("src/main.rs")))
4539 );
4540
4541 editor_b.update_in(cx_b, |editor, window, cx| {
4542 editor.change_selections(Default::default(), window, cx, |s| {
4543 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4544 });
4545 editor.copy_file_location(&CopyFileLocation, window, cx);
4546 });
4547
4548 assert_eq!(
4549 cx_b.read_from_clipboard().and_then(|item| item.text()),
4550 Some(format!("{}:2", path!("src/main.rs")))
4551 );
4552}
4553
4554#[track_caller]
4555fn tab_undo_assert(
4556 cx_a: &mut EditorTestContext,
4557 cx_b: &mut EditorTestContext,
4558 expected_initial: &str,
4559 expected_tabbed: &str,
4560 a_tabs: bool,
4561) {
4562 cx_a.assert_editor_state(expected_initial);
4563 cx_b.assert_editor_state(expected_initial);
4564
4565 if a_tabs {
4566 cx_a.update_editor(|editor, window, cx| {
4567 editor.tab(&editor::actions::Tab, window, cx);
4568 });
4569 } else {
4570 cx_b.update_editor(|editor, window, cx| {
4571 editor.tab(&editor::actions::Tab, window, cx);
4572 });
4573 }
4574
4575 cx_a.run_until_parked();
4576 cx_b.run_until_parked();
4577
4578 cx_a.assert_editor_state(expected_tabbed);
4579 cx_b.assert_editor_state(expected_tabbed);
4580
4581 if a_tabs {
4582 cx_a.update_editor(|editor, window, cx| {
4583 editor.undo(&editor::actions::Undo, window, cx);
4584 });
4585 } else {
4586 cx_b.update_editor(|editor, window, cx| {
4587 editor.undo(&editor::actions::Undo, window, cx);
4588 });
4589 }
4590 cx_a.run_until_parked();
4591 cx_b.run_until_parked();
4592 cx_a.assert_editor_state(expected_initial);
4593 cx_b.assert_editor_state(expected_initial);
4594}
4595
4596fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4597 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4598
4599 let mut all_cached_labels = Vec::new();
4600 let mut all_fetched_hints = Vec::new();
4601 for buffer in editor.buffer().read(cx).all_buffers() {
4602 lsp_store.update(cx, |lsp_store, cx| {
4603 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4604 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4605 let mut label = hint.text().to_string();
4606 if hint.padding_left {
4607 label.insert(0, ' ');
4608 }
4609 if hint.padding_right {
4610 label.push_str(" ");
4611 }
4612 label
4613 }));
4614 all_fetched_hints.extend(hints.all_fetched_hints());
4615 });
4616 }
4617
4618 assert!(
4619 all_fetched_hints.is_empty(),
4620 "Did not expect background hints fetch tasks, but got {} of them",
4621 all_fetched_hints.len()
4622 );
4623
4624 all_cached_labels
4625}
4626
4627#[track_caller]
4628fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
4629 editor
4630 .all_inlays(cx)
4631 .into_iter()
4632 .filter_map(|inlay| inlay.get_color())
4633 .map(Rgba::from)
4634 .collect()
4635}
4636
4637fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
4638 git::blame::BlameEntry {
4639 sha: sha.parse().unwrap(),
4640 range,
4641 ..Default::default()
4642 }
4643}