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 // Workspace refresh now also triggers document diagnostic pulls for all open buffers
3372 pull_diagnostics_handle.next().await.unwrap();
3373 pull_diagnostics_handle.next().await.unwrap();
3374 assert_eq!(
3375 13,
3376 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3377 "Workspace refresh should trigger document pulls for all open buffers (main.rs and lib.rs)"
3378 );
3379 workspace_diagnostics_pulls_handle.next().await.unwrap();
3380 assert_eq!(
3381 workspace_diagnostic_start_count + 4,
3382 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3383 "Another workspace diagnostics pull should happen after the diagnostics refresh server request"
3384 );
3385 {
3386 assert!(
3387 diagnostics_pulls_result_ids.lock().await.len() > diagnostic_pulls_result_ids,
3388 "Document diagnostic pulls should happen after workspace refresh"
3389 );
3390 assert!(
3391 workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
3392 "More workspace diagnostics should be pulled"
3393 );
3394 }
3395 editor_b_lib.update(cx_b, |editor, cx| {
3396 let snapshot = editor.buffer().read(cx).snapshot(cx);
3397 let all_diagnostics = snapshot
3398 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3399 .collect::<Vec<_>>();
3400 let expected_messages = [
3401 expected_workspace_pull_diagnostics_lib_message,
3402 expected_pull_diagnostic_lib_message,
3403 expected_push_diagnostic_lib_message,
3404 ];
3405 assert_eq!(all_diagnostics.len(), 2);
3406 for diagnostic in &all_diagnostics {
3407 assert!(
3408 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3409 "Unexpected diagnostics: {all_diagnostics:?}"
3410 );
3411 }
3412 });
3413 editor_b_main.update(cx_b, |editor, cx| {
3414 let snapshot = editor.buffer().read(cx).snapshot(cx);
3415 let all_diagnostics = snapshot
3416 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3417 .collect::<Vec<_>>();
3418 assert_eq!(all_diagnostics.len(), 2);
3419
3420 let expected_messages = [
3421 expected_workspace_pull_diagnostics_main_message,
3422 expected_pull_diagnostic_main_message,
3423 expected_push_diagnostic_main_message,
3424 ];
3425 for diagnostic in &all_diagnostics {
3426 assert!(
3427 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3428 "Unexpected diagnostics: {all_diagnostics:?}"
3429 );
3430 }
3431 });
3432 editor_a_main.update(cx_a, |editor, cx| {
3433 let snapshot = editor.buffer().read(cx).snapshot(cx);
3434 let all_diagnostics = snapshot
3435 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3436 .collect::<Vec<_>>();
3437 assert_eq!(all_diagnostics.len(), 2);
3438 let expected_messages = [
3439 expected_workspace_pull_diagnostics_main_message,
3440 expected_pull_diagnostic_main_message,
3441 expected_push_diagnostic_main_message,
3442 ];
3443 for diagnostic in &all_diagnostics {
3444 assert!(
3445 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3446 "Unexpected diagnostics: {all_diagnostics:?}"
3447 );
3448 }
3449 });
3450}
3451
3452#[gpui::test(iterations = 10)]
3453async fn test_non_streamed_lsp_pull_diagnostics(
3454 cx_a: &mut TestAppContext,
3455 cx_b: &mut TestAppContext,
3456) {
3457 test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
3458}
3459
3460#[gpui::test(iterations = 10)]
3461async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3462 test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
3463}
3464
3465#[gpui::test(iterations = 10)]
3466async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3467 let mut server = TestServer::start(cx_a.executor()).await;
3468 let client_a = server.create_client(cx_a, "user_a").await;
3469 let client_b = server.create_client(cx_b, "user_b").await;
3470 server
3471 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3472 .await;
3473 let active_call_a = cx_a.read(ActiveCall::global);
3474
3475 cx_a.update(editor::init);
3476 cx_b.update(editor::init);
3477 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
3478 let inline_blame_off_settings = Some(InlineBlameSettings {
3479 enabled: Some(false),
3480 ..Default::default()
3481 });
3482 cx_a.update(|cx| {
3483 SettingsStore::update_global(cx, |store, cx| {
3484 store.update_user_settings(cx, |settings| {
3485 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3486 });
3487 });
3488 });
3489 cx_b.update(|cx| {
3490 SettingsStore::update_global(cx, |store, cx| {
3491 store.update_user_settings(cx, |settings| {
3492 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3493 });
3494 });
3495 });
3496
3497 client_a
3498 .fs()
3499 .insert_tree(
3500 path!("/my-repo"),
3501 json!({
3502 ".git": {},
3503 "file.txt": "line1\nline2\nline3\nline\n",
3504 }),
3505 )
3506 .await;
3507
3508 let blame = git::blame::Blame {
3509 entries: vec![
3510 blame_entry("1b1b1b", 0..1),
3511 blame_entry("0d0d0d", 1..2),
3512 blame_entry("3a3a3a", 2..3),
3513 blame_entry("4c4c4c", 3..4),
3514 ],
3515 messages: [
3516 ("1b1b1b", "message for idx-0"),
3517 ("0d0d0d", "message for idx-1"),
3518 ("3a3a3a", "message for idx-2"),
3519 ("4c4c4c", "message for idx-3"),
3520 ]
3521 .into_iter()
3522 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3523 .collect(),
3524 };
3525 client_a.fs().set_blame_for_repo(
3526 Path::new(path!("/my-repo/.git")),
3527 vec![(repo_path("file.txt"), blame)],
3528 );
3529
3530 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3531 let project_id = active_call_a
3532 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3533 .await
3534 .unwrap();
3535
3536 // Create editor_a
3537 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3538 let editor_a = workspace_a
3539 .update_in(cx_a, |workspace, window, cx| {
3540 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3541 })
3542 .await
3543 .unwrap()
3544 .downcast::<Editor>()
3545 .unwrap();
3546
3547 // Join the project as client B.
3548 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3549 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3550 let editor_b = workspace_b
3551 .update_in(cx_b, |workspace, window, cx| {
3552 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3553 })
3554 .await
3555 .unwrap()
3556 .downcast::<Editor>()
3557 .unwrap();
3558 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3559 editor_b
3560 .buffer()
3561 .read(cx)
3562 .as_singleton()
3563 .unwrap()
3564 .read(cx)
3565 .remote_id()
3566 });
3567
3568 // client_b now requests git blame for the open buffer
3569 editor_b.update_in(cx_b, |editor_b, window, cx| {
3570 assert!(editor_b.blame().is_none());
3571 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3572 });
3573
3574 cx_a.executor().run_until_parked();
3575 cx_b.executor().run_until_parked();
3576
3577 editor_b.update(cx_b, |editor_b, cx| {
3578 let blame = editor_b.blame().expect("editor_b should have blame now");
3579 let entries = blame.update(cx, |blame, cx| {
3580 blame
3581 .blame_for_rows(
3582 &(0..4)
3583 .map(|row| RowInfo {
3584 buffer_row: Some(row),
3585 buffer_id: Some(buffer_id_b),
3586 ..Default::default()
3587 })
3588 .collect::<Vec<_>>(),
3589 cx,
3590 )
3591 .collect::<Vec<_>>()
3592 });
3593
3594 assert_eq!(
3595 entries,
3596 vec![
3597 Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
3598 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3599 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3600 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3601 ]
3602 );
3603
3604 blame.update(cx, |blame, _| {
3605 for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
3606 let details = blame.details_for_entry(*buffer, entry).unwrap();
3607 assert_eq!(details.message, format!("message for idx-{}", idx));
3608 }
3609 });
3610 });
3611
3612 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3613 // which gets back to client_b.
3614 editor_b.update_in(cx_b, |editor_b, _, cx| {
3615 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3616 });
3617
3618 cx_a.executor().run_until_parked();
3619 cx_b.executor().run_until_parked();
3620
3621 editor_b.update(cx_b, |editor_b, cx| {
3622 let blame = editor_b.blame().expect("editor_b should have blame now");
3623 let entries = blame.update(cx, |blame, cx| {
3624 blame
3625 .blame_for_rows(
3626 &(0..4)
3627 .map(|row| RowInfo {
3628 buffer_row: Some(row),
3629 buffer_id: Some(buffer_id_b),
3630 ..Default::default()
3631 })
3632 .collect::<Vec<_>>(),
3633 cx,
3634 )
3635 .collect::<Vec<_>>()
3636 });
3637
3638 assert_eq!(
3639 entries,
3640 vec![
3641 None,
3642 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3643 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3644 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3645 ]
3646 );
3647 });
3648
3649 // Now editor_a also updates the file
3650 editor_a.update_in(cx_a, |editor_a, _, cx| {
3651 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3652 });
3653
3654 cx_a.executor().run_until_parked();
3655 cx_b.executor().run_until_parked();
3656
3657 editor_b.update(cx_b, |editor_b, cx| {
3658 let blame = editor_b.blame().expect("editor_b should have blame now");
3659 let entries = blame.update(cx, |blame, cx| {
3660 blame
3661 .blame_for_rows(
3662 &(0..4)
3663 .map(|row| RowInfo {
3664 buffer_row: Some(row),
3665 buffer_id: Some(buffer_id_b),
3666 ..Default::default()
3667 })
3668 .collect::<Vec<_>>(),
3669 cx,
3670 )
3671 .collect::<Vec<_>>()
3672 });
3673
3674 assert_eq!(
3675 entries,
3676 vec![
3677 None,
3678 None,
3679 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3680 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3681 ]
3682 );
3683 });
3684}
3685
3686#[gpui::test(iterations = 30)]
3687async fn test_collaborating_with_editorconfig(
3688 cx_a: &mut TestAppContext,
3689 cx_b: &mut TestAppContext,
3690) {
3691 let mut server = TestServer::start(cx_a.executor()).await;
3692 let client_a = server.create_client(cx_a, "user_a").await;
3693 let client_b = server.create_client(cx_b, "user_b").await;
3694 server
3695 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3696 .await;
3697 let active_call_a = cx_a.read(ActiveCall::global);
3698
3699 cx_b.update(editor::init);
3700
3701 // Set up a fake language server.
3702 client_a.language_registry().add(rust_lang());
3703 client_a
3704 .fs()
3705 .insert_tree(
3706 path!("/a"),
3707 json!({
3708 "src": {
3709 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3710 "other_mod": {
3711 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3712 ".editorconfig": "",
3713 },
3714 },
3715 ".editorconfig": "[*]\ntab_width = 2\n",
3716 }),
3717 )
3718 .await;
3719 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3720 let project_id = active_call_a
3721 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3722 .await
3723 .unwrap();
3724 let main_buffer_a = project_a
3725 .update(cx_a, |p, cx| {
3726 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3727 })
3728 .await
3729 .unwrap();
3730 let other_buffer_a = project_a
3731 .update(cx_a, |p, cx| {
3732 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3733 })
3734 .await
3735 .unwrap();
3736 let cx_a = cx_a.add_empty_window();
3737 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3738 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3739 });
3740 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3741 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3742 });
3743 let mut main_editor_cx_a = EditorTestContext {
3744 cx: cx_a.clone(),
3745 window: cx_a.window_handle(),
3746 editor: main_editor_a,
3747 assertion_cx: AssertionContextManager::new(),
3748 };
3749 let mut other_editor_cx_a = EditorTestContext {
3750 cx: cx_a.clone(),
3751 window: cx_a.window_handle(),
3752 editor: other_editor_a,
3753 assertion_cx: AssertionContextManager::new(),
3754 };
3755
3756 // Join the project as client B.
3757 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3758 let main_buffer_b = project_b
3759 .update(cx_b, |p, cx| {
3760 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3761 })
3762 .await
3763 .unwrap();
3764 let other_buffer_b = project_b
3765 .update(cx_b, |p, cx| {
3766 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3767 })
3768 .await
3769 .unwrap();
3770 let cx_b = cx_b.add_empty_window();
3771 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3772 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3773 });
3774 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3775 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3776 });
3777 let mut main_editor_cx_b = EditorTestContext {
3778 cx: cx_b.clone(),
3779 window: cx_b.window_handle(),
3780 editor: main_editor_b,
3781 assertion_cx: AssertionContextManager::new(),
3782 };
3783 let mut other_editor_cx_b = EditorTestContext {
3784 cx: cx_b.clone(),
3785 window: cx_b.window_handle(),
3786 editor: other_editor_b,
3787 assertion_cx: AssertionContextManager::new(),
3788 };
3789
3790 let initial_main = indoc! {"
3791ˇmod other;
3792fn main() { let foo = other::foo(); }"};
3793 let initial_other = indoc! {"
3794ˇpub fn foo() -> usize {
3795 4
3796}"};
3797
3798 let first_tabbed_main = indoc! {"
3799 ˇmod other;
3800fn main() { let foo = other::foo(); }"};
3801 tab_undo_assert(
3802 &mut main_editor_cx_a,
3803 &mut main_editor_cx_b,
3804 initial_main,
3805 first_tabbed_main,
3806 true,
3807 );
3808 tab_undo_assert(
3809 &mut main_editor_cx_a,
3810 &mut main_editor_cx_b,
3811 initial_main,
3812 first_tabbed_main,
3813 false,
3814 );
3815
3816 let first_tabbed_other = indoc! {"
3817 ˇpub fn foo() -> usize {
3818 4
3819}"};
3820 tab_undo_assert(
3821 &mut other_editor_cx_a,
3822 &mut other_editor_cx_b,
3823 initial_other,
3824 first_tabbed_other,
3825 true,
3826 );
3827 tab_undo_assert(
3828 &mut other_editor_cx_a,
3829 &mut other_editor_cx_b,
3830 initial_other,
3831 first_tabbed_other,
3832 false,
3833 );
3834
3835 client_a
3836 .fs()
3837 .atomic_write(
3838 PathBuf::from(path!("/a/src/.editorconfig")),
3839 "[*]\ntab_width = 3\n".to_owned(),
3840 )
3841 .await
3842 .unwrap();
3843 cx_a.run_until_parked();
3844 cx_b.run_until_parked();
3845
3846 let second_tabbed_main = indoc! {"
3847 ˇmod other;
3848fn main() { let foo = other::foo(); }"};
3849 tab_undo_assert(
3850 &mut main_editor_cx_a,
3851 &mut main_editor_cx_b,
3852 initial_main,
3853 second_tabbed_main,
3854 true,
3855 );
3856 tab_undo_assert(
3857 &mut main_editor_cx_a,
3858 &mut main_editor_cx_b,
3859 initial_main,
3860 second_tabbed_main,
3861 false,
3862 );
3863
3864 let second_tabbed_other = indoc! {"
3865 ˇpub fn foo() -> usize {
3866 4
3867}"};
3868 tab_undo_assert(
3869 &mut other_editor_cx_a,
3870 &mut other_editor_cx_b,
3871 initial_other,
3872 second_tabbed_other,
3873 true,
3874 );
3875 tab_undo_assert(
3876 &mut other_editor_cx_a,
3877 &mut other_editor_cx_b,
3878 initial_other,
3879 second_tabbed_other,
3880 false,
3881 );
3882
3883 let editorconfig_buffer_b = project_b
3884 .update(cx_b, |p, cx| {
3885 p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
3886 })
3887 .await
3888 .unwrap();
3889 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3890 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3891 });
3892 project_b
3893 .update(cx_b, |project, cx| {
3894 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3895 })
3896 .await
3897 .unwrap();
3898 cx_a.run_until_parked();
3899 cx_b.run_until_parked();
3900
3901 tab_undo_assert(
3902 &mut main_editor_cx_a,
3903 &mut main_editor_cx_b,
3904 initial_main,
3905 second_tabbed_main,
3906 true,
3907 );
3908 tab_undo_assert(
3909 &mut main_editor_cx_a,
3910 &mut main_editor_cx_b,
3911 initial_main,
3912 second_tabbed_main,
3913 false,
3914 );
3915
3916 let third_tabbed_other = indoc! {"
3917 ˇpub fn foo() -> usize {
3918 4
3919}"};
3920 tab_undo_assert(
3921 &mut other_editor_cx_a,
3922 &mut other_editor_cx_b,
3923 initial_other,
3924 third_tabbed_other,
3925 true,
3926 );
3927
3928 tab_undo_assert(
3929 &mut other_editor_cx_a,
3930 &mut other_editor_cx_b,
3931 initial_other,
3932 third_tabbed_other,
3933 false,
3934 );
3935}
3936
3937#[gpui::test]
3938async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3939 let executor = cx_a.executor();
3940 let mut server = TestServer::start(executor.clone()).await;
3941 let client_a = server.create_client(cx_a, "user_a").await;
3942 let client_b = server.create_client(cx_b, "user_b").await;
3943 server
3944 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3945 .await;
3946 let active_call_a = cx_a.read(ActiveCall::global);
3947 let active_call_b = cx_b.read(ActiveCall::global);
3948 cx_a.update(editor::init);
3949 cx_b.update(editor::init);
3950 client_a
3951 .fs()
3952 .insert_tree(
3953 "/a",
3954 json!({
3955 "test.txt": "one\ntwo\nthree\nfour\nfive",
3956 }),
3957 )
3958 .await;
3959 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3960 let project_path = ProjectPath {
3961 worktree_id,
3962 path: rel_path(&"test.txt").into(),
3963 };
3964 let abs_path = project_a.read_with(cx_a, |project, cx| {
3965 project
3966 .absolute_path(&project_path, cx)
3967 .map(Arc::from)
3968 .unwrap()
3969 });
3970
3971 active_call_a
3972 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
3973 .await
3974 .unwrap();
3975 let project_id = active_call_a
3976 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3977 .await
3978 .unwrap();
3979 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3980 active_call_b
3981 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
3982 .await
3983 .unwrap();
3984 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3985 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3986
3987 // Client A opens an editor.
3988 let editor_a = workspace_a
3989 .update_in(cx_a, |workspace, window, cx| {
3990 workspace.open_path(project_path.clone(), None, true, window, cx)
3991 })
3992 .await
3993 .unwrap()
3994 .downcast::<Editor>()
3995 .unwrap();
3996
3997 // Client B opens same editor as A.
3998 let editor_b = workspace_b
3999 .update_in(cx_b, |workspace, window, cx| {
4000 workspace.open_path(project_path.clone(), None, true, window, cx)
4001 })
4002 .await
4003 .unwrap()
4004 .downcast::<Editor>()
4005 .unwrap();
4006
4007 cx_a.run_until_parked();
4008 cx_b.run_until_parked();
4009
4010 // Client A adds breakpoint on line (1)
4011 editor_a.update_in(cx_a, |editor, window, cx| {
4012 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4013 });
4014
4015 cx_a.run_until_parked();
4016 cx_b.run_until_parked();
4017
4018 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4019 editor
4020 .breakpoint_store()
4021 .unwrap()
4022 .read(cx)
4023 .all_source_breakpoints(cx)
4024 });
4025 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4026 editor
4027 .breakpoint_store()
4028 .unwrap()
4029 .read(cx)
4030 .all_source_breakpoints(cx)
4031 });
4032
4033 assert_eq!(1, breakpoints_a.len());
4034 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4035 assert_eq!(breakpoints_a, breakpoints_b);
4036
4037 // Client B adds breakpoint on line(2)
4038 editor_b.update_in(cx_b, |editor, window, cx| {
4039 editor.move_down(&editor::actions::MoveDown, window, cx);
4040 editor.move_down(&editor::actions::MoveDown, window, cx);
4041 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4042 });
4043
4044 cx_a.run_until_parked();
4045 cx_b.run_until_parked();
4046
4047 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4048 editor
4049 .breakpoint_store()
4050 .unwrap()
4051 .read(cx)
4052 .all_source_breakpoints(cx)
4053 });
4054 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4055 editor
4056 .breakpoint_store()
4057 .unwrap()
4058 .read(cx)
4059 .all_source_breakpoints(cx)
4060 });
4061
4062 assert_eq!(1, breakpoints_a.len());
4063 assert_eq!(breakpoints_a, breakpoints_b);
4064 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
4065
4066 // Client A removes last added breakpoint from client B
4067 editor_a.update_in(cx_a, |editor, window, cx| {
4068 editor.move_down(&editor::actions::MoveDown, window, cx);
4069 editor.move_down(&editor::actions::MoveDown, window, cx);
4070 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4071 });
4072
4073 cx_a.run_until_parked();
4074 cx_b.run_until_parked();
4075
4076 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4077 editor
4078 .breakpoint_store()
4079 .unwrap()
4080 .read(cx)
4081 .all_source_breakpoints(cx)
4082 });
4083 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4084 editor
4085 .breakpoint_store()
4086 .unwrap()
4087 .read(cx)
4088 .all_source_breakpoints(cx)
4089 });
4090
4091 assert_eq!(1, breakpoints_a.len());
4092 assert_eq!(breakpoints_a, breakpoints_b);
4093 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4094
4095 // Client B removes first added breakpoint by client A
4096 editor_b.update_in(cx_b, |editor, window, cx| {
4097 editor.move_up(&editor::actions::MoveUp, window, cx);
4098 editor.move_up(&editor::actions::MoveUp, window, cx);
4099 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4100 });
4101
4102 cx_a.run_until_parked();
4103 cx_b.run_until_parked();
4104
4105 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4106 editor
4107 .breakpoint_store()
4108 .unwrap()
4109 .read(cx)
4110 .all_source_breakpoints(cx)
4111 });
4112 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4113 editor
4114 .breakpoint_store()
4115 .unwrap()
4116 .read(cx)
4117 .all_source_breakpoints(cx)
4118 });
4119
4120 assert_eq!(0, breakpoints_a.len());
4121 assert_eq!(breakpoints_a, breakpoints_b);
4122}
4123
4124#[gpui::test]
4125async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4126 let mut server = TestServer::start(cx_a.executor()).await;
4127 let client_a = server.create_client(cx_a, "user_a").await;
4128 let client_b = server.create_client(cx_b, "user_b").await;
4129 server
4130 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4131 .await;
4132 let active_call_a = cx_a.read(ActiveCall::global);
4133 let active_call_b = cx_b.read(ActiveCall::global);
4134
4135 cx_a.update(editor::init);
4136 cx_b.update(editor::init);
4137
4138 client_a.language_registry().add(rust_lang());
4139 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4140 "Rust",
4141 FakeLspAdapter {
4142 name: "rust-analyzer",
4143 ..FakeLspAdapter::default()
4144 },
4145 );
4146 client_b.language_registry().add(rust_lang());
4147 client_b.language_registry().register_fake_lsp_adapter(
4148 "Rust",
4149 FakeLspAdapter {
4150 name: "rust-analyzer",
4151 ..FakeLspAdapter::default()
4152 },
4153 );
4154
4155 client_a
4156 .fs()
4157 .insert_tree(
4158 path!("/a"),
4159 json!({
4160 "main.rs": "fn main() {}",
4161 }),
4162 )
4163 .await;
4164 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4165 active_call_a
4166 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4167 .await
4168 .unwrap();
4169 let project_id = active_call_a
4170 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4171 .await
4172 .unwrap();
4173
4174 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4175 active_call_b
4176 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4177 .await
4178 .unwrap();
4179
4180 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4181 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4182
4183 let editor_a = workspace_a
4184 .update_in(cx_a, |workspace, window, cx| {
4185 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4186 })
4187 .await
4188 .unwrap()
4189 .downcast::<Editor>()
4190 .unwrap();
4191
4192 let editor_b = workspace_b
4193 .update_in(cx_b, |workspace, window, cx| {
4194 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4195 })
4196 .await
4197 .unwrap()
4198 .downcast::<Editor>()
4199 .unwrap();
4200
4201 let fake_language_server = fake_language_servers.next().await.unwrap();
4202
4203 // host
4204 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4205 |params, _| async move {
4206 assert_eq!(
4207 params.text_document.uri,
4208 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4209 );
4210 assert_eq!(params.position, lsp::Position::new(0, 0));
4211 Ok(Some(ExpandedMacro {
4212 name: "test_macro_name".to_string(),
4213 expansion: "test_macro_expansion on the host".to_string(),
4214 }))
4215 },
4216 );
4217
4218 editor_a.update_in(cx_a, |editor, window, cx| {
4219 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4220 });
4221 expand_request_a.next().await.unwrap();
4222 cx_a.run_until_parked();
4223
4224 workspace_a.update(cx_a, |workspace, cx| {
4225 workspace.active_pane().update(cx, |pane, cx| {
4226 assert_eq!(
4227 pane.items_len(),
4228 2,
4229 "Should have added a macro expansion to the host's pane"
4230 );
4231 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4232 new_editor.update(cx, |editor, cx| {
4233 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
4234 });
4235 })
4236 });
4237
4238 // client
4239 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4240 |params, _| async move {
4241 assert_eq!(
4242 params.text_document.uri,
4243 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4244 );
4245 assert_eq!(
4246 params.position,
4247 lsp::Position::new(0, 12),
4248 "editor_b has selected the entire text and should query for a different position"
4249 );
4250 Ok(Some(ExpandedMacro {
4251 name: "test_macro_name".to_string(),
4252 expansion: "test_macro_expansion on the client".to_string(),
4253 }))
4254 },
4255 );
4256
4257 editor_b.update_in(cx_b, |editor, window, cx| {
4258 editor.select_all(&SelectAll, window, cx);
4259 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4260 });
4261 expand_request_b.next().await.unwrap();
4262 cx_b.run_until_parked();
4263
4264 workspace_b.update(cx_b, |workspace, cx| {
4265 workspace.active_pane().update(cx, |pane, cx| {
4266 assert_eq!(
4267 pane.items_len(),
4268 2,
4269 "Should have added a macro expansion to the client's pane"
4270 );
4271 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4272 new_editor.update(cx, |editor, cx| {
4273 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
4274 });
4275 })
4276 });
4277}
4278
4279#[gpui::test]
4280async fn test_copy_file_name_without_extension(
4281 cx_a: &mut TestAppContext,
4282 cx_b: &mut TestAppContext,
4283) {
4284 let mut server = TestServer::start(cx_a.executor()).await;
4285 let client_a = server.create_client(cx_a, "user_a").await;
4286 let client_b = server.create_client(cx_b, "user_b").await;
4287 server
4288 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4289 .await;
4290
4291 cx_b.update(editor::init);
4292
4293 client_a
4294 .fs()
4295 .insert_tree(
4296 path!("/root"),
4297 json!({
4298 "src": {
4299 "main.rs": indoc! {"
4300 fn main() {
4301 println!(\"Hello, world!\");
4302 }
4303 "},
4304 }
4305 }),
4306 )
4307 .await;
4308
4309 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4310 let active_call_a = cx_a.read(ActiveCall::global);
4311 let project_id = active_call_a
4312 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4313 .await
4314 .unwrap();
4315
4316 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4317
4318 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4319 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4320
4321 let editor_a = workspace_a
4322 .update_in(cx_a, |workspace, window, cx| {
4323 workspace.open_path(
4324 (worktree_id, rel_path("src/main.rs")),
4325 None,
4326 true,
4327 window,
4328 cx,
4329 )
4330 })
4331 .await
4332 .unwrap()
4333 .downcast::<Editor>()
4334 .unwrap();
4335
4336 let editor_b = workspace_b
4337 .update_in(cx_b, |workspace, window, cx| {
4338 workspace.open_path(
4339 (worktree_id, rel_path("src/main.rs")),
4340 None,
4341 true,
4342 window,
4343 cx,
4344 )
4345 })
4346 .await
4347 .unwrap()
4348 .downcast::<Editor>()
4349 .unwrap();
4350
4351 cx_a.run_until_parked();
4352 cx_b.run_until_parked();
4353
4354 editor_a.update_in(cx_a, |editor, window, cx| {
4355 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4356 });
4357
4358 assert_eq!(
4359 cx_a.read_from_clipboard().and_then(|item| item.text()),
4360 Some("main".to_string())
4361 );
4362
4363 editor_b.update_in(cx_b, |editor, window, cx| {
4364 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4365 });
4366
4367 assert_eq!(
4368 cx_b.read_from_clipboard().and_then(|item| item.text()),
4369 Some("main".to_string())
4370 );
4371}
4372
4373#[gpui::test]
4374async fn test_copy_file_name(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4375 let mut server = TestServer::start(cx_a.executor()).await;
4376 let client_a = server.create_client(cx_a, "user_a").await;
4377 let client_b = server.create_client(cx_b, "user_b").await;
4378 server
4379 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4380 .await;
4381
4382 cx_b.update(editor::init);
4383
4384 client_a
4385 .fs()
4386 .insert_tree(
4387 path!("/root"),
4388 json!({
4389 "src": {
4390 "main.rs": indoc! {"
4391 fn main() {
4392 println!(\"Hello, world!\");
4393 }
4394 "},
4395 }
4396 }),
4397 )
4398 .await;
4399
4400 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4401 let active_call_a = cx_a.read(ActiveCall::global);
4402 let project_id = active_call_a
4403 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4404 .await
4405 .unwrap();
4406
4407 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4408
4409 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4410 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4411
4412 let editor_a = workspace_a
4413 .update_in(cx_a, |workspace, window, cx| {
4414 workspace.open_path(
4415 (worktree_id, rel_path("src/main.rs")),
4416 None,
4417 true,
4418 window,
4419 cx,
4420 )
4421 })
4422 .await
4423 .unwrap()
4424 .downcast::<Editor>()
4425 .unwrap();
4426
4427 let editor_b = workspace_b
4428 .update_in(cx_b, |workspace, window, cx| {
4429 workspace.open_path(
4430 (worktree_id, rel_path("src/main.rs")),
4431 None,
4432 true,
4433 window,
4434 cx,
4435 )
4436 })
4437 .await
4438 .unwrap()
4439 .downcast::<Editor>()
4440 .unwrap();
4441
4442 cx_a.run_until_parked();
4443 cx_b.run_until_parked();
4444
4445 editor_a.update_in(cx_a, |editor, window, cx| {
4446 editor.copy_file_name(&CopyFileName, window, cx);
4447 });
4448
4449 assert_eq!(
4450 cx_a.read_from_clipboard().and_then(|item| item.text()),
4451 Some("main.rs".to_string())
4452 );
4453
4454 editor_b.update_in(cx_b, |editor, window, cx| {
4455 editor.copy_file_name(&CopyFileName, window, cx);
4456 });
4457
4458 assert_eq!(
4459 cx_b.read_from_clipboard().and_then(|item| item.text()),
4460 Some("main.rs".to_string())
4461 );
4462}
4463
4464#[gpui::test]
4465async fn test_copy_file_location(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4466 let mut server = TestServer::start(cx_a.executor()).await;
4467 let client_a = server.create_client(cx_a, "user_a").await;
4468 let client_b = server.create_client(cx_b, "user_b").await;
4469 server
4470 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4471 .await;
4472
4473 cx_b.update(editor::init);
4474
4475 client_a
4476 .fs()
4477 .insert_tree(
4478 path!("/root"),
4479 json!({
4480 "src": {
4481 "main.rs": indoc! {"
4482 fn main() {
4483 println!(\"Hello, world!\");
4484 }
4485 "},
4486 }
4487 }),
4488 )
4489 .await;
4490
4491 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4492 let active_call_a = cx_a.read(ActiveCall::global);
4493 let project_id = active_call_a
4494 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4495 .await
4496 .unwrap();
4497
4498 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4499
4500 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4501 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4502
4503 let editor_a = workspace_a
4504 .update_in(cx_a, |workspace, window, cx| {
4505 workspace.open_path(
4506 (worktree_id, rel_path("src/main.rs")),
4507 None,
4508 true,
4509 window,
4510 cx,
4511 )
4512 })
4513 .await
4514 .unwrap()
4515 .downcast::<Editor>()
4516 .unwrap();
4517
4518 let editor_b = workspace_b
4519 .update_in(cx_b, |workspace, window, cx| {
4520 workspace.open_path(
4521 (worktree_id, rel_path("src/main.rs")),
4522 None,
4523 true,
4524 window,
4525 cx,
4526 )
4527 })
4528 .await
4529 .unwrap()
4530 .downcast::<Editor>()
4531 .unwrap();
4532
4533 cx_a.run_until_parked();
4534 cx_b.run_until_parked();
4535
4536 editor_a.update_in(cx_a, |editor, window, cx| {
4537 editor.change_selections(Default::default(), window, cx, |s| {
4538 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4539 });
4540 editor.copy_file_location(&CopyFileLocation, window, cx);
4541 });
4542
4543 assert_eq!(
4544 cx_a.read_from_clipboard().and_then(|item| item.text()),
4545 Some(format!("{}:2", path!("src/main.rs")))
4546 );
4547
4548 editor_b.update_in(cx_b, |editor, window, cx| {
4549 editor.change_selections(Default::default(), window, cx, |s| {
4550 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4551 });
4552 editor.copy_file_location(&CopyFileLocation, window, cx);
4553 });
4554
4555 assert_eq!(
4556 cx_b.read_from_clipboard().and_then(|item| item.text()),
4557 Some(format!("{}:2", path!("src/main.rs")))
4558 );
4559}
4560
4561#[track_caller]
4562fn tab_undo_assert(
4563 cx_a: &mut EditorTestContext,
4564 cx_b: &mut EditorTestContext,
4565 expected_initial: &str,
4566 expected_tabbed: &str,
4567 a_tabs: bool,
4568) {
4569 cx_a.assert_editor_state(expected_initial);
4570 cx_b.assert_editor_state(expected_initial);
4571
4572 if a_tabs {
4573 cx_a.update_editor(|editor, window, cx| {
4574 editor.tab(&editor::actions::Tab, window, cx);
4575 });
4576 } else {
4577 cx_b.update_editor(|editor, window, cx| {
4578 editor.tab(&editor::actions::Tab, window, cx);
4579 });
4580 }
4581
4582 cx_a.run_until_parked();
4583 cx_b.run_until_parked();
4584
4585 cx_a.assert_editor_state(expected_tabbed);
4586 cx_b.assert_editor_state(expected_tabbed);
4587
4588 if a_tabs {
4589 cx_a.update_editor(|editor, window, cx| {
4590 editor.undo(&editor::actions::Undo, window, cx);
4591 });
4592 } else {
4593 cx_b.update_editor(|editor, window, cx| {
4594 editor.undo(&editor::actions::Undo, window, cx);
4595 });
4596 }
4597 cx_a.run_until_parked();
4598 cx_b.run_until_parked();
4599 cx_a.assert_editor_state(expected_initial);
4600 cx_b.assert_editor_state(expected_initial);
4601}
4602
4603fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4604 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4605
4606 let mut all_cached_labels = Vec::new();
4607 let mut all_fetched_hints = Vec::new();
4608 for buffer in editor.buffer().read(cx).all_buffers() {
4609 lsp_store.update(cx, |lsp_store, cx| {
4610 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4611 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4612 let mut label = hint.text().to_string();
4613 if hint.padding_left {
4614 label.insert(0, ' ');
4615 }
4616 if hint.padding_right {
4617 label.push_str(" ");
4618 }
4619 label
4620 }));
4621 all_fetched_hints.extend(hints.all_fetched_hints());
4622 });
4623 }
4624
4625 assert!(
4626 all_fetched_hints.is_empty(),
4627 "Did not expect background hints fetch tasks, but got {} of them",
4628 all_fetched_hints.len()
4629 );
4630
4631 all_cached_labels
4632}
4633
4634#[track_caller]
4635fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
4636 editor
4637 .all_inlays(cx)
4638 .into_iter()
4639 .filter_map(|inlay| inlay.get_color())
4640 .map(Rgba::from)
4641 .collect()
4642}
4643
4644#[gpui::test]
4645async fn test_remote_project_worktree_trust(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4646 let has_restricted_worktrees = |project: &gpui::Entity<project::Project>,
4647 cx: &mut VisualTestContext| {
4648 cx.update(|_, cx| {
4649 let worktree_store = project.read(cx).worktree_store();
4650 TrustedWorktrees::try_get_global(cx)
4651 .unwrap()
4652 .read(cx)
4653 .has_restricted_worktrees(&worktree_store, cx)
4654 })
4655 };
4656
4657 cx_a.update(|cx| {
4658 project::trusted_worktrees::init(HashMap::default(), None, None, cx);
4659 });
4660 cx_b.update(|cx| {
4661 project::trusted_worktrees::init(HashMap::default(), None, None, cx);
4662 });
4663
4664 let mut server = TestServer::start(cx_a.executor()).await;
4665 let client_a = server.create_client(cx_a, "user_a").await;
4666 let client_b = server.create_client(cx_b, "user_b").await;
4667 server
4668 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4669 .await;
4670
4671 client_a
4672 .fs()
4673 .insert_tree(
4674 path!("/a"),
4675 json!({
4676 "file.txt": "contents",
4677 }),
4678 )
4679 .await;
4680
4681 let (project_a, worktree_id) = client_a
4682 .build_local_project_with_trust(path!("/a"), cx_a)
4683 .await;
4684 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4685 let active_call_a = cx_a.read(ActiveCall::global);
4686 let project_id = active_call_a
4687 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4688 .await
4689 .unwrap();
4690
4691 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4692 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4693
4694 let _editor_a = workspace_a
4695 .update_in(cx_a, |workspace, window, cx| {
4696 workspace.open_path(
4697 (worktree_id, rel_path("src/main.rs")),
4698 None,
4699 true,
4700 window,
4701 cx,
4702 )
4703 })
4704 .await
4705 .unwrap()
4706 .downcast::<Editor>()
4707 .unwrap();
4708
4709 let _editor_b = workspace_b
4710 .update_in(cx_b, |workspace, window, cx| {
4711 workspace.open_path(
4712 (worktree_id, rel_path("src/main.rs")),
4713 None,
4714 true,
4715 window,
4716 cx,
4717 )
4718 })
4719 .await
4720 .unwrap()
4721 .downcast::<Editor>()
4722 .unwrap();
4723
4724 cx_a.run_until_parked();
4725 cx_b.run_until_parked();
4726
4727 assert!(
4728 has_restricted_worktrees(&project_a, cx_a),
4729 "local client should have restricted worktrees after opening it"
4730 );
4731 assert!(
4732 !has_restricted_worktrees(&project_b, cx_b),
4733 "remote client joined a project should have no restricted worktrees"
4734 );
4735
4736 cx_a.update(|_, cx| {
4737 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
4738 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
4739 trusted_worktrees.trust(
4740 &project_a.read(cx).worktree_store(),
4741 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
4742 cx,
4743 );
4744 });
4745 }
4746 });
4747 assert!(
4748 !has_restricted_worktrees(&project_a, cx_a),
4749 "local client should have no worktrees after trusting those"
4750 );
4751 assert!(
4752 !has_restricted_worktrees(&project_b, cx_b),
4753 "remote client should still be trusted"
4754 );
4755}
4756
4757fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
4758 git::blame::BlameEntry {
4759 sha: sha.parse().unwrap(),
4760 range,
4761 ..Default::default()
4762 }
4763}