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