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