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