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