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