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![
2173 lsp::InlayHint {
2174 position: lsp::Position::new(0, character),
2175 label: lsp::InlayHintLabel::String(label.to_string()),
2176 kind: None,
2177 text_edits: None,
2178 tooltip: None,
2179 padding_left: None,
2180 padding_right: None,
2181 data: None,
2182 },
2183 lsp::InlayHint {
2184 position: lsp::Position::new(1090, 1090),
2185 label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
2186 kind: None,
2187 text_edits: None,
2188 tooltip: None,
2189 padding_left: None,
2190 padding_right: None,
2191 data: None,
2192 },
2193 ]))
2194 }
2195 })
2196 .next()
2197 .await
2198 .unwrap();
2199 executor.finish_waiting();
2200
2201 executor.run_until_parked();
2202 editor_a.update(cx_a, |editor, cx| {
2203 assert!(
2204 extract_hint_labels(editor, cx).is_empty(),
2205 "Host should get no hints due to them turned off"
2206 );
2207 });
2208
2209 executor.run_until_parked();
2210 editor_b.update(cx_b, |editor, cx| {
2211 assert_eq!(
2212 vec!["initial hint".to_string()],
2213 extract_hint_labels(editor, cx),
2214 "Client should get its first hints when opens an editor"
2215 );
2216 });
2217
2218 other_hints.fetch_or(true, atomic::Ordering::Release);
2219 fake_language_server
2220 .request::<lsp::request::InlayHintRefreshRequest>(())
2221 .await
2222 .into_response()
2223 .expect("inlay refresh request failed");
2224 executor.run_until_parked();
2225 editor_a.update(cx_a, |editor, cx| {
2226 assert!(
2227 extract_hint_labels(editor, cx).is_empty(),
2228 "Host should get no hints due to them turned off, even after the /refresh"
2229 );
2230 });
2231
2232 executor.run_until_parked();
2233 editor_b.update(cx_b, |editor, cx| {
2234 assert_eq!(
2235 vec!["other hint".to_string()],
2236 extract_hint_labels(editor, cx),
2237 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
2238 );
2239 });
2240}
2241
2242#[gpui::test(iterations = 10)]
2243async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2244 let expected_color = Rgba {
2245 r: 0.33,
2246 g: 0.33,
2247 b: 0.33,
2248 a: 0.33,
2249 };
2250 let mut server = TestServer::start(cx_a.executor()).await;
2251 let executor = cx_a.executor();
2252 let client_a = server.create_client(cx_a, "user_a").await;
2253 let client_b = server.create_client(cx_b, "user_b").await;
2254 server
2255 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2256 .await;
2257 let active_call_a = cx_a.read(ActiveCall::global);
2258 let active_call_b = cx_b.read(ActiveCall::global);
2259
2260 cx_a.update(editor::init);
2261 cx_b.update(editor::init);
2262
2263 cx_a.update(|cx| {
2264 SettingsStore::update_global(cx, |store, cx| {
2265 store.update_user_settings(cx, |settings| {
2266 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None);
2267 });
2268 });
2269 });
2270 cx_b.update(|cx| {
2271 SettingsStore::update_global(cx, |store, cx| {
2272 store.update_user_settings(cx, |settings| {
2273 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2274 });
2275 });
2276 });
2277
2278 let capabilities = lsp::ServerCapabilities {
2279 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
2280 ..lsp::ServerCapabilities::default()
2281 };
2282 client_a.language_registry().add(rust_lang());
2283 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2284 "Rust",
2285 FakeLspAdapter {
2286 capabilities: capabilities.clone(),
2287 ..FakeLspAdapter::default()
2288 },
2289 );
2290 client_b.language_registry().add(rust_lang());
2291 client_b.language_registry().register_fake_lsp_adapter(
2292 "Rust",
2293 FakeLspAdapter {
2294 capabilities,
2295 ..FakeLspAdapter::default()
2296 },
2297 );
2298
2299 // Client A opens a project.
2300 client_a
2301 .fs()
2302 .insert_tree(
2303 path!("/a"),
2304 json!({
2305 "main.rs": "fn main() { a }",
2306 }),
2307 )
2308 .await;
2309 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2310 active_call_a
2311 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2312 .await
2313 .unwrap();
2314 let project_id = active_call_a
2315 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2316 .await
2317 .unwrap();
2318
2319 // Client B joins the project
2320 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2321 active_call_b
2322 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2323 .await
2324 .unwrap();
2325
2326 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2327
2328 // The host opens a rust file.
2329 let _buffer_a = project_a
2330 .update(cx_a, |project, cx| {
2331 project.open_local_buffer(path!("/a/main.rs"), cx)
2332 })
2333 .await
2334 .unwrap();
2335 let editor_a = workspace_a
2336 .update_in(cx_a, |workspace, window, cx| {
2337 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2338 })
2339 .await
2340 .unwrap()
2341 .downcast::<Editor>()
2342 .unwrap();
2343
2344 let fake_language_server = fake_language_servers.next().await.unwrap();
2345 cx_a.run_until_parked();
2346 cx_b.run_until_parked();
2347
2348 let requests_made = Arc::new(AtomicUsize::new(0));
2349 let closure_requests_made = Arc::clone(&requests_made);
2350 let mut color_request_handle = fake_language_server
2351 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
2352 let requests_made = Arc::clone(&closure_requests_made);
2353 async move {
2354 assert_eq!(
2355 params.text_document.uri,
2356 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2357 );
2358 requests_made.fetch_add(1, atomic::Ordering::Release);
2359 Ok(vec![lsp::ColorInformation {
2360 range: lsp::Range {
2361 start: lsp::Position {
2362 line: 0,
2363 character: 0,
2364 },
2365 end: lsp::Position {
2366 line: 0,
2367 character: 1,
2368 },
2369 },
2370 color: lsp::Color {
2371 red: 0.33,
2372 green: 0.33,
2373 blue: 0.33,
2374 alpha: 0.33,
2375 },
2376 }])
2377 }
2378 });
2379 executor.run_until_parked();
2380
2381 assert_eq!(
2382 0,
2383 requests_made.load(atomic::Ordering::Acquire),
2384 "Host did not enable document colors, hence should query for none"
2385 );
2386 editor_a.update(cx_a, |editor, cx| {
2387 assert_eq!(
2388 Vec::<Rgba>::new(),
2389 extract_color_inlays(editor, cx),
2390 "No query colors should result in no hints"
2391 );
2392 });
2393
2394 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2395 let editor_b = workspace_b
2396 .update_in(cx_b, |workspace, window, cx| {
2397 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2398 })
2399 .await
2400 .unwrap()
2401 .downcast::<Editor>()
2402 .unwrap();
2403
2404 color_request_handle.next().await.unwrap();
2405 executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
2406 executor.run_until_parked();
2407
2408 assert_eq!(
2409 1,
2410 requests_made.load(atomic::Ordering::Acquire),
2411 "The client opened the file and got its first colors back"
2412 );
2413 editor_b.update(cx_b, |editor, cx| {
2414 assert_eq!(
2415 vec![expected_color],
2416 extract_color_inlays(editor, cx),
2417 "With document colors as inlays, color inlays should be pushed"
2418 );
2419 });
2420
2421 editor_a.update_in(cx_a, |editor, window, cx| {
2422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2423 s.select_ranges([13..13].clone())
2424 });
2425 editor.handle_input(":", window, cx);
2426 });
2427 color_request_handle.next().await.unwrap();
2428 executor.run_until_parked();
2429 assert_eq!(
2430 2,
2431 requests_made.load(atomic::Ordering::Acquire),
2432 "After the host edits his file, the client should request the colors again"
2433 );
2434 editor_a.update(cx_a, |editor, cx| {
2435 assert_eq!(
2436 Vec::<Rgba>::new(),
2437 extract_color_inlays(editor, cx),
2438 "Host has no colors still"
2439 );
2440 });
2441 editor_b.update(cx_b, |editor, cx| {
2442 assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
2443 });
2444
2445 cx_b.update(|_, cx| {
2446 SettingsStore::update_global(cx, |store, cx| {
2447 store.update_user_settings(cx, |settings| {
2448 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
2449 });
2450 });
2451 });
2452 executor.run_until_parked();
2453 assert_eq!(
2454 2,
2455 requests_made.load(atomic::Ordering::Acquire),
2456 "After the client have changed the colors settings, no extra queries should happen"
2457 );
2458 editor_a.update(cx_a, |editor, cx| {
2459 assert_eq!(
2460 Vec::<Rgba>::new(),
2461 extract_color_inlays(editor, cx),
2462 "Host is unaffected by the client's settings changes"
2463 );
2464 });
2465 editor_b.update(cx_b, |editor, cx| {
2466 assert_eq!(
2467 Vec::<Rgba>::new(),
2468 extract_color_inlays(editor, cx),
2469 "Client should have no colors hints, as in the settings"
2470 );
2471 });
2472
2473 cx_b.update(|_, cx| {
2474 SettingsStore::update_global(cx, |store, cx| {
2475 store.update_user_settings(cx, |settings| {
2476 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2477 });
2478 });
2479 });
2480 executor.run_until_parked();
2481 assert_eq!(
2482 2,
2483 requests_made.load(atomic::Ordering::Acquire),
2484 "After falling back to colors as inlays, no extra LSP queries are made"
2485 );
2486 editor_a.update(cx_a, |editor, cx| {
2487 assert_eq!(
2488 Vec::<Rgba>::new(),
2489 extract_color_inlays(editor, cx),
2490 "Host is unaffected by the client's settings changes, again"
2491 );
2492 });
2493 editor_b.update(cx_b, |editor, cx| {
2494 assert_eq!(
2495 vec![expected_color],
2496 extract_color_inlays(editor, cx),
2497 "Client should have its color hints back"
2498 );
2499 });
2500
2501 cx_a.update(|_, cx| {
2502 SettingsStore::update_global(cx, |store, cx| {
2503 store.update_user_settings(cx, |settings| {
2504 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
2505 });
2506 });
2507 });
2508 color_request_handle.next().await.unwrap();
2509 executor.run_until_parked();
2510 assert_eq!(
2511 3,
2512 requests_made.load(atomic::Ordering::Acquire),
2513 "After the host enables document colors, another LSP query should be made"
2514 );
2515 editor_a.update(cx_a, |editor, cx| {
2516 assert_eq!(
2517 Vec::<Rgba>::new(),
2518 extract_color_inlays(editor, cx),
2519 "Host did not configure document colors as hints hence gets nothing"
2520 );
2521 });
2522 editor_b.update(cx_b, |editor, cx| {
2523 assert_eq!(
2524 vec![expected_color],
2525 extract_color_inlays(editor, cx),
2526 "Client should be unaffected by the host's settings changes"
2527 );
2528 });
2529}
2530
2531async fn test_lsp_pull_diagnostics(
2532 should_stream_workspace_diagnostic: bool,
2533 cx_a: &mut TestAppContext,
2534 cx_b: &mut TestAppContext,
2535) {
2536 let mut server = TestServer::start(cx_a.executor()).await;
2537 let executor = cx_a.executor();
2538 let client_a = server.create_client(cx_a, "user_a").await;
2539 let client_b = server.create_client(cx_b, "user_b").await;
2540 server
2541 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2542 .await;
2543 let active_call_a = cx_a.read(ActiveCall::global);
2544 let active_call_b = cx_b.read(ActiveCall::global);
2545
2546 cx_a.update(editor::init);
2547 cx_b.update(editor::init);
2548
2549 let expected_push_diagnostic_main_message = "pushed main diagnostic";
2550 let expected_push_diagnostic_lib_message = "pushed lib diagnostic";
2551 let expected_pull_diagnostic_main_message = "pulled main diagnostic";
2552 let expected_pull_diagnostic_lib_message = "pulled lib diagnostic";
2553 let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic";
2554 let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic";
2555
2556 let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<Option<String>>::new()));
2557 let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<String>::new()));
2558 let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2559 let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone();
2560 let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone();
2561 let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2562 let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone();
2563 let closure_workspace_diagnostics_pulls_result_ids =
2564 workspace_diagnostics_pulls_result_ids.clone();
2565 let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) =
2566 smol::channel::bounded::<()>(1);
2567 let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
2568 smol::channel::bounded::<()>(1);
2569
2570 let capabilities = lsp::ServerCapabilities {
2571 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
2572 lsp::DiagnosticOptions {
2573 identifier: Some("test-pulls".to_string()),
2574 inter_file_dependencies: true,
2575 workspace_diagnostics: true,
2576 work_done_progress_options: lsp::WorkDoneProgressOptions {
2577 work_done_progress: None,
2578 },
2579 },
2580 )),
2581 ..lsp::ServerCapabilities::default()
2582 };
2583 client_a.language_registry().add(rust_lang());
2584
2585 let pull_diagnostics_handle = Arc::new(parking_lot::Mutex::new(None));
2586 let workspace_diagnostics_pulls_handle = Arc::new(parking_lot::Mutex::new(None));
2587
2588 let closure_pull_diagnostics_handle = pull_diagnostics_handle.clone();
2589 let closure_workspace_diagnostics_pulls_handle = workspace_diagnostics_pulls_handle.clone();
2590 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2591 "Rust",
2592 FakeLspAdapter {
2593 capabilities: capabilities.clone(),
2594 initializer: Some(Box::new(move |fake_language_server| {
2595 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2596 "workspace/diagnostic/{}/1",
2597 fake_language_server.server.server_id()
2598 ));
2599 let closure_workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone();
2600 let diagnostics_pulls_made = closure_diagnostics_pulls_made.clone();
2601 let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
2602 let closure_pull_diagnostics_handle = closure_pull_diagnostics_handle.clone();
2603 let closure_workspace_diagnostics_pulls_handle = closure_workspace_diagnostics_pulls_handle.clone();
2604 let closure_workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2605 let closure_workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2606 let pull_diagnostics_handle = fake_language_server
2607 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(
2608 move |params, _| {
2609 let requests_made = diagnostics_pulls_made.clone();
2610 let diagnostics_pulls_result_ids =
2611 diagnostics_pulls_result_ids.clone();
2612 async move {
2613 let message = if lsp::Uri::from_file_path(path!("/a/main.rs"))
2614 .unwrap()
2615 == params.text_document.uri
2616 {
2617 expected_pull_diagnostic_main_message.to_string()
2618 } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2619 == params.text_document.uri
2620 {
2621 expected_pull_diagnostic_lib_message.to_string()
2622 } else {
2623 panic!("Unexpected document: {}", params.text_document.uri)
2624 };
2625 {
2626 diagnostics_pulls_result_ids
2627 .lock()
2628 .await
2629 .insert(params.previous_result_id);
2630 }
2631 let new_requests_count =
2632 requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2633 Ok(lsp::DocumentDiagnosticReportResult::Report(
2634 lsp::DocumentDiagnosticReport::Full(
2635 lsp::RelatedFullDocumentDiagnosticReport {
2636 related_documents: None,
2637 full_document_diagnostic_report:
2638 lsp::FullDocumentDiagnosticReport {
2639 result_id: Some(format!(
2640 "pull-{new_requests_count}"
2641 )),
2642 items: vec![lsp::Diagnostic {
2643 range: lsp::Range {
2644 start: lsp::Position {
2645 line: 0,
2646 character: 0,
2647 },
2648 end: lsp::Position {
2649 line: 0,
2650 character: 2,
2651 },
2652 },
2653 severity: Some(
2654 lsp::DiagnosticSeverity::ERROR,
2655 ),
2656 message,
2657 ..lsp::Diagnostic::default()
2658 }],
2659 },
2660 },
2661 ),
2662 ))
2663 }
2664 },
2665 );
2666 let _ = closure_pull_diagnostics_handle.lock().insert(pull_diagnostics_handle);
2667
2668 let closure_workspace_diagnostics_pulls_made = closure_workspace_diagnostics_pulls_made.clone();
2669 let workspace_diagnostics_pulls_handle = fake_language_server.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
2670 move |params, _| {
2671 let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone();
2672 let workspace_diagnostics_pulls_result_ids =
2673 closure_workspace_diagnostics_pulls_result_ids.clone();
2674 let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2675 let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2676 let expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
2677 async move {
2678 let workspace_request_count =
2679 workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2680 {
2681 workspace_diagnostics_pulls_result_ids
2682 .lock()
2683 .await
2684 .extend(params.previous_result_ids.into_iter().map(|id| id.value));
2685 }
2686 if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed()
2687 {
2688 assert_eq!(
2689 params.partial_result_params.partial_result_token,
2690 Some(expected_workspace_diagnostic_token)
2691 );
2692 workspace_diagnostic_received_tx.send(()).await.unwrap();
2693 workspace_diagnostic_cancel_rx.recv().await.unwrap();
2694 workspace_diagnostic_cancel_rx.close();
2695 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
2696 // > The final response has to be empty in terms of result values.
2697 return Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2698 lsp::WorkspaceDiagnosticReport { items: Vec::new() },
2699 ));
2700 }
2701 Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2702 lsp::WorkspaceDiagnosticReport {
2703 items: vec![
2704 lsp::WorkspaceDocumentDiagnosticReport::Full(
2705 lsp::WorkspaceFullDocumentDiagnosticReport {
2706 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2707 version: None,
2708 full_document_diagnostic_report:
2709 lsp::FullDocumentDiagnosticReport {
2710 result_id: Some(format!(
2711 "workspace_{workspace_request_count}"
2712 )),
2713 items: vec![lsp::Diagnostic {
2714 range: lsp::Range {
2715 start: lsp::Position {
2716 line: 0,
2717 character: 1,
2718 },
2719 end: lsp::Position {
2720 line: 0,
2721 character: 3,
2722 },
2723 },
2724 severity: Some(lsp::DiagnosticSeverity::WARNING),
2725 message:
2726 expected_workspace_pull_diagnostics_main_message
2727 .to_string(),
2728 ..lsp::Diagnostic::default()
2729 }],
2730 },
2731 },
2732 ),
2733 lsp::WorkspaceDocumentDiagnosticReport::Full(
2734 lsp::WorkspaceFullDocumentDiagnosticReport {
2735 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
2736 version: None,
2737 full_document_diagnostic_report:
2738 lsp::FullDocumentDiagnosticReport {
2739 result_id: Some(format!(
2740 "workspace_{workspace_request_count}"
2741 )),
2742 items: vec![lsp::Diagnostic {
2743 range: lsp::Range {
2744 start: lsp::Position {
2745 line: 0,
2746 character: 1,
2747 },
2748 end: lsp::Position {
2749 line: 0,
2750 character: 3,
2751 },
2752 },
2753 severity: Some(lsp::DiagnosticSeverity::WARNING),
2754 message:
2755 expected_workspace_pull_diagnostics_lib_message
2756 .to_string(),
2757 ..lsp::Diagnostic::default()
2758 }],
2759 },
2760 },
2761 ),
2762 ],
2763 },
2764 ))
2765 }
2766 });
2767 let _ = closure_workspace_diagnostics_pulls_handle.lock().insert(workspace_diagnostics_pulls_handle);
2768 })),
2769 ..FakeLspAdapter::default()
2770 },
2771 );
2772
2773 client_b.language_registry().add(rust_lang());
2774 client_b.language_registry().register_fake_lsp_adapter(
2775 "Rust",
2776 FakeLspAdapter {
2777 capabilities,
2778 ..FakeLspAdapter::default()
2779 },
2780 );
2781
2782 // Client A opens a project.
2783 client_a
2784 .fs()
2785 .insert_tree(
2786 path!("/a"),
2787 json!({
2788 "main.rs": "fn main() { a }",
2789 "lib.rs": "fn other() {}",
2790 }),
2791 )
2792 .await;
2793 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2794 active_call_a
2795 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2796 .await
2797 .unwrap();
2798 let project_id = active_call_a
2799 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2800 .await
2801 .unwrap();
2802
2803 // Client B joins the project
2804 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2805 active_call_b
2806 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2807 .await
2808 .unwrap();
2809
2810 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2811 executor.start_waiting();
2812
2813 // The host opens a rust file.
2814 let _buffer_a = project_a
2815 .update(cx_a, |project, cx| {
2816 project.open_local_buffer(path!("/a/main.rs"), cx)
2817 })
2818 .await
2819 .unwrap();
2820 let editor_a_main = workspace_a
2821 .update_in(cx_a, |workspace, window, cx| {
2822 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2823 })
2824 .await
2825 .unwrap()
2826 .downcast::<Editor>()
2827 .unwrap();
2828
2829 let fake_language_server = fake_language_servers.next().await.unwrap();
2830 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2831 "workspace/diagnostic-{}-1",
2832 fake_language_server.server.server_id()
2833 ));
2834 cx_a.run_until_parked();
2835 cx_b.run_until_parked();
2836 let mut pull_diagnostics_handle = pull_diagnostics_handle.lock().take().unwrap();
2837 let mut workspace_diagnostics_pulls_handle =
2838 workspace_diagnostics_pulls_handle.lock().take().unwrap();
2839
2840 if should_stream_workspace_diagnostic {
2841 workspace_diagnostic_received_rx.recv().await.unwrap();
2842 } else {
2843 workspace_diagnostics_pulls_handle.next().await.unwrap();
2844 }
2845 assert_eq!(
2846 1,
2847 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
2848 "Workspace diagnostics should be pulled initially on a server startup"
2849 );
2850 pull_diagnostics_handle.next().await.unwrap();
2851 assert_eq!(
2852 1,
2853 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
2854 "Host should query pull diagnostics when the editor is opened"
2855 );
2856 executor.run_until_parked();
2857 editor_a_main.update(cx_a, |editor, cx| {
2858 let snapshot = editor.buffer().read(cx).snapshot(cx);
2859 let all_diagnostics = snapshot
2860 .diagnostics_in_range(0..snapshot.len())
2861 .collect::<Vec<_>>();
2862 assert_eq!(
2863 all_diagnostics.len(),
2864 1,
2865 "Expected single diagnostic, but got: {all_diagnostics:?}"
2866 );
2867 let diagnostic = &all_diagnostics[0];
2868 let mut expected_messages = vec![expected_pull_diagnostic_main_message];
2869 if !should_stream_workspace_diagnostic {
2870 expected_messages.push(expected_workspace_pull_diagnostics_main_message);
2871 }
2872 assert!(
2873 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
2874 "Expected {expected_messages:?} on the host, but got: {}",
2875 diagnostic.diagnostic.message
2876 );
2877 });
2878
2879 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
2880 lsp::PublishDiagnosticsParams {
2881 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2882 diagnostics: vec![lsp::Diagnostic {
2883 range: lsp::Range {
2884 start: lsp::Position {
2885 line: 0,
2886 character: 3,
2887 },
2888 end: lsp::Position {
2889 line: 0,
2890 character: 4,
2891 },
2892 },
2893 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
2894 message: expected_push_diagnostic_main_message.to_string(),
2895 ..lsp::Diagnostic::default()
2896 }],
2897 version: None,
2898 },
2899 );
2900 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
2901 lsp::PublishDiagnosticsParams {
2902 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
2903 diagnostics: vec![lsp::Diagnostic {
2904 range: lsp::Range {
2905 start: lsp::Position {
2906 line: 0,
2907 character: 3,
2908 },
2909 end: lsp::Position {
2910 line: 0,
2911 character: 4,
2912 },
2913 },
2914 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
2915 message: expected_push_diagnostic_lib_message.to_string(),
2916 ..lsp::Diagnostic::default()
2917 }],
2918 version: None,
2919 },
2920 );
2921
2922 if should_stream_workspace_diagnostic {
2923 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
2924 token: expected_workspace_diagnostic_token.clone(),
2925 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
2926 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
2927 items: vec![
2928 lsp::WorkspaceDocumentDiagnosticReport::Full(
2929 lsp::WorkspaceFullDocumentDiagnosticReport {
2930 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2931 version: None,
2932 full_document_diagnostic_report:
2933 lsp::FullDocumentDiagnosticReport {
2934 result_id: Some(format!(
2935 "workspace_{}",
2936 workspace_diagnostics_pulls_made
2937 .fetch_add(1, atomic::Ordering::Release)
2938 + 1
2939 )),
2940 items: vec![lsp::Diagnostic {
2941 range: lsp::Range {
2942 start: lsp::Position {
2943 line: 0,
2944 character: 1,
2945 },
2946 end: lsp::Position {
2947 line: 0,
2948 character: 2,
2949 },
2950 },
2951 severity: Some(lsp::DiagnosticSeverity::ERROR),
2952 message:
2953 expected_workspace_pull_diagnostics_main_message
2954 .to_string(),
2955 ..lsp::Diagnostic::default()
2956 }],
2957 },
2958 },
2959 ),
2960 lsp::WorkspaceDocumentDiagnosticReport::Full(
2961 lsp::WorkspaceFullDocumentDiagnosticReport {
2962 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
2963 version: None,
2964 full_document_diagnostic_report:
2965 lsp::FullDocumentDiagnosticReport {
2966 result_id: Some(format!(
2967 "workspace_{}",
2968 workspace_diagnostics_pulls_made
2969 .fetch_add(1, atomic::Ordering::Release)
2970 + 1
2971 )),
2972 items: Vec::new(),
2973 },
2974 },
2975 ),
2976 ],
2977 }),
2978 ),
2979 });
2980 };
2981
2982 let mut workspace_diagnostic_start_count =
2983 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
2984
2985 executor.run_until_parked();
2986 editor_a_main.update(cx_a, |editor, cx| {
2987 let snapshot = editor.buffer().read(cx).snapshot(cx);
2988 let all_diagnostics = snapshot
2989 .diagnostics_in_range(0..snapshot.len())
2990 .collect::<Vec<_>>();
2991 assert_eq!(
2992 all_diagnostics.len(),
2993 2,
2994 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
2995 );
2996 let expected_messages = [
2997 expected_workspace_pull_diagnostics_main_message,
2998 expected_pull_diagnostic_main_message,
2999 expected_push_diagnostic_main_message,
3000 ];
3001 for diagnostic in all_diagnostics {
3002 assert!(
3003 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3004 "Expected push and pull messages on the host: {expected_messages:?}, but got: {}",
3005 diagnostic.diagnostic.message
3006 );
3007 }
3008 });
3009
3010 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3011 let editor_b_main = workspace_b
3012 .update_in(cx_b, |workspace, window, cx| {
3013 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
3014 })
3015 .await
3016 .unwrap()
3017 .downcast::<Editor>()
3018 .unwrap();
3019 cx_b.run_until_parked();
3020
3021 pull_diagnostics_handle.next().await.unwrap();
3022 assert_eq!(
3023 2,
3024 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3025 "Client should query pull diagnostics when its editor is opened"
3026 );
3027 executor.run_until_parked();
3028 assert_eq!(
3029 workspace_diagnostic_start_count,
3030 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3031 "Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull"
3032 );
3033 editor_b_main.update(cx_b, |editor, cx| {
3034 let snapshot = editor.buffer().read(cx).snapshot(cx);
3035 let all_diagnostics = snapshot
3036 .diagnostics_in_range(0..snapshot.len())
3037 .collect::<Vec<_>>();
3038 assert_eq!(
3039 all_diagnostics.len(),
3040 2,
3041 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3042 );
3043
3044 // Despite the workspace diagnostics not re-initialized for the remote client, we can still expect its message synced from the host.
3045 let expected_messages = [
3046 expected_workspace_pull_diagnostics_main_message,
3047 expected_pull_diagnostic_main_message,
3048 expected_push_diagnostic_main_message,
3049 ];
3050 for diagnostic in all_diagnostics {
3051 assert!(
3052 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3053 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3054 diagnostic.diagnostic.message
3055 );
3056 }
3057 });
3058
3059 let editor_b_lib = workspace_b
3060 .update_in(cx_b, |workspace, window, cx| {
3061 workspace.open_path((worktree_id, rel_path("lib.rs")), None, true, window, cx)
3062 })
3063 .await
3064 .unwrap()
3065 .downcast::<Editor>()
3066 .unwrap();
3067
3068 pull_diagnostics_handle.next().await.unwrap();
3069 assert_eq!(
3070 3,
3071 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3072 "Client should query pull diagnostics when its another editor is opened"
3073 );
3074 executor.run_until_parked();
3075 assert_eq!(
3076 workspace_diagnostic_start_count,
3077 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3078 "The remote client still did not anything to trigger the workspace diagnostics pull"
3079 );
3080 editor_b_lib.update(cx_b, |editor, cx| {
3081 let snapshot = editor.buffer().read(cx).snapshot(cx);
3082 let all_diagnostics = snapshot
3083 .diagnostics_in_range(0..snapshot.len())
3084 .collect::<Vec<_>>();
3085 let expected_messages = [
3086 expected_pull_diagnostic_lib_message,
3087 // TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
3088 // expected_push_diagnostic_lib_message,
3089 ];
3090 assert_eq!(
3091 all_diagnostics.len(),
3092 1,
3093 "Expected pull diagnostics, but got: {all_diagnostics:?}"
3094 );
3095 for diagnostic in all_diagnostics {
3096 assert!(
3097 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3098 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3099 diagnostic.diagnostic.message
3100 );
3101 }
3102 });
3103
3104 if should_stream_workspace_diagnostic {
3105 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3106 token: expected_workspace_diagnostic_token.clone(),
3107 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3108 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3109 items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
3110 lsp::WorkspaceFullDocumentDiagnosticReport {
3111 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3112 version: None,
3113 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
3114 result_id: Some(format!(
3115 "workspace_{}",
3116 workspace_diagnostics_pulls_made
3117 .fetch_add(1, atomic::Ordering::Release)
3118 + 1
3119 )),
3120 items: vec![lsp::Diagnostic {
3121 range: lsp::Range {
3122 start: lsp::Position {
3123 line: 0,
3124 character: 1,
3125 },
3126 end: lsp::Position {
3127 line: 0,
3128 character: 2,
3129 },
3130 },
3131 severity: Some(lsp::DiagnosticSeverity::ERROR),
3132 message: expected_workspace_pull_diagnostics_lib_message
3133 .to_string(),
3134 ..lsp::Diagnostic::default()
3135 }],
3136 },
3137 },
3138 )],
3139 }),
3140 ),
3141 });
3142 workspace_diagnostic_start_count =
3143 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3144 workspace_diagnostic_cancel_tx.send(()).await.unwrap();
3145 workspace_diagnostics_pulls_handle.next().await.unwrap();
3146 executor.run_until_parked();
3147 editor_b_lib.update(cx_b, |editor, cx| {
3148 let snapshot = editor.buffer().read(cx).snapshot(cx);
3149 let all_diagnostics = snapshot
3150 .diagnostics_in_range(0..snapshot.len())
3151 .collect::<Vec<_>>();
3152 let expected_messages = [
3153 expected_workspace_pull_diagnostics_lib_message,
3154 // TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
3155 // expected_push_diagnostic_lib_message,
3156 ];
3157 assert_eq!(
3158 all_diagnostics.len(),
3159 1,
3160 "Expected pull diagnostics, but got: {all_diagnostics:?}"
3161 );
3162 for diagnostic in all_diagnostics {
3163 assert!(
3164 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3165 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3166 diagnostic.diagnostic.message
3167 );
3168 }
3169 });
3170 };
3171
3172 {
3173 assert!(
3174 !diagnostics_pulls_result_ids.lock().await.is_empty(),
3175 "Initial diagnostics pulls should report None at least"
3176 );
3177 assert_eq!(
3178 0,
3179 workspace_diagnostics_pulls_result_ids
3180 .lock()
3181 .await
3182 .deref()
3183 .len(),
3184 "After the initial workspace request, opening files should not reuse any result ids"
3185 );
3186 }
3187
3188 editor_b_lib.update_in(cx_b, |editor, window, cx| {
3189 editor.move_to_end(&MoveToEnd, window, cx);
3190 editor.handle_input(":", window, cx);
3191 });
3192 pull_diagnostics_handle.next().await.unwrap();
3193 assert_eq!(
3194 4,
3195 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3196 "Client lib.rs edits should trigger another diagnostics pull for a buffer"
3197 );
3198 workspace_diagnostics_pulls_handle.next().await.unwrap();
3199 assert_eq!(
3200 workspace_diagnostic_start_count + 1,
3201 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3202 "After client lib.rs edits, the workspace diagnostics request should follow"
3203 );
3204 executor.run_until_parked();
3205
3206 editor_b_main.update_in(cx_b, |editor, window, cx| {
3207 editor.move_to_end(&MoveToEnd, window, cx);
3208 editor.handle_input(":", window, cx);
3209 });
3210 pull_diagnostics_handle.next().await.unwrap();
3211 pull_diagnostics_handle.next().await.unwrap();
3212 assert_eq!(
3213 6,
3214 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3215 "Client main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
3216 );
3217 workspace_diagnostics_pulls_handle.next().await.unwrap();
3218 assert_eq!(
3219 workspace_diagnostic_start_count + 2,
3220 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3221 "After client main.rs edits, the workspace diagnostics pull should follow"
3222 );
3223 executor.run_until_parked();
3224
3225 editor_a_main.update_in(cx_a, |editor, window, cx| {
3226 editor.move_to_end(&MoveToEnd, window, cx);
3227 editor.handle_input(":", window, cx);
3228 });
3229 pull_diagnostics_handle.next().await.unwrap();
3230 pull_diagnostics_handle.next().await.unwrap();
3231 assert_eq!(
3232 8,
3233 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3234 "Host main.rs edits should trigger another diagnostics pull by both client and host as they share the buffer"
3235 );
3236 workspace_diagnostics_pulls_handle.next().await.unwrap();
3237 assert_eq!(
3238 workspace_diagnostic_start_count + 3,
3239 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3240 "After host main.rs edits, the workspace diagnostics pull should follow"
3241 );
3242 executor.run_until_parked();
3243 let diagnostic_pulls_result_ids = diagnostics_pulls_result_ids.lock().await.len();
3244 let workspace_pulls_result_ids = workspace_diagnostics_pulls_result_ids.lock().await.len();
3245 {
3246 assert!(
3247 diagnostic_pulls_result_ids > 1,
3248 "Should have sent result ids when pulling diagnostics"
3249 );
3250 assert!(
3251 workspace_pulls_result_ids > 1,
3252 "Should have sent result ids when pulling workspace diagnostics"
3253 );
3254 }
3255
3256 fake_language_server
3257 .request::<lsp::request::WorkspaceDiagnosticRefresh>(())
3258 .await
3259 .into_response()
3260 .expect("workspace diagnostics refresh request failed");
3261 assert_eq!(
3262 8,
3263 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3264 "No single file pulls should happen after the diagnostics refresh server request"
3265 );
3266 workspace_diagnostics_pulls_handle.next().await.unwrap();
3267 assert_eq!(
3268 workspace_diagnostic_start_count + 4,
3269 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3270 "Another workspace diagnostics pull should happen after the diagnostics refresh server request"
3271 );
3272 {
3273 assert!(
3274 diagnostics_pulls_result_ids.lock().await.len() == diagnostic_pulls_result_ids,
3275 "Pulls should not happen hence no extra ids should appear"
3276 );
3277 assert!(
3278 workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
3279 "More workspace diagnostics should be pulled"
3280 );
3281 }
3282 editor_b_lib.update(cx_b, |editor, cx| {
3283 let snapshot = editor.buffer().read(cx).snapshot(cx);
3284 let all_diagnostics = snapshot
3285 .diagnostics_in_range(0..snapshot.len())
3286 .collect::<Vec<_>>();
3287 let expected_messages = [
3288 expected_workspace_pull_diagnostics_lib_message,
3289 expected_pull_diagnostic_lib_message,
3290 expected_push_diagnostic_lib_message,
3291 ];
3292 assert_eq!(all_diagnostics.len(), 1);
3293 for diagnostic in &all_diagnostics {
3294 assert!(
3295 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3296 "Unexpected diagnostics: {all_diagnostics:?}"
3297 );
3298 }
3299 });
3300 editor_b_main.update(cx_b, |editor, cx| {
3301 let snapshot = editor.buffer().read(cx).snapshot(cx);
3302 let all_diagnostics = snapshot
3303 .diagnostics_in_range(0..snapshot.len())
3304 .collect::<Vec<_>>();
3305 assert_eq!(all_diagnostics.len(), 2);
3306
3307 let expected_messages = [
3308 expected_workspace_pull_diagnostics_main_message,
3309 expected_pull_diagnostic_main_message,
3310 expected_push_diagnostic_main_message,
3311 ];
3312 for diagnostic in &all_diagnostics {
3313 assert!(
3314 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3315 "Unexpected diagnostics: {all_diagnostics:?}"
3316 );
3317 }
3318 });
3319 editor_a_main.update(cx_a, |editor, cx| {
3320 let snapshot = editor.buffer().read(cx).snapshot(cx);
3321 let all_diagnostics = snapshot
3322 .diagnostics_in_range(0..snapshot.len())
3323 .collect::<Vec<_>>();
3324 assert_eq!(all_diagnostics.len(), 2);
3325 let expected_messages = [
3326 expected_workspace_pull_diagnostics_main_message,
3327 expected_pull_diagnostic_main_message,
3328 expected_push_diagnostic_main_message,
3329 ];
3330 for diagnostic in &all_diagnostics {
3331 assert!(
3332 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3333 "Unexpected diagnostics: {all_diagnostics:?}"
3334 );
3335 }
3336 });
3337}
3338
3339#[gpui::test(iterations = 10)]
3340async fn test_non_streamed_lsp_pull_diagnostics(
3341 cx_a: &mut TestAppContext,
3342 cx_b: &mut TestAppContext,
3343) {
3344 test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
3345}
3346
3347#[gpui::test(iterations = 10)]
3348async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3349 test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
3350}
3351
3352#[gpui::test(iterations = 10)]
3353async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3354 let mut server = TestServer::start(cx_a.executor()).await;
3355 let client_a = server.create_client(cx_a, "user_a").await;
3356 let client_b = server.create_client(cx_b, "user_b").await;
3357 server
3358 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3359 .await;
3360 let active_call_a = cx_a.read(ActiveCall::global);
3361
3362 cx_a.update(editor::init);
3363 cx_b.update(editor::init);
3364 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
3365 let inline_blame_off_settings = Some(InlineBlameSettings {
3366 enabled: Some(false),
3367 ..Default::default()
3368 });
3369 cx_a.update(|cx| {
3370 SettingsStore::update_global(cx, |store, cx| {
3371 store.update_user_settings(cx, |settings| {
3372 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3373 });
3374 });
3375 });
3376 cx_b.update(|cx| {
3377 SettingsStore::update_global(cx, |store, cx| {
3378 store.update_user_settings(cx, |settings| {
3379 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3380 });
3381 });
3382 });
3383
3384 client_a
3385 .fs()
3386 .insert_tree(
3387 path!("/my-repo"),
3388 json!({
3389 ".git": {},
3390 "file.txt": "line1\nline2\nline3\nline\n",
3391 }),
3392 )
3393 .await;
3394
3395 let blame = git::blame::Blame {
3396 entries: vec![
3397 blame_entry("1b1b1b", 0..1),
3398 blame_entry("0d0d0d", 1..2),
3399 blame_entry("3a3a3a", 2..3),
3400 blame_entry("4c4c4c", 3..4),
3401 ],
3402 messages: [
3403 ("1b1b1b", "message for idx-0"),
3404 ("0d0d0d", "message for idx-1"),
3405 ("3a3a3a", "message for idx-2"),
3406 ("4c4c4c", "message for idx-3"),
3407 ]
3408 .into_iter()
3409 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3410 .collect(),
3411 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
3412 };
3413 client_a.fs().set_blame_for_repo(
3414 Path::new(path!("/my-repo/.git")),
3415 vec![(repo_path("file.txt"), blame)],
3416 );
3417
3418 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3419 let project_id = active_call_a
3420 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3421 .await
3422 .unwrap();
3423
3424 // Create editor_a
3425 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3426 let editor_a = workspace_a
3427 .update_in(cx_a, |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
3435 // Join the project as client B.
3436 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3437 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3438 let editor_b = workspace_b
3439 .update_in(cx_b, |workspace, window, cx| {
3440 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3441 })
3442 .await
3443 .unwrap()
3444 .downcast::<Editor>()
3445 .unwrap();
3446 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3447 editor_b
3448 .buffer()
3449 .read(cx)
3450 .as_singleton()
3451 .unwrap()
3452 .read(cx)
3453 .remote_id()
3454 });
3455
3456 // client_b now requests git blame for the open buffer
3457 editor_b.update_in(cx_b, |editor_b, window, cx| {
3458 assert!(editor_b.blame().is_none());
3459 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3460 });
3461
3462 cx_a.executor().run_until_parked();
3463 cx_b.executor().run_until_parked();
3464
3465 editor_b.update(cx_b, |editor_b, cx| {
3466 let blame = editor_b.blame().expect("editor_b should have blame now");
3467 let entries = blame.update(cx, |blame, cx| {
3468 blame
3469 .blame_for_rows(
3470 &(0..4)
3471 .map(|row| RowInfo {
3472 buffer_row: Some(row),
3473 buffer_id: Some(buffer_id_b),
3474 ..Default::default()
3475 })
3476 .collect::<Vec<_>>(),
3477 cx,
3478 )
3479 .collect::<Vec<_>>()
3480 });
3481
3482 assert_eq!(
3483 entries,
3484 vec![
3485 Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
3486 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3487 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3488 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3489 ]
3490 );
3491
3492 blame.update(cx, |blame, _| {
3493 for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
3494 let details = blame.details_for_entry(*buffer, entry).unwrap();
3495 assert_eq!(details.message, format!("message for idx-{}", idx));
3496 assert_eq!(
3497 details.permalink.unwrap().to_string(),
3498 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
3499 );
3500 }
3501 });
3502 });
3503
3504 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3505 // which gets back to client_b.
3506 editor_b.update_in(cx_b, |editor_b, _, cx| {
3507 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3508 });
3509
3510 cx_a.executor().run_until_parked();
3511 cx_b.executor().run_until_parked();
3512
3513 editor_b.update(cx_b, |editor_b, cx| {
3514 let blame = editor_b.blame().expect("editor_b should have blame now");
3515 let entries = blame.update(cx, |blame, cx| {
3516 blame
3517 .blame_for_rows(
3518 &(0..4)
3519 .map(|row| RowInfo {
3520 buffer_row: Some(row),
3521 buffer_id: Some(buffer_id_b),
3522 ..Default::default()
3523 })
3524 .collect::<Vec<_>>(),
3525 cx,
3526 )
3527 .collect::<Vec<_>>()
3528 });
3529
3530 assert_eq!(
3531 entries,
3532 vec![
3533 None,
3534 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3535 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3536 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3537 ]
3538 );
3539 });
3540
3541 // Now editor_a also updates the file
3542 editor_a.update_in(cx_a, |editor_a, _, cx| {
3543 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3544 });
3545
3546 cx_a.executor().run_until_parked();
3547 cx_b.executor().run_until_parked();
3548
3549 editor_b.update(cx_b, |editor_b, cx| {
3550 let blame = editor_b.blame().expect("editor_b should have blame now");
3551 let entries = blame.update(cx, |blame, cx| {
3552 blame
3553 .blame_for_rows(
3554 &(0..4)
3555 .map(|row| RowInfo {
3556 buffer_row: Some(row),
3557 buffer_id: Some(buffer_id_b),
3558 ..Default::default()
3559 })
3560 .collect::<Vec<_>>(),
3561 cx,
3562 )
3563 .collect::<Vec<_>>()
3564 });
3565
3566 assert_eq!(
3567 entries,
3568 vec![
3569 None,
3570 None,
3571 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3572 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3573 ]
3574 );
3575 });
3576}
3577
3578#[gpui::test(iterations = 30)]
3579async fn test_collaborating_with_editorconfig(
3580 cx_a: &mut TestAppContext,
3581 cx_b: &mut TestAppContext,
3582) {
3583 let mut server = TestServer::start(cx_a.executor()).await;
3584 let client_a = server.create_client(cx_a, "user_a").await;
3585 let client_b = server.create_client(cx_b, "user_b").await;
3586 server
3587 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3588 .await;
3589 let active_call_a = cx_a.read(ActiveCall::global);
3590
3591 cx_b.update(editor::init);
3592
3593 // Set up a fake language server.
3594 client_a.language_registry().add(rust_lang());
3595 client_a
3596 .fs()
3597 .insert_tree(
3598 path!("/a"),
3599 json!({
3600 "src": {
3601 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3602 "other_mod": {
3603 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3604 ".editorconfig": "",
3605 },
3606 },
3607 ".editorconfig": "[*]\ntab_width = 2\n",
3608 }),
3609 )
3610 .await;
3611 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3612 let project_id = active_call_a
3613 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3614 .await
3615 .unwrap();
3616 let main_buffer_a = project_a
3617 .update(cx_a, |p, cx| {
3618 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3619 })
3620 .await
3621 .unwrap();
3622 let other_buffer_a = project_a
3623 .update(cx_a, |p, cx| {
3624 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3625 })
3626 .await
3627 .unwrap();
3628 let cx_a = cx_a.add_empty_window();
3629 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3630 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3631 });
3632 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3633 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3634 });
3635 let mut main_editor_cx_a = EditorTestContext {
3636 cx: cx_a.clone(),
3637 window: cx_a.window_handle(),
3638 editor: main_editor_a,
3639 assertion_cx: AssertionContextManager::new(),
3640 };
3641 let mut other_editor_cx_a = EditorTestContext {
3642 cx: cx_a.clone(),
3643 window: cx_a.window_handle(),
3644 editor: other_editor_a,
3645 assertion_cx: AssertionContextManager::new(),
3646 };
3647
3648 // Join the project as client B.
3649 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3650 let main_buffer_b = project_b
3651 .update(cx_b, |p, cx| {
3652 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3653 })
3654 .await
3655 .unwrap();
3656 let other_buffer_b = project_b
3657 .update(cx_b, |p, cx| {
3658 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3659 })
3660 .await
3661 .unwrap();
3662 let cx_b = cx_b.add_empty_window();
3663 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3664 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3665 });
3666 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3667 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3668 });
3669 let mut main_editor_cx_b = EditorTestContext {
3670 cx: cx_b.clone(),
3671 window: cx_b.window_handle(),
3672 editor: main_editor_b,
3673 assertion_cx: AssertionContextManager::new(),
3674 };
3675 let mut other_editor_cx_b = EditorTestContext {
3676 cx: cx_b.clone(),
3677 window: cx_b.window_handle(),
3678 editor: other_editor_b,
3679 assertion_cx: AssertionContextManager::new(),
3680 };
3681
3682 let initial_main = indoc! {"
3683ˇmod other;
3684fn main() { let foo = other::foo(); }"};
3685 let initial_other = indoc! {"
3686ˇpub fn foo() -> usize {
3687 4
3688}"};
3689
3690 let first_tabbed_main = indoc! {"
3691 ˇmod other;
3692fn main() { let foo = other::foo(); }"};
3693 tab_undo_assert(
3694 &mut main_editor_cx_a,
3695 &mut main_editor_cx_b,
3696 initial_main,
3697 first_tabbed_main,
3698 true,
3699 );
3700 tab_undo_assert(
3701 &mut main_editor_cx_a,
3702 &mut main_editor_cx_b,
3703 initial_main,
3704 first_tabbed_main,
3705 false,
3706 );
3707
3708 let first_tabbed_other = indoc! {"
3709 ˇpub fn foo() -> usize {
3710 4
3711}"};
3712 tab_undo_assert(
3713 &mut other_editor_cx_a,
3714 &mut other_editor_cx_b,
3715 initial_other,
3716 first_tabbed_other,
3717 true,
3718 );
3719 tab_undo_assert(
3720 &mut other_editor_cx_a,
3721 &mut other_editor_cx_b,
3722 initial_other,
3723 first_tabbed_other,
3724 false,
3725 );
3726
3727 client_a
3728 .fs()
3729 .atomic_write(
3730 PathBuf::from(path!("/a/src/.editorconfig")),
3731 "[*]\ntab_width = 3\n".to_owned(),
3732 )
3733 .await
3734 .unwrap();
3735 cx_a.run_until_parked();
3736 cx_b.run_until_parked();
3737
3738 let second_tabbed_main = indoc! {"
3739 ˇmod other;
3740fn main() { let foo = other::foo(); }"};
3741 tab_undo_assert(
3742 &mut main_editor_cx_a,
3743 &mut main_editor_cx_b,
3744 initial_main,
3745 second_tabbed_main,
3746 true,
3747 );
3748 tab_undo_assert(
3749 &mut main_editor_cx_a,
3750 &mut main_editor_cx_b,
3751 initial_main,
3752 second_tabbed_main,
3753 false,
3754 );
3755
3756 let second_tabbed_other = indoc! {"
3757 ˇpub fn foo() -> usize {
3758 4
3759}"};
3760 tab_undo_assert(
3761 &mut other_editor_cx_a,
3762 &mut other_editor_cx_b,
3763 initial_other,
3764 second_tabbed_other,
3765 true,
3766 );
3767 tab_undo_assert(
3768 &mut other_editor_cx_a,
3769 &mut other_editor_cx_b,
3770 initial_other,
3771 second_tabbed_other,
3772 false,
3773 );
3774
3775 let editorconfig_buffer_b = project_b
3776 .update(cx_b, |p, cx| {
3777 p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
3778 })
3779 .await
3780 .unwrap();
3781 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3782 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3783 });
3784 project_b
3785 .update(cx_b, |project, cx| {
3786 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3787 })
3788 .await
3789 .unwrap();
3790 cx_a.run_until_parked();
3791 cx_b.run_until_parked();
3792
3793 tab_undo_assert(
3794 &mut main_editor_cx_a,
3795 &mut main_editor_cx_b,
3796 initial_main,
3797 second_tabbed_main,
3798 true,
3799 );
3800 tab_undo_assert(
3801 &mut main_editor_cx_a,
3802 &mut main_editor_cx_b,
3803 initial_main,
3804 second_tabbed_main,
3805 false,
3806 );
3807
3808 let third_tabbed_other = indoc! {"
3809 ˇpub fn foo() -> usize {
3810 4
3811}"};
3812 tab_undo_assert(
3813 &mut other_editor_cx_a,
3814 &mut other_editor_cx_b,
3815 initial_other,
3816 third_tabbed_other,
3817 true,
3818 );
3819
3820 tab_undo_assert(
3821 &mut other_editor_cx_a,
3822 &mut other_editor_cx_b,
3823 initial_other,
3824 third_tabbed_other,
3825 false,
3826 );
3827}
3828
3829#[gpui::test]
3830async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3831 let executor = cx_a.executor();
3832 let mut server = TestServer::start(executor.clone()).await;
3833 let client_a = server.create_client(cx_a, "user_a").await;
3834 let client_b = server.create_client(cx_b, "user_b").await;
3835 server
3836 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3837 .await;
3838 let active_call_a = cx_a.read(ActiveCall::global);
3839 let active_call_b = cx_b.read(ActiveCall::global);
3840 cx_a.update(editor::init);
3841 cx_b.update(editor::init);
3842 client_a
3843 .fs()
3844 .insert_tree(
3845 "/a",
3846 json!({
3847 "test.txt": "one\ntwo\nthree\nfour\nfive",
3848 }),
3849 )
3850 .await;
3851 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3852 let project_path = ProjectPath {
3853 worktree_id,
3854 path: rel_path(&"test.txt").into(),
3855 };
3856 let abs_path = project_a.read_with(cx_a, |project, cx| {
3857 project
3858 .absolute_path(&project_path, cx)
3859 .map(Arc::from)
3860 .unwrap()
3861 });
3862
3863 active_call_a
3864 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
3865 .await
3866 .unwrap();
3867 let project_id = active_call_a
3868 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3869 .await
3870 .unwrap();
3871 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3872 active_call_b
3873 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
3874 .await
3875 .unwrap();
3876 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3877 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3878
3879 // Client A opens an editor.
3880 let editor_a = workspace_a
3881 .update_in(cx_a, |workspace, window, cx| {
3882 workspace.open_path(project_path.clone(), None, true, window, cx)
3883 })
3884 .await
3885 .unwrap()
3886 .downcast::<Editor>()
3887 .unwrap();
3888
3889 // Client B opens same editor as A.
3890 let editor_b = workspace_b
3891 .update_in(cx_b, |workspace, window, cx| {
3892 workspace.open_path(project_path.clone(), None, true, window, cx)
3893 })
3894 .await
3895 .unwrap()
3896 .downcast::<Editor>()
3897 .unwrap();
3898
3899 cx_a.run_until_parked();
3900 cx_b.run_until_parked();
3901
3902 // Client A adds breakpoint on line (1)
3903 editor_a.update_in(cx_a, |editor, window, cx| {
3904 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3905 });
3906
3907 cx_a.run_until_parked();
3908 cx_b.run_until_parked();
3909
3910 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3911 editor
3912 .breakpoint_store()
3913 .unwrap()
3914 .read(cx)
3915 .all_source_breakpoints(cx)
3916 });
3917 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
3918 editor
3919 .breakpoint_store()
3920 .unwrap()
3921 .read(cx)
3922 .all_source_breakpoints(cx)
3923 });
3924
3925 assert_eq!(1, breakpoints_a.len());
3926 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
3927 assert_eq!(breakpoints_a, breakpoints_b);
3928
3929 // Client B adds breakpoint on line(2)
3930 editor_b.update_in(cx_b, |editor, window, cx| {
3931 editor.move_down(&editor::actions::MoveDown, window, cx);
3932 editor.move_down(&editor::actions::MoveDown, window, cx);
3933 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3934 });
3935
3936 cx_a.run_until_parked();
3937 cx_b.run_until_parked();
3938
3939 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3940 editor
3941 .breakpoint_store()
3942 .unwrap()
3943 .read(cx)
3944 .all_source_breakpoints(cx)
3945 });
3946 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
3947 editor
3948 .breakpoint_store()
3949 .unwrap()
3950 .read(cx)
3951 .all_source_breakpoints(cx)
3952 });
3953
3954 assert_eq!(1, breakpoints_a.len());
3955 assert_eq!(breakpoints_a, breakpoints_b);
3956 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
3957
3958 // Client A removes last added breakpoint from client B
3959 editor_a.update_in(cx_a, |editor, window, cx| {
3960 editor.move_down(&editor::actions::MoveDown, window, cx);
3961 editor.move_down(&editor::actions::MoveDown, window, cx);
3962 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3963 });
3964
3965 cx_a.run_until_parked();
3966 cx_b.run_until_parked();
3967
3968 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3969 editor
3970 .breakpoint_store()
3971 .unwrap()
3972 .read(cx)
3973 .all_source_breakpoints(cx)
3974 });
3975 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
3976 editor
3977 .breakpoint_store()
3978 .unwrap()
3979 .read(cx)
3980 .all_source_breakpoints(cx)
3981 });
3982
3983 assert_eq!(1, breakpoints_a.len());
3984 assert_eq!(breakpoints_a, breakpoints_b);
3985 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
3986
3987 // Client B removes first added breakpoint by client A
3988 editor_b.update_in(cx_b, |editor, window, cx| {
3989 editor.move_up(&editor::actions::MoveUp, window, cx);
3990 editor.move_up(&editor::actions::MoveUp, window, cx);
3991 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3992 });
3993
3994 cx_a.run_until_parked();
3995 cx_b.run_until_parked();
3996
3997 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3998 editor
3999 .breakpoint_store()
4000 .unwrap()
4001 .read(cx)
4002 .all_source_breakpoints(cx)
4003 });
4004 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4005 editor
4006 .breakpoint_store()
4007 .unwrap()
4008 .read(cx)
4009 .all_source_breakpoints(cx)
4010 });
4011
4012 assert_eq!(0, breakpoints_a.len());
4013 assert_eq!(breakpoints_a, breakpoints_b);
4014}
4015
4016#[gpui::test]
4017async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4018 let mut server = TestServer::start(cx_a.executor()).await;
4019 let client_a = server.create_client(cx_a, "user_a").await;
4020 let client_b = server.create_client(cx_b, "user_b").await;
4021 server
4022 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4023 .await;
4024 let active_call_a = cx_a.read(ActiveCall::global);
4025 let active_call_b = cx_b.read(ActiveCall::global);
4026
4027 cx_a.update(editor::init);
4028 cx_b.update(editor::init);
4029
4030 client_a.language_registry().add(rust_lang());
4031 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4032 "Rust",
4033 FakeLspAdapter {
4034 name: "rust-analyzer",
4035 ..FakeLspAdapter::default()
4036 },
4037 );
4038 client_b.language_registry().add(rust_lang());
4039 client_b.language_registry().register_fake_lsp_adapter(
4040 "Rust",
4041 FakeLspAdapter {
4042 name: "rust-analyzer",
4043 ..FakeLspAdapter::default()
4044 },
4045 );
4046
4047 client_a
4048 .fs()
4049 .insert_tree(
4050 path!("/a"),
4051 json!({
4052 "main.rs": "fn main() {}",
4053 }),
4054 )
4055 .await;
4056 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4057 active_call_a
4058 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4059 .await
4060 .unwrap();
4061 let project_id = active_call_a
4062 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4063 .await
4064 .unwrap();
4065
4066 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4067 active_call_b
4068 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4069 .await
4070 .unwrap();
4071
4072 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4073 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4074
4075 let editor_a = workspace_a
4076 .update_in(cx_a, |workspace, window, cx| {
4077 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4078 })
4079 .await
4080 .unwrap()
4081 .downcast::<Editor>()
4082 .unwrap();
4083
4084 let editor_b = workspace_b
4085 .update_in(cx_b, |workspace, window, cx| {
4086 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4087 })
4088 .await
4089 .unwrap()
4090 .downcast::<Editor>()
4091 .unwrap();
4092
4093 let fake_language_server = fake_language_servers.next().await.unwrap();
4094
4095 // host
4096 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4097 |params, _| async move {
4098 assert_eq!(
4099 params.text_document.uri,
4100 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4101 );
4102 assert_eq!(params.position, lsp::Position::new(0, 0));
4103 Ok(Some(ExpandedMacro {
4104 name: "test_macro_name".to_string(),
4105 expansion: "test_macro_expansion on the host".to_string(),
4106 }))
4107 },
4108 );
4109
4110 editor_a.update_in(cx_a, |editor, window, cx| {
4111 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4112 });
4113 expand_request_a.next().await.unwrap();
4114 cx_a.run_until_parked();
4115
4116 workspace_a.update(cx_a, |workspace, cx| {
4117 workspace.active_pane().update(cx, |pane, cx| {
4118 assert_eq!(
4119 pane.items_len(),
4120 2,
4121 "Should have added a macro expansion to the host's pane"
4122 );
4123 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4124 new_editor.update(cx, |editor, cx| {
4125 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
4126 });
4127 })
4128 });
4129
4130 // client
4131 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4132 |params, _| async move {
4133 assert_eq!(
4134 params.text_document.uri,
4135 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4136 );
4137 assert_eq!(
4138 params.position,
4139 lsp::Position::new(0, 12),
4140 "editor_b has selected the entire text and should query for a different position"
4141 );
4142 Ok(Some(ExpandedMacro {
4143 name: "test_macro_name".to_string(),
4144 expansion: "test_macro_expansion on the client".to_string(),
4145 }))
4146 },
4147 );
4148
4149 editor_b.update_in(cx_b, |editor, window, cx| {
4150 editor.select_all(&SelectAll, window, cx);
4151 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4152 });
4153 expand_request_b.next().await.unwrap();
4154 cx_b.run_until_parked();
4155
4156 workspace_b.update(cx_b, |workspace, cx| {
4157 workspace.active_pane().update(cx, |pane, cx| {
4158 assert_eq!(
4159 pane.items_len(),
4160 2,
4161 "Should have added a macro expansion to the client's pane"
4162 );
4163 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4164 new_editor.update(cx, |editor, cx| {
4165 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
4166 });
4167 })
4168 });
4169}
4170
4171#[track_caller]
4172fn tab_undo_assert(
4173 cx_a: &mut EditorTestContext,
4174 cx_b: &mut EditorTestContext,
4175 expected_initial: &str,
4176 expected_tabbed: &str,
4177 a_tabs: bool,
4178) {
4179 cx_a.assert_editor_state(expected_initial);
4180 cx_b.assert_editor_state(expected_initial);
4181
4182 if a_tabs {
4183 cx_a.update_editor(|editor, window, cx| {
4184 editor.tab(&editor::actions::Tab, window, cx);
4185 });
4186 } else {
4187 cx_b.update_editor(|editor, window, cx| {
4188 editor.tab(&editor::actions::Tab, window, cx);
4189 });
4190 }
4191
4192 cx_a.run_until_parked();
4193 cx_b.run_until_parked();
4194
4195 cx_a.assert_editor_state(expected_tabbed);
4196 cx_b.assert_editor_state(expected_tabbed);
4197
4198 if a_tabs {
4199 cx_a.update_editor(|editor, window, cx| {
4200 editor.undo(&editor::actions::Undo, window, cx);
4201 });
4202 } else {
4203 cx_b.update_editor(|editor, window, cx| {
4204 editor.undo(&editor::actions::Undo, window, cx);
4205 });
4206 }
4207 cx_a.run_until_parked();
4208 cx_b.run_until_parked();
4209 cx_a.assert_editor_state(expected_initial);
4210 cx_b.assert_editor_state(expected_initial);
4211}
4212
4213fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4214 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4215
4216 let mut all_cached_labels = Vec::new();
4217 let mut all_fetched_hints = Vec::new();
4218 for buffer in editor.buffer().read(cx).all_buffers() {
4219 lsp_store.update(cx, |lsp_store, cx| {
4220 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4221 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4222 let mut label = hint.text().to_string();
4223 if hint.padding_left {
4224 label.insert(0, ' ');
4225 }
4226 if hint.padding_right {
4227 label.push_str(" ");
4228 }
4229 label
4230 }));
4231 all_fetched_hints.extend(hints.all_fetched_hints());
4232 });
4233 }
4234
4235 assert!(
4236 all_fetched_hints.is_empty(),
4237 "Did not expect background hints fetch tasks, but got {} of them",
4238 all_fetched_hints.len()
4239 );
4240
4241 all_cached_labels
4242}
4243
4244#[track_caller]
4245fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
4246 editor
4247 .all_inlays(cx)
4248 .into_iter()
4249 .filter_map(|inlay| inlay.get_color())
4250 .map(Rgba::from)
4251 .collect()
4252}
4253
4254fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
4255 git::blame::BlameEntry {
4256 sha: sha.parse().unwrap(),
4257 range,
4258 ..Default::default()
4259 }
4260}