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