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