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