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