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