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