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