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