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