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