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