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