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