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