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