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(text::Anchor::MIN..text::Anchor::MAX, false)
1585 .count()
1586 == 1
1587 });
1588
1589 // Edit the buffer as client B and see that edit as client A.
1590 editor_b.update_in(cx_b, |editor, window, cx| {
1591 editor.handle_input("ok, ", window, cx)
1592 });
1593 executor.run_until_parked();
1594
1595 buffer_a.read_with(cx_a, |buffer, _| {
1596 assert_eq!(buffer.text(), "ok, b-contents")
1597 });
1598
1599 // Client B can invite client C on a project shared by client A.
1600 active_call_b
1601 .update(cx_b, |call, cx| {
1602 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1603 })
1604 .await
1605 .unwrap();
1606
1607 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1608 executor.run_until_parked();
1609 let call = incoming_call_c.borrow().clone().unwrap();
1610 assert_eq!(call.calling_user.github_login, "user_b");
1611 let initial_project = call.initial_project.unwrap();
1612 active_call_c
1613 .update(cx_c, |call, cx| call.accept_incoming(cx))
1614 .await
1615 .unwrap();
1616 let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1617
1618 // Client B closes the editor, and client A sees client B's selections removed.
1619 cx_b.update(move |_, _| drop(editor_b));
1620 executor.run_until_parked();
1621
1622 buffer_a.read_with(cx_a, |buffer, _| {
1623 buffer
1624 .snapshot()
1625 .selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
1626 .count()
1627 == 0
1628 });
1629}
1630
1631#[gpui::test(iterations = 10)]
1632async fn test_on_input_format_from_host_to_guest(
1633 cx_a: &mut TestAppContext,
1634 cx_b: &mut TestAppContext,
1635) {
1636 let mut server = TestServer::start(cx_a.executor()).await;
1637 let executor = cx_a.executor();
1638 let client_a = server.create_client(cx_a, "user_a").await;
1639 let client_b = server.create_client(cx_b, "user_b").await;
1640 server
1641 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1642 .await;
1643 let active_call_a = cx_a.read(ActiveCall::global);
1644
1645 client_a.language_registry().add(rust_lang());
1646 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1647 "Rust",
1648 FakeLspAdapter {
1649 capabilities: lsp::ServerCapabilities {
1650 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1651 first_trigger_character: ":".to_string(),
1652 more_trigger_character: Some(vec![">".to_string()]),
1653 }),
1654 ..Default::default()
1655 },
1656 ..Default::default()
1657 },
1658 );
1659
1660 client_a
1661 .fs()
1662 .insert_tree(
1663 path!("/a"),
1664 json!({
1665 "main.rs": "fn main() { a }",
1666 "other.rs": "// Test file",
1667 }),
1668 )
1669 .await;
1670 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1671 let project_id = active_call_a
1672 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1673 .await
1674 .unwrap();
1675 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1676
1677 // Open a file in an editor as the host.
1678 let buffer_a = project_a
1679 .update(cx_a, |p, cx| {
1680 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1681 })
1682 .await
1683 .unwrap();
1684 let cx_a = cx_a.add_empty_window();
1685 let editor_a = cx_a.new_window_entity(|window, cx| {
1686 Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
1687 });
1688
1689 let fake_language_server = fake_language_servers.next().await.unwrap();
1690 executor.run_until_parked();
1691
1692 // Receive an OnTypeFormatting request as the host's language server.
1693 // Return some formatting from the host's language server.
1694 fake_language_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
1695 |params, _| async move {
1696 assert_eq!(
1697 params.text_document_position.text_document.uri,
1698 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1699 );
1700 assert_eq!(
1701 params.text_document_position.position,
1702 lsp::Position::new(0, 14),
1703 );
1704
1705 Ok(Some(vec![lsp::TextEdit {
1706 new_text: "~<".to_string(),
1707 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1708 }]))
1709 },
1710 );
1711
1712 // Open the buffer on the guest and see that the formatting worked
1713 let buffer_b = project_b
1714 .update(cx_b, |p, cx| {
1715 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1716 })
1717 .await
1718 .unwrap();
1719
1720 // Type a on type formatting trigger character as the guest.
1721 cx_a.focus(&editor_a);
1722 editor_a.update_in(cx_a, |editor, window, cx| {
1723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1724 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1725 });
1726 editor.handle_input(">", window, cx);
1727 });
1728
1729 executor.run_until_parked();
1730
1731 buffer_b.read_with(cx_b, |buffer, _| {
1732 assert_eq!(buffer.text(), "fn main() { a>~< }")
1733 });
1734
1735 // Undo should remove LSP edits first
1736 editor_a.update_in(cx_a, |editor, window, cx| {
1737 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1738 editor.undo(&Undo, window, cx);
1739 assert_eq!(editor.text(cx), "fn main() { a> }");
1740 });
1741 executor.run_until_parked();
1742
1743 buffer_b.read_with(cx_b, |buffer, _| {
1744 assert_eq!(buffer.text(), "fn main() { a> }")
1745 });
1746
1747 editor_a.update_in(cx_a, |editor, window, cx| {
1748 assert_eq!(editor.text(cx), "fn main() { a> }");
1749 editor.undo(&Undo, window, cx);
1750 assert_eq!(editor.text(cx), "fn main() { a }");
1751 });
1752 executor.run_until_parked();
1753
1754 buffer_b.read_with(cx_b, |buffer, _| {
1755 assert_eq!(buffer.text(), "fn main() { a }")
1756 });
1757}
1758
1759#[gpui::test(iterations = 10)]
1760async fn test_on_input_format_from_guest_to_host(
1761 cx_a: &mut TestAppContext,
1762 cx_b: &mut TestAppContext,
1763) {
1764 let mut server = TestServer::start(cx_a.executor()).await;
1765 let executor = cx_a.executor();
1766 let client_a = server.create_client(cx_a, "user_a").await;
1767 let client_b = server.create_client(cx_b, "user_b").await;
1768 server
1769 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1770 .await;
1771 let active_call_a = cx_a.read(ActiveCall::global);
1772
1773 let capabilities = lsp::ServerCapabilities {
1774 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1775 first_trigger_character: ":".to_string(),
1776 more_trigger_character: Some(vec![">".to_string()]),
1777 }),
1778 ..lsp::ServerCapabilities::default()
1779 };
1780 client_a.language_registry().add(rust_lang());
1781 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1782 "Rust",
1783 FakeLspAdapter {
1784 capabilities: capabilities.clone(),
1785 ..FakeLspAdapter::default()
1786 },
1787 );
1788 client_b.language_registry().add(rust_lang());
1789 client_b.language_registry().register_fake_lsp_adapter(
1790 "Rust",
1791 FakeLspAdapter {
1792 capabilities,
1793 ..FakeLspAdapter::default()
1794 },
1795 );
1796
1797 client_a
1798 .fs()
1799 .insert_tree(
1800 path!("/a"),
1801 json!({
1802 "main.rs": "fn main() { a }",
1803 "other.rs": "// Test file",
1804 }),
1805 )
1806 .await;
1807 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1808 let project_id = active_call_a
1809 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1810 .await
1811 .unwrap();
1812 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1813
1814 // Open a file in an editor as the guest.
1815 let buffer_b = project_b
1816 .update(cx_b, |p, cx| {
1817 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1818 })
1819 .await
1820 .unwrap();
1821 let cx_b = cx_b.add_empty_window();
1822 let editor_b = cx_b.new_window_entity(|window, cx| {
1823 Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
1824 });
1825
1826 let fake_language_server = fake_language_servers.next().await.unwrap();
1827 executor.run_until_parked();
1828
1829 // Type a on type formatting trigger character as the guest.
1830 cx_b.focus(&editor_b);
1831 editor_b.update_in(cx_b, |editor, window, cx| {
1832 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1833 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1834 });
1835 editor.handle_input(":", window, cx);
1836 });
1837
1838 // Receive an OnTypeFormatting request as the host's language server.
1839 // Return some formatting from the host's language server.
1840 executor.start_waiting();
1841 fake_language_server
1842 .set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1843 assert_eq!(
1844 params.text_document_position.text_document.uri,
1845 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1846 );
1847 assert_eq!(
1848 params.text_document_position.position,
1849 lsp::Position::new(0, 14),
1850 );
1851
1852 Ok(Some(vec![lsp::TextEdit {
1853 new_text: "~:".to_string(),
1854 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1855 }]))
1856 })
1857 .next()
1858 .await
1859 .unwrap();
1860 executor.finish_waiting();
1861
1862 // Open the buffer on the host and see that the formatting worked
1863 let buffer_a = project_a
1864 .update(cx_a, |p, cx| {
1865 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1866 })
1867 .await
1868 .unwrap();
1869 executor.run_until_parked();
1870
1871 buffer_a.read_with(cx_a, |buffer, _| {
1872 assert_eq!(buffer.text(), "fn main() { a:~: }")
1873 });
1874
1875 // Undo should remove LSP edits first
1876 editor_b.update_in(cx_b, |editor, window, cx| {
1877 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1878 editor.undo(&Undo, window, cx);
1879 assert_eq!(editor.text(cx), "fn main() { a: }");
1880 });
1881 executor.run_until_parked();
1882
1883 buffer_a.read_with(cx_a, |buffer, _| {
1884 assert_eq!(buffer.text(), "fn main() { a: }")
1885 });
1886
1887 editor_b.update_in(cx_b, |editor, window, cx| {
1888 assert_eq!(editor.text(cx), "fn main() { a: }");
1889 editor.undo(&Undo, window, cx);
1890 assert_eq!(editor.text(cx), "fn main() { a }");
1891 });
1892 executor.run_until_parked();
1893
1894 buffer_a.read_with(cx_a, |buffer, _| {
1895 assert_eq!(buffer.text(), "fn main() { a }")
1896 });
1897}
1898
1899#[gpui::test(iterations = 10)]
1900async fn test_mutual_editor_inlay_hint_cache_update(
1901 cx_a: &mut TestAppContext,
1902 cx_b: &mut TestAppContext,
1903) {
1904 let mut server = TestServer::start(cx_a.executor()).await;
1905 let executor = cx_a.executor();
1906 let client_a = server.create_client(cx_a, "user_a").await;
1907 let client_b = server.create_client(cx_b, "user_b").await;
1908 server
1909 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1910 .await;
1911 let active_call_a = cx_a.read(ActiveCall::global);
1912 let active_call_b = cx_b.read(ActiveCall::global);
1913
1914 cx_a.update(editor::init);
1915 cx_b.update(editor::init);
1916
1917 cx_a.update(|cx| {
1918 SettingsStore::update_global(cx, |store, cx| {
1919 store.update_user_settings(cx, |settings| {
1920 settings.project.all_languages.defaults.inlay_hints =
1921 Some(InlayHintSettingsContent {
1922 enabled: Some(true),
1923 ..InlayHintSettingsContent::default()
1924 })
1925 });
1926 });
1927 });
1928 cx_b.update(|cx| {
1929 SettingsStore::update_global(cx, |store, cx| {
1930 store.update_user_settings(cx, |settings| {
1931 settings.project.all_languages.defaults.inlay_hints =
1932 Some(InlayHintSettingsContent {
1933 enabled: Some(true),
1934 ..InlayHintSettingsContent::default()
1935 })
1936 });
1937 });
1938 });
1939
1940 let capabilities = lsp::ServerCapabilities {
1941 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1942 ..lsp::ServerCapabilities::default()
1943 };
1944 client_a.language_registry().add(rust_lang());
1945
1946 // Set up the language server to return an additional inlay hint on each request.
1947 let edits_made = Arc::new(AtomicUsize::new(0));
1948 let closure_edits_made = Arc::clone(&edits_made);
1949 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1950 "Rust",
1951 FakeLspAdapter {
1952 capabilities: capabilities.clone(),
1953 initializer: Some(Box::new(move |fake_language_server| {
1954 let closure_edits_made = closure_edits_made.clone();
1955 fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1956 move |params, _| {
1957 let edits_made_2 = Arc::clone(&closure_edits_made);
1958 async move {
1959 assert_eq!(
1960 params.text_document.uri,
1961 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1962 );
1963 let edits_made =
1964 AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
1965 Ok(Some(vec![lsp::InlayHint {
1966 position: lsp::Position::new(0, edits_made as u32),
1967 label: lsp::InlayHintLabel::String(edits_made.to_string()),
1968 kind: None,
1969 text_edits: None,
1970 tooltip: None,
1971 padding_left: None,
1972 padding_right: None,
1973 data: None,
1974 }]))
1975 }
1976 },
1977 );
1978 })),
1979 ..FakeLspAdapter::default()
1980 },
1981 );
1982 client_b.language_registry().add(rust_lang());
1983 client_b.language_registry().register_fake_lsp_adapter(
1984 "Rust",
1985 FakeLspAdapter {
1986 capabilities,
1987 ..FakeLspAdapter::default()
1988 },
1989 );
1990
1991 // Client A opens a project.
1992 client_a
1993 .fs()
1994 .insert_tree(
1995 path!("/a"),
1996 json!({
1997 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
1998 "other.rs": "// Test file",
1999 }),
2000 )
2001 .await;
2002 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2003 active_call_a
2004 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2005 .await
2006 .unwrap();
2007 let project_id = active_call_a
2008 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2009 .await
2010 .unwrap();
2011
2012 // Client B joins the project
2013 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2014 active_call_b
2015 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2016 .await
2017 .unwrap();
2018
2019 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2020
2021 // The host opens a rust file.
2022 let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
2023 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2024 });
2025 let fake_language_server = fake_language_servers.next().await.unwrap();
2026 let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
2027 executor.advance_clock(Duration::from_millis(100));
2028 executor.run_until_parked();
2029
2030 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
2031 editor_a.update(cx_a, |editor, cx| {
2032 assert_eq!(
2033 vec![initial_edit.to_string()],
2034 extract_hint_labels(editor, cx),
2035 "Host should get its first hints when opens an editor"
2036 );
2037 });
2038 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2039 let editor_b = workspace_b
2040 .update_in(cx_b, |workspace, window, cx| {
2041 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2042 })
2043 .await
2044 .unwrap()
2045 .downcast::<Editor>()
2046 .unwrap();
2047
2048 executor.advance_clock(Duration::from_millis(100));
2049 executor.run_until_parked();
2050 editor_b.update(cx_b, |editor, cx| {
2051 assert_eq!(
2052 vec![initial_edit.to_string()],
2053 extract_hint_labels(editor, cx),
2054 "Client should get its first hints when opens an editor"
2055 );
2056 });
2057
2058 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2059 editor_b.update_in(cx_b, |editor, window, cx| {
2060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2061 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
2062 });
2063 editor.handle_input(":", window, cx);
2064 });
2065 cx_b.focus(&editor_b);
2066
2067 executor.advance_clock(Duration::from_secs(1));
2068 executor.run_until_parked();
2069 editor_a.update(cx_a, |editor, cx| {
2070 assert_eq!(
2071 vec![after_client_edit.to_string()],
2072 extract_hint_labels(editor, cx),
2073 );
2074 });
2075 editor_b.update(cx_b, |editor, cx| {
2076 assert_eq!(
2077 vec![after_client_edit.to_string()],
2078 extract_hint_labels(editor, cx),
2079 );
2080 });
2081
2082 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2083 editor_a.update_in(cx_a, |editor, window, cx| {
2084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2085 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
2086 });
2087 editor.handle_input("a change to increment both buffers' versions", window, cx);
2088 });
2089 cx_a.focus(&editor_a);
2090
2091 executor.advance_clock(Duration::from_secs(1));
2092 executor.run_until_parked();
2093 editor_a.update(cx_a, |editor, cx| {
2094 assert_eq!(
2095 vec![after_host_edit.to_string()],
2096 extract_hint_labels(editor, cx),
2097 );
2098 });
2099 editor_b.update(cx_b, |editor, cx| {
2100 assert_eq!(
2101 vec![after_host_edit.to_string()],
2102 extract_hint_labels(editor, cx),
2103 );
2104 });
2105
2106 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2107 fake_language_server
2108 .request::<lsp::request::InlayHintRefreshRequest>(())
2109 .await
2110 .into_response()
2111 .expect("inlay refresh request failed");
2112
2113 executor.advance_clock(Duration::from_secs(1));
2114 executor.run_until_parked();
2115 editor_a.update(cx_a, |editor, cx| {
2116 assert_eq!(
2117 vec![after_special_edit_for_refresh.to_string()],
2118 extract_hint_labels(editor, cx),
2119 "Host should react to /refresh LSP request"
2120 );
2121 });
2122 editor_b.update(cx_b, |editor, cx| {
2123 assert_eq!(
2124 vec![after_special_edit_for_refresh.to_string()],
2125 extract_hint_labels(editor, cx),
2126 "Guest should get a /refresh LSP request propagated by host"
2127 );
2128 });
2129}
2130
2131#[gpui::test(iterations = 10)]
2132async fn test_inlay_hint_refresh_is_forwarded(
2133 cx_a: &mut TestAppContext,
2134 cx_b: &mut TestAppContext,
2135) {
2136 let mut server = TestServer::start(cx_a.executor()).await;
2137 let executor = cx_a.executor();
2138 let client_a = server.create_client(cx_a, "user_a").await;
2139 let client_b = server.create_client(cx_b, "user_b").await;
2140 server
2141 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2142 .await;
2143 let active_call_a = cx_a.read(ActiveCall::global);
2144 let active_call_b = cx_b.read(ActiveCall::global);
2145
2146 cx_a.update(editor::init);
2147 cx_b.update(editor::init);
2148
2149 cx_a.update(|cx| {
2150 SettingsStore::update_global(cx, |store, cx| {
2151 store.update_user_settings(cx, |settings| {
2152 settings.project.all_languages.defaults.inlay_hints =
2153 Some(InlayHintSettingsContent {
2154 show_value_hints: Some(true),
2155 enabled: Some(false),
2156 edit_debounce_ms: Some(0),
2157 scroll_debounce_ms: Some(0),
2158 show_type_hints: Some(false),
2159 show_parameter_hints: Some(false),
2160 show_other_hints: Some(false),
2161 show_background: Some(false),
2162 toggle_on_modifiers_press: None,
2163 })
2164 });
2165 });
2166 });
2167 cx_b.update(|cx| {
2168 SettingsStore::update_global(cx, |store, cx| {
2169 store.update_user_settings(cx, |settings| {
2170 settings.project.all_languages.defaults.inlay_hints =
2171 Some(InlayHintSettingsContent {
2172 show_value_hints: Some(true),
2173 enabled: Some(true),
2174 edit_debounce_ms: Some(0),
2175 scroll_debounce_ms: Some(0),
2176 show_type_hints: Some(true),
2177 show_parameter_hints: Some(true),
2178 show_other_hints: Some(true),
2179 show_background: Some(false),
2180 toggle_on_modifiers_press: None,
2181 })
2182 });
2183 });
2184 });
2185
2186 let capabilities = lsp::ServerCapabilities {
2187 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2188 ..lsp::ServerCapabilities::default()
2189 };
2190 client_a.language_registry().add(rust_lang());
2191 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2192 "Rust",
2193 FakeLspAdapter {
2194 capabilities: capabilities.clone(),
2195 ..FakeLspAdapter::default()
2196 },
2197 );
2198 client_b.language_registry().add(rust_lang());
2199 client_b.language_registry().register_fake_lsp_adapter(
2200 "Rust",
2201 FakeLspAdapter {
2202 capabilities,
2203 ..FakeLspAdapter::default()
2204 },
2205 );
2206
2207 client_a
2208 .fs()
2209 .insert_tree(
2210 path!("/a"),
2211 json!({
2212 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
2213 "other.rs": "// Test file",
2214 }),
2215 )
2216 .await;
2217 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2218 active_call_a
2219 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2220 .await
2221 .unwrap();
2222 let project_id = active_call_a
2223 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2224 .await
2225 .unwrap();
2226
2227 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2228 active_call_b
2229 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2230 .await
2231 .unwrap();
2232
2233 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2234 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2235
2236 cx_a.background_executor.start_waiting();
2237
2238 let editor_a = workspace_a
2239 .update_in(cx_a, |workspace, window, cx| {
2240 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2241 })
2242 .await
2243 .unwrap()
2244 .downcast::<Editor>()
2245 .unwrap();
2246
2247 let editor_b = workspace_b
2248 .update_in(cx_b, |workspace, window, cx| {
2249 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2250 })
2251 .await
2252 .unwrap()
2253 .downcast::<Editor>()
2254 .unwrap();
2255
2256 let other_hints = Arc::new(AtomicBool::new(false));
2257 let fake_language_server = fake_language_servers.next().await.unwrap();
2258 let closure_other_hints = Arc::clone(&other_hints);
2259 fake_language_server
2260 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2261 let task_other_hints = Arc::clone(&closure_other_hints);
2262 async move {
2263 assert_eq!(
2264 params.text_document.uri,
2265 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2266 );
2267 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
2268 let character = if other_hints { 0 } else { 2 };
2269 let label = if other_hints {
2270 "other hint"
2271 } else {
2272 "initial hint"
2273 };
2274 Ok(Some(vec![
2275 lsp::InlayHint {
2276 position: lsp::Position::new(0, character),
2277 label: lsp::InlayHintLabel::String(label.to_string()),
2278 kind: None,
2279 text_edits: None,
2280 tooltip: None,
2281 padding_left: None,
2282 padding_right: None,
2283 data: None,
2284 },
2285 lsp::InlayHint {
2286 position: lsp::Position::new(1090, 1090),
2287 label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
2288 kind: None,
2289 text_edits: None,
2290 tooltip: None,
2291 padding_left: None,
2292 padding_right: None,
2293 data: None,
2294 },
2295 ]))
2296 }
2297 })
2298 .next()
2299 .await
2300 .unwrap();
2301 executor.finish_waiting();
2302
2303 executor.run_until_parked();
2304 editor_a.update(cx_a, |editor, cx| {
2305 assert!(
2306 extract_hint_labels(editor, cx).is_empty(),
2307 "Host should get no hints due to them turned off"
2308 );
2309 });
2310
2311 executor.run_until_parked();
2312 editor_b.update(cx_b, |editor, cx| {
2313 assert_eq!(
2314 vec!["initial hint".to_string()],
2315 extract_hint_labels(editor, cx),
2316 "Client should get its first hints when opens an editor"
2317 );
2318 });
2319
2320 other_hints.fetch_or(true, atomic::Ordering::Release);
2321 fake_language_server
2322 .request::<lsp::request::InlayHintRefreshRequest>(())
2323 .await
2324 .into_response()
2325 .expect("inlay refresh request failed");
2326 executor.run_until_parked();
2327 editor_a.update(cx_a, |editor, cx| {
2328 assert!(
2329 extract_hint_labels(editor, cx).is_empty(),
2330 "Host should get no hints due to them turned off, even after the /refresh"
2331 );
2332 });
2333
2334 executor.run_until_parked();
2335 editor_b.update(cx_b, |editor, cx| {
2336 assert_eq!(
2337 vec!["other hint".to_string()],
2338 extract_hint_labels(editor, cx),
2339 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
2340 );
2341 });
2342}
2343
2344#[gpui::test(iterations = 10)]
2345async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2346 let expected_color = Rgba {
2347 r: 0.33,
2348 g: 0.33,
2349 b: 0.33,
2350 a: 0.33,
2351 };
2352 let mut server = TestServer::start(cx_a.executor()).await;
2353 let executor = cx_a.executor();
2354 let client_a = server.create_client(cx_a, "user_a").await;
2355 let client_b = server.create_client(cx_b, "user_b").await;
2356 server
2357 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2358 .await;
2359 let active_call_a = cx_a.read(ActiveCall::global);
2360 let active_call_b = cx_b.read(ActiveCall::global);
2361
2362 cx_a.update(editor::init);
2363 cx_b.update(editor::init);
2364
2365 cx_a.update(|cx| {
2366 SettingsStore::update_global(cx, |store, cx| {
2367 store.update_user_settings(cx, |settings| {
2368 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None);
2369 });
2370 });
2371 });
2372 cx_b.update(|cx| {
2373 SettingsStore::update_global(cx, |store, cx| {
2374 store.update_user_settings(cx, |settings| {
2375 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2376 });
2377 });
2378 });
2379
2380 let capabilities = lsp::ServerCapabilities {
2381 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
2382 ..lsp::ServerCapabilities::default()
2383 };
2384 client_a.language_registry().add(rust_lang());
2385 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2386 "Rust",
2387 FakeLspAdapter {
2388 capabilities: capabilities.clone(),
2389 ..FakeLspAdapter::default()
2390 },
2391 );
2392 client_b.language_registry().add(rust_lang());
2393 client_b.language_registry().register_fake_lsp_adapter(
2394 "Rust",
2395 FakeLspAdapter {
2396 capabilities,
2397 ..FakeLspAdapter::default()
2398 },
2399 );
2400
2401 // Client A opens a project.
2402 client_a
2403 .fs()
2404 .insert_tree(
2405 path!("/a"),
2406 json!({
2407 "main.rs": "fn main() { a }",
2408 }),
2409 )
2410 .await;
2411 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2412 active_call_a
2413 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2414 .await
2415 .unwrap();
2416 let project_id = active_call_a
2417 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2418 .await
2419 .unwrap();
2420
2421 // Client B joins the project
2422 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2423 active_call_b
2424 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2425 .await
2426 .unwrap();
2427
2428 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2429
2430 // The host opens a rust file.
2431 let _buffer_a = project_a
2432 .update(cx_a, |project, cx| {
2433 project.open_local_buffer(path!("/a/main.rs"), cx)
2434 })
2435 .await
2436 .unwrap();
2437 let editor_a = workspace_a
2438 .update_in(cx_a, |workspace, window, cx| {
2439 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2440 })
2441 .await
2442 .unwrap()
2443 .downcast::<Editor>()
2444 .unwrap();
2445
2446 let fake_language_server = fake_language_servers.next().await.unwrap();
2447 cx_a.run_until_parked();
2448 cx_b.run_until_parked();
2449
2450 let requests_made = Arc::new(AtomicUsize::new(0));
2451 let closure_requests_made = Arc::clone(&requests_made);
2452 let mut color_request_handle = fake_language_server
2453 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
2454 let requests_made = Arc::clone(&closure_requests_made);
2455 async move {
2456 assert_eq!(
2457 params.text_document.uri,
2458 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2459 );
2460 requests_made.fetch_add(1, atomic::Ordering::Release);
2461 Ok(vec![lsp::ColorInformation {
2462 range: lsp::Range {
2463 start: lsp::Position {
2464 line: 0,
2465 character: 0,
2466 },
2467 end: lsp::Position {
2468 line: 0,
2469 character: 1,
2470 },
2471 },
2472 color: lsp::Color {
2473 red: 0.33,
2474 green: 0.33,
2475 blue: 0.33,
2476 alpha: 0.33,
2477 },
2478 }])
2479 }
2480 });
2481 executor.run_until_parked();
2482
2483 assert_eq!(
2484 0,
2485 requests_made.load(atomic::Ordering::Acquire),
2486 "Host did not enable document colors, hence should query for none"
2487 );
2488 editor_a.update(cx_a, |editor, cx| {
2489 assert_eq!(
2490 Vec::<Rgba>::new(),
2491 extract_color_inlays(editor, cx),
2492 "No query colors should result in no hints"
2493 );
2494 });
2495
2496 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2497 let editor_b = workspace_b
2498 .update_in(cx_b, |workspace, window, cx| {
2499 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2500 })
2501 .await
2502 .unwrap()
2503 .downcast::<Editor>()
2504 .unwrap();
2505
2506 color_request_handle.next().await.unwrap();
2507 executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
2508 executor.run_until_parked();
2509
2510 assert_eq!(
2511 1,
2512 requests_made.load(atomic::Ordering::Acquire),
2513 "The client opened the file and got its first colors back"
2514 );
2515 editor_b.update(cx_b, |editor, cx| {
2516 assert_eq!(
2517 vec![expected_color],
2518 extract_color_inlays(editor, cx),
2519 "With document colors as inlays, color inlays should be pushed"
2520 );
2521 });
2522
2523 editor_a.update_in(cx_a, |editor, window, cx| {
2524 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2525 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
2526 });
2527 editor.handle_input(":", window, cx);
2528 });
2529 color_request_handle.next().await.unwrap();
2530 executor.run_until_parked();
2531 assert_eq!(
2532 2,
2533 requests_made.load(atomic::Ordering::Acquire),
2534 "After the host edits his file, the client should request the colors again"
2535 );
2536 editor_a.update(cx_a, |editor, cx| {
2537 assert_eq!(
2538 Vec::<Rgba>::new(),
2539 extract_color_inlays(editor, cx),
2540 "Host has no colors still"
2541 );
2542 });
2543 editor_b.update(cx_b, |editor, cx| {
2544 assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
2545 });
2546
2547 cx_b.update(|_, cx| {
2548 SettingsStore::update_global(cx, |store, cx| {
2549 store.update_user_settings(cx, |settings| {
2550 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
2551 });
2552 });
2553 });
2554 executor.run_until_parked();
2555 assert_eq!(
2556 2,
2557 requests_made.load(atomic::Ordering::Acquire),
2558 "After the client have changed the colors settings, no extra queries should happen"
2559 );
2560 editor_a.update(cx_a, |editor, cx| {
2561 assert_eq!(
2562 Vec::<Rgba>::new(),
2563 extract_color_inlays(editor, cx),
2564 "Host is unaffected by the client's settings changes"
2565 );
2566 });
2567 editor_b.update(cx_b, |editor, cx| {
2568 assert_eq!(
2569 Vec::<Rgba>::new(),
2570 extract_color_inlays(editor, cx),
2571 "Client should have no colors hints, as in the settings"
2572 );
2573 });
2574
2575 cx_b.update(|_, cx| {
2576 SettingsStore::update_global(cx, |store, cx| {
2577 store.update_user_settings(cx, |settings| {
2578 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2579 });
2580 });
2581 });
2582 executor.run_until_parked();
2583 assert_eq!(
2584 2,
2585 requests_made.load(atomic::Ordering::Acquire),
2586 "After falling back to colors as inlays, no extra LSP queries are made"
2587 );
2588 editor_a.update(cx_a, |editor, cx| {
2589 assert_eq!(
2590 Vec::<Rgba>::new(),
2591 extract_color_inlays(editor, cx),
2592 "Host is unaffected by the client's settings changes, again"
2593 );
2594 });
2595 editor_b.update(cx_b, |editor, cx| {
2596 assert_eq!(
2597 vec![expected_color],
2598 extract_color_inlays(editor, cx),
2599 "Client should have its color hints back"
2600 );
2601 });
2602
2603 cx_a.update(|_, cx| {
2604 SettingsStore::update_global(cx, |store, cx| {
2605 store.update_user_settings(cx, |settings| {
2606 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
2607 });
2608 });
2609 });
2610 color_request_handle.next().await.unwrap();
2611 executor.run_until_parked();
2612 assert_eq!(
2613 3,
2614 requests_made.load(atomic::Ordering::Acquire),
2615 "After the host enables document colors, another LSP query should be made"
2616 );
2617 editor_a.update(cx_a, |editor, cx| {
2618 assert_eq!(
2619 Vec::<Rgba>::new(),
2620 extract_color_inlays(editor, cx),
2621 "Host did not configure document colors as hints hence gets nothing"
2622 );
2623 });
2624 editor_b.update(cx_b, |editor, cx| {
2625 assert_eq!(
2626 vec![expected_color],
2627 extract_color_inlays(editor, cx),
2628 "Client should be unaffected by the host's settings changes"
2629 );
2630 });
2631}
2632
2633async fn test_lsp_pull_diagnostics(
2634 should_stream_workspace_diagnostic: bool,
2635 cx_a: &mut TestAppContext,
2636 cx_b: &mut TestAppContext,
2637) {
2638 let mut server = TestServer::start(cx_a.executor()).await;
2639 let executor = cx_a.executor();
2640 let client_a = server.create_client(cx_a, "user_a").await;
2641 let client_b = server.create_client(cx_b, "user_b").await;
2642 server
2643 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2644 .await;
2645 let active_call_a = cx_a.read(ActiveCall::global);
2646 let active_call_b = cx_b.read(ActiveCall::global);
2647
2648 cx_a.update(editor::init);
2649 cx_b.update(editor::init);
2650
2651 let expected_push_diagnostic_main_message = "pushed main diagnostic";
2652 let expected_push_diagnostic_lib_message = "pushed lib diagnostic";
2653 let expected_pull_diagnostic_main_message = "pulled main diagnostic";
2654 let expected_pull_diagnostic_lib_message = "pulled lib diagnostic";
2655 let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic";
2656 let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic";
2657
2658 let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<Option<String>>::new()));
2659 let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<String>::new()));
2660 let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2661 let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone();
2662 let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone();
2663 let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2664 let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone();
2665 let closure_workspace_diagnostics_pulls_result_ids =
2666 workspace_diagnostics_pulls_result_ids.clone();
2667 let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) =
2668 smol::channel::bounded::<()>(1);
2669 let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
2670 smol::channel::bounded::<()>(1);
2671
2672 let capabilities = lsp::ServerCapabilities {
2673 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
2674 lsp::DiagnosticOptions {
2675 identifier: Some("test-pulls".to_string()),
2676 inter_file_dependencies: true,
2677 workspace_diagnostics: true,
2678 work_done_progress_options: lsp::WorkDoneProgressOptions {
2679 work_done_progress: None,
2680 },
2681 },
2682 )),
2683 ..lsp::ServerCapabilities::default()
2684 };
2685 client_a.language_registry().add(rust_lang());
2686
2687 let pull_diagnostics_handle = Arc::new(parking_lot::Mutex::new(None));
2688 let workspace_diagnostics_pulls_handle = Arc::new(parking_lot::Mutex::new(None));
2689
2690 let closure_pull_diagnostics_handle = pull_diagnostics_handle.clone();
2691 let closure_workspace_diagnostics_pulls_handle = workspace_diagnostics_pulls_handle.clone();
2692 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2693 "Rust",
2694 FakeLspAdapter {
2695 capabilities: capabilities.clone(),
2696 initializer: Some(Box::new(move |fake_language_server| {
2697 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2698 "workspace/diagnostic/{}/1",
2699 fake_language_server.server.server_id()
2700 ));
2701 let closure_workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone();
2702 let diagnostics_pulls_made = closure_diagnostics_pulls_made.clone();
2703 let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
2704 let closure_pull_diagnostics_handle = closure_pull_diagnostics_handle.clone();
2705 let closure_workspace_diagnostics_pulls_handle = closure_workspace_diagnostics_pulls_handle.clone();
2706 let closure_workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2707 let closure_workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2708 let pull_diagnostics_handle = fake_language_server
2709 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(
2710 move |params, _| {
2711 let requests_made = diagnostics_pulls_made.clone();
2712 let diagnostics_pulls_result_ids =
2713 diagnostics_pulls_result_ids.clone();
2714 async move {
2715 let message = if lsp::Uri::from_file_path(path!("/a/main.rs"))
2716 .unwrap()
2717 == params.text_document.uri
2718 {
2719 expected_pull_diagnostic_main_message.to_string()
2720 } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2721 == params.text_document.uri
2722 {
2723 expected_pull_diagnostic_lib_message.to_string()
2724 } else {
2725 panic!("Unexpected document: {}", params.text_document.uri)
2726 };
2727 {
2728 diagnostics_pulls_result_ids
2729 .lock()
2730 .await
2731 .insert(params.previous_result_id);
2732 }
2733 let new_requests_count =
2734 requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2735 Ok(lsp::DocumentDiagnosticReportResult::Report(
2736 lsp::DocumentDiagnosticReport::Full(
2737 lsp::RelatedFullDocumentDiagnosticReport {
2738 related_documents: None,
2739 full_document_diagnostic_report:
2740 lsp::FullDocumentDiagnosticReport {
2741 result_id: Some(format!(
2742 "pull-{new_requests_count}"
2743 )),
2744 items: vec![lsp::Diagnostic {
2745 range: lsp::Range {
2746 start: lsp::Position {
2747 line: 0,
2748 character: 0,
2749 },
2750 end: lsp::Position {
2751 line: 0,
2752 character: 2,
2753 },
2754 },
2755 severity: Some(
2756 lsp::DiagnosticSeverity::ERROR,
2757 ),
2758 message,
2759 ..lsp::Diagnostic::default()
2760 }],
2761 },
2762 },
2763 ),
2764 ))
2765 }
2766 },
2767 );
2768 let _ = closure_pull_diagnostics_handle.lock().insert(pull_diagnostics_handle);
2769
2770 let closure_workspace_diagnostics_pulls_made = closure_workspace_diagnostics_pulls_made.clone();
2771 let workspace_diagnostics_pulls_handle = fake_language_server.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
2772 move |params, _| {
2773 let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone();
2774 let workspace_diagnostics_pulls_result_ids =
2775 closure_workspace_diagnostics_pulls_result_ids.clone();
2776 let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2777 let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2778 let expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
2779 async move {
2780 let workspace_request_count =
2781 workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2782 {
2783 workspace_diagnostics_pulls_result_ids
2784 .lock()
2785 .await
2786 .extend(params.previous_result_ids.into_iter().map(|id| id.value));
2787 }
2788 if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed()
2789 {
2790 assert_eq!(
2791 params.partial_result_params.partial_result_token,
2792 Some(expected_workspace_diagnostic_token)
2793 );
2794 workspace_diagnostic_received_tx.send(()).await.unwrap();
2795 workspace_diagnostic_cancel_rx.recv().await.unwrap();
2796 workspace_diagnostic_cancel_rx.close();
2797 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
2798 // > The final response has to be empty in terms of result values.
2799 return Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2800 lsp::WorkspaceDiagnosticReport { items: Vec::new() },
2801 ));
2802 }
2803 Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2804 lsp::WorkspaceDiagnosticReport {
2805 items: vec![
2806 lsp::WorkspaceDocumentDiagnosticReport::Full(
2807 lsp::WorkspaceFullDocumentDiagnosticReport {
2808 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2809 version: None,
2810 full_document_diagnostic_report:
2811 lsp::FullDocumentDiagnosticReport {
2812 result_id: Some(format!(
2813 "workspace_{workspace_request_count}"
2814 )),
2815 items: vec![lsp::Diagnostic {
2816 range: lsp::Range {
2817 start: lsp::Position {
2818 line: 0,
2819 character: 1,
2820 },
2821 end: lsp::Position {
2822 line: 0,
2823 character: 3,
2824 },
2825 },
2826 severity: Some(lsp::DiagnosticSeverity::WARNING),
2827 message:
2828 expected_workspace_pull_diagnostics_main_message
2829 .to_string(),
2830 ..lsp::Diagnostic::default()
2831 }],
2832 },
2833 },
2834 ),
2835 lsp::WorkspaceDocumentDiagnosticReport::Full(
2836 lsp::WorkspaceFullDocumentDiagnosticReport {
2837 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
2838 version: None,
2839 full_document_diagnostic_report:
2840 lsp::FullDocumentDiagnosticReport {
2841 result_id: Some(format!(
2842 "workspace_{workspace_request_count}"
2843 )),
2844 items: vec![lsp::Diagnostic {
2845 range: lsp::Range {
2846 start: lsp::Position {
2847 line: 0,
2848 character: 1,
2849 },
2850 end: lsp::Position {
2851 line: 0,
2852 character: 3,
2853 },
2854 },
2855 severity: Some(lsp::DiagnosticSeverity::WARNING),
2856 message:
2857 expected_workspace_pull_diagnostics_lib_message
2858 .to_string(),
2859 ..lsp::Diagnostic::default()
2860 }],
2861 },
2862 },
2863 ),
2864 ],
2865 },
2866 ))
2867 }
2868 });
2869 let _ = closure_workspace_diagnostics_pulls_handle.lock().insert(workspace_diagnostics_pulls_handle);
2870 })),
2871 ..FakeLspAdapter::default()
2872 },
2873 );
2874
2875 client_b.language_registry().add(rust_lang());
2876 client_b.language_registry().register_fake_lsp_adapter(
2877 "Rust",
2878 FakeLspAdapter {
2879 capabilities,
2880 ..FakeLspAdapter::default()
2881 },
2882 );
2883
2884 // Client A opens a project.
2885 client_a
2886 .fs()
2887 .insert_tree(
2888 path!("/a"),
2889 json!({
2890 "main.rs": "fn main() { a }",
2891 "lib.rs": "fn other() {}",
2892 }),
2893 )
2894 .await;
2895 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2896 active_call_a
2897 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2898 .await
2899 .unwrap();
2900 let project_id = active_call_a
2901 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2902 .await
2903 .unwrap();
2904
2905 // Client B joins the project
2906 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2907 active_call_b
2908 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2909 .await
2910 .unwrap();
2911
2912 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2913 executor.start_waiting();
2914
2915 // The host opens a rust file.
2916 let _buffer_a = project_a
2917 .update(cx_a, |project, cx| {
2918 project.open_local_buffer(path!("/a/main.rs"), cx)
2919 })
2920 .await
2921 .unwrap();
2922 let editor_a_main = workspace_a
2923 .update_in(cx_a, |workspace, window, cx| {
2924 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2925 })
2926 .await
2927 .unwrap()
2928 .downcast::<Editor>()
2929 .unwrap();
2930
2931 let fake_language_server = fake_language_servers.next().await.unwrap();
2932 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2933 "workspace/diagnostic-{}-1",
2934 fake_language_server.server.server_id()
2935 ));
2936 cx_a.run_until_parked();
2937 cx_b.run_until_parked();
2938 let mut pull_diagnostics_handle = pull_diagnostics_handle.lock().take().unwrap();
2939 let mut workspace_diagnostics_pulls_handle =
2940 workspace_diagnostics_pulls_handle.lock().take().unwrap();
2941
2942 if should_stream_workspace_diagnostic {
2943 workspace_diagnostic_received_rx.recv().await.unwrap();
2944 } else {
2945 workspace_diagnostics_pulls_handle.next().await.unwrap();
2946 }
2947 assert_eq!(
2948 1,
2949 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
2950 "Workspace diagnostics should be pulled initially on a server startup"
2951 );
2952 pull_diagnostics_handle.next().await.unwrap();
2953 assert_eq!(
2954 1,
2955 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
2956 "Host should query pull diagnostics when the editor is opened"
2957 );
2958 executor.run_until_parked();
2959 editor_a_main.update(cx_a, |editor, cx| {
2960 let snapshot = editor.buffer().read(cx).snapshot(cx);
2961 let all_diagnostics = snapshot
2962 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
2963 .collect::<Vec<_>>();
2964 assert_eq!(
2965 all_diagnostics.len(),
2966 1,
2967 "Expected single diagnostic, but got: {all_diagnostics:?}"
2968 );
2969 let diagnostic = &all_diagnostics[0];
2970 let mut expected_messages = vec![expected_pull_diagnostic_main_message];
2971 if !should_stream_workspace_diagnostic {
2972 expected_messages.push(expected_workspace_pull_diagnostics_main_message);
2973 }
2974 assert!(
2975 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
2976 "Expected {expected_messages:?} on the host, but got: {}",
2977 diagnostic.diagnostic.message
2978 );
2979 });
2980
2981 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
2982 lsp::PublishDiagnosticsParams {
2983 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2984 diagnostics: vec![lsp::Diagnostic {
2985 range: lsp::Range {
2986 start: lsp::Position {
2987 line: 0,
2988 character: 3,
2989 },
2990 end: lsp::Position {
2991 line: 0,
2992 character: 4,
2993 },
2994 },
2995 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
2996 message: expected_push_diagnostic_main_message.to_string(),
2997 ..lsp::Diagnostic::default()
2998 }],
2999 version: None,
3000 },
3001 );
3002 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3003 lsp::PublishDiagnosticsParams {
3004 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3005 diagnostics: vec![lsp::Diagnostic {
3006 range: lsp::Range {
3007 start: lsp::Position {
3008 line: 0,
3009 character: 3,
3010 },
3011 end: lsp::Position {
3012 line: 0,
3013 character: 4,
3014 },
3015 },
3016 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
3017 message: expected_push_diagnostic_lib_message.to_string(),
3018 ..lsp::Diagnostic::default()
3019 }],
3020 version: None,
3021 },
3022 );
3023
3024 if should_stream_workspace_diagnostic {
3025 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3026 token: expected_workspace_diagnostic_token.clone(),
3027 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3028 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3029 items: vec![
3030 lsp::WorkspaceDocumentDiagnosticReport::Full(
3031 lsp::WorkspaceFullDocumentDiagnosticReport {
3032 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3033 version: None,
3034 full_document_diagnostic_report:
3035 lsp::FullDocumentDiagnosticReport {
3036 result_id: Some(format!(
3037 "workspace_{}",
3038 workspace_diagnostics_pulls_made
3039 .fetch_add(1, atomic::Ordering::Release)
3040 + 1
3041 )),
3042 items: vec![lsp::Diagnostic {
3043 range: lsp::Range {
3044 start: lsp::Position {
3045 line: 0,
3046 character: 1,
3047 },
3048 end: lsp::Position {
3049 line: 0,
3050 character: 2,
3051 },
3052 },
3053 severity: Some(lsp::DiagnosticSeverity::ERROR),
3054 message:
3055 expected_workspace_pull_diagnostics_main_message
3056 .to_string(),
3057 ..lsp::Diagnostic::default()
3058 }],
3059 },
3060 },
3061 ),
3062 lsp::WorkspaceDocumentDiagnosticReport::Full(
3063 lsp::WorkspaceFullDocumentDiagnosticReport {
3064 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3065 version: None,
3066 full_document_diagnostic_report:
3067 lsp::FullDocumentDiagnosticReport {
3068 result_id: Some(format!(
3069 "workspace_{}",
3070 workspace_diagnostics_pulls_made
3071 .fetch_add(1, atomic::Ordering::Release)
3072 + 1
3073 )),
3074 items: Vec::new(),
3075 },
3076 },
3077 ),
3078 ],
3079 }),
3080 ),
3081 });
3082 };
3083
3084 let mut workspace_diagnostic_start_count =
3085 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3086
3087 executor.run_until_parked();
3088 editor_a_main.update(cx_a, |editor, cx| {
3089 let snapshot = editor.buffer().read(cx).snapshot(cx);
3090 let all_diagnostics = snapshot
3091 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3092 .collect::<Vec<_>>();
3093 assert_eq!(
3094 all_diagnostics.len(),
3095 2,
3096 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3097 );
3098 let expected_messages = [
3099 expected_workspace_pull_diagnostics_main_message,
3100 expected_pull_diagnostic_main_message,
3101 expected_push_diagnostic_main_message,
3102 ];
3103 for diagnostic in all_diagnostics {
3104 assert!(
3105 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3106 "Expected push and pull messages on the host: {expected_messages:?}, but got: {}",
3107 diagnostic.diagnostic.message
3108 );
3109 }
3110 });
3111
3112 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3113 let editor_b_main = workspace_b
3114 .update_in(cx_b, |workspace, window, cx| {
3115 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
3116 })
3117 .await
3118 .unwrap()
3119 .downcast::<Editor>()
3120 .unwrap();
3121 cx_b.run_until_parked();
3122
3123 pull_diagnostics_handle.next().await.unwrap();
3124 assert_eq!(
3125 2,
3126 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3127 "Client should query pull diagnostics when its editor is opened"
3128 );
3129 executor.run_until_parked();
3130 assert_eq!(
3131 workspace_diagnostic_start_count,
3132 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3133 "Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull"
3134 );
3135 editor_b_main.update(cx_b, |editor, cx| {
3136 let snapshot = editor.buffer().read(cx).snapshot(cx);
3137 let all_diagnostics = snapshot
3138 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3139 .collect::<Vec<_>>();
3140 assert_eq!(
3141 all_diagnostics.len(),
3142 2,
3143 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3144 );
3145
3146 // Despite the workspace diagnostics not re-initialized for the remote client, we can still expect its message synced from the host.
3147 let expected_messages = [
3148 expected_workspace_pull_diagnostics_main_message,
3149 expected_pull_diagnostic_main_message,
3150 expected_push_diagnostic_main_message,
3151 ];
3152 for diagnostic in all_diagnostics {
3153 assert!(
3154 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3155 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3156 diagnostic.diagnostic.message
3157 );
3158 }
3159 });
3160
3161 let editor_b_lib = workspace_b
3162 .update_in(cx_b, |workspace, window, cx| {
3163 workspace.open_path((worktree_id, rel_path("lib.rs")), None, true, window, cx)
3164 })
3165 .await
3166 .unwrap()
3167 .downcast::<Editor>()
3168 .unwrap();
3169
3170 pull_diagnostics_handle.next().await.unwrap();
3171 assert_eq!(
3172 3,
3173 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3174 "Client should query pull diagnostics when its another editor is opened"
3175 );
3176 executor.run_until_parked();
3177 assert_eq!(
3178 workspace_diagnostic_start_count,
3179 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3180 "The remote client still did not anything to trigger the workspace diagnostics pull"
3181 );
3182 editor_b_lib.update(cx_b, |editor, cx| {
3183 let snapshot = editor.buffer().read(cx).snapshot(cx);
3184 let all_diagnostics = snapshot
3185 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3186 .collect::<Vec<_>>();
3187 let expected_messages = [
3188 expected_pull_diagnostic_lib_message,
3189 // TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
3190 // expected_push_diagnostic_lib_message,
3191 ];
3192 assert_eq!(
3193 all_diagnostics.len(),
3194 1,
3195 "Expected pull diagnostics, but got: {all_diagnostics:?}"
3196 );
3197 for diagnostic in all_diagnostics {
3198 assert!(
3199 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3200 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3201 diagnostic.diagnostic.message
3202 );
3203 }
3204 });
3205
3206 if should_stream_workspace_diagnostic {
3207 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3208 token: expected_workspace_diagnostic_token.clone(),
3209 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3210 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3211 items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
3212 lsp::WorkspaceFullDocumentDiagnosticReport {
3213 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3214 version: None,
3215 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
3216 result_id: Some(format!(
3217 "workspace_{}",
3218 workspace_diagnostics_pulls_made
3219 .fetch_add(1, atomic::Ordering::Release)
3220 + 1
3221 )),
3222 items: vec![lsp::Diagnostic {
3223 range: lsp::Range {
3224 start: lsp::Position {
3225 line: 0,
3226 character: 1,
3227 },
3228 end: lsp::Position {
3229 line: 0,
3230 character: 2,
3231 },
3232 },
3233 severity: Some(lsp::DiagnosticSeverity::ERROR),
3234 message: expected_workspace_pull_diagnostics_lib_message
3235 .to_string(),
3236 ..lsp::Diagnostic::default()
3237 }],
3238 },
3239 },
3240 )],
3241 }),
3242 ),
3243 });
3244 workspace_diagnostic_start_count =
3245 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3246 workspace_diagnostic_cancel_tx.send(()).await.unwrap();
3247 workspace_diagnostics_pulls_handle.next().await.unwrap();
3248 executor.run_until_parked();
3249 editor_b_lib.update(cx_b, |editor, cx| {
3250 let snapshot = editor.buffer().read(cx).snapshot(cx);
3251 let all_diagnostics = snapshot
3252 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3253 .collect::<Vec<_>>();
3254 let expected_messages = [
3255 expected_workspace_pull_diagnostics_lib_message,
3256 // TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
3257 // expected_push_diagnostic_lib_message,
3258 ];
3259 assert_eq!(
3260 all_diagnostics.len(),
3261 1,
3262 "Expected pull diagnostics, but got: {all_diagnostics:?}"
3263 );
3264 for diagnostic in all_diagnostics {
3265 assert!(
3266 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3267 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3268 diagnostic.diagnostic.message
3269 );
3270 }
3271 });
3272 };
3273
3274 {
3275 assert!(
3276 !diagnostics_pulls_result_ids.lock().await.is_empty(),
3277 "Initial diagnostics pulls should report None at least"
3278 );
3279 assert_eq!(
3280 0,
3281 workspace_diagnostics_pulls_result_ids
3282 .lock()
3283 .await
3284 .deref()
3285 .len(),
3286 "After the initial workspace request, opening files should not reuse any result ids"
3287 );
3288 }
3289
3290 editor_b_lib.update_in(cx_b, |editor, window, cx| {
3291 editor.move_to_end(&MoveToEnd, window, cx);
3292 editor.handle_input(":", window, cx);
3293 });
3294 pull_diagnostics_handle.next().await.unwrap();
3295 assert_eq!(
3296 4,
3297 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3298 "Client lib.rs edits should trigger another diagnostics pull for a buffer"
3299 );
3300 workspace_diagnostics_pulls_handle.next().await.unwrap();
3301 assert_eq!(
3302 workspace_diagnostic_start_count + 1,
3303 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3304 "After client lib.rs edits, the workspace diagnostics request should follow"
3305 );
3306 executor.run_until_parked();
3307
3308 editor_b_main.update_in(cx_b, |editor, window, cx| {
3309 editor.move_to_end(&MoveToEnd, window, cx);
3310 editor.handle_input(":", window, cx);
3311 });
3312 pull_diagnostics_handle.next().await.unwrap();
3313 pull_diagnostics_handle.next().await.unwrap();
3314 assert_eq!(
3315 6,
3316 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3317 "Client main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
3318 );
3319 workspace_diagnostics_pulls_handle.next().await.unwrap();
3320 assert_eq!(
3321 workspace_diagnostic_start_count + 2,
3322 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3323 "After client main.rs edits, the workspace diagnostics pull should follow"
3324 );
3325 executor.run_until_parked();
3326
3327 editor_a_main.update_in(cx_a, |editor, window, cx| {
3328 editor.move_to_end(&MoveToEnd, window, cx);
3329 editor.handle_input(":", window, cx);
3330 });
3331 pull_diagnostics_handle.next().await.unwrap();
3332 pull_diagnostics_handle.next().await.unwrap();
3333 assert_eq!(
3334 8,
3335 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3336 "Host main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
3337 );
3338 workspace_diagnostics_pulls_handle.next().await.unwrap();
3339 assert_eq!(
3340 workspace_diagnostic_start_count + 3,
3341 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3342 "After host main.rs edits, the workspace diagnostics pull should follow"
3343 );
3344 executor.run_until_parked();
3345 let diagnostic_pulls_result_ids = diagnostics_pulls_result_ids.lock().await.len();
3346 let workspace_pulls_result_ids = workspace_diagnostics_pulls_result_ids.lock().await.len();
3347 {
3348 assert!(
3349 diagnostic_pulls_result_ids > 1,
3350 "Should have sent result ids when pulling diagnostics"
3351 );
3352 assert!(
3353 workspace_pulls_result_ids > 1,
3354 "Should have sent result ids when pulling workspace diagnostics"
3355 );
3356 }
3357
3358 fake_language_server
3359 .request::<lsp::request::WorkspaceDiagnosticRefresh>(())
3360 .await
3361 .into_response()
3362 .expect("workspace diagnostics refresh request failed");
3363 assert_eq!(
3364 8,
3365 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3366 "No single file pulls should happen after the diagnostics refresh server request"
3367 );
3368 workspace_diagnostics_pulls_handle.next().await.unwrap();
3369 assert_eq!(
3370 workspace_diagnostic_start_count + 4,
3371 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3372 "Another workspace diagnostics pull should happen after the diagnostics refresh server request"
3373 );
3374 {
3375 assert!(
3376 diagnostics_pulls_result_ids.lock().await.len() == diagnostic_pulls_result_ids,
3377 "Pulls should not happen hence no extra ids should appear"
3378 );
3379 assert!(
3380 workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
3381 "More workspace diagnostics should be pulled"
3382 );
3383 }
3384 editor_b_lib.update(cx_b, |editor, cx| {
3385 let snapshot = editor.buffer().read(cx).snapshot(cx);
3386 let all_diagnostics = snapshot
3387 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3388 .collect::<Vec<_>>();
3389 let expected_messages = [
3390 expected_workspace_pull_diagnostics_lib_message,
3391 expected_pull_diagnostic_lib_message,
3392 expected_push_diagnostic_lib_message,
3393 ];
3394 assert_eq!(all_diagnostics.len(), 1);
3395 for diagnostic in &all_diagnostics {
3396 assert!(
3397 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3398 "Unexpected diagnostics: {all_diagnostics:?}"
3399 );
3400 }
3401 });
3402 editor_b_main.update(cx_b, |editor, cx| {
3403 let snapshot = editor.buffer().read(cx).snapshot(cx);
3404 let all_diagnostics = snapshot
3405 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3406 .collect::<Vec<_>>();
3407 assert_eq!(all_diagnostics.len(), 2);
3408
3409 let expected_messages = [
3410 expected_workspace_pull_diagnostics_main_message,
3411 expected_pull_diagnostic_main_message,
3412 expected_push_diagnostic_main_message,
3413 ];
3414 for diagnostic in &all_diagnostics {
3415 assert!(
3416 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3417 "Unexpected diagnostics: {all_diagnostics:?}"
3418 );
3419 }
3420 });
3421 editor_a_main.update(cx_a, |editor, cx| {
3422 let snapshot = editor.buffer().read(cx).snapshot(cx);
3423 let all_diagnostics = snapshot
3424 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3425 .collect::<Vec<_>>();
3426 assert_eq!(all_diagnostics.len(), 2);
3427 let expected_messages = [
3428 expected_workspace_pull_diagnostics_main_message,
3429 expected_pull_diagnostic_main_message,
3430 expected_push_diagnostic_main_message,
3431 ];
3432 for diagnostic in &all_diagnostics {
3433 assert!(
3434 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3435 "Unexpected diagnostics: {all_diagnostics:?}"
3436 );
3437 }
3438 });
3439}
3440
3441#[gpui::test(iterations = 10)]
3442async fn test_non_streamed_lsp_pull_diagnostics(
3443 cx_a: &mut TestAppContext,
3444 cx_b: &mut TestAppContext,
3445) {
3446 test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
3447}
3448
3449#[gpui::test(iterations = 10)]
3450async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3451 test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
3452}
3453
3454#[gpui::test(iterations = 10)]
3455async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3456 let mut server = TestServer::start(cx_a.executor()).await;
3457 let client_a = server.create_client(cx_a, "user_a").await;
3458 let client_b = server.create_client(cx_b, "user_b").await;
3459 server
3460 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3461 .await;
3462 let active_call_a = cx_a.read(ActiveCall::global);
3463
3464 cx_a.update(editor::init);
3465 cx_b.update(editor::init);
3466 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
3467 let inline_blame_off_settings = Some(InlineBlameSettings {
3468 enabled: Some(false),
3469 ..Default::default()
3470 });
3471 cx_a.update(|cx| {
3472 SettingsStore::update_global(cx, |store, cx| {
3473 store.update_user_settings(cx, |settings| {
3474 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3475 });
3476 });
3477 });
3478 cx_b.update(|cx| {
3479 SettingsStore::update_global(cx, |store, cx| {
3480 store.update_user_settings(cx, |settings| {
3481 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3482 });
3483 });
3484 });
3485
3486 client_a
3487 .fs()
3488 .insert_tree(
3489 path!("/my-repo"),
3490 json!({
3491 ".git": {},
3492 "file.txt": "line1\nline2\nline3\nline\n",
3493 }),
3494 )
3495 .await;
3496
3497 let blame = git::blame::Blame {
3498 entries: vec![
3499 blame_entry("1b1b1b", 0..1),
3500 blame_entry("0d0d0d", 1..2),
3501 blame_entry("3a3a3a", 2..3),
3502 blame_entry("4c4c4c", 3..4),
3503 ],
3504 messages: [
3505 ("1b1b1b", "message for idx-0"),
3506 ("0d0d0d", "message for idx-1"),
3507 ("3a3a3a", "message for idx-2"),
3508 ("4c4c4c", "message for idx-3"),
3509 ]
3510 .into_iter()
3511 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3512 .collect(),
3513 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
3514 };
3515 client_a.fs().set_blame_for_repo(
3516 Path::new(path!("/my-repo/.git")),
3517 vec![(repo_path("file.txt"), blame)],
3518 );
3519
3520 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3521 let project_id = active_call_a
3522 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3523 .await
3524 .unwrap();
3525
3526 // Create editor_a
3527 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3528 let editor_a = workspace_a
3529 .update_in(cx_a, |workspace, window, cx| {
3530 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3531 })
3532 .await
3533 .unwrap()
3534 .downcast::<Editor>()
3535 .unwrap();
3536
3537 // Join the project as client B.
3538 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3539 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3540 let editor_b = workspace_b
3541 .update_in(cx_b, |workspace, window, cx| {
3542 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3543 })
3544 .await
3545 .unwrap()
3546 .downcast::<Editor>()
3547 .unwrap();
3548 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3549 editor_b
3550 .buffer()
3551 .read(cx)
3552 .as_singleton()
3553 .unwrap()
3554 .read(cx)
3555 .remote_id()
3556 });
3557
3558 // client_b now requests git blame for the open buffer
3559 editor_b.update_in(cx_b, |editor_b, window, cx| {
3560 assert!(editor_b.blame().is_none());
3561 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3562 });
3563
3564 cx_a.executor().run_until_parked();
3565 cx_b.executor().run_until_parked();
3566
3567 editor_b.update(cx_b, |editor_b, cx| {
3568 let blame = editor_b.blame().expect("editor_b should have blame now");
3569 let entries = blame.update(cx, |blame, cx| {
3570 blame
3571 .blame_for_rows(
3572 &(0..4)
3573 .map(|row| RowInfo {
3574 buffer_row: Some(row),
3575 buffer_id: Some(buffer_id_b),
3576 ..Default::default()
3577 })
3578 .collect::<Vec<_>>(),
3579 cx,
3580 )
3581 .collect::<Vec<_>>()
3582 });
3583
3584 assert_eq!(
3585 entries,
3586 vec![
3587 Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
3588 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3589 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3590 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3591 ]
3592 );
3593
3594 blame.update(cx, |blame, _| {
3595 for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
3596 let details = blame.details_for_entry(*buffer, entry).unwrap();
3597 assert_eq!(details.message, format!("message for idx-{}", idx));
3598 assert_eq!(
3599 details.permalink.unwrap().to_string(),
3600 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
3601 );
3602 }
3603 });
3604 });
3605
3606 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3607 // which gets back to client_b.
3608 editor_b.update_in(cx_b, |editor_b, _, cx| {
3609 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3610 });
3611
3612 cx_a.executor().run_until_parked();
3613 cx_b.executor().run_until_parked();
3614
3615 editor_b.update(cx_b, |editor_b, cx| {
3616 let blame = editor_b.blame().expect("editor_b should have blame now");
3617 let entries = blame.update(cx, |blame, cx| {
3618 blame
3619 .blame_for_rows(
3620 &(0..4)
3621 .map(|row| RowInfo {
3622 buffer_row: Some(row),
3623 buffer_id: Some(buffer_id_b),
3624 ..Default::default()
3625 })
3626 .collect::<Vec<_>>(),
3627 cx,
3628 )
3629 .collect::<Vec<_>>()
3630 });
3631
3632 assert_eq!(
3633 entries,
3634 vec![
3635 None,
3636 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3637 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3638 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3639 ]
3640 );
3641 });
3642
3643 // Now editor_a also updates the file
3644 editor_a.update_in(cx_a, |editor_a, _, cx| {
3645 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3646 });
3647
3648 cx_a.executor().run_until_parked();
3649 cx_b.executor().run_until_parked();
3650
3651 editor_b.update(cx_b, |editor_b, cx| {
3652 let blame = editor_b.blame().expect("editor_b should have blame now");
3653 let entries = blame.update(cx, |blame, cx| {
3654 blame
3655 .blame_for_rows(
3656 &(0..4)
3657 .map(|row| RowInfo {
3658 buffer_row: Some(row),
3659 buffer_id: Some(buffer_id_b),
3660 ..Default::default()
3661 })
3662 .collect::<Vec<_>>(),
3663 cx,
3664 )
3665 .collect::<Vec<_>>()
3666 });
3667
3668 assert_eq!(
3669 entries,
3670 vec![
3671 None,
3672 None,
3673 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3674 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3675 ]
3676 );
3677 });
3678}
3679
3680#[gpui::test(iterations = 30)]
3681async fn test_collaborating_with_editorconfig(
3682 cx_a: &mut TestAppContext,
3683 cx_b: &mut TestAppContext,
3684) {
3685 let mut server = TestServer::start(cx_a.executor()).await;
3686 let client_a = server.create_client(cx_a, "user_a").await;
3687 let client_b = server.create_client(cx_b, "user_b").await;
3688 server
3689 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3690 .await;
3691 let active_call_a = cx_a.read(ActiveCall::global);
3692
3693 cx_b.update(editor::init);
3694
3695 // Set up a fake language server.
3696 client_a.language_registry().add(rust_lang());
3697 client_a
3698 .fs()
3699 .insert_tree(
3700 path!("/a"),
3701 json!({
3702 "src": {
3703 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3704 "other_mod": {
3705 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3706 ".editorconfig": "",
3707 },
3708 },
3709 ".editorconfig": "[*]\ntab_width = 2\n",
3710 }),
3711 )
3712 .await;
3713 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3714 let project_id = active_call_a
3715 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3716 .await
3717 .unwrap();
3718 let main_buffer_a = project_a
3719 .update(cx_a, |p, cx| {
3720 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3721 })
3722 .await
3723 .unwrap();
3724 let other_buffer_a = project_a
3725 .update(cx_a, |p, cx| {
3726 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3727 })
3728 .await
3729 .unwrap();
3730 let cx_a = cx_a.add_empty_window();
3731 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3732 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3733 });
3734 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3735 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3736 });
3737 let mut main_editor_cx_a = EditorTestContext {
3738 cx: cx_a.clone(),
3739 window: cx_a.window_handle(),
3740 editor: main_editor_a,
3741 assertion_cx: AssertionContextManager::new(),
3742 };
3743 let mut other_editor_cx_a = EditorTestContext {
3744 cx: cx_a.clone(),
3745 window: cx_a.window_handle(),
3746 editor: other_editor_a,
3747 assertion_cx: AssertionContextManager::new(),
3748 };
3749
3750 // Join the project as client B.
3751 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3752 let main_buffer_b = project_b
3753 .update(cx_b, |p, cx| {
3754 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3755 })
3756 .await
3757 .unwrap();
3758 let other_buffer_b = project_b
3759 .update(cx_b, |p, cx| {
3760 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3761 })
3762 .await
3763 .unwrap();
3764 let cx_b = cx_b.add_empty_window();
3765 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3766 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3767 });
3768 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3769 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3770 });
3771 let mut main_editor_cx_b = EditorTestContext {
3772 cx: cx_b.clone(),
3773 window: cx_b.window_handle(),
3774 editor: main_editor_b,
3775 assertion_cx: AssertionContextManager::new(),
3776 };
3777 let mut other_editor_cx_b = EditorTestContext {
3778 cx: cx_b.clone(),
3779 window: cx_b.window_handle(),
3780 editor: other_editor_b,
3781 assertion_cx: AssertionContextManager::new(),
3782 };
3783
3784 let initial_main = indoc! {"
3785ˇmod other;
3786fn main() { let foo = other::foo(); }"};
3787 let initial_other = indoc! {"
3788ˇpub fn foo() -> usize {
3789 4
3790}"};
3791
3792 let first_tabbed_main = indoc! {"
3793 ˇmod other;
3794fn main() { let foo = other::foo(); }"};
3795 tab_undo_assert(
3796 &mut main_editor_cx_a,
3797 &mut main_editor_cx_b,
3798 initial_main,
3799 first_tabbed_main,
3800 true,
3801 );
3802 tab_undo_assert(
3803 &mut main_editor_cx_a,
3804 &mut main_editor_cx_b,
3805 initial_main,
3806 first_tabbed_main,
3807 false,
3808 );
3809
3810 let first_tabbed_other = indoc! {"
3811 ˇpub fn foo() -> usize {
3812 4
3813}"};
3814 tab_undo_assert(
3815 &mut other_editor_cx_a,
3816 &mut other_editor_cx_b,
3817 initial_other,
3818 first_tabbed_other,
3819 true,
3820 );
3821 tab_undo_assert(
3822 &mut other_editor_cx_a,
3823 &mut other_editor_cx_b,
3824 initial_other,
3825 first_tabbed_other,
3826 false,
3827 );
3828
3829 client_a
3830 .fs()
3831 .atomic_write(
3832 PathBuf::from(path!("/a/src/.editorconfig")),
3833 "[*]\ntab_width = 3\n".to_owned(),
3834 )
3835 .await
3836 .unwrap();
3837 cx_a.run_until_parked();
3838 cx_b.run_until_parked();
3839
3840 let second_tabbed_main = indoc! {"
3841 ˇmod other;
3842fn main() { let foo = other::foo(); }"};
3843 tab_undo_assert(
3844 &mut main_editor_cx_a,
3845 &mut main_editor_cx_b,
3846 initial_main,
3847 second_tabbed_main,
3848 true,
3849 );
3850 tab_undo_assert(
3851 &mut main_editor_cx_a,
3852 &mut main_editor_cx_b,
3853 initial_main,
3854 second_tabbed_main,
3855 false,
3856 );
3857
3858 let second_tabbed_other = indoc! {"
3859 ˇpub fn foo() -> usize {
3860 4
3861}"};
3862 tab_undo_assert(
3863 &mut other_editor_cx_a,
3864 &mut other_editor_cx_b,
3865 initial_other,
3866 second_tabbed_other,
3867 true,
3868 );
3869 tab_undo_assert(
3870 &mut other_editor_cx_a,
3871 &mut other_editor_cx_b,
3872 initial_other,
3873 second_tabbed_other,
3874 false,
3875 );
3876
3877 let editorconfig_buffer_b = project_b
3878 .update(cx_b, |p, cx| {
3879 p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
3880 })
3881 .await
3882 .unwrap();
3883 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3884 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3885 });
3886 project_b
3887 .update(cx_b, |project, cx| {
3888 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3889 })
3890 .await
3891 .unwrap();
3892 cx_a.run_until_parked();
3893 cx_b.run_until_parked();
3894
3895 tab_undo_assert(
3896 &mut main_editor_cx_a,
3897 &mut main_editor_cx_b,
3898 initial_main,
3899 second_tabbed_main,
3900 true,
3901 );
3902 tab_undo_assert(
3903 &mut main_editor_cx_a,
3904 &mut main_editor_cx_b,
3905 initial_main,
3906 second_tabbed_main,
3907 false,
3908 );
3909
3910 let third_tabbed_other = indoc! {"
3911 ˇpub fn foo() -> usize {
3912 4
3913}"};
3914 tab_undo_assert(
3915 &mut other_editor_cx_a,
3916 &mut other_editor_cx_b,
3917 initial_other,
3918 third_tabbed_other,
3919 true,
3920 );
3921
3922 tab_undo_assert(
3923 &mut other_editor_cx_a,
3924 &mut other_editor_cx_b,
3925 initial_other,
3926 third_tabbed_other,
3927 false,
3928 );
3929}
3930
3931#[gpui::test]
3932async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3933 let executor = cx_a.executor();
3934 let mut server = TestServer::start(executor.clone()).await;
3935 let client_a = server.create_client(cx_a, "user_a").await;
3936 let client_b = server.create_client(cx_b, "user_b").await;
3937 server
3938 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3939 .await;
3940 let active_call_a = cx_a.read(ActiveCall::global);
3941 let active_call_b = cx_b.read(ActiveCall::global);
3942 cx_a.update(editor::init);
3943 cx_b.update(editor::init);
3944 client_a
3945 .fs()
3946 .insert_tree(
3947 "/a",
3948 json!({
3949 "test.txt": "one\ntwo\nthree\nfour\nfive",
3950 }),
3951 )
3952 .await;
3953 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3954 let project_path = ProjectPath {
3955 worktree_id,
3956 path: rel_path(&"test.txt").into(),
3957 };
3958 let abs_path = project_a.read_with(cx_a, |project, cx| {
3959 project
3960 .absolute_path(&project_path, cx)
3961 .map(Arc::from)
3962 .unwrap()
3963 });
3964
3965 active_call_a
3966 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
3967 .await
3968 .unwrap();
3969 let project_id = active_call_a
3970 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3971 .await
3972 .unwrap();
3973 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3974 active_call_b
3975 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
3976 .await
3977 .unwrap();
3978 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3979 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3980
3981 // Client A opens an editor.
3982 let editor_a = workspace_a
3983 .update_in(cx_a, |workspace, window, cx| {
3984 workspace.open_path(project_path.clone(), None, true, window, cx)
3985 })
3986 .await
3987 .unwrap()
3988 .downcast::<Editor>()
3989 .unwrap();
3990
3991 // Client B opens same editor as A.
3992 let editor_b = workspace_b
3993 .update_in(cx_b, |workspace, window, cx| {
3994 workspace.open_path(project_path.clone(), None, true, window, cx)
3995 })
3996 .await
3997 .unwrap()
3998 .downcast::<Editor>()
3999 .unwrap();
4000
4001 cx_a.run_until_parked();
4002 cx_b.run_until_parked();
4003
4004 // Client A adds breakpoint on line (1)
4005 editor_a.update_in(cx_a, |editor, window, cx| {
4006 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4007 });
4008
4009 cx_a.run_until_parked();
4010 cx_b.run_until_parked();
4011
4012 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4013 editor
4014 .breakpoint_store()
4015 .unwrap()
4016 .read(cx)
4017 .all_source_breakpoints(cx)
4018 });
4019 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4020 editor
4021 .breakpoint_store()
4022 .unwrap()
4023 .read(cx)
4024 .all_source_breakpoints(cx)
4025 });
4026
4027 assert_eq!(1, breakpoints_a.len());
4028 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4029 assert_eq!(breakpoints_a, breakpoints_b);
4030
4031 // Client B adds breakpoint on line(2)
4032 editor_b.update_in(cx_b, |editor, window, cx| {
4033 editor.move_down(&editor::actions::MoveDown, window, cx);
4034 editor.move_down(&editor::actions::MoveDown, window, cx);
4035 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4036 });
4037
4038 cx_a.run_until_parked();
4039 cx_b.run_until_parked();
4040
4041 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4042 editor
4043 .breakpoint_store()
4044 .unwrap()
4045 .read(cx)
4046 .all_source_breakpoints(cx)
4047 });
4048 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4049 editor
4050 .breakpoint_store()
4051 .unwrap()
4052 .read(cx)
4053 .all_source_breakpoints(cx)
4054 });
4055
4056 assert_eq!(1, breakpoints_a.len());
4057 assert_eq!(breakpoints_a, breakpoints_b);
4058 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
4059
4060 // Client A removes last added breakpoint from client B
4061 editor_a.update_in(cx_a, |editor, window, cx| {
4062 editor.move_down(&editor::actions::MoveDown, window, cx);
4063 editor.move_down(&editor::actions::MoveDown, window, cx);
4064 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4065 });
4066
4067 cx_a.run_until_parked();
4068 cx_b.run_until_parked();
4069
4070 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4071 editor
4072 .breakpoint_store()
4073 .unwrap()
4074 .read(cx)
4075 .all_source_breakpoints(cx)
4076 });
4077 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4078 editor
4079 .breakpoint_store()
4080 .unwrap()
4081 .read(cx)
4082 .all_source_breakpoints(cx)
4083 });
4084
4085 assert_eq!(1, breakpoints_a.len());
4086 assert_eq!(breakpoints_a, breakpoints_b);
4087 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4088
4089 // Client B removes first added breakpoint by client A
4090 editor_b.update_in(cx_b, |editor, window, cx| {
4091 editor.move_up(&editor::actions::MoveUp, window, cx);
4092 editor.move_up(&editor::actions::MoveUp, window, cx);
4093 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4094 });
4095
4096 cx_a.run_until_parked();
4097 cx_b.run_until_parked();
4098
4099 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4100 editor
4101 .breakpoint_store()
4102 .unwrap()
4103 .read(cx)
4104 .all_source_breakpoints(cx)
4105 });
4106 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4107 editor
4108 .breakpoint_store()
4109 .unwrap()
4110 .read(cx)
4111 .all_source_breakpoints(cx)
4112 });
4113
4114 assert_eq!(0, breakpoints_a.len());
4115 assert_eq!(breakpoints_a, breakpoints_b);
4116}
4117
4118#[gpui::test]
4119async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4120 let mut server = TestServer::start(cx_a.executor()).await;
4121 let client_a = server.create_client(cx_a, "user_a").await;
4122 let client_b = server.create_client(cx_b, "user_b").await;
4123 server
4124 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4125 .await;
4126 let active_call_a = cx_a.read(ActiveCall::global);
4127 let active_call_b = cx_b.read(ActiveCall::global);
4128
4129 cx_a.update(editor::init);
4130 cx_b.update(editor::init);
4131
4132 client_a.language_registry().add(rust_lang());
4133 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4134 "Rust",
4135 FakeLspAdapter {
4136 name: "rust-analyzer",
4137 ..FakeLspAdapter::default()
4138 },
4139 );
4140 client_b.language_registry().add(rust_lang());
4141 client_b.language_registry().register_fake_lsp_adapter(
4142 "Rust",
4143 FakeLspAdapter {
4144 name: "rust-analyzer",
4145 ..FakeLspAdapter::default()
4146 },
4147 );
4148
4149 client_a
4150 .fs()
4151 .insert_tree(
4152 path!("/a"),
4153 json!({
4154 "main.rs": "fn main() {}",
4155 }),
4156 )
4157 .await;
4158 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4159 active_call_a
4160 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4161 .await
4162 .unwrap();
4163 let project_id = active_call_a
4164 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4165 .await
4166 .unwrap();
4167
4168 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4169 active_call_b
4170 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4171 .await
4172 .unwrap();
4173
4174 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4175 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4176
4177 let editor_a = workspace_a
4178 .update_in(cx_a, |workspace, window, cx| {
4179 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4180 })
4181 .await
4182 .unwrap()
4183 .downcast::<Editor>()
4184 .unwrap();
4185
4186 let editor_b = workspace_b
4187 .update_in(cx_b, |workspace, window, cx| {
4188 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4189 })
4190 .await
4191 .unwrap()
4192 .downcast::<Editor>()
4193 .unwrap();
4194
4195 let fake_language_server = fake_language_servers.next().await.unwrap();
4196
4197 // host
4198 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4199 |params, _| async move {
4200 assert_eq!(
4201 params.text_document.uri,
4202 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4203 );
4204 assert_eq!(params.position, lsp::Position::new(0, 0));
4205 Ok(Some(ExpandedMacro {
4206 name: "test_macro_name".to_string(),
4207 expansion: "test_macro_expansion on the host".to_string(),
4208 }))
4209 },
4210 );
4211
4212 editor_a.update_in(cx_a, |editor, window, cx| {
4213 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4214 });
4215 expand_request_a.next().await.unwrap();
4216 cx_a.run_until_parked();
4217
4218 workspace_a.update(cx_a, |workspace, cx| {
4219 workspace.active_pane().update(cx, |pane, cx| {
4220 assert_eq!(
4221 pane.items_len(),
4222 2,
4223 "Should have added a macro expansion to the host's pane"
4224 );
4225 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4226 new_editor.update(cx, |editor, cx| {
4227 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
4228 });
4229 })
4230 });
4231
4232 // client
4233 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4234 |params, _| async move {
4235 assert_eq!(
4236 params.text_document.uri,
4237 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4238 );
4239 assert_eq!(
4240 params.position,
4241 lsp::Position::new(0, 12),
4242 "editor_b has selected the entire text and should query for a different position"
4243 );
4244 Ok(Some(ExpandedMacro {
4245 name: "test_macro_name".to_string(),
4246 expansion: "test_macro_expansion on the client".to_string(),
4247 }))
4248 },
4249 );
4250
4251 editor_b.update_in(cx_b, |editor, window, cx| {
4252 editor.select_all(&SelectAll, window, cx);
4253 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4254 });
4255 expand_request_b.next().await.unwrap();
4256 cx_b.run_until_parked();
4257
4258 workspace_b.update(cx_b, |workspace, cx| {
4259 workspace.active_pane().update(cx, |pane, cx| {
4260 assert_eq!(
4261 pane.items_len(),
4262 2,
4263 "Should have added a macro expansion to the client's pane"
4264 );
4265 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4266 new_editor.update(cx, |editor, cx| {
4267 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
4268 });
4269 })
4270 });
4271}
4272
4273#[gpui::test]
4274async fn test_copy_file_name_without_extension(
4275 cx_a: &mut TestAppContext,
4276 cx_b: &mut TestAppContext,
4277) {
4278 let mut server = TestServer::start(cx_a.executor()).await;
4279 let client_a = server.create_client(cx_a, "user_a").await;
4280 let client_b = server.create_client(cx_b, "user_b").await;
4281 server
4282 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4283 .await;
4284
4285 cx_b.update(editor::init);
4286
4287 client_a
4288 .fs()
4289 .insert_tree(
4290 path!("/root"),
4291 json!({
4292 "src": {
4293 "main.rs": indoc! {"
4294 fn main() {
4295 println!(\"Hello, world!\");
4296 }
4297 "},
4298 }
4299 }),
4300 )
4301 .await;
4302
4303 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4304 let active_call_a = cx_a.read(ActiveCall::global);
4305 let project_id = active_call_a
4306 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4307 .await
4308 .unwrap();
4309
4310 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4311
4312 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4313 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4314
4315 let editor_a = workspace_a
4316 .update_in(cx_a, |workspace, window, cx| {
4317 workspace.open_path(
4318 (worktree_id, rel_path("src/main.rs")),
4319 None,
4320 true,
4321 window,
4322 cx,
4323 )
4324 })
4325 .await
4326 .unwrap()
4327 .downcast::<Editor>()
4328 .unwrap();
4329
4330 let editor_b = workspace_b
4331 .update_in(cx_b, |workspace, window, cx| {
4332 workspace.open_path(
4333 (worktree_id, rel_path("src/main.rs")),
4334 None,
4335 true,
4336 window,
4337 cx,
4338 )
4339 })
4340 .await
4341 .unwrap()
4342 .downcast::<Editor>()
4343 .unwrap();
4344
4345 cx_a.run_until_parked();
4346 cx_b.run_until_parked();
4347
4348 editor_a.update_in(cx_a, |editor, window, cx| {
4349 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4350 });
4351
4352 assert_eq!(
4353 cx_a.read_from_clipboard().and_then(|item| item.text()),
4354 Some("main".to_string())
4355 );
4356
4357 editor_b.update_in(cx_b, |editor, window, cx| {
4358 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4359 });
4360
4361 assert_eq!(
4362 cx_b.read_from_clipboard().and_then(|item| item.text()),
4363 Some("main".to_string())
4364 );
4365}
4366
4367#[gpui::test]
4368async fn test_copy_file_name(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4369 let mut server = TestServer::start(cx_a.executor()).await;
4370 let client_a = server.create_client(cx_a, "user_a").await;
4371 let client_b = server.create_client(cx_b, "user_b").await;
4372 server
4373 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4374 .await;
4375
4376 cx_b.update(editor::init);
4377
4378 client_a
4379 .fs()
4380 .insert_tree(
4381 path!("/root"),
4382 json!({
4383 "src": {
4384 "main.rs": indoc! {"
4385 fn main() {
4386 println!(\"Hello, world!\");
4387 }
4388 "},
4389 }
4390 }),
4391 )
4392 .await;
4393
4394 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4395 let active_call_a = cx_a.read(ActiveCall::global);
4396 let project_id = active_call_a
4397 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4398 .await
4399 .unwrap();
4400
4401 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4402
4403 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4404 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4405
4406 let editor_a = workspace_a
4407 .update_in(cx_a, |workspace, window, cx| {
4408 workspace.open_path(
4409 (worktree_id, rel_path("src/main.rs")),
4410 None,
4411 true,
4412 window,
4413 cx,
4414 )
4415 })
4416 .await
4417 .unwrap()
4418 .downcast::<Editor>()
4419 .unwrap();
4420
4421 let editor_b = workspace_b
4422 .update_in(cx_b, |workspace, window, cx| {
4423 workspace.open_path(
4424 (worktree_id, rel_path("src/main.rs")),
4425 None,
4426 true,
4427 window,
4428 cx,
4429 )
4430 })
4431 .await
4432 .unwrap()
4433 .downcast::<Editor>()
4434 .unwrap();
4435
4436 cx_a.run_until_parked();
4437 cx_b.run_until_parked();
4438
4439 editor_a.update_in(cx_a, |editor, window, cx| {
4440 editor.copy_file_name(&CopyFileName, window, cx);
4441 });
4442
4443 assert_eq!(
4444 cx_a.read_from_clipboard().and_then(|item| item.text()),
4445 Some("main.rs".to_string())
4446 );
4447
4448 editor_b.update_in(cx_b, |editor, window, cx| {
4449 editor.copy_file_name(&CopyFileName, window, cx);
4450 });
4451
4452 assert_eq!(
4453 cx_b.read_from_clipboard().and_then(|item| item.text()),
4454 Some("main.rs".to_string())
4455 );
4456}
4457
4458#[gpui::test]
4459async fn test_copy_file_location(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4460 let mut server = TestServer::start(cx_a.executor()).await;
4461 let client_a = server.create_client(cx_a, "user_a").await;
4462 let client_b = server.create_client(cx_b, "user_b").await;
4463 server
4464 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4465 .await;
4466
4467 cx_b.update(editor::init);
4468
4469 client_a
4470 .fs()
4471 .insert_tree(
4472 path!("/root"),
4473 json!({
4474 "src": {
4475 "main.rs": indoc! {"
4476 fn main() {
4477 println!(\"Hello, world!\");
4478 }
4479 "},
4480 }
4481 }),
4482 )
4483 .await;
4484
4485 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4486 let active_call_a = cx_a.read(ActiveCall::global);
4487 let project_id = active_call_a
4488 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4489 .await
4490 .unwrap();
4491
4492 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4493
4494 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4495 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4496
4497 let editor_a = workspace_a
4498 .update_in(cx_a, |workspace, window, cx| {
4499 workspace.open_path(
4500 (worktree_id, rel_path("src/main.rs")),
4501 None,
4502 true,
4503 window,
4504 cx,
4505 )
4506 })
4507 .await
4508 .unwrap()
4509 .downcast::<Editor>()
4510 .unwrap();
4511
4512 let editor_b = workspace_b
4513 .update_in(cx_b, |workspace, window, cx| {
4514 workspace.open_path(
4515 (worktree_id, rel_path("src/main.rs")),
4516 None,
4517 true,
4518 window,
4519 cx,
4520 )
4521 })
4522 .await
4523 .unwrap()
4524 .downcast::<Editor>()
4525 .unwrap();
4526
4527 cx_a.run_until_parked();
4528 cx_b.run_until_parked();
4529
4530 editor_a.update_in(cx_a, |editor, window, cx| {
4531 editor.change_selections(Default::default(), window, cx, |s| {
4532 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4533 });
4534 editor.copy_file_location(&CopyFileLocation, window, cx);
4535 });
4536
4537 assert_eq!(
4538 cx_a.read_from_clipboard().and_then(|item| item.text()),
4539 Some(format!("{}:2", path!("src/main.rs")))
4540 );
4541
4542 editor_b.update_in(cx_b, |editor, window, cx| {
4543 editor.change_selections(Default::default(), window, cx, |s| {
4544 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4545 });
4546 editor.copy_file_location(&CopyFileLocation, window, cx);
4547 });
4548
4549 assert_eq!(
4550 cx_b.read_from_clipboard().and_then(|item| item.text()),
4551 Some(format!("{}:2", path!("src/main.rs")))
4552 );
4553}
4554
4555#[track_caller]
4556fn tab_undo_assert(
4557 cx_a: &mut EditorTestContext,
4558 cx_b: &mut EditorTestContext,
4559 expected_initial: &str,
4560 expected_tabbed: &str,
4561 a_tabs: bool,
4562) {
4563 cx_a.assert_editor_state(expected_initial);
4564 cx_b.assert_editor_state(expected_initial);
4565
4566 if a_tabs {
4567 cx_a.update_editor(|editor, window, cx| {
4568 editor.tab(&editor::actions::Tab, window, cx);
4569 });
4570 } else {
4571 cx_b.update_editor(|editor, window, cx| {
4572 editor.tab(&editor::actions::Tab, window, cx);
4573 });
4574 }
4575
4576 cx_a.run_until_parked();
4577 cx_b.run_until_parked();
4578
4579 cx_a.assert_editor_state(expected_tabbed);
4580 cx_b.assert_editor_state(expected_tabbed);
4581
4582 if a_tabs {
4583 cx_a.update_editor(|editor, window, cx| {
4584 editor.undo(&editor::actions::Undo, window, cx);
4585 });
4586 } else {
4587 cx_b.update_editor(|editor, window, cx| {
4588 editor.undo(&editor::actions::Undo, window, cx);
4589 });
4590 }
4591 cx_a.run_until_parked();
4592 cx_b.run_until_parked();
4593 cx_a.assert_editor_state(expected_initial);
4594 cx_b.assert_editor_state(expected_initial);
4595}
4596
4597fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4598 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4599
4600 let mut all_cached_labels = Vec::new();
4601 let mut all_fetched_hints = Vec::new();
4602 for buffer in editor.buffer().read(cx).all_buffers() {
4603 lsp_store.update(cx, |lsp_store, cx| {
4604 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4605 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4606 let mut label = hint.text().to_string();
4607 if hint.padding_left {
4608 label.insert(0, ' ');
4609 }
4610 if hint.padding_right {
4611 label.push_str(" ");
4612 }
4613 label
4614 }));
4615 all_fetched_hints.extend(hints.all_fetched_hints());
4616 });
4617 }
4618
4619 assert!(
4620 all_fetched_hints.is_empty(),
4621 "Did not expect background hints fetch tasks, but got {} of them",
4622 all_fetched_hints.len()
4623 );
4624
4625 all_cached_labels
4626}
4627
4628#[track_caller]
4629fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
4630 editor
4631 .all_inlays(cx)
4632 .into_iter()
4633 .filter_map(|inlay| inlay.get_color())
4634 .map(Rgba::from)
4635 .collect()
4636}
4637
4638fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
4639 git::blame::BlameEntry {
4640 sha: sha.parse().unwrap(),
4641 range,
4642 ..Default::default()
4643 }
4644}