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