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.len() > 0,
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 delay_ms: None,
3105 min_column: None,
3106 show_commit_summary: false,
3107 });
3108 cx_a.update(|cx| {
3109 SettingsStore::update_global(cx, |store, cx| {
3110 store.update_user_settings::<ProjectSettings>(cx, |settings| {
3111 settings.git.inline_blame = inline_blame_off_settings;
3112 });
3113 });
3114 });
3115 cx_b.update(|cx| {
3116 SettingsStore::update_global(cx, |store, cx| {
3117 store.update_user_settings::<ProjectSettings>(cx, |settings| {
3118 settings.git.inline_blame = inline_blame_off_settings;
3119 });
3120 });
3121 });
3122
3123 client_a
3124 .fs()
3125 .insert_tree(
3126 path!("/my-repo"),
3127 json!({
3128 ".git": {},
3129 "file.txt": "line1\nline2\nline3\nline\n",
3130 }),
3131 )
3132 .await;
3133
3134 let blame = git::blame::Blame {
3135 entries: vec![
3136 blame_entry("1b1b1b", 0..1),
3137 blame_entry("0d0d0d", 1..2),
3138 blame_entry("3a3a3a", 2..3),
3139 blame_entry("4c4c4c", 3..4),
3140 ],
3141 messages: [
3142 ("1b1b1b", "message for idx-0"),
3143 ("0d0d0d", "message for idx-1"),
3144 ("3a3a3a", "message for idx-2"),
3145 ("4c4c4c", "message for idx-3"),
3146 ]
3147 .into_iter()
3148 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3149 .collect(),
3150 remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
3151 };
3152 client_a.fs().set_blame_for_repo(
3153 Path::new(path!("/my-repo/.git")),
3154 vec![("file.txt".into(), blame)],
3155 );
3156
3157 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3158 let project_id = active_call_a
3159 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3160 .await
3161 .unwrap();
3162
3163 // Create editor_a
3164 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3165 let editor_a = workspace_a
3166 .update_in(cx_a, |workspace, window, cx| {
3167 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
3168 })
3169 .await
3170 .unwrap()
3171 .downcast::<Editor>()
3172 .unwrap();
3173
3174 // Join the project as client B.
3175 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3176 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3177 let editor_b = workspace_b
3178 .update_in(cx_b, |workspace, window, cx| {
3179 workspace.open_path((worktree_id, "file.txt"), None, true, window, cx)
3180 })
3181 .await
3182 .unwrap()
3183 .downcast::<Editor>()
3184 .unwrap();
3185 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3186 editor_b
3187 .buffer()
3188 .read(cx)
3189 .as_singleton()
3190 .unwrap()
3191 .read(cx)
3192 .remote_id()
3193 });
3194
3195 // client_b now requests git blame for the open buffer
3196 editor_b.update_in(cx_b, |editor_b, window, cx| {
3197 assert!(editor_b.blame().is_none());
3198 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3199 });
3200
3201 cx_a.executor().run_until_parked();
3202 cx_b.executor().run_until_parked();
3203
3204 editor_b.update(cx_b, |editor_b, cx| {
3205 let blame = editor_b.blame().expect("editor_b should have blame now");
3206 let entries = blame.update(cx, |blame, cx| {
3207 blame
3208 .blame_for_rows(
3209 &(0..4)
3210 .map(|row| RowInfo {
3211 buffer_row: Some(row),
3212 buffer_id: Some(buffer_id_b),
3213 ..Default::default()
3214 })
3215 .collect::<Vec<_>>(),
3216 cx,
3217 )
3218 .collect::<Vec<_>>()
3219 });
3220
3221 assert_eq!(
3222 entries,
3223 vec![
3224 Some(blame_entry("1b1b1b", 0..1)),
3225 Some(blame_entry("0d0d0d", 1..2)),
3226 Some(blame_entry("3a3a3a", 2..3)),
3227 Some(blame_entry("4c4c4c", 3..4)),
3228 ]
3229 );
3230
3231 blame.update(cx, |blame, _| {
3232 for (idx, entry) in entries.iter().flatten().enumerate() {
3233 let details = blame.details_for_entry(entry).unwrap();
3234 assert_eq!(details.message, format!("message for idx-{}", idx));
3235 assert_eq!(
3236 details.permalink.unwrap().to_string(),
3237 format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
3238 );
3239 }
3240 });
3241 });
3242
3243 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3244 // which gets back to client_b.
3245 editor_b.update_in(cx_b, |editor_b, _, cx| {
3246 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3247 });
3248
3249 cx_a.executor().run_until_parked();
3250 cx_b.executor().run_until_parked();
3251
3252 editor_b.update(cx_b, |editor_b, cx| {
3253 let blame = editor_b.blame().expect("editor_b should have blame now");
3254 let entries = blame.update(cx, |blame, cx| {
3255 blame
3256 .blame_for_rows(
3257 &(0..4)
3258 .map(|row| RowInfo {
3259 buffer_row: Some(row),
3260 buffer_id: Some(buffer_id_b),
3261 ..Default::default()
3262 })
3263 .collect::<Vec<_>>(),
3264 cx,
3265 )
3266 .collect::<Vec<_>>()
3267 });
3268
3269 assert_eq!(
3270 entries,
3271 vec![
3272 None,
3273 Some(blame_entry("0d0d0d", 1..2)),
3274 Some(blame_entry("3a3a3a", 2..3)),
3275 Some(blame_entry("4c4c4c", 3..4)),
3276 ]
3277 );
3278 });
3279
3280 // Now editor_a also updates the file
3281 editor_a.update_in(cx_a, |editor_a, _, cx| {
3282 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3283 });
3284
3285 cx_a.executor().run_until_parked();
3286 cx_b.executor().run_until_parked();
3287
3288 editor_b.update(cx_b, |editor_b, cx| {
3289 let blame = editor_b.blame().expect("editor_b should have blame now");
3290 let entries = blame.update(cx, |blame, cx| {
3291 blame
3292 .blame_for_rows(
3293 &(0..4)
3294 .map(|row| RowInfo {
3295 buffer_row: Some(row),
3296 buffer_id: Some(buffer_id_b),
3297 ..Default::default()
3298 })
3299 .collect::<Vec<_>>(),
3300 cx,
3301 )
3302 .collect::<Vec<_>>()
3303 });
3304
3305 assert_eq!(
3306 entries,
3307 vec![
3308 None,
3309 None,
3310 Some(blame_entry("3a3a3a", 2..3)),
3311 Some(blame_entry("4c4c4c", 3..4)),
3312 ]
3313 );
3314 });
3315}
3316
3317#[gpui::test(iterations = 30)]
3318async fn test_collaborating_with_editorconfig(
3319 cx_a: &mut TestAppContext,
3320 cx_b: &mut TestAppContext,
3321) {
3322 let mut server = TestServer::start(cx_a.executor()).await;
3323 let client_a = server.create_client(cx_a, "user_a").await;
3324 let client_b = server.create_client(cx_b, "user_b").await;
3325 server
3326 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3327 .await;
3328 let active_call_a = cx_a.read(ActiveCall::global);
3329
3330 cx_b.update(editor::init);
3331
3332 // Set up a fake language server.
3333 client_a.language_registry().add(rust_lang());
3334 client_a
3335 .fs()
3336 .insert_tree(
3337 path!("/a"),
3338 json!({
3339 "src": {
3340 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3341 "other_mod": {
3342 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3343 ".editorconfig": "",
3344 },
3345 },
3346 ".editorconfig": "[*]\ntab_width = 2\n",
3347 }),
3348 )
3349 .await;
3350 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3351 let project_id = active_call_a
3352 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3353 .await
3354 .unwrap();
3355 let main_buffer_a = project_a
3356 .update(cx_a, |p, cx| {
3357 p.open_buffer((worktree_id, "src/main.rs"), cx)
3358 })
3359 .await
3360 .unwrap();
3361 let other_buffer_a = project_a
3362 .update(cx_a, |p, cx| {
3363 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
3364 })
3365 .await
3366 .unwrap();
3367 let cx_a = cx_a.add_empty_window();
3368 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3369 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3370 });
3371 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3372 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3373 });
3374 let mut main_editor_cx_a = EditorTestContext {
3375 cx: cx_a.clone(),
3376 window: cx_a.window_handle(),
3377 editor: main_editor_a,
3378 assertion_cx: AssertionContextManager::new(),
3379 };
3380 let mut other_editor_cx_a = EditorTestContext {
3381 cx: cx_a.clone(),
3382 window: cx_a.window_handle(),
3383 editor: other_editor_a,
3384 assertion_cx: AssertionContextManager::new(),
3385 };
3386
3387 // Join the project as client B.
3388 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3389 let main_buffer_b = project_b
3390 .update(cx_b, |p, cx| {
3391 p.open_buffer((worktree_id, "src/main.rs"), cx)
3392 })
3393 .await
3394 .unwrap();
3395 let other_buffer_b = project_b
3396 .update(cx_b, |p, cx| {
3397 p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
3398 })
3399 .await
3400 .unwrap();
3401 let cx_b = cx_b.add_empty_window();
3402 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3403 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3404 });
3405 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3406 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3407 });
3408 let mut main_editor_cx_b = EditorTestContext {
3409 cx: cx_b.clone(),
3410 window: cx_b.window_handle(),
3411 editor: main_editor_b,
3412 assertion_cx: AssertionContextManager::new(),
3413 };
3414 let mut other_editor_cx_b = EditorTestContext {
3415 cx: cx_b.clone(),
3416 window: cx_b.window_handle(),
3417 editor: other_editor_b,
3418 assertion_cx: AssertionContextManager::new(),
3419 };
3420
3421 let initial_main = indoc! {"
3422ˇmod other;
3423fn main() { let foo = other::foo(); }"};
3424 let initial_other = indoc! {"
3425ˇpub fn foo() -> usize {
3426 4
3427}"};
3428
3429 let first_tabbed_main = indoc! {"
3430 ˇmod other;
3431fn main() { let foo = other::foo(); }"};
3432 tab_undo_assert(
3433 &mut main_editor_cx_a,
3434 &mut main_editor_cx_b,
3435 initial_main,
3436 first_tabbed_main,
3437 true,
3438 );
3439 tab_undo_assert(
3440 &mut main_editor_cx_a,
3441 &mut main_editor_cx_b,
3442 initial_main,
3443 first_tabbed_main,
3444 false,
3445 );
3446
3447 let first_tabbed_other = indoc! {"
3448 ˇpub fn foo() -> usize {
3449 4
3450}"};
3451 tab_undo_assert(
3452 &mut other_editor_cx_a,
3453 &mut other_editor_cx_b,
3454 initial_other,
3455 first_tabbed_other,
3456 true,
3457 );
3458 tab_undo_assert(
3459 &mut other_editor_cx_a,
3460 &mut other_editor_cx_b,
3461 initial_other,
3462 first_tabbed_other,
3463 false,
3464 );
3465
3466 client_a
3467 .fs()
3468 .atomic_write(
3469 PathBuf::from(path!("/a/src/.editorconfig")),
3470 "[*]\ntab_width = 3\n".to_owned(),
3471 )
3472 .await
3473 .unwrap();
3474 cx_a.run_until_parked();
3475 cx_b.run_until_parked();
3476
3477 let second_tabbed_main = indoc! {"
3478 ˇmod other;
3479fn main() { let foo = other::foo(); }"};
3480 tab_undo_assert(
3481 &mut main_editor_cx_a,
3482 &mut main_editor_cx_b,
3483 initial_main,
3484 second_tabbed_main,
3485 true,
3486 );
3487 tab_undo_assert(
3488 &mut main_editor_cx_a,
3489 &mut main_editor_cx_b,
3490 initial_main,
3491 second_tabbed_main,
3492 false,
3493 );
3494
3495 let second_tabbed_other = indoc! {"
3496 ˇpub fn foo() -> usize {
3497 4
3498}"};
3499 tab_undo_assert(
3500 &mut other_editor_cx_a,
3501 &mut other_editor_cx_b,
3502 initial_other,
3503 second_tabbed_other,
3504 true,
3505 );
3506 tab_undo_assert(
3507 &mut other_editor_cx_a,
3508 &mut other_editor_cx_b,
3509 initial_other,
3510 second_tabbed_other,
3511 false,
3512 );
3513
3514 let editorconfig_buffer_b = project_b
3515 .update(cx_b, |p, cx| {
3516 p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
3517 })
3518 .await
3519 .unwrap();
3520 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3521 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3522 });
3523 project_b
3524 .update(cx_b, |project, cx| {
3525 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3526 })
3527 .await
3528 .unwrap();
3529 cx_a.run_until_parked();
3530 cx_b.run_until_parked();
3531
3532 tab_undo_assert(
3533 &mut main_editor_cx_a,
3534 &mut main_editor_cx_b,
3535 initial_main,
3536 second_tabbed_main,
3537 true,
3538 );
3539 tab_undo_assert(
3540 &mut main_editor_cx_a,
3541 &mut main_editor_cx_b,
3542 initial_main,
3543 second_tabbed_main,
3544 false,
3545 );
3546
3547 let third_tabbed_other = indoc! {"
3548 ˇpub fn foo() -> usize {
3549 4
3550}"};
3551 tab_undo_assert(
3552 &mut other_editor_cx_a,
3553 &mut other_editor_cx_b,
3554 initial_other,
3555 third_tabbed_other,
3556 true,
3557 );
3558
3559 tab_undo_assert(
3560 &mut other_editor_cx_a,
3561 &mut other_editor_cx_b,
3562 initial_other,
3563 third_tabbed_other,
3564 false,
3565 );
3566}
3567
3568#[gpui::test]
3569async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3570 let executor = cx_a.executor();
3571 let mut server = TestServer::start(executor.clone()).await;
3572 let client_a = server.create_client(cx_a, "user_a").await;
3573 let client_b = server.create_client(cx_b, "user_b").await;
3574 server
3575 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3576 .await;
3577 let active_call_a = cx_a.read(ActiveCall::global);
3578 let active_call_b = cx_b.read(ActiveCall::global);
3579 cx_a.update(editor::init);
3580 cx_b.update(editor::init);
3581 client_a
3582 .fs()
3583 .insert_tree(
3584 "/a",
3585 json!({
3586 "test.txt": "one\ntwo\nthree\nfour\nfive",
3587 }),
3588 )
3589 .await;
3590 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3591 let project_path = ProjectPath {
3592 worktree_id,
3593 path: Arc::from(Path::new(&"test.txt")),
3594 };
3595 let abs_path = project_a.read_with(cx_a, |project, cx| {
3596 project
3597 .absolute_path(&project_path, cx)
3598 .map(|path_buf| Arc::from(path_buf.to_owned()))
3599 .unwrap()
3600 });
3601
3602 active_call_a
3603 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
3604 .await
3605 .unwrap();
3606 let project_id = active_call_a
3607 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3608 .await
3609 .unwrap();
3610 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3611 active_call_b
3612 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
3613 .await
3614 .unwrap();
3615 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3616 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3617
3618 // Client A opens an editor.
3619 let editor_a = workspace_a
3620 .update_in(cx_a, |workspace, window, cx| {
3621 workspace.open_path(project_path.clone(), None, true, window, cx)
3622 })
3623 .await
3624 .unwrap()
3625 .downcast::<Editor>()
3626 .unwrap();
3627
3628 // Client B opens same editor as A.
3629 let editor_b = workspace_b
3630 .update_in(cx_b, |workspace, window, cx| {
3631 workspace.open_path(project_path.clone(), None, true, window, cx)
3632 })
3633 .await
3634 .unwrap()
3635 .downcast::<Editor>()
3636 .unwrap();
3637
3638 cx_a.run_until_parked();
3639 cx_b.run_until_parked();
3640
3641 // Client A adds breakpoint on line (1)
3642 editor_a.update_in(cx_a, |editor, window, cx| {
3643 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3644 });
3645
3646 cx_a.run_until_parked();
3647 cx_b.run_until_parked();
3648
3649 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3650 editor
3651 .breakpoint_store()
3652 .clone()
3653 .unwrap()
3654 .read(cx)
3655 .all_source_breakpoints(cx)
3656 .clone()
3657 });
3658 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
3659 editor
3660 .breakpoint_store()
3661 .clone()
3662 .unwrap()
3663 .read(cx)
3664 .all_source_breakpoints(cx)
3665 .clone()
3666 });
3667
3668 assert_eq!(1, breakpoints_a.len());
3669 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
3670 assert_eq!(breakpoints_a, breakpoints_b);
3671
3672 // Client B adds breakpoint on line(2)
3673 editor_b.update_in(cx_b, |editor, window, cx| {
3674 editor.move_down(&editor::actions::MoveDown, window, cx);
3675 editor.move_down(&editor::actions::MoveDown, window, cx);
3676 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3677 });
3678
3679 cx_a.run_until_parked();
3680 cx_b.run_until_parked();
3681
3682 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3683 editor
3684 .breakpoint_store()
3685 .clone()
3686 .unwrap()
3687 .read(cx)
3688 .all_source_breakpoints(cx)
3689 .clone()
3690 });
3691 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
3692 editor
3693 .breakpoint_store()
3694 .clone()
3695 .unwrap()
3696 .read(cx)
3697 .all_source_breakpoints(cx)
3698 .clone()
3699 });
3700
3701 assert_eq!(1, breakpoints_a.len());
3702 assert_eq!(breakpoints_a, breakpoints_b);
3703 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
3704
3705 // Client A removes last added breakpoint from client B
3706 editor_a.update_in(cx_a, |editor, window, cx| {
3707 editor.move_down(&editor::actions::MoveDown, window, cx);
3708 editor.move_down(&editor::actions::MoveDown, window, cx);
3709 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3710 });
3711
3712 cx_a.run_until_parked();
3713 cx_b.run_until_parked();
3714
3715 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3716 editor
3717 .breakpoint_store()
3718 .clone()
3719 .unwrap()
3720 .read(cx)
3721 .all_source_breakpoints(cx)
3722 .clone()
3723 });
3724 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
3725 editor
3726 .breakpoint_store()
3727 .clone()
3728 .unwrap()
3729 .read(cx)
3730 .all_source_breakpoints(cx)
3731 .clone()
3732 });
3733
3734 assert_eq!(1, breakpoints_a.len());
3735 assert_eq!(breakpoints_a, breakpoints_b);
3736 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
3737
3738 // Client B removes first added breakpoint by client A
3739 editor_b.update_in(cx_b, |editor, window, cx| {
3740 editor.move_up(&editor::actions::MoveUp, window, cx);
3741 editor.move_up(&editor::actions::MoveUp, window, cx);
3742 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
3743 });
3744
3745 cx_a.run_until_parked();
3746 cx_b.run_until_parked();
3747
3748 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
3749 editor
3750 .breakpoint_store()
3751 .clone()
3752 .unwrap()
3753 .read(cx)
3754 .all_source_breakpoints(cx)
3755 .clone()
3756 });
3757 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
3758 editor
3759 .breakpoint_store()
3760 .clone()
3761 .unwrap()
3762 .read(cx)
3763 .all_source_breakpoints(cx)
3764 .clone()
3765 });
3766
3767 assert_eq!(0, breakpoints_a.len());
3768 assert_eq!(breakpoints_a, breakpoints_b);
3769}
3770
3771#[gpui::test]
3772async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3773 let mut server = TestServer::start(cx_a.executor()).await;
3774 let client_a = server.create_client(cx_a, "user_a").await;
3775 let client_b = server.create_client(cx_b, "user_b").await;
3776 server
3777 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3778 .await;
3779 let active_call_a = cx_a.read(ActiveCall::global);
3780 let active_call_b = cx_b.read(ActiveCall::global);
3781
3782 cx_a.update(editor::init);
3783 cx_b.update(editor::init);
3784
3785 client_a.language_registry().add(rust_lang());
3786 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
3787 "Rust",
3788 FakeLspAdapter {
3789 name: "rust-analyzer",
3790 ..FakeLspAdapter::default()
3791 },
3792 );
3793 client_b.language_registry().add(rust_lang());
3794 client_b.language_registry().register_fake_lsp_adapter(
3795 "Rust",
3796 FakeLspAdapter {
3797 name: "rust-analyzer",
3798 ..FakeLspAdapter::default()
3799 },
3800 );
3801
3802 client_a
3803 .fs()
3804 .insert_tree(
3805 path!("/a"),
3806 json!({
3807 "main.rs": "fn main() {}",
3808 }),
3809 )
3810 .await;
3811 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3812 active_call_a
3813 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
3814 .await
3815 .unwrap();
3816 let project_id = active_call_a
3817 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3818 .await
3819 .unwrap();
3820
3821 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3822 active_call_b
3823 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
3824 .await
3825 .unwrap();
3826
3827 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3828 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3829
3830 let editor_a = workspace_a
3831 .update_in(cx_a, |workspace, window, cx| {
3832 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
3833 })
3834 .await
3835 .unwrap()
3836 .downcast::<Editor>()
3837 .unwrap();
3838
3839 let editor_b = workspace_b
3840 .update_in(cx_b, |workspace, window, cx| {
3841 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
3842 })
3843 .await
3844 .unwrap()
3845 .downcast::<Editor>()
3846 .unwrap();
3847
3848 let fake_language_server = fake_language_servers.next().await.unwrap();
3849
3850 // host
3851 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
3852 |params, _| async move {
3853 assert_eq!(
3854 params.text_document.uri,
3855 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3856 );
3857 assert_eq!(params.position, lsp::Position::new(0, 0));
3858 Ok(Some(ExpandedMacro {
3859 name: "test_macro_name".to_string(),
3860 expansion: "test_macro_expansion on the host".to_string(),
3861 }))
3862 },
3863 );
3864
3865 editor_a.update_in(cx_a, |editor, window, cx| {
3866 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
3867 });
3868 expand_request_a.next().await.unwrap();
3869 cx_a.run_until_parked();
3870
3871 workspace_a.update(cx_a, |workspace, cx| {
3872 workspace.active_pane().update(cx, |pane, cx| {
3873 assert_eq!(
3874 pane.items_len(),
3875 2,
3876 "Should have added a macro expansion to the host's pane"
3877 );
3878 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
3879 new_editor.update(cx, |editor, cx| {
3880 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
3881 });
3882 })
3883 });
3884
3885 // client
3886 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
3887 |params, _| async move {
3888 assert_eq!(
3889 params.text_document.uri,
3890 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
3891 );
3892 assert_eq!(
3893 params.position,
3894 lsp::Position::new(0, 12),
3895 "editor_b has selected the entire text and should query for a different position"
3896 );
3897 Ok(Some(ExpandedMacro {
3898 name: "test_macro_name".to_string(),
3899 expansion: "test_macro_expansion on the client".to_string(),
3900 }))
3901 },
3902 );
3903
3904 editor_b.update_in(cx_b, |editor, window, cx| {
3905 editor.select_all(&SelectAll, window, cx);
3906 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
3907 });
3908 expand_request_b.next().await.unwrap();
3909 cx_b.run_until_parked();
3910
3911 workspace_b.update(cx_b, |workspace, cx| {
3912 workspace.active_pane().update(cx, |pane, cx| {
3913 assert_eq!(
3914 pane.items_len(),
3915 2,
3916 "Should have added a macro expansion to the client's pane"
3917 );
3918 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
3919 new_editor.update(cx, |editor, cx| {
3920 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
3921 });
3922 })
3923 });
3924}
3925
3926#[track_caller]
3927fn tab_undo_assert(
3928 cx_a: &mut EditorTestContext,
3929 cx_b: &mut EditorTestContext,
3930 expected_initial: &str,
3931 expected_tabbed: &str,
3932 a_tabs: bool,
3933) {
3934 cx_a.assert_editor_state(expected_initial);
3935 cx_b.assert_editor_state(expected_initial);
3936
3937 if a_tabs {
3938 cx_a.update_editor(|editor, window, cx| {
3939 editor.tab(&editor::actions::Tab, window, cx);
3940 });
3941 } else {
3942 cx_b.update_editor(|editor, window, cx| {
3943 editor.tab(&editor::actions::Tab, window, cx);
3944 });
3945 }
3946
3947 cx_a.run_until_parked();
3948 cx_b.run_until_parked();
3949
3950 cx_a.assert_editor_state(expected_tabbed);
3951 cx_b.assert_editor_state(expected_tabbed);
3952
3953 if a_tabs {
3954 cx_a.update_editor(|editor, window, cx| {
3955 editor.undo(&editor::actions::Undo, window, cx);
3956 });
3957 } else {
3958 cx_b.update_editor(|editor, window, cx| {
3959 editor.undo(&editor::actions::Undo, window, cx);
3960 });
3961 }
3962 cx_a.run_until_parked();
3963 cx_b.run_until_parked();
3964 cx_a.assert_editor_state(expected_initial);
3965 cx_b.assert_editor_state(expected_initial);
3966}
3967
3968fn extract_hint_labels(editor: &Editor) -> Vec<String> {
3969 let mut labels = Vec::new();
3970 for hint in editor.inlay_hint_cache().hints() {
3971 match hint.label {
3972 project::InlayHintLabel::String(s) => labels.push(s),
3973 _ => unreachable!(),
3974 }
3975 }
3976 labels
3977}
3978
3979#[track_caller]
3980fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
3981 editor
3982 .all_inlays(cx)
3983 .into_iter()
3984 .filter_map(|inlay| inlay.get_color())
3985 .map(Rgba::from)
3986 .collect()
3987}
3988
3989fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
3990 git::blame::BlameEntry {
3991 sha: sha.parse().unwrap(),
3992 range,
3993 ..Default::default()
3994 }
3995}