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