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