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 cx_b.update(|cx| {
1207 SettingsStore::update_global(cx, |store, cx| {
1208 store.update_user_settings(cx, |settings| {
1209 settings.editor.code_lens = Some(settings::CodeLens::Menu);
1210 });
1211 });
1212 });
1213
1214 let command_name = "test_command";
1215 let capabilities = lsp::ServerCapabilities {
1216 code_lens_provider: Some(lsp::CodeLensOptions {
1217 resolve_provider: None,
1218 }),
1219 execute_command_provider: Some(lsp::ExecuteCommandOptions {
1220 commands: vec![command_name.to_string()],
1221 ..lsp::ExecuteCommandOptions::default()
1222 }),
1223 ..lsp::ServerCapabilities::default()
1224 };
1225 client_a.language_registry().add(rust_lang());
1226 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1227 "Rust",
1228 FakeLspAdapter {
1229 capabilities: capabilities.clone(),
1230 ..FakeLspAdapter::default()
1231 },
1232 );
1233 client_b.language_registry().add(rust_lang());
1234 client_b.language_registry().register_fake_lsp_adapter(
1235 "Rust",
1236 FakeLspAdapter {
1237 capabilities,
1238 ..FakeLspAdapter::default()
1239 },
1240 );
1241
1242 client_a
1243 .fs()
1244 .insert_tree(
1245 path!("/dir"),
1246 json!({
1247 "one.rs": "const ONE: usize = 1;"
1248 }),
1249 )
1250 .await;
1251 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
1252 let project_id = active_call_a
1253 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1254 .await
1255 .unwrap();
1256 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1257
1258 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1259 let editor_b = workspace_b
1260 .update_in(cx_b, |workspace, window, cx| {
1261 workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
1262 })
1263 .await
1264 .unwrap()
1265 .downcast::<Editor>()
1266 .unwrap();
1267 let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
1268 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
1269 let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1270 (lsp_store, buffer)
1271 });
1272 let fake_language_server = fake_language_servers.next().await.unwrap();
1273 cx_a.run_until_parked();
1274 cx_b.run_until_parked();
1275
1276 let long_request_time = DEFAULT_LSP_REQUEST_TIMEOUT / 2;
1277 let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
1278 let requests_started = Arc::new(AtomicUsize::new(0));
1279 let requests_completed = Arc::new(AtomicUsize::new(0));
1280 let _lens_requests = fake_language_server
1281 .set_request_handler::<lsp::request::CodeLensRequest, _, _>({
1282 let request_started_tx = request_started_tx.clone();
1283 let requests_started = requests_started.clone();
1284 let requests_completed = requests_completed.clone();
1285 move |params, cx| {
1286 let mut request_started_tx = request_started_tx.clone();
1287 let requests_started = requests_started.clone();
1288 let requests_completed = requests_completed.clone();
1289 async move {
1290 assert_eq!(
1291 params.text_document.uri.as_str(),
1292 uri!("file:///dir/one.rs")
1293 );
1294 requests_started.fetch_add(1, atomic::Ordering::Release);
1295 request_started_tx.send(()).await.unwrap();
1296 cx.background_executor().timer(long_request_time).await;
1297 let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
1298 Ok(Some(vec![lsp::CodeLens {
1299 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
1300 command: Some(lsp::Command {
1301 title: format!("LSP Command {i}"),
1302 command: command_name.to_string(),
1303 arguments: None,
1304 }),
1305 data: None,
1306 }]))
1307 }
1308 }
1309 });
1310
1311 // Move cursor to a location, this should trigger the code lens call.
1312 editor_b.update_in(cx_b, |editor, window, cx| {
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_ranges([MultiBufferOffset(7)..MultiBufferOffset(7)])
1315 });
1316 });
1317 let () = request_started_rx.next().await.unwrap();
1318 assert_eq!(
1319 requests_started.load(atomic::Ordering::Acquire),
1320 1,
1321 "Selection change should have initiated the first request"
1322 );
1323 assert_eq!(
1324 requests_completed.load(atomic::Ordering::Acquire),
1325 0,
1326 "Slow requests should be running still"
1327 );
1328 let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
1329 lsp_store
1330 .forget_code_lens_task(buffer_b.read(cx).remote_id())
1331 .expect("Should have the fetch task started")
1332 });
1333
1334 editor_b.update_in(cx_b, |editor, window, cx| {
1335 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1336 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
1337 });
1338 });
1339 let () = request_started_rx.next().await.unwrap();
1340 assert_eq!(
1341 requests_started.load(atomic::Ordering::Acquire),
1342 2,
1343 "Selection change should have initiated the second request"
1344 );
1345 assert_eq!(
1346 requests_completed.load(atomic::Ordering::Acquire),
1347 0,
1348 "Slow requests should be running still"
1349 );
1350 let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
1351 lsp_store
1352 .forget_code_lens_task(buffer_b.read(cx).remote_id())
1353 .expect("Should have the fetch task started for the 2nd time")
1354 });
1355
1356 editor_b.update_in(cx_b, |editor, window, cx| {
1357 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1358 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
1359 });
1360 });
1361 let () = request_started_rx.next().await.unwrap();
1362 assert_eq!(
1363 requests_started.load(atomic::Ordering::Acquire),
1364 3,
1365 "Selection change should have initiated the third request"
1366 );
1367 assert_eq!(
1368 requests_completed.load(atomic::Ordering::Acquire),
1369 0,
1370 "Slow requests should be running still"
1371 );
1372
1373 _first_task.await.unwrap();
1374 _second_task.await.unwrap();
1375 cx_b.run_until_parked();
1376 assert_eq!(
1377 requests_started.load(atomic::Ordering::Acquire),
1378 3,
1379 "No selection changes should trigger no more code lens requests"
1380 );
1381 assert_eq!(
1382 requests_completed.load(atomic::Ordering::Acquire),
1383 1,
1384 "After enough time, a single, deduplicated, LSP request should have been served by the language server"
1385 );
1386 let resulting_lens_actions = editor_b
1387 .update(cx_b, |editor, cx| {
1388 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
1389 lsp_store.update(cx, |lsp_store, cx| {
1390 lsp_store.code_lens_actions(&buffer_b, cx)
1391 })
1392 })
1393 .await
1394 .unwrap()
1395 .unwrap();
1396 assert_eq!(
1397 resulting_lens_actions.len(),
1398 1,
1399 "Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
1400 );
1401 assert_eq!(
1402 resulting_lens_actions.first().unwrap().lsp_action.title(),
1403 "LSP Command 1",
1404 "Only the final code lens action should be in the data"
1405 )
1406}
1407
1408#[gpui::test(iterations = 10)]
1409async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1410 let mut server = TestServer::start(cx_a.executor()).await;
1411 let executor = cx_a.executor();
1412 let client_a = server.create_client(cx_a, "user_a").await;
1413 let client_b = server.create_client(cx_b, "user_b").await;
1414 server
1415 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1416 .await;
1417 let active_call_a = cx_a.read(ActiveCall::global);
1418
1419 cx_b.update(editor::init);
1420
1421 client_a.language_registry().add(rust_lang());
1422 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1423 "Rust",
1424 FakeLspAdapter {
1425 name: "the-language-server",
1426 ..Default::default()
1427 },
1428 );
1429
1430 client_a
1431 .fs()
1432 .insert_tree(
1433 path!("/dir"),
1434 json!({
1435 "main.rs": "const ONE: usize = 1;",
1436 }),
1437 )
1438 .await;
1439 let (project_a, _) = client_a.build_local_project(path!("/dir"), cx_a).await;
1440
1441 let _buffer_a = project_a
1442 .update(cx_a, |p, cx| {
1443 p.open_local_buffer_with_lsp(path!("/dir/main.rs"), cx)
1444 })
1445 .await
1446 .unwrap();
1447
1448 let fake_language_server = fake_language_servers.next().await.unwrap();
1449 executor.run_until_parked();
1450 fake_language_server.start_progress("the-token").await;
1451
1452 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1453 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1454 token: lsp::NumberOrString::String("the-token".to_string()),
1455 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1456 lsp::WorkDoneProgressReport {
1457 message: Some("the-message".to_string()),
1458 ..Default::default()
1459 },
1460 )),
1461 });
1462 executor.run_until_parked();
1463
1464 let token = ProgressToken::String(SharedString::from("the-token"));
1465
1466 project_a.read_with(cx_a, |project, cx| {
1467 let status = project.language_server_statuses(cx).next().unwrap().1;
1468 assert_eq!(status.name.0, "the-language-server");
1469 assert_eq!(status.pending_work.len(), 1);
1470 assert_eq!(
1471 status.pending_work[&token].message.as_ref().unwrap(),
1472 "the-message"
1473 );
1474 });
1475
1476 let project_id = active_call_a
1477 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1478 .await
1479 .unwrap();
1480 executor.run_until_parked();
1481 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1482
1483 project_b.read_with(cx_b, |project, cx| {
1484 let status = project.language_server_statuses(cx).next().unwrap().1;
1485 assert_eq!(status.name.0, "the-language-server");
1486 });
1487
1488 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1489 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1490 token: lsp::NumberOrString::String("the-token".to_string()),
1491 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1492 lsp::WorkDoneProgressReport {
1493 message: Some("the-message-2".to_string()),
1494 ..Default::default()
1495 },
1496 )),
1497 });
1498 executor.run_until_parked();
1499
1500 project_a.read_with(cx_a, |project, cx| {
1501 let status = project.language_server_statuses(cx).next().unwrap().1;
1502 assert_eq!(status.name.0, "the-language-server");
1503 assert_eq!(status.pending_work.len(), 1);
1504 assert_eq!(
1505 status.pending_work[&token].message.as_ref().unwrap(),
1506 "the-message-2"
1507 );
1508 });
1509
1510 project_b.read_with(cx_b, |project, cx| {
1511 let status = project.language_server_statuses(cx).next().unwrap().1;
1512 assert_eq!(status.name.0, "the-language-server");
1513 assert_eq!(status.pending_work.len(), 1);
1514 assert_eq!(
1515 status.pending_work[&token].message.as_ref().unwrap(),
1516 "the-message-2"
1517 );
1518 });
1519}
1520
1521#[gpui::test(iterations = 10)]
1522async fn test_share_project(
1523 cx_a: &mut TestAppContext,
1524 cx_b: &mut TestAppContext,
1525 cx_c: &mut TestAppContext,
1526) {
1527 let executor = cx_a.executor();
1528 let cx_b = cx_b.add_empty_window();
1529 let mut server = TestServer::start(executor.clone()).await;
1530 let client_a = server.create_client(cx_a, "user_a").await;
1531 let client_b = server.create_client(cx_b, "user_b").await;
1532 let client_c = server.create_client(cx_c, "user_c").await;
1533 server
1534 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1535 .await;
1536 let active_call_a = cx_a.read(ActiveCall::global);
1537 let active_call_b = cx_b.read(ActiveCall::global);
1538 let active_call_c = cx_c.read(ActiveCall::global);
1539
1540 client_a
1541 .fs()
1542 .insert_tree(
1543 path!("/a"),
1544 json!({
1545 ".gitignore": "ignored-dir",
1546 "a.txt": "a-contents",
1547 "b.txt": "b-contents",
1548 "ignored-dir": {
1549 "c.txt": "",
1550 "d.txt": "",
1551 }
1552 }),
1553 )
1554 .await;
1555
1556 // Invite client B to collaborate on a project
1557 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1558 active_call_a
1559 .update(cx_a, |call, cx| {
1560 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1561 })
1562 .await
1563 .unwrap();
1564
1565 // Join that project as client B
1566
1567 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1568 executor.run_until_parked();
1569 let call = incoming_call_b.borrow().clone().unwrap();
1570 assert_eq!(call.calling_user.github_login, "user_a");
1571 let initial_project = call.initial_project.unwrap();
1572 active_call_b
1573 .update(cx_b, |call, cx| call.accept_incoming(cx))
1574 .await
1575 .unwrap();
1576 let client_b_peer_id = client_b.peer_id().unwrap();
1577 let project_b = client_b.join_remote_project(initial_project.id, cx_b).await;
1578
1579 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1580
1581 executor.run_until_parked();
1582
1583 project_a.read_with(cx_a, |project, _| {
1584 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1585 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1586 });
1587
1588 project_b.read_with(cx_b, |project, cx| {
1589 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1590 assert_eq!(
1591 worktree.paths().collect::<Vec<_>>(),
1592 [
1593 rel_path(".gitignore"),
1594 rel_path("a.txt"),
1595 rel_path("b.txt"),
1596 rel_path("ignored-dir"),
1597 ]
1598 );
1599 });
1600
1601 project_b
1602 .update(cx_b, |project, cx| {
1603 let worktree = project.worktrees(cx).next().unwrap();
1604 let entry = worktree
1605 .read(cx)
1606 .entry_for_path(rel_path("ignored-dir"))
1607 .unwrap();
1608 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1609 })
1610 .await
1611 .unwrap();
1612
1613 project_b.read_with(cx_b, |project, cx| {
1614 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1615 assert_eq!(
1616 worktree.paths().collect::<Vec<_>>(),
1617 [
1618 rel_path(".gitignore"),
1619 rel_path("a.txt"),
1620 rel_path("b.txt"),
1621 rel_path("ignored-dir"),
1622 rel_path("ignored-dir/c.txt"),
1623 rel_path("ignored-dir/d.txt"),
1624 ]
1625 );
1626 });
1627
1628 // Open the same file as client B and client A.
1629 let buffer_b = project_b
1630 .update(cx_b, |p, cx| {
1631 p.open_buffer((worktree_id, rel_path("b.txt")), cx)
1632 })
1633 .await
1634 .unwrap();
1635
1636 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1637
1638 project_a.read_with(cx_a, |project, cx| {
1639 assert!(project.has_open_buffer((worktree_id, rel_path("b.txt")), cx))
1640 });
1641 let buffer_a = project_a
1642 .update(cx_a, |p, cx| {
1643 p.open_buffer((worktree_id, rel_path("b.txt")), cx)
1644 })
1645 .await
1646 .unwrap();
1647
1648 let editor_b =
1649 cx_b.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
1650
1651 // Client A sees client B's selection
1652 executor.run_until_parked();
1653
1654 buffer_a.read_with(cx_a, |buffer, _| {
1655 buffer
1656 .snapshot()
1657 .selections_in_range(
1658 text::Anchor::min_max_range_for_buffer(buffer.remote_id()),
1659 false,
1660 )
1661 .count()
1662 == 1
1663 });
1664
1665 // Edit the buffer as client B and see that edit as client A.
1666 editor_b.update_in(cx_b, |editor, window, cx| {
1667 editor.handle_input("ok, ", window, cx)
1668 });
1669 executor.run_until_parked();
1670
1671 buffer_a.read_with(cx_a, |buffer, _| {
1672 assert_eq!(buffer.text(), "ok, b-contents")
1673 });
1674
1675 // Client B can invite client C on a project shared by client A.
1676 active_call_b
1677 .update(cx_b, |call, cx| {
1678 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1679 })
1680 .await
1681 .unwrap();
1682
1683 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1684 executor.run_until_parked();
1685 let call = incoming_call_c.borrow().clone().unwrap();
1686 assert_eq!(call.calling_user.github_login, "user_b");
1687 let initial_project = call.initial_project.unwrap();
1688 active_call_c
1689 .update(cx_c, |call, cx| call.accept_incoming(cx))
1690 .await
1691 .unwrap();
1692 let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1693
1694 // Client B closes the editor, and client A sees client B's selections removed.
1695 cx_b.update(move |_, _| drop(editor_b));
1696 executor.run_until_parked();
1697
1698 buffer_a.read_with(cx_a, |buffer, _| {
1699 buffer
1700 .snapshot()
1701 .selections_in_range(
1702 text::Anchor::min_max_range_for_buffer(buffer.remote_id()),
1703 false,
1704 )
1705 .count()
1706 == 0
1707 });
1708}
1709
1710#[gpui::test(iterations = 10)]
1711async fn test_on_input_format_from_host_to_guest(
1712 cx_a: &mut TestAppContext,
1713 cx_b: &mut TestAppContext,
1714) {
1715 let mut server = TestServer::start(cx_a.executor()).await;
1716 let executor = cx_a.executor();
1717 let client_a = server.create_client(cx_a, "user_a").await;
1718 let client_b = server.create_client(cx_b, "user_b").await;
1719 server
1720 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1721 .await;
1722 let active_call_a = cx_a.read(ActiveCall::global);
1723
1724 client_a.language_registry().add(rust_lang());
1725 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1726 "Rust",
1727 FakeLspAdapter {
1728 capabilities: lsp::ServerCapabilities {
1729 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1730 first_trigger_character: ":".to_string(),
1731 more_trigger_character: Some(vec![">".to_string()]),
1732 }),
1733 ..Default::default()
1734 },
1735 ..Default::default()
1736 },
1737 );
1738
1739 client_a
1740 .fs()
1741 .insert_tree(
1742 path!("/a"),
1743 json!({
1744 "main.rs": "fn main() { a }",
1745 "other.rs": "// Test file",
1746 }),
1747 )
1748 .await;
1749 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1750 let project_id = active_call_a
1751 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1752 .await
1753 .unwrap();
1754 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1755
1756 // Open a file in an editor as the host.
1757 let buffer_a = project_a
1758 .update(cx_a, |p, cx| {
1759 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1760 })
1761 .await
1762 .unwrap();
1763 let cx_a = cx_a.add_empty_window();
1764 let editor_a = cx_a.new_window_entity(|window, cx| {
1765 Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
1766 });
1767
1768 let fake_language_server = fake_language_servers.next().await.unwrap();
1769 executor.run_until_parked();
1770
1771 // Receive an OnTypeFormatting request as the host's language server.
1772 // Return some formatting from the host's language server.
1773 fake_language_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
1774 |params, _| async move {
1775 assert_eq!(
1776 params.text_document_position.text_document.uri,
1777 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1778 );
1779 assert_eq!(
1780 params.text_document_position.position,
1781 lsp::Position::new(0, 14),
1782 );
1783
1784 Ok(Some(vec![lsp::TextEdit {
1785 new_text: "~<".to_string(),
1786 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1787 }]))
1788 },
1789 );
1790
1791 // Open the buffer on the guest and see that the formatting worked
1792 let buffer_b = project_b
1793 .update(cx_b, |p, cx| {
1794 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1795 })
1796 .await
1797 .unwrap();
1798
1799 // Type a on type formatting trigger character as the guest.
1800 cx_a.focus(&editor_a);
1801 editor_a.update_in(cx_a, |editor, window, cx| {
1802 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1803 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1804 });
1805 editor.handle_input(">", window, cx);
1806 });
1807
1808 executor.run_until_parked();
1809
1810 buffer_b.read_with(cx_b, |buffer, _| {
1811 assert_eq!(buffer.text(), "fn main() { a>~< }")
1812 });
1813
1814 // Undo should remove LSP edits first
1815 editor_a.update_in(cx_a, |editor, window, cx| {
1816 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1817 editor.undo(&Undo, window, cx);
1818 assert_eq!(editor.text(cx), "fn main() { a> }");
1819 });
1820 executor.run_until_parked();
1821
1822 buffer_b.read_with(cx_b, |buffer, _| {
1823 assert_eq!(buffer.text(), "fn main() { a> }")
1824 });
1825
1826 editor_a.update_in(cx_a, |editor, window, cx| {
1827 assert_eq!(editor.text(cx), "fn main() { a> }");
1828 editor.undo(&Undo, window, cx);
1829 assert_eq!(editor.text(cx), "fn main() { a }");
1830 });
1831 executor.run_until_parked();
1832
1833 buffer_b.read_with(cx_b, |buffer, _| {
1834 assert_eq!(buffer.text(), "fn main() { a }")
1835 });
1836}
1837
1838#[gpui::test(iterations = 10)]
1839async fn test_on_input_format_from_guest_to_host(
1840 cx_a: &mut TestAppContext,
1841 cx_b: &mut TestAppContext,
1842) {
1843 let mut server = TestServer::start(cx_a.executor()).await;
1844 let executor = cx_a.executor();
1845 let client_a = server.create_client(cx_a, "user_a").await;
1846 let client_b = server.create_client(cx_b, "user_b").await;
1847 server
1848 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1849 .await;
1850 let active_call_a = cx_a.read(ActiveCall::global);
1851
1852 let capabilities = lsp::ServerCapabilities {
1853 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1854 first_trigger_character: ":".to_string(),
1855 more_trigger_character: Some(vec![">".to_string()]),
1856 }),
1857 ..lsp::ServerCapabilities::default()
1858 };
1859 client_a.language_registry().add(rust_lang());
1860 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1861 "Rust",
1862 FakeLspAdapter {
1863 capabilities: capabilities.clone(),
1864 ..FakeLspAdapter::default()
1865 },
1866 );
1867 client_b.language_registry().add(rust_lang());
1868 client_b.language_registry().register_fake_lsp_adapter(
1869 "Rust",
1870 FakeLspAdapter {
1871 capabilities,
1872 ..FakeLspAdapter::default()
1873 },
1874 );
1875
1876 client_a
1877 .fs()
1878 .insert_tree(
1879 path!("/a"),
1880 json!({
1881 "main.rs": "fn main() { a }",
1882 "other.rs": "// Test file",
1883 }),
1884 )
1885 .await;
1886 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1887 let project_id = active_call_a
1888 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1889 .await
1890 .unwrap();
1891 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1892
1893 // Open a file in an editor as the guest.
1894 let buffer_b = project_b
1895 .update(cx_b, |p, cx| {
1896 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1897 })
1898 .await
1899 .unwrap();
1900 let cx_b = cx_b.add_empty_window();
1901 let editor_b = cx_b.new_window_entity(|window, cx| {
1902 Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
1903 });
1904
1905 let fake_language_server = fake_language_servers.next().await.unwrap();
1906 executor.run_until_parked();
1907
1908 // Type a on type formatting trigger character as the guest.
1909 cx_b.focus(&editor_b);
1910 editor_b.update_in(cx_b, |editor, window, cx| {
1911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1912 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1913 });
1914 editor.handle_input(":", window, cx);
1915 });
1916
1917 // Receive an OnTypeFormatting request as the host's language server.
1918 // Return some formatting from the host's language server.
1919 fake_language_server
1920 .set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1921 assert_eq!(
1922 params.text_document_position.text_document.uri,
1923 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1924 );
1925 assert_eq!(
1926 params.text_document_position.position,
1927 lsp::Position::new(0, 14),
1928 );
1929
1930 Ok(Some(vec![lsp::TextEdit {
1931 new_text: "~:".to_string(),
1932 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1933 }]))
1934 })
1935 .next()
1936 .await
1937 .unwrap();
1938
1939 // Open the buffer on the host and see that the formatting worked
1940 let buffer_a = project_a
1941 .update(cx_a, |p, cx| {
1942 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1943 })
1944 .await
1945 .unwrap();
1946 executor.run_until_parked();
1947
1948 buffer_a.read_with(cx_a, |buffer, _| {
1949 assert_eq!(buffer.text(), "fn main() { a:~: }")
1950 });
1951
1952 // Undo should remove LSP edits first
1953 editor_b.update_in(cx_b, |editor, window, cx| {
1954 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1955 editor.undo(&Undo, window, cx);
1956 assert_eq!(editor.text(cx), "fn main() { a: }");
1957 });
1958 executor.run_until_parked();
1959
1960 buffer_a.read_with(cx_a, |buffer, _| {
1961 assert_eq!(buffer.text(), "fn main() { a: }")
1962 });
1963
1964 editor_b.update_in(cx_b, |editor, window, cx| {
1965 assert_eq!(editor.text(cx), "fn main() { a: }");
1966 editor.undo(&Undo, window, cx);
1967 assert_eq!(editor.text(cx), "fn main() { a }");
1968 });
1969 executor.run_until_parked();
1970
1971 buffer_a.read_with(cx_a, |buffer, _| {
1972 assert_eq!(buffer.text(), "fn main() { a }")
1973 });
1974}
1975
1976#[gpui::test(iterations = 10)]
1977async fn test_mutual_editor_inlay_hint_cache_update(
1978 cx_a: &mut TestAppContext,
1979 cx_b: &mut TestAppContext,
1980) {
1981 let mut server = TestServer::start(cx_a.executor()).await;
1982 let executor = cx_a.executor();
1983 let client_a = server.create_client(cx_a, "user_a").await;
1984 let client_b = server.create_client(cx_b, "user_b").await;
1985 server
1986 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1987 .await;
1988 let active_call_a = cx_a.read(ActiveCall::global);
1989 let active_call_b = cx_b.read(ActiveCall::global);
1990
1991 cx_a.update(editor::init);
1992 cx_b.update(editor::init);
1993
1994 cx_a.update(|cx| {
1995 SettingsStore::update_global(cx, |store, cx| {
1996 store.update_user_settings(cx, |settings| {
1997 settings.project.all_languages.defaults.inlay_hints =
1998 Some(InlayHintSettingsContent {
1999 enabled: Some(true),
2000 ..InlayHintSettingsContent::default()
2001 })
2002 });
2003 });
2004 });
2005 cx_b.update(|cx| {
2006 SettingsStore::update_global(cx, |store, cx| {
2007 store.update_user_settings(cx, |settings| {
2008 settings.project.all_languages.defaults.inlay_hints =
2009 Some(InlayHintSettingsContent {
2010 enabled: Some(true),
2011 ..InlayHintSettingsContent::default()
2012 })
2013 });
2014 });
2015 });
2016
2017 let capabilities = lsp::ServerCapabilities {
2018 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2019 ..lsp::ServerCapabilities::default()
2020 };
2021 client_a.language_registry().add(rust_lang());
2022
2023 // Set up the language server to return an additional inlay hint on each request.
2024 let edits_made = Arc::new(AtomicUsize::new(0));
2025 let closure_edits_made = Arc::clone(&edits_made);
2026 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2027 "Rust",
2028 FakeLspAdapter {
2029 capabilities: capabilities.clone(),
2030 initializer: Some(Box::new(move |fake_language_server| {
2031 let closure_edits_made = closure_edits_made.clone();
2032 fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2033 move |params, _| {
2034 let edits_made_2 = Arc::clone(&closure_edits_made);
2035 async move {
2036 assert_eq!(
2037 params.text_document.uri,
2038 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2039 );
2040 let edits_made =
2041 AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
2042 Ok(Some(vec![lsp::InlayHint {
2043 position: lsp::Position::new(0, edits_made as u32),
2044 label: lsp::InlayHintLabel::String(edits_made.to_string()),
2045 kind: None,
2046 text_edits: None,
2047 tooltip: None,
2048 padding_left: None,
2049 padding_right: None,
2050 data: None,
2051 }]))
2052 }
2053 },
2054 );
2055 })),
2056 ..FakeLspAdapter::default()
2057 },
2058 );
2059 client_b.language_registry().add(rust_lang());
2060 client_b.language_registry().register_fake_lsp_adapter(
2061 "Rust",
2062 FakeLspAdapter {
2063 capabilities,
2064 ..FakeLspAdapter::default()
2065 },
2066 );
2067
2068 // Client A opens a project.
2069 client_a
2070 .fs()
2071 .insert_tree(
2072 path!("/a"),
2073 json!({
2074 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
2075 "other.rs": "// Test file",
2076 }),
2077 )
2078 .await;
2079 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2080 active_call_a
2081 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2082 .await
2083 .unwrap();
2084 let project_id = active_call_a
2085 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2086 .await
2087 .unwrap();
2088
2089 // Client B joins the project
2090 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2091 active_call_b
2092 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2093 .await
2094 .unwrap();
2095
2096 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2097
2098 // The host opens a rust file.
2099 let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
2100 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2101 });
2102 let fake_language_server = fake_language_servers.next().await.unwrap();
2103 let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
2104 executor.advance_clock(Duration::from_millis(100));
2105 executor.run_until_parked();
2106
2107 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
2108 editor_a.update(cx_a, |editor, cx| {
2109 assert_eq!(
2110 vec![initial_edit.to_string()],
2111 extract_hint_labels(editor, cx),
2112 "Host should get its first hints when opens an editor"
2113 );
2114 });
2115 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2116 let editor_b = workspace_b
2117 .update_in(cx_b, |workspace, window, cx| {
2118 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2119 })
2120 .await
2121 .unwrap()
2122 .downcast::<Editor>()
2123 .unwrap();
2124
2125 executor.advance_clock(Duration::from_millis(100));
2126 executor.run_until_parked();
2127 editor_b.update(cx_b, |editor, cx| {
2128 assert_eq!(
2129 vec![initial_edit.to_string()],
2130 extract_hint_labels(editor, cx),
2131 "Client should get its first hints when opens an editor"
2132 );
2133 });
2134
2135 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2136 editor_b.update_in(cx_b, |editor, window, cx| {
2137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2138 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
2139 });
2140 editor.handle_input(":", window, cx);
2141 });
2142 cx_b.focus(&editor_b);
2143
2144 executor.advance_clock(Duration::from_secs(1));
2145 executor.run_until_parked();
2146 editor_a.update(cx_a, |editor, cx| {
2147 assert_eq!(
2148 vec![after_client_edit.to_string()],
2149 extract_hint_labels(editor, cx),
2150 );
2151 });
2152 editor_b.update(cx_b, |editor, cx| {
2153 assert_eq!(
2154 vec![after_client_edit.to_string()],
2155 extract_hint_labels(editor, cx),
2156 );
2157 });
2158
2159 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2160 editor_a.update_in(cx_a, |editor, window, cx| {
2161 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2162 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
2163 });
2164 editor.handle_input("a change to increment both buffers' versions", window, cx);
2165 });
2166 cx_a.focus(&editor_a);
2167
2168 executor.advance_clock(Duration::from_secs(1));
2169 executor.run_until_parked();
2170 editor_a.update(cx_a, |editor, cx| {
2171 assert_eq!(
2172 vec![after_host_edit.to_string()],
2173 extract_hint_labels(editor, cx),
2174 );
2175 });
2176 editor_b.update(cx_b, |editor, cx| {
2177 assert_eq!(
2178 vec![after_host_edit.to_string()],
2179 extract_hint_labels(editor, cx),
2180 );
2181 });
2182
2183 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2184 fake_language_server
2185 .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
2186 .await
2187 .into_response()
2188 .expect("inlay refresh request failed");
2189
2190 executor.advance_clock(Duration::from_secs(1));
2191 executor.run_until_parked();
2192 editor_a.update(cx_a, |editor, cx| {
2193 assert_eq!(
2194 vec![after_special_edit_for_refresh.to_string()],
2195 extract_hint_labels(editor, cx),
2196 "Host should react to /refresh LSP request"
2197 );
2198 });
2199 editor_b.update(cx_b, |editor, cx| {
2200 assert_eq!(
2201 vec![after_special_edit_for_refresh.to_string()],
2202 extract_hint_labels(editor, cx),
2203 "Guest should get a /refresh LSP request propagated by host"
2204 );
2205 });
2206}
2207
2208#[gpui::test(iterations = 10)]
2209async fn test_inlay_hint_refresh_is_forwarded(
2210 cx_a: &mut TestAppContext,
2211 cx_b: &mut TestAppContext,
2212) {
2213 let mut server = TestServer::start(cx_a.executor()).await;
2214 let executor = cx_a.executor();
2215 let client_a = server.create_client(cx_a, "user_a").await;
2216 let client_b = server.create_client(cx_b, "user_b").await;
2217 server
2218 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2219 .await;
2220 let active_call_a = cx_a.read(ActiveCall::global);
2221 let active_call_b = cx_b.read(ActiveCall::global);
2222
2223 cx_a.update(editor::init);
2224 cx_b.update(editor::init);
2225
2226 cx_a.update(|cx| {
2227 SettingsStore::update_global(cx, |store, cx| {
2228 store.update_user_settings(cx, |settings| {
2229 settings.project.all_languages.defaults.inlay_hints =
2230 Some(InlayHintSettingsContent {
2231 show_value_hints: Some(true),
2232 enabled: Some(false),
2233 edit_debounce_ms: Some(0),
2234 scroll_debounce_ms: Some(0),
2235 show_type_hints: Some(false),
2236 show_parameter_hints: Some(false),
2237 show_other_hints: Some(false),
2238 show_background: Some(false),
2239 toggle_on_modifiers_press: None,
2240 })
2241 });
2242 });
2243 });
2244 cx_b.update(|cx| {
2245 SettingsStore::update_global(cx, |store, cx| {
2246 store.update_user_settings(cx, |settings| {
2247 settings.project.all_languages.defaults.inlay_hints =
2248 Some(InlayHintSettingsContent {
2249 show_value_hints: Some(true),
2250 enabled: Some(true),
2251 edit_debounce_ms: Some(0),
2252 scroll_debounce_ms: Some(0),
2253 show_type_hints: Some(true),
2254 show_parameter_hints: Some(true),
2255 show_other_hints: Some(true),
2256 show_background: Some(false),
2257 toggle_on_modifiers_press: None,
2258 })
2259 });
2260 });
2261 });
2262
2263 let capabilities = lsp::ServerCapabilities {
2264 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2265 ..lsp::ServerCapabilities::default()
2266 };
2267 client_a.language_registry().add(rust_lang());
2268 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2269 "Rust",
2270 FakeLspAdapter {
2271 capabilities: capabilities.clone(),
2272 ..FakeLspAdapter::default()
2273 },
2274 );
2275 client_b.language_registry().add(rust_lang());
2276 client_b.language_registry().register_fake_lsp_adapter(
2277 "Rust",
2278 FakeLspAdapter {
2279 capabilities,
2280 ..FakeLspAdapter::default()
2281 },
2282 );
2283
2284 client_a
2285 .fs()
2286 .insert_tree(
2287 path!("/a"),
2288 json!({
2289 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
2290 "other.rs": "// Test file",
2291 }),
2292 )
2293 .await;
2294 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2295 active_call_a
2296 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2297 .await
2298 .unwrap();
2299 let project_id = active_call_a
2300 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2301 .await
2302 .unwrap();
2303
2304 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2305 active_call_b
2306 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2307 .await
2308 .unwrap();
2309
2310 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2311 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2312
2313 let editor_a = workspace_a
2314 .update_in(cx_a, |workspace, window, cx| {
2315 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2316 })
2317 .await
2318 .unwrap()
2319 .downcast::<Editor>()
2320 .unwrap();
2321
2322 let editor_b = workspace_b
2323 .update_in(cx_b, |workspace, window, cx| {
2324 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2325 })
2326 .await
2327 .unwrap()
2328 .downcast::<Editor>()
2329 .unwrap();
2330
2331 let other_hints = Arc::new(AtomicBool::new(false));
2332 let fake_language_server = fake_language_servers.next().await.unwrap();
2333 let closure_other_hints = Arc::clone(&other_hints);
2334 fake_language_server
2335 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2336 let task_other_hints = Arc::clone(&closure_other_hints);
2337 async move {
2338 assert_eq!(
2339 params.text_document.uri,
2340 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2341 );
2342 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
2343 let character = if other_hints { 0 } else { 2 };
2344 let label = if other_hints {
2345 "other hint"
2346 } else {
2347 "initial hint"
2348 };
2349 Ok(Some(vec![
2350 lsp::InlayHint {
2351 position: lsp::Position::new(0, character),
2352 label: lsp::InlayHintLabel::String(label.to_string()),
2353 kind: None,
2354 text_edits: None,
2355 tooltip: None,
2356 padding_left: None,
2357 padding_right: None,
2358 data: None,
2359 },
2360 lsp::InlayHint {
2361 position: lsp::Position::new(1090, 1090),
2362 label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
2363 kind: None,
2364 text_edits: None,
2365 tooltip: None,
2366 padding_left: None,
2367 padding_right: None,
2368 data: None,
2369 },
2370 ]))
2371 }
2372 })
2373 .next()
2374 .await
2375 .unwrap();
2376
2377 executor.run_until_parked();
2378 editor_a.update(cx_a, |editor, cx| {
2379 assert!(
2380 extract_hint_labels(editor, cx).is_empty(),
2381 "Host should get no hints due to them turned off"
2382 );
2383 });
2384
2385 executor.run_until_parked();
2386 editor_b.update(cx_b, |editor, cx| {
2387 assert_eq!(
2388 vec!["initial hint".to_string()],
2389 extract_hint_labels(editor, cx),
2390 "Client should get its first hints when opens an editor"
2391 );
2392 });
2393
2394 other_hints.fetch_or(true, atomic::Ordering::Release);
2395 fake_language_server
2396 .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
2397 .await
2398 .into_response()
2399 .expect("inlay refresh request failed");
2400 executor.run_until_parked();
2401 editor_a.update(cx_a, |editor, cx| {
2402 assert!(
2403 extract_hint_labels(editor, cx).is_empty(),
2404 "Host should get no hints due to them turned off, even after the /refresh"
2405 );
2406 });
2407
2408 executor.run_until_parked();
2409 editor_b.update(cx_b, |editor, cx| {
2410 assert_eq!(
2411 vec!["other hint".to_string()],
2412 extract_hint_labels(editor, cx),
2413 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
2414 );
2415 });
2416}
2417
2418#[gpui::test(iterations = 10)]
2419async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2420 let expected_color = Rgba {
2421 r: 0.33,
2422 g: 0.33,
2423 b: 0.33,
2424 a: 0.33,
2425 };
2426 let mut server = TestServer::start(cx_a.executor()).await;
2427 let executor = cx_a.executor();
2428 let client_a = server.create_client(cx_a, "user_a").await;
2429 let client_b = server.create_client(cx_b, "user_b").await;
2430 server
2431 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2432 .await;
2433 let active_call_a = cx_a.read(ActiveCall::global);
2434 let active_call_b = cx_b.read(ActiveCall::global);
2435
2436 cx_a.update(editor::init);
2437 cx_b.update(editor::init);
2438
2439 cx_a.update(|cx| {
2440 SettingsStore::update_global(cx, |store, cx| {
2441 store.update_user_settings(cx, |settings| {
2442 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None);
2443 });
2444 });
2445 });
2446 cx_b.update(|cx| {
2447 SettingsStore::update_global(cx, |store, cx| {
2448 store.update_user_settings(cx, |settings| {
2449 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2450 });
2451 });
2452 });
2453
2454 let capabilities = lsp::ServerCapabilities {
2455 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
2456 ..lsp::ServerCapabilities::default()
2457 };
2458 client_a.language_registry().add(rust_lang());
2459 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2460 "Rust",
2461 FakeLspAdapter {
2462 capabilities: capabilities.clone(),
2463 ..FakeLspAdapter::default()
2464 },
2465 );
2466 client_b.language_registry().add(rust_lang());
2467 client_b.language_registry().register_fake_lsp_adapter(
2468 "Rust",
2469 FakeLspAdapter {
2470 capabilities,
2471 ..FakeLspAdapter::default()
2472 },
2473 );
2474
2475 // Client A opens a project.
2476 client_a
2477 .fs()
2478 .insert_tree(
2479 path!("/a"),
2480 json!({
2481 "main.rs": "fn main() { a }",
2482 }),
2483 )
2484 .await;
2485 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2486 active_call_a
2487 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2488 .await
2489 .unwrap();
2490 let project_id = active_call_a
2491 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2492 .await
2493 .unwrap();
2494
2495 // Client B joins the project
2496 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2497 active_call_b
2498 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2499 .await
2500 .unwrap();
2501
2502 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2503
2504 // The host opens a rust file.
2505 let _buffer_a = project_a
2506 .update(cx_a, |project, cx| {
2507 project.open_local_buffer(path!("/a/main.rs"), cx)
2508 })
2509 .await
2510 .unwrap();
2511 let editor_a = workspace_a
2512 .update_in(cx_a, |workspace, window, cx| {
2513 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2514 })
2515 .await
2516 .unwrap()
2517 .downcast::<Editor>()
2518 .unwrap();
2519
2520 let fake_language_server = fake_language_servers.next().await.unwrap();
2521 cx_a.run_until_parked();
2522 cx_b.run_until_parked();
2523
2524 let requests_made = Arc::new(AtomicUsize::new(0));
2525 let closure_requests_made = Arc::clone(&requests_made);
2526 let mut color_request_handle = fake_language_server
2527 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
2528 let requests_made = Arc::clone(&closure_requests_made);
2529 async move {
2530 assert_eq!(
2531 params.text_document.uri,
2532 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2533 );
2534 requests_made.fetch_add(1, atomic::Ordering::Release);
2535 Ok(vec![lsp::ColorInformation {
2536 range: lsp::Range {
2537 start: lsp::Position {
2538 line: 0,
2539 character: 0,
2540 },
2541 end: lsp::Position {
2542 line: 0,
2543 character: 1,
2544 },
2545 },
2546 color: lsp::Color {
2547 red: 0.33,
2548 green: 0.33,
2549 blue: 0.33,
2550 alpha: 0.33,
2551 },
2552 }])
2553 }
2554 });
2555 executor.run_until_parked();
2556
2557 assert_eq!(
2558 0,
2559 requests_made.load(atomic::Ordering::Acquire),
2560 "Host did not enable document colors, hence should query for none"
2561 );
2562 editor_a.update(cx_a, |editor, cx| {
2563 assert_eq!(
2564 Vec::<Rgba>::new(),
2565 extract_color_inlays(editor, cx),
2566 "No query colors should result in no hints"
2567 );
2568 });
2569
2570 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2571 let editor_b = workspace_b
2572 .update_in(cx_b, |workspace, window, cx| {
2573 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2574 })
2575 .await
2576 .unwrap()
2577 .downcast::<Editor>()
2578 .unwrap();
2579
2580 color_request_handle.next().await.unwrap();
2581 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
2582 executor.run_until_parked();
2583
2584 assert_eq!(
2585 1,
2586 requests_made.load(atomic::Ordering::Acquire),
2587 "The client opened the file and got its first colors back"
2588 );
2589 editor_b.update(cx_b, |editor, cx| {
2590 assert_eq!(
2591 vec![expected_color],
2592 extract_color_inlays(editor, cx),
2593 "With document colors as inlays, color inlays should be pushed"
2594 );
2595 });
2596
2597 editor_a.update_in(cx_a, |editor, window, cx| {
2598 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2599 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
2600 });
2601 editor.handle_input(":", window, cx);
2602 });
2603 color_request_handle.next().await.unwrap();
2604 executor.run_until_parked();
2605 assert_eq!(
2606 2,
2607 requests_made.load(atomic::Ordering::Acquire),
2608 "After the host edits his file, the client should request the colors again"
2609 );
2610 editor_a.update(cx_a, |editor, cx| {
2611 assert_eq!(
2612 Vec::<Rgba>::new(),
2613 extract_color_inlays(editor, cx),
2614 "Host has no colors still"
2615 );
2616 });
2617 editor_b.update(cx_b, |editor, cx| {
2618 assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
2619 });
2620
2621 cx_b.update(|_, cx| {
2622 SettingsStore::update_global(cx, |store, cx| {
2623 store.update_user_settings(cx, |settings| {
2624 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
2625 });
2626 });
2627 });
2628 executor.run_until_parked();
2629 assert_eq!(
2630 2,
2631 requests_made.load(atomic::Ordering::Acquire),
2632 "After the client have changed the colors settings, no extra queries should happen"
2633 );
2634 editor_a.update(cx_a, |editor, cx| {
2635 assert_eq!(
2636 Vec::<Rgba>::new(),
2637 extract_color_inlays(editor, cx),
2638 "Host is unaffected by the client's settings changes"
2639 );
2640 });
2641 editor_b.update(cx_b, |editor, cx| {
2642 assert_eq!(
2643 Vec::<Rgba>::new(),
2644 extract_color_inlays(editor, cx),
2645 "Client should have no colors hints, as in the settings"
2646 );
2647 });
2648
2649 cx_b.update(|_, cx| {
2650 SettingsStore::update_global(cx, |store, cx| {
2651 store.update_user_settings(cx, |settings| {
2652 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2653 });
2654 });
2655 });
2656 executor.run_until_parked();
2657 assert_eq!(
2658 2,
2659 requests_made.load(atomic::Ordering::Acquire),
2660 "After falling back to colors as inlays, no extra LSP queries are made"
2661 );
2662 editor_a.update(cx_a, |editor, cx| {
2663 assert_eq!(
2664 Vec::<Rgba>::new(),
2665 extract_color_inlays(editor, cx),
2666 "Host is unaffected by the client's settings changes, again"
2667 );
2668 });
2669 editor_b.update(cx_b, |editor, cx| {
2670 assert_eq!(
2671 vec![expected_color],
2672 extract_color_inlays(editor, cx),
2673 "Client should have its color hints back"
2674 );
2675 });
2676
2677 cx_a.update(|_, cx| {
2678 SettingsStore::update_global(cx, |store, cx| {
2679 store.update_user_settings(cx, |settings| {
2680 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
2681 });
2682 });
2683 });
2684 color_request_handle.next().await.unwrap();
2685 executor.run_until_parked();
2686 assert_eq!(
2687 3,
2688 requests_made.load(atomic::Ordering::Acquire),
2689 "After the host enables document colors, another LSP query should be made"
2690 );
2691 editor_a.update(cx_a, |editor, cx| {
2692 assert_eq!(
2693 Vec::<Rgba>::new(),
2694 extract_color_inlays(editor, cx),
2695 "Host did not configure document colors as hints hence gets nothing"
2696 );
2697 });
2698 editor_b.update(cx_b, |editor, cx| {
2699 assert_eq!(
2700 vec![expected_color],
2701 extract_color_inlays(editor, cx),
2702 "Client should be unaffected by the host's settings changes"
2703 );
2704 });
2705}
2706
2707async fn test_lsp_pull_diagnostics(
2708 should_stream_workspace_diagnostic: bool,
2709 cx_a: &mut TestAppContext,
2710 cx_b: &mut TestAppContext,
2711) {
2712 let mut server = TestServer::start(cx_a.executor()).await;
2713 let executor = cx_a.executor();
2714 let client_a = server.create_client(cx_a, "user_a").await;
2715 let client_b = server.create_client(cx_b, "user_b").await;
2716 server
2717 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2718 .await;
2719 let active_call_a = cx_a.read(ActiveCall::global);
2720 let active_call_b = cx_b.read(ActiveCall::global);
2721
2722 cx_a.update(editor::init);
2723 cx_b.update(editor::init);
2724
2725 let expected_push_diagnostic_main_message = "pushed main diagnostic";
2726 let expected_push_diagnostic_lib_message = "pushed lib diagnostic";
2727 let expected_pull_diagnostic_main_message = "pulled main diagnostic";
2728 let expected_pull_diagnostic_lib_message = "pulled lib diagnostic";
2729 let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic";
2730 let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic";
2731
2732 let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<Option<String>>::new()));
2733 let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<String>::new()));
2734 let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2735 let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone();
2736 let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone();
2737 let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2738 let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone();
2739 let closure_workspace_diagnostics_pulls_result_ids =
2740 workspace_diagnostics_pulls_result_ids.clone();
2741 let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) =
2742 smol::channel::bounded::<()>(1);
2743 let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
2744 smol::channel::bounded::<()>(1);
2745
2746 let capabilities = lsp::ServerCapabilities {
2747 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
2748 lsp::DiagnosticOptions {
2749 identifier: Some("test-pulls".to_string()),
2750 inter_file_dependencies: true,
2751 workspace_diagnostics: true,
2752 work_done_progress_options: lsp::WorkDoneProgressOptions {
2753 work_done_progress: None,
2754 },
2755 },
2756 )),
2757 ..lsp::ServerCapabilities::default()
2758 };
2759 client_a.language_registry().add(rust_lang());
2760
2761 let pull_diagnostics_handle = Arc::new(parking_lot::Mutex::new(None));
2762 let workspace_diagnostics_pulls_handle = Arc::new(parking_lot::Mutex::new(None));
2763
2764 let closure_pull_diagnostics_handle = pull_diagnostics_handle.clone();
2765 let closure_workspace_diagnostics_pulls_handle = workspace_diagnostics_pulls_handle.clone();
2766 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2767 "Rust",
2768 FakeLspAdapter {
2769 capabilities: capabilities.clone(),
2770 initializer: Some(Box::new(move |fake_language_server| {
2771 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2772 "workspace/diagnostic/{}/1",
2773 fake_language_server.server.server_id()
2774 ));
2775 let closure_workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone();
2776 let diagnostics_pulls_made = closure_diagnostics_pulls_made.clone();
2777 let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
2778 let closure_pull_diagnostics_handle = closure_pull_diagnostics_handle.clone();
2779 let closure_workspace_diagnostics_pulls_handle = closure_workspace_diagnostics_pulls_handle.clone();
2780 let closure_workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2781 let closure_workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2782 let pull_diagnostics_handle = fake_language_server
2783 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(
2784 move |params, _| {
2785 let requests_made = diagnostics_pulls_made.clone();
2786 let diagnostics_pulls_result_ids =
2787 diagnostics_pulls_result_ids.clone();
2788 async move {
2789 let message = if lsp::Uri::from_file_path(path!("/a/main.rs"))
2790 .unwrap()
2791 == params.text_document.uri
2792 {
2793 expected_pull_diagnostic_main_message.to_string()
2794 } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2795 == params.text_document.uri
2796 {
2797 expected_pull_diagnostic_lib_message.to_string()
2798 } else {
2799 panic!("Unexpected document: {}", params.text_document.uri)
2800 };
2801 {
2802 diagnostics_pulls_result_ids
2803 .lock()
2804 .await
2805 .insert(params.previous_result_id);
2806 }
2807 let new_requests_count =
2808 requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2809 Ok(lsp::DocumentDiagnosticReportResult::Report(
2810 lsp::DocumentDiagnosticReport::Full(
2811 lsp::RelatedFullDocumentDiagnosticReport {
2812 related_documents: None,
2813 full_document_diagnostic_report:
2814 lsp::FullDocumentDiagnosticReport {
2815 result_id: Some(format!(
2816 "pull-{new_requests_count}"
2817 )),
2818 items: vec![lsp::Diagnostic {
2819 range: lsp::Range {
2820 start: lsp::Position {
2821 line: 0,
2822 character: 0,
2823 },
2824 end: lsp::Position {
2825 line: 0,
2826 character: 2,
2827 },
2828 },
2829 severity: Some(
2830 lsp::DiagnosticSeverity::ERROR,
2831 ),
2832 message,
2833 ..lsp::Diagnostic::default()
2834 }],
2835 },
2836 },
2837 ),
2838 ))
2839 }
2840 },
2841 );
2842 let _ = closure_pull_diagnostics_handle.lock().insert(pull_diagnostics_handle);
2843
2844 let closure_workspace_diagnostics_pulls_made = closure_workspace_diagnostics_pulls_made.clone();
2845 let workspace_diagnostics_pulls_handle = fake_language_server.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
2846 move |params, _| {
2847 let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone();
2848 let workspace_diagnostics_pulls_result_ids =
2849 closure_workspace_diagnostics_pulls_result_ids.clone();
2850 let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2851 let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2852 let expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
2853 async move {
2854 let workspace_request_count =
2855 workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2856 {
2857 workspace_diagnostics_pulls_result_ids
2858 .lock()
2859 .await
2860 .extend(params.previous_result_ids.into_iter().map(|id| id.value));
2861 }
2862 if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed()
2863 {
2864 assert_eq!(
2865 params.partial_result_params.partial_result_token,
2866 Some(expected_workspace_diagnostic_token)
2867 );
2868 workspace_diagnostic_received_tx.send(()).await.unwrap();
2869 workspace_diagnostic_cancel_rx.recv().await.unwrap();
2870 workspace_diagnostic_cancel_rx.close();
2871 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
2872 // > The final response has to be empty in terms of result values.
2873 return Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2874 lsp::WorkspaceDiagnosticReport { items: Vec::new() },
2875 ));
2876 }
2877 Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2878 lsp::WorkspaceDiagnosticReport {
2879 items: vec![
2880 lsp::WorkspaceDocumentDiagnosticReport::Full(
2881 lsp::WorkspaceFullDocumentDiagnosticReport {
2882 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2883 version: None,
2884 full_document_diagnostic_report:
2885 lsp::FullDocumentDiagnosticReport {
2886 result_id: Some(format!(
2887 "workspace_{workspace_request_count}"
2888 )),
2889 items: vec![lsp::Diagnostic {
2890 range: lsp::Range {
2891 start: lsp::Position {
2892 line: 0,
2893 character: 1,
2894 },
2895 end: lsp::Position {
2896 line: 0,
2897 character: 3,
2898 },
2899 },
2900 severity: Some(lsp::DiagnosticSeverity::WARNING),
2901 message:
2902 expected_workspace_pull_diagnostics_main_message
2903 .to_string(),
2904 ..lsp::Diagnostic::default()
2905 }],
2906 },
2907 },
2908 ),
2909 lsp::WorkspaceDocumentDiagnosticReport::Full(
2910 lsp::WorkspaceFullDocumentDiagnosticReport {
2911 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
2912 version: None,
2913 full_document_diagnostic_report:
2914 lsp::FullDocumentDiagnosticReport {
2915 result_id: Some(format!(
2916 "workspace_{workspace_request_count}"
2917 )),
2918 items: vec![lsp::Diagnostic {
2919 range: lsp::Range {
2920 start: lsp::Position {
2921 line: 0,
2922 character: 1,
2923 },
2924 end: lsp::Position {
2925 line: 0,
2926 character: 3,
2927 },
2928 },
2929 severity: Some(lsp::DiagnosticSeverity::WARNING),
2930 message:
2931 expected_workspace_pull_diagnostics_lib_message
2932 .to_string(),
2933 ..lsp::Diagnostic::default()
2934 }],
2935 },
2936 },
2937 ),
2938 ],
2939 },
2940 ))
2941 }
2942 });
2943 let _ = closure_workspace_diagnostics_pulls_handle.lock().insert(workspace_diagnostics_pulls_handle);
2944 })),
2945 ..FakeLspAdapter::default()
2946 },
2947 );
2948
2949 client_b.language_registry().add(rust_lang());
2950 client_b.language_registry().register_fake_lsp_adapter(
2951 "Rust",
2952 FakeLspAdapter {
2953 capabilities,
2954 ..FakeLspAdapter::default()
2955 },
2956 );
2957
2958 // Client A opens a project.
2959 client_a
2960 .fs()
2961 .insert_tree(
2962 path!("/a"),
2963 json!({
2964 "main.rs": "fn main() { a }",
2965 "lib.rs": "fn other() {}",
2966 }),
2967 )
2968 .await;
2969 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2970 active_call_a
2971 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2972 .await
2973 .unwrap();
2974 let project_id = active_call_a
2975 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2976 .await
2977 .unwrap();
2978
2979 // Client B joins the project
2980 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2981 active_call_b
2982 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2983 .await
2984 .unwrap();
2985
2986 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2987
2988 // The host opens a rust file.
2989 let _buffer_a = project_a
2990 .update(cx_a, |project, cx| {
2991 project.open_local_buffer(path!("/a/main.rs"), cx)
2992 })
2993 .await
2994 .unwrap();
2995 let editor_a_main = workspace_a
2996 .update_in(cx_a, |workspace, window, cx| {
2997 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2998 })
2999 .await
3000 .unwrap()
3001 .downcast::<Editor>()
3002 .unwrap();
3003
3004 let fake_language_server = fake_language_servers.next().await.unwrap();
3005 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
3006 "workspace/diagnostic-{}-1",
3007 fake_language_server.server.server_id()
3008 ));
3009 cx_a.run_until_parked();
3010 cx_b.run_until_parked();
3011 let mut pull_diagnostics_handle = pull_diagnostics_handle.lock().take().unwrap();
3012 let mut workspace_diagnostics_pulls_handle =
3013 workspace_diagnostics_pulls_handle.lock().take().unwrap();
3014
3015 if should_stream_workspace_diagnostic {
3016 workspace_diagnostic_received_rx.recv().await.unwrap();
3017 } else {
3018 workspace_diagnostics_pulls_handle.next().await.unwrap();
3019 }
3020 assert_eq!(
3021 1,
3022 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3023 "Workspace diagnostics should be pulled initially on a server startup"
3024 );
3025 pull_diagnostics_handle.next().await.unwrap();
3026 assert_eq!(
3027 1,
3028 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3029 "Host should query pull diagnostics when the editor is opened"
3030 );
3031 executor.run_until_parked();
3032 editor_a_main.update(cx_a, |editor, cx| {
3033 let snapshot = editor.buffer().read(cx).snapshot(cx);
3034 let all_diagnostics = snapshot
3035 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3036 .collect::<Vec<_>>();
3037 assert_eq!(
3038 all_diagnostics.len(),
3039 1,
3040 "Expected single diagnostic, but got: {all_diagnostics:?}"
3041 );
3042 let diagnostic = &all_diagnostics[0];
3043 let mut expected_messages = vec![expected_pull_diagnostic_main_message];
3044 if !should_stream_workspace_diagnostic {
3045 expected_messages.push(expected_workspace_pull_diagnostics_main_message);
3046 }
3047 assert!(
3048 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3049 "Expected {expected_messages:?} on the host, but got: {}",
3050 diagnostic.diagnostic.message
3051 );
3052 });
3053
3054 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3055 lsp::PublishDiagnosticsParams {
3056 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3057 diagnostics: vec![lsp::Diagnostic {
3058 range: lsp::Range {
3059 start: lsp::Position {
3060 line: 0,
3061 character: 3,
3062 },
3063 end: lsp::Position {
3064 line: 0,
3065 character: 4,
3066 },
3067 },
3068 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
3069 message: expected_push_diagnostic_main_message.to_string(),
3070 ..lsp::Diagnostic::default()
3071 }],
3072 version: None,
3073 },
3074 );
3075 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3076 lsp::PublishDiagnosticsParams {
3077 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3078 diagnostics: vec![lsp::Diagnostic {
3079 range: lsp::Range {
3080 start: lsp::Position {
3081 line: 0,
3082 character: 3,
3083 },
3084 end: lsp::Position {
3085 line: 0,
3086 character: 4,
3087 },
3088 },
3089 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
3090 message: expected_push_diagnostic_lib_message.to_string(),
3091 ..lsp::Diagnostic::default()
3092 }],
3093 version: None,
3094 },
3095 );
3096
3097 if should_stream_workspace_diagnostic {
3098 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3099 token: expected_workspace_diagnostic_token.clone(),
3100 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3101 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3102 items: vec![
3103 lsp::WorkspaceDocumentDiagnosticReport::Full(
3104 lsp::WorkspaceFullDocumentDiagnosticReport {
3105 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3106 version: None,
3107 full_document_diagnostic_report:
3108 lsp::FullDocumentDiagnosticReport {
3109 result_id: Some(format!(
3110 "workspace_{}",
3111 workspace_diagnostics_pulls_made
3112 .fetch_add(1, atomic::Ordering::Release)
3113 + 1
3114 )),
3115 items: vec![lsp::Diagnostic {
3116 range: lsp::Range {
3117 start: lsp::Position {
3118 line: 0,
3119 character: 1,
3120 },
3121 end: lsp::Position {
3122 line: 0,
3123 character: 2,
3124 },
3125 },
3126 severity: Some(lsp::DiagnosticSeverity::ERROR),
3127 message:
3128 expected_workspace_pull_diagnostics_main_message
3129 .to_string(),
3130 ..lsp::Diagnostic::default()
3131 }],
3132 },
3133 },
3134 ),
3135 lsp::WorkspaceDocumentDiagnosticReport::Full(
3136 lsp::WorkspaceFullDocumentDiagnosticReport {
3137 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3138 version: None,
3139 full_document_diagnostic_report:
3140 lsp::FullDocumentDiagnosticReport {
3141 result_id: Some(format!(
3142 "workspace_{}",
3143 workspace_diagnostics_pulls_made
3144 .fetch_add(1, atomic::Ordering::Release)
3145 + 1
3146 )),
3147 items: Vec::new(),
3148 },
3149 },
3150 ),
3151 ],
3152 }),
3153 ),
3154 });
3155 };
3156
3157 let mut workspace_diagnostic_start_count =
3158 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3159
3160 executor.run_until_parked();
3161 editor_a_main.update(cx_a, |editor, cx| {
3162 let snapshot = editor.buffer().read(cx).snapshot(cx);
3163 let all_diagnostics = snapshot
3164 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3165 .collect::<Vec<_>>();
3166 assert_eq!(
3167 all_diagnostics.len(),
3168 2,
3169 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3170 );
3171 let expected_messages = [
3172 expected_workspace_pull_diagnostics_main_message,
3173 expected_pull_diagnostic_main_message,
3174 expected_push_diagnostic_main_message,
3175 ];
3176 for diagnostic in all_diagnostics {
3177 assert!(
3178 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3179 "Expected push and pull messages on the host: {expected_messages:?}, but got: {}",
3180 diagnostic.diagnostic.message
3181 );
3182 }
3183 });
3184
3185 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3186 let editor_b_main = workspace_b
3187 .update_in(cx_b, |workspace, window, cx| {
3188 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
3189 })
3190 .await
3191 .unwrap()
3192 .downcast::<Editor>()
3193 .unwrap();
3194 cx_b.run_until_parked();
3195
3196 pull_diagnostics_handle.next().await.unwrap();
3197 assert_eq!(
3198 2,
3199 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3200 "Client should query pull diagnostics when its editor is opened"
3201 );
3202 executor.run_until_parked();
3203 assert_eq!(
3204 workspace_diagnostic_start_count,
3205 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3206 "Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull"
3207 );
3208 editor_b_main.update(cx_b, |editor, cx| {
3209 let snapshot = editor.buffer().read(cx).snapshot(cx);
3210 let all_diagnostics = snapshot
3211 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3212 .collect::<Vec<_>>();
3213 assert_eq!(
3214 all_diagnostics.len(),
3215 2,
3216 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3217 );
3218
3219 // Despite the workspace diagnostics not re-initialized for the remote client, we can still expect its message synced from the host.
3220 let expected_messages = [
3221 expected_workspace_pull_diagnostics_main_message,
3222 expected_pull_diagnostic_main_message,
3223 expected_push_diagnostic_main_message,
3224 ];
3225 for diagnostic in all_diagnostics {
3226 assert!(
3227 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3228 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3229 diagnostic.diagnostic.message
3230 );
3231 }
3232 });
3233
3234 let editor_b_lib = workspace_b
3235 .update_in(cx_b, |workspace, window, cx| {
3236 workspace.open_path((worktree_id, rel_path("lib.rs")), None, true, window, cx)
3237 })
3238 .await
3239 .unwrap()
3240 .downcast::<Editor>()
3241 .unwrap();
3242
3243 pull_diagnostics_handle.next().await.unwrap();
3244 assert_eq!(
3245 3,
3246 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3247 "Client should query pull diagnostics when its another editor is opened"
3248 );
3249 executor.run_until_parked();
3250 assert_eq!(
3251 workspace_diagnostic_start_count,
3252 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3253 "The remote client still did not anything to trigger the workspace diagnostics pull"
3254 );
3255 editor_b_lib.update(cx_b, |editor, cx| {
3256 let snapshot = editor.buffer().read(cx).snapshot(cx);
3257 let all_diagnostics = snapshot
3258 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3259 .collect::<Vec<_>>();
3260 let expected_messages = [
3261 expected_pull_diagnostic_lib_message,
3262 expected_push_diagnostic_lib_message,
3263 ];
3264 assert_eq!(
3265 all_diagnostics.len(),
3266 2,
3267 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3268 );
3269 for diagnostic in all_diagnostics {
3270 assert!(
3271 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3272 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3273 diagnostic.diagnostic.message
3274 );
3275 }
3276 });
3277
3278 if should_stream_workspace_diagnostic {
3279 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3280 token: expected_workspace_diagnostic_token.clone(),
3281 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3282 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3283 items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
3284 lsp::WorkspaceFullDocumentDiagnosticReport {
3285 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3286 version: None,
3287 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
3288 result_id: Some(format!(
3289 "workspace_{}",
3290 workspace_diagnostics_pulls_made
3291 .fetch_add(1, atomic::Ordering::Release)
3292 + 1
3293 )),
3294 items: vec![lsp::Diagnostic {
3295 range: lsp::Range {
3296 start: lsp::Position {
3297 line: 0,
3298 character: 1,
3299 },
3300 end: lsp::Position {
3301 line: 0,
3302 character: 2,
3303 },
3304 },
3305 severity: Some(lsp::DiagnosticSeverity::ERROR),
3306 message: expected_workspace_pull_diagnostics_lib_message
3307 .to_string(),
3308 ..lsp::Diagnostic::default()
3309 }],
3310 },
3311 },
3312 )],
3313 }),
3314 ),
3315 });
3316 workspace_diagnostic_start_count =
3317 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3318 workspace_diagnostic_cancel_tx.send(()).await.unwrap();
3319 workspace_diagnostics_pulls_handle.next().await.unwrap();
3320 executor.run_until_parked();
3321 editor_b_lib.update(cx_b, |editor, cx| {
3322 let snapshot = editor.buffer().read(cx).snapshot(cx);
3323 let all_diagnostics = snapshot
3324 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3325 .collect::<Vec<_>>();
3326 let expected_messages = [
3327 // Despite workspace diagnostics provided,
3328 // the currently open file's diagnostics should be preferred, as LSP suggests.
3329 expected_pull_diagnostic_lib_message,
3330 expected_push_diagnostic_lib_message,
3331 ];
3332 assert_eq!(
3333 all_diagnostics.len(),
3334 2,
3335 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3336 );
3337 for diagnostic in all_diagnostics {
3338 assert!(
3339 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3340 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3341 diagnostic.diagnostic.message
3342 );
3343 }
3344 });
3345 };
3346
3347 {
3348 assert!(
3349 !diagnostics_pulls_result_ids.lock().await.is_empty(),
3350 "Initial diagnostics pulls should report None at least"
3351 );
3352 assert_eq!(
3353 0,
3354 workspace_diagnostics_pulls_result_ids
3355 .lock()
3356 .await
3357 .deref()
3358 .len(),
3359 "After the initial workspace request, opening files should not reuse any result ids"
3360 );
3361 }
3362
3363 editor_b_lib.update_in(cx_b, |editor, window, cx| {
3364 editor.move_to_end(&MoveToEnd, window, cx);
3365 editor.handle_input(":", window, cx);
3366 });
3367 pull_diagnostics_handle.next().await.unwrap();
3368 // pull_diagnostics_handle.next().await.unwrap();
3369 assert_eq!(
3370 4,
3371 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3372 "Client lib.rs edits should trigger another diagnostics pull for open buffers"
3373 );
3374 workspace_diagnostics_pulls_handle.next().await.unwrap();
3375 assert_eq!(
3376 workspace_diagnostic_start_count + 1,
3377 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3378 "After client lib.rs edits, the workspace diagnostics request should follow"
3379 );
3380 executor.run_until_parked();
3381
3382 editor_b_main.update_in(cx_b, |editor, window, cx| {
3383 editor.move_to_end(&MoveToEnd, window, cx);
3384 editor.handle_input(":", window, cx);
3385 });
3386 pull_diagnostics_handle.next().await.unwrap();
3387 pull_diagnostics_handle.next().await.unwrap();
3388 pull_diagnostics_handle.next().await.unwrap();
3389 assert_eq!(
3390 7,
3391 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3392 "Client main.rs edits should trigger diagnostics pull by both client and host and an extra pull for the client's lib.rs"
3393 );
3394 workspace_diagnostics_pulls_handle.next().await.unwrap();
3395 assert_eq!(
3396 workspace_diagnostic_start_count + 2,
3397 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3398 "After client main.rs edits, the workspace diagnostics pull should follow"
3399 );
3400 executor.run_until_parked();
3401
3402 editor_a_main.update_in(cx_a, |editor, window, cx| {
3403 editor.move_to_end(&MoveToEnd, window, cx);
3404 editor.handle_input(":", window, cx);
3405 });
3406 pull_diagnostics_handle.next().await.unwrap();
3407 pull_diagnostics_handle.next().await.unwrap();
3408 pull_diagnostics_handle.next().await.unwrap();
3409 assert_eq!(
3410 10,
3411 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3412 "Host main.rs edits should trigger another diagnostics pull by both client and host and another pull for the client's lib.rs"
3413 );
3414 workspace_diagnostics_pulls_handle.next().await.unwrap();
3415 assert_eq!(
3416 workspace_diagnostic_start_count + 3,
3417 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3418 "After host main.rs edits, the workspace diagnostics pull should follow"
3419 );
3420 executor.run_until_parked();
3421 let diagnostic_pulls_result_ids = diagnostics_pulls_result_ids.lock().await.len();
3422 let workspace_pulls_result_ids = workspace_diagnostics_pulls_result_ids.lock().await.len();
3423 {
3424 assert!(
3425 diagnostic_pulls_result_ids > 1,
3426 "Should have sent result ids when pulling diagnostics"
3427 );
3428 assert!(
3429 workspace_pulls_result_ids > 1,
3430 "Should have sent result ids when pulling workspace diagnostics"
3431 );
3432 }
3433
3434 fake_language_server
3435 .request::<lsp::request::WorkspaceDiagnosticRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
3436 .await
3437 .into_response()
3438 .expect("workspace diagnostics refresh request failed");
3439 // Workspace refresh now also triggers document diagnostic pulls for all open buffers
3440 pull_diagnostics_handle.next().await.unwrap();
3441 pull_diagnostics_handle.next().await.unwrap();
3442 assert_eq!(
3443 12,
3444 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3445 "Workspace refresh should trigger document pulls for all open buffers (main.rs and lib.rs)"
3446 );
3447 workspace_diagnostics_pulls_handle.next().await.unwrap();
3448 assert_eq!(
3449 workspace_diagnostic_start_count + 4,
3450 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3451 "Another workspace diagnostics pull should happen after the diagnostics refresh server request"
3452 );
3453 {
3454 assert!(
3455 diagnostics_pulls_result_ids.lock().await.len() > diagnostic_pulls_result_ids,
3456 "Document diagnostic pulls should happen after workspace refresh"
3457 );
3458 assert!(
3459 workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
3460 "More workspace diagnostics should be pulled"
3461 );
3462 }
3463 editor_b_lib.update(cx_b, |editor, cx| {
3464 let snapshot = editor.buffer().read(cx).snapshot(cx);
3465 let all_diagnostics = snapshot
3466 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3467 .collect::<Vec<_>>();
3468 let expected_messages = [
3469 expected_workspace_pull_diagnostics_lib_message,
3470 expected_pull_diagnostic_lib_message,
3471 expected_push_diagnostic_lib_message,
3472 ];
3473 assert_eq!(all_diagnostics.len(), 2);
3474 for diagnostic in &all_diagnostics {
3475 assert!(
3476 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3477 "Unexpected diagnostics: {all_diagnostics:?}"
3478 );
3479 }
3480 });
3481 editor_b_main.update(cx_b, |editor, cx| {
3482 let snapshot = editor.buffer().read(cx).snapshot(cx);
3483 let all_diagnostics = snapshot
3484 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3485 .collect::<Vec<_>>();
3486 assert_eq!(all_diagnostics.len(), 2);
3487
3488 let expected_messages = [
3489 expected_workspace_pull_diagnostics_main_message,
3490 expected_pull_diagnostic_main_message,
3491 expected_push_diagnostic_main_message,
3492 ];
3493 for diagnostic in &all_diagnostics {
3494 assert!(
3495 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3496 "Unexpected diagnostics: {all_diagnostics:?}"
3497 );
3498 }
3499 });
3500 editor_a_main.update(cx_a, |editor, cx| {
3501 let snapshot = editor.buffer().read(cx).snapshot(cx);
3502 let all_diagnostics = snapshot
3503 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3504 .collect::<Vec<_>>();
3505 assert_eq!(all_diagnostics.len(), 2);
3506 let expected_messages = [
3507 expected_workspace_pull_diagnostics_main_message,
3508 expected_pull_diagnostic_main_message,
3509 expected_push_diagnostic_main_message,
3510 ];
3511 for diagnostic in &all_diagnostics {
3512 assert!(
3513 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3514 "Unexpected diagnostics: {all_diagnostics:?}"
3515 );
3516 }
3517 });
3518}
3519
3520#[gpui::test(iterations = 10)]
3521async fn test_non_streamed_lsp_pull_diagnostics(
3522 cx_a: &mut TestAppContext,
3523 cx_b: &mut TestAppContext,
3524) {
3525 test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
3526}
3527
3528#[gpui::test(iterations = 10)]
3529async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3530 test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
3531}
3532
3533#[gpui::test(iterations = 10)]
3534async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3535 let mut server = TestServer::start(cx_a.executor()).await;
3536 let client_a = server.create_client(cx_a, "user_a").await;
3537 let client_b = server.create_client(cx_b, "user_b").await;
3538 server
3539 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3540 .await;
3541 let active_call_a = cx_a.read(ActiveCall::global);
3542
3543 cx_a.update(editor::init);
3544 cx_b.update(editor::init);
3545 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
3546 let inline_blame_off_settings = Some(InlineBlameSettings {
3547 enabled: Some(false),
3548 ..Default::default()
3549 });
3550 cx_a.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 cx_b.update(|cx| {
3558 SettingsStore::update_global(cx, |store, cx| {
3559 store.update_user_settings(cx, |settings| {
3560 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3561 });
3562 });
3563 });
3564
3565 client_a
3566 .fs()
3567 .insert_tree(
3568 path!("/my-repo"),
3569 json!({
3570 ".git": {},
3571 "file.txt": "line1\nline2\nline3\nline\n",
3572 }),
3573 )
3574 .await;
3575
3576 let blame = git::blame::Blame {
3577 entries: vec![
3578 blame_entry("1b1b1b", 0..1),
3579 blame_entry("0d0d0d", 1..2),
3580 blame_entry("3a3a3a", 2..3),
3581 blame_entry("4c4c4c", 3..4),
3582 ],
3583 messages: [
3584 ("1b1b1b", "message for idx-0"),
3585 ("0d0d0d", "message for idx-1"),
3586 ("3a3a3a", "message for idx-2"),
3587 ("4c4c4c", "message for idx-3"),
3588 ]
3589 .into_iter()
3590 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3591 .collect(),
3592 };
3593 client_a.fs().set_blame_for_repo(
3594 Path::new(path!("/my-repo/.git")),
3595 vec![(repo_path("file.txt"), blame)],
3596 );
3597
3598 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3599 let project_id = active_call_a
3600 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3601 .await
3602 .unwrap();
3603
3604 // Create editor_a
3605 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3606 let editor_a = workspace_a
3607 .update_in(cx_a, |workspace, window, cx| {
3608 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3609 })
3610 .await
3611 .unwrap()
3612 .downcast::<Editor>()
3613 .unwrap();
3614
3615 // Join the project as client B.
3616 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3617 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3618 let editor_b = workspace_b
3619 .update_in(cx_b, |workspace, window, cx| {
3620 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3621 })
3622 .await
3623 .unwrap()
3624 .downcast::<Editor>()
3625 .unwrap();
3626 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3627 editor_b
3628 .buffer()
3629 .read(cx)
3630 .as_singleton()
3631 .unwrap()
3632 .read(cx)
3633 .remote_id()
3634 });
3635
3636 // client_b now requests git blame for the open buffer
3637 editor_b.update_in(cx_b, |editor_b, window, cx| {
3638 assert!(editor_b.blame().is_none());
3639 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3640 });
3641
3642 cx_a.executor().run_until_parked();
3643 cx_b.executor().run_until_parked();
3644
3645 editor_b.update(cx_b, |editor_b, cx| {
3646 let blame = editor_b.blame().expect("editor_b should have blame now");
3647 let entries = blame.update(cx, |blame, cx| {
3648 blame
3649 .blame_for_rows(
3650 &(0..4)
3651 .map(|row| RowInfo {
3652 buffer_row: Some(row),
3653 buffer_id: Some(buffer_id_b),
3654 ..Default::default()
3655 })
3656 .collect::<Vec<_>>(),
3657 cx,
3658 )
3659 .collect::<Vec<_>>()
3660 });
3661
3662 assert_eq!(
3663 entries,
3664 vec![
3665 Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
3666 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3667 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3668 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3669 ]
3670 );
3671
3672 blame.update(cx, |blame, _| {
3673 for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
3674 let details = blame.details_for_entry(*buffer, entry).unwrap();
3675 assert_eq!(details.message, format!("message for idx-{}", idx));
3676 }
3677 });
3678 });
3679
3680 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3681 // which gets back to client_b.
3682 editor_b.update_in(cx_b, |editor_b, _, cx| {
3683 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3684 });
3685
3686 cx_a.executor().run_until_parked();
3687 cx_b.executor().run_until_parked();
3688
3689 editor_b.update(cx_b, |editor_b, cx| {
3690 let blame = editor_b.blame().expect("editor_b should have blame now");
3691 let entries = blame.update(cx, |blame, cx| {
3692 blame
3693 .blame_for_rows(
3694 &(0..4)
3695 .map(|row| RowInfo {
3696 buffer_row: Some(row),
3697 buffer_id: Some(buffer_id_b),
3698 ..Default::default()
3699 })
3700 .collect::<Vec<_>>(),
3701 cx,
3702 )
3703 .collect::<Vec<_>>()
3704 });
3705
3706 assert_eq!(
3707 entries,
3708 vec![
3709 None,
3710 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3711 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3712 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3713 ]
3714 );
3715 });
3716
3717 // Now editor_a also updates the file
3718 editor_a.update_in(cx_a, |editor_a, _, cx| {
3719 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3720 });
3721
3722 cx_a.executor().run_until_parked();
3723 cx_b.executor().run_until_parked();
3724
3725 editor_b.update(cx_b, |editor_b, cx| {
3726 let blame = editor_b.blame().expect("editor_b should have blame now");
3727 let entries = blame.update(cx, |blame, cx| {
3728 blame
3729 .blame_for_rows(
3730 &(0..4)
3731 .map(|row| RowInfo {
3732 buffer_row: Some(row),
3733 buffer_id: Some(buffer_id_b),
3734 ..Default::default()
3735 })
3736 .collect::<Vec<_>>(),
3737 cx,
3738 )
3739 .collect::<Vec<_>>()
3740 });
3741
3742 assert_eq!(
3743 entries,
3744 vec![
3745 None,
3746 None,
3747 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3748 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3749 ]
3750 );
3751 });
3752}
3753
3754#[gpui::test(iterations = 30)]
3755async fn test_collaborating_with_editorconfig(
3756 cx_a: &mut TestAppContext,
3757 cx_b: &mut TestAppContext,
3758) {
3759 let mut server = TestServer::start(cx_a.executor()).await;
3760 let client_a = server.create_client(cx_a, "user_a").await;
3761 let client_b = server.create_client(cx_b, "user_b").await;
3762 server
3763 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3764 .await;
3765 let active_call_a = cx_a.read(ActiveCall::global);
3766
3767 cx_b.update(editor::init);
3768
3769 // Set up a fake language server.
3770 client_a.language_registry().add(rust_lang());
3771 client_a
3772 .fs()
3773 .insert_tree(
3774 path!("/a"),
3775 json!({
3776 "src": {
3777 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3778 "other_mod": {
3779 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3780 ".editorconfig": "",
3781 },
3782 },
3783 ".editorconfig": "[*]\ntab_width = 2\n",
3784 }),
3785 )
3786 .await;
3787 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3788 let project_id = active_call_a
3789 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3790 .await
3791 .unwrap();
3792 let main_buffer_a = project_a
3793 .update(cx_a, |p, cx| {
3794 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3795 })
3796 .await
3797 .unwrap();
3798 let other_buffer_a = project_a
3799 .update(cx_a, |p, cx| {
3800 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3801 })
3802 .await
3803 .unwrap();
3804 let cx_a = cx_a.add_empty_window();
3805 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3806 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3807 });
3808 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3809 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3810 });
3811 let mut main_editor_cx_a = EditorTestContext {
3812 cx: cx_a.clone(),
3813 window: cx_a.window_handle(),
3814 editor: main_editor_a,
3815 assertion_cx: AssertionContextManager::new(),
3816 };
3817 let mut other_editor_cx_a = EditorTestContext {
3818 cx: cx_a.clone(),
3819 window: cx_a.window_handle(),
3820 editor: other_editor_a,
3821 assertion_cx: AssertionContextManager::new(),
3822 };
3823
3824 // Join the project as client B.
3825 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3826 let main_buffer_b = project_b
3827 .update(cx_b, |p, cx| {
3828 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3829 })
3830 .await
3831 .unwrap();
3832 let other_buffer_b = project_b
3833 .update(cx_b, |p, cx| {
3834 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3835 })
3836 .await
3837 .unwrap();
3838 let cx_b = cx_b.add_empty_window();
3839 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3840 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3841 });
3842 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3843 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3844 });
3845 let mut main_editor_cx_b = EditorTestContext {
3846 cx: cx_b.clone(),
3847 window: cx_b.window_handle(),
3848 editor: main_editor_b,
3849 assertion_cx: AssertionContextManager::new(),
3850 };
3851 let mut other_editor_cx_b = EditorTestContext {
3852 cx: cx_b.clone(),
3853 window: cx_b.window_handle(),
3854 editor: other_editor_b,
3855 assertion_cx: AssertionContextManager::new(),
3856 };
3857
3858 let initial_main = indoc! {"
3859ˇmod other;
3860fn main() { let foo = other::foo(); }"};
3861 let initial_other = indoc! {"
3862ˇpub fn foo() -> usize {
3863 4
3864}"};
3865
3866 let first_tabbed_main = indoc! {"
3867 ˇmod other;
3868fn main() { let foo = other::foo(); }"};
3869 tab_undo_assert(
3870 &mut main_editor_cx_a,
3871 &mut main_editor_cx_b,
3872 initial_main,
3873 first_tabbed_main,
3874 true,
3875 );
3876 tab_undo_assert(
3877 &mut main_editor_cx_a,
3878 &mut main_editor_cx_b,
3879 initial_main,
3880 first_tabbed_main,
3881 false,
3882 );
3883
3884 let first_tabbed_other = indoc! {"
3885 ˇpub fn foo() -> usize {
3886 4
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 true,
3894 );
3895 tab_undo_assert(
3896 &mut other_editor_cx_a,
3897 &mut other_editor_cx_b,
3898 initial_other,
3899 first_tabbed_other,
3900 false,
3901 );
3902
3903 client_a
3904 .fs()
3905 .atomic_write(
3906 PathBuf::from(path!("/a/src/.editorconfig")),
3907 "[*]\ntab_width = 3\n".to_owned(),
3908 )
3909 .await
3910 .unwrap();
3911 cx_a.run_until_parked();
3912 cx_b.run_until_parked();
3913
3914 let second_tabbed_main = indoc! {"
3915 ˇmod other;
3916fn main() { let foo = other::foo(); }"};
3917 tab_undo_assert(
3918 &mut main_editor_cx_a,
3919 &mut main_editor_cx_b,
3920 initial_main,
3921 second_tabbed_main,
3922 true,
3923 );
3924 tab_undo_assert(
3925 &mut main_editor_cx_a,
3926 &mut main_editor_cx_b,
3927 initial_main,
3928 second_tabbed_main,
3929 false,
3930 );
3931
3932 let second_tabbed_other = indoc! {"
3933 ˇpub fn foo() -> usize {
3934 4
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 true,
3942 );
3943 tab_undo_assert(
3944 &mut other_editor_cx_a,
3945 &mut other_editor_cx_b,
3946 initial_other,
3947 second_tabbed_other,
3948 false,
3949 );
3950
3951 let editorconfig_buffer_b = project_b
3952 .update(cx_b, |p, cx| {
3953 p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
3954 })
3955 .await
3956 .unwrap();
3957 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3958 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3959 });
3960 project_b
3961 .update(cx_b, |project, cx| {
3962 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3963 })
3964 .await
3965 .unwrap();
3966 cx_a.run_until_parked();
3967 cx_b.run_until_parked();
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 true,
3975 );
3976 tab_undo_assert(
3977 &mut main_editor_cx_a,
3978 &mut main_editor_cx_b,
3979 initial_main,
3980 second_tabbed_main,
3981 false,
3982 );
3983
3984 let third_tabbed_other = indoc! {"
3985 ˇpub fn foo() -> usize {
3986 4
3987}"};
3988 tab_undo_assert(
3989 &mut other_editor_cx_a,
3990 &mut other_editor_cx_b,
3991 initial_other,
3992 third_tabbed_other,
3993 true,
3994 );
3995
3996 tab_undo_assert(
3997 &mut other_editor_cx_a,
3998 &mut other_editor_cx_b,
3999 initial_other,
4000 third_tabbed_other,
4001 false,
4002 );
4003}
4004
4005#[gpui::test(iterations = 10)]
4006async fn test_collaborating_with_external_editorconfig(
4007 cx_a: &mut TestAppContext,
4008 cx_b: &mut TestAppContext,
4009) {
4010 let mut server = TestServer::start(cx_a.executor()).await;
4011 let client_a = server.create_client(cx_a, "user_a").await;
4012 let client_b = server.create_client(cx_b, "user_b").await;
4013 server
4014 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4015 .await;
4016 let active_call_a = cx_a.read(ActiveCall::global);
4017
4018 client_a.language_registry().add(rust_lang());
4019 client_b.language_registry().add(rust_lang());
4020
4021 // Set up external .editorconfig in parent directory
4022 client_a
4023 .fs()
4024 .insert_tree(
4025 path!("/parent"),
4026 json!({
4027 ".editorconfig": "[*]\nindent_size = 5\n",
4028 "worktree": {
4029 ".editorconfig": "[*]\n",
4030 "src": {
4031 "main.rs": "fn main() {}",
4032 },
4033 },
4034 }),
4035 )
4036 .await;
4037
4038 let (project_a, worktree_id) = client_a
4039 .build_local_project(path!("/parent/worktree"), cx_a)
4040 .await;
4041 let project_id = active_call_a
4042 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4043 .await
4044 .unwrap();
4045
4046 project_a.update(cx_a, |project, _| project.languages().add(rust_lang()));
4047
4048 // Open buffer on client A
4049 let buffer_a = project_a
4050 .update(cx_a, |p, cx| {
4051 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
4052 })
4053 .await
4054 .unwrap();
4055
4056 cx_a.run_until_parked();
4057
4058 // Verify client A sees external editorconfig settings
4059 cx_a.read(|cx| {
4060 let settings = LanguageSettings::for_buffer(&buffer_a.read(cx), cx);
4061 assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
4062 });
4063
4064 // Client B joins the project
4065 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4066 project_b.update(cx_b, |project, _| project.languages().add(rust_lang()));
4067 let buffer_b = project_b
4068 .update(cx_b, |p, cx| {
4069 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
4070 })
4071 .await
4072 .unwrap();
4073
4074 cx_b.run_until_parked();
4075
4076 // Verify client B also sees external editorconfig settings
4077 cx_b.read(|cx| {
4078 let settings = LanguageSettings::for_buffer(&buffer_b.read(cx), cx);
4079 assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
4080 });
4081
4082 // Client A modifies the external .editorconfig
4083 client_a
4084 .fs()
4085 .atomic_write(
4086 PathBuf::from(path!("/parent/.editorconfig")),
4087 "[*]\nindent_size = 9\n".to_owned(),
4088 )
4089 .await
4090 .unwrap();
4091
4092 cx_a.run_until_parked();
4093 cx_b.run_until_parked();
4094
4095 // Verify client A sees updated settings
4096 cx_a.read(|cx| {
4097 let settings = LanguageSettings::for_buffer(&buffer_a.read(cx), cx);
4098 assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
4099 });
4100
4101 // Verify client B also sees updated settings
4102 cx_b.read(|cx| {
4103 let settings = LanguageSettings::for_buffer(&buffer_b.read(cx), cx);
4104 assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
4105 });
4106}
4107
4108#[gpui::test]
4109async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4110 let executor = cx_a.executor();
4111 let mut server = TestServer::start(executor.clone()).await;
4112 let client_a = server.create_client(cx_a, "user_a").await;
4113 let client_b = server.create_client(cx_b, "user_b").await;
4114 server
4115 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4116 .await;
4117 let active_call_a = cx_a.read(ActiveCall::global);
4118 let active_call_b = cx_b.read(ActiveCall::global);
4119 cx_a.update(editor::init);
4120 cx_b.update(editor::init);
4121 client_a
4122 .fs()
4123 .insert_tree(
4124 "/a",
4125 json!({
4126 "test.txt": "one\ntwo\nthree\nfour\nfive",
4127 }),
4128 )
4129 .await;
4130 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4131 let project_path = ProjectPath {
4132 worktree_id,
4133 path: rel_path(&"test.txt").into(),
4134 };
4135 let abs_path = project_a.read_with(cx_a, |project, cx| {
4136 project
4137 .absolute_path(&project_path, cx)
4138 .map(Arc::from)
4139 .unwrap()
4140 });
4141
4142 active_call_a
4143 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4144 .await
4145 .unwrap();
4146 let project_id = active_call_a
4147 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4148 .await
4149 .unwrap();
4150 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4151 active_call_b
4152 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4153 .await
4154 .unwrap();
4155 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4156 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4157
4158 // Client A opens an editor.
4159 let editor_a = workspace_a
4160 .update_in(cx_a, |workspace, window, cx| {
4161 workspace.open_path(project_path.clone(), None, true, window, cx)
4162 })
4163 .await
4164 .unwrap()
4165 .downcast::<Editor>()
4166 .unwrap();
4167
4168 // Client B opens same editor as A.
4169 let editor_b = workspace_b
4170 .update_in(cx_b, |workspace, window, cx| {
4171 workspace.open_path(project_path.clone(), None, true, window, cx)
4172 })
4173 .await
4174 .unwrap()
4175 .downcast::<Editor>()
4176 .unwrap();
4177
4178 cx_a.run_until_parked();
4179 cx_b.run_until_parked();
4180
4181 // Client A adds breakpoint on line (1)
4182 editor_a.update_in(cx_a, |editor, window, cx| {
4183 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4184 });
4185
4186 cx_a.run_until_parked();
4187 cx_b.run_until_parked();
4188
4189 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4190 editor
4191 .breakpoint_store()
4192 .unwrap()
4193 .read(cx)
4194 .all_source_breakpoints(cx)
4195 });
4196 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4197 editor
4198 .breakpoint_store()
4199 .unwrap()
4200 .read(cx)
4201 .all_source_breakpoints(cx)
4202 });
4203
4204 assert_eq!(1, breakpoints_a.len());
4205 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4206 assert_eq!(breakpoints_a, breakpoints_b);
4207
4208 // Client B adds breakpoint on line(2)
4209 editor_b.update_in(cx_b, |editor, window, cx| {
4210 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4211 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4212 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4213 });
4214
4215 cx_a.run_until_parked();
4216 cx_b.run_until_parked();
4217
4218 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4219 editor
4220 .breakpoint_store()
4221 .unwrap()
4222 .read(cx)
4223 .all_source_breakpoints(cx)
4224 });
4225 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4226 editor
4227 .breakpoint_store()
4228 .unwrap()
4229 .read(cx)
4230 .all_source_breakpoints(cx)
4231 });
4232
4233 assert_eq!(1, breakpoints_a.len());
4234 assert_eq!(breakpoints_a, breakpoints_b);
4235 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
4236
4237 // Client A removes last added breakpoint from client B
4238 editor_a.update_in(cx_a, |editor, window, cx| {
4239 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4240 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4241 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4242 });
4243
4244 cx_a.run_until_parked();
4245 cx_b.run_until_parked();
4246
4247 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4248 editor
4249 .breakpoint_store()
4250 .unwrap()
4251 .read(cx)
4252 .all_source_breakpoints(cx)
4253 });
4254 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4255 editor
4256 .breakpoint_store()
4257 .unwrap()
4258 .read(cx)
4259 .all_source_breakpoints(cx)
4260 });
4261
4262 assert_eq!(1, breakpoints_a.len());
4263 assert_eq!(breakpoints_a, breakpoints_b);
4264 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4265
4266 // Client B removes first added breakpoint by client A
4267 editor_b.update_in(cx_b, |editor, window, cx| {
4268 editor.move_up(&zed_actions::editor::MoveUp, window, cx);
4269 editor.move_up(&zed_actions::editor::MoveUp, window, cx);
4270 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4271 });
4272
4273 cx_a.run_until_parked();
4274 cx_b.run_until_parked();
4275
4276 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4277 editor
4278 .breakpoint_store()
4279 .unwrap()
4280 .read(cx)
4281 .all_source_breakpoints(cx)
4282 });
4283 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4284 editor
4285 .breakpoint_store()
4286 .unwrap()
4287 .read(cx)
4288 .all_source_breakpoints(cx)
4289 });
4290
4291 assert_eq!(0, breakpoints_a.len());
4292 assert_eq!(breakpoints_a, breakpoints_b);
4293}
4294
4295#[gpui::test]
4296async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4297 let mut server = TestServer::start(cx_a.executor()).await;
4298 let client_a = server.create_client(cx_a, "user_a").await;
4299 let client_b = server.create_client(cx_b, "user_b").await;
4300 server
4301 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4302 .await;
4303 let active_call_a = cx_a.read(ActiveCall::global);
4304 let active_call_b = cx_b.read(ActiveCall::global);
4305
4306 cx_a.update(editor::init);
4307 cx_b.update(editor::init);
4308
4309 client_a.language_registry().add(rust_lang());
4310 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4311 "Rust",
4312 FakeLspAdapter {
4313 name: "rust-analyzer",
4314 ..FakeLspAdapter::default()
4315 },
4316 );
4317 client_b.language_registry().add(rust_lang());
4318 client_b.language_registry().register_fake_lsp_adapter(
4319 "Rust",
4320 FakeLspAdapter {
4321 name: "rust-analyzer",
4322 ..FakeLspAdapter::default()
4323 },
4324 );
4325
4326 client_a
4327 .fs()
4328 .insert_tree(
4329 path!("/a"),
4330 json!({
4331 "main.rs": "fn main() {}",
4332 }),
4333 )
4334 .await;
4335 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4336 active_call_a
4337 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4338 .await
4339 .unwrap();
4340 let project_id = active_call_a
4341 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4342 .await
4343 .unwrap();
4344
4345 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4346 active_call_b
4347 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4348 .await
4349 .unwrap();
4350
4351 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4352 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4353
4354 let editor_a = workspace_a
4355 .update_in(cx_a, |workspace, window, cx| {
4356 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4357 })
4358 .await
4359 .unwrap()
4360 .downcast::<Editor>()
4361 .unwrap();
4362
4363 let editor_b = workspace_b
4364 .update_in(cx_b, |workspace, window, cx| {
4365 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4366 })
4367 .await
4368 .unwrap()
4369 .downcast::<Editor>()
4370 .unwrap();
4371
4372 let fake_language_server = fake_language_servers.next().await.unwrap();
4373
4374 // host
4375 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4376 |params, _| async move {
4377 assert_eq!(
4378 params.text_document.uri,
4379 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4380 );
4381 assert_eq!(params.position, lsp::Position::new(0, 0));
4382 Ok(Some(ExpandedMacro {
4383 name: "test_macro_name".to_string(),
4384 expansion: "test_macro_expansion on the host".to_string(),
4385 }))
4386 },
4387 );
4388
4389 editor_a.update_in(cx_a, |editor, window, cx| {
4390 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4391 });
4392 expand_request_a.next().await.unwrap();
4393 cx_a.run_until_parked();
4394
4395 workspace_a.update(cx_a, |workspace, cx| {
4396 workspace.active_pane().update(cx, |pane, cx| {
4397 assert_eq!(
4398 pane.items_len(),
4399 2,
4400 "Should have added a macro expansion to the host's pane"
4401 );
4402 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4403 new_editor.update(cx, |editor, cx| {
4404 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
4405 });
4406 })
4407 });
4408
4409 // client
4410 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4411 |params, _| async move {
4412 assert_eq!(
4413 params.text_document.uri,
4414 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4415 );
4416 assert_eq!(
4417 params.position,
4418 lsp::Position::new(0, 12),
4419 "editor_b has selected the entire text and should query for a different position"
4420 );
4421 Ok(Some(ExpandedMacro {
4422 name: "test_macro_name".to_string(),
4423 expansion: "test_macro_expansion on the client".to_string(),
4424 }))
4425 },
4426 );
4427
4428 editor_b.update_in(cx_b, |editor, window, cx| {
4429 editor.select_all(&SelectAll, window, cx);
4430 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4431 });
4432 expand_request_b.next().await.unwrap();
4433 cx_b.run_until_parked();
4434
4435 workspace_b.update(cx_b, |workspace, cx| {
4436 workspace.active_pane().update(cx, |pane, cx| {
4437 assert_eq!(
4438 pane.items_len(),
4439 2,
4440 "Should have added a macro expansion to the client's pane"
4441 );
4442 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4443 new_editor.update(cx, |editor, cx| {
4444 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
4445 });
4446 })
4447 });
4448}
4449
4450#[gpui::test]
4451async fn test_copy_file_name_without_extension(
4452 cx_a: &mut TestAppContext,
4453 cx_b: &mut TestAppContext,
4454) {
4455 let mut server = TestServer::start(cx_a.executor()).await;
4456 let client_a = server.create_client(cx_a, "user_a").await;
4457 let client_b = server.create_client(cx_b, "user_b").await;
4458 server
4459 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4460 .await;
4461
4462 cx_b.update(editor::init);
4463
4464 client_a
4465 .fs()
4466 .insert_tree(
4467 path!("/root"),
4468 json!({
4469 "src": {
4470 "main.rs": indoc! {"
4471 fn main() {
4472 println!(\"Hello, world!\");
4473 }
4474 "},
4475 }
4476 }),
4477 )
4478 .await;
4479
4480 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4481 let active_call_a = cx_a.read(ActiveCall::global);
4482 let project_id = active_call_a
4483 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4484 .await
4485 .unwrap();
4486
4487 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4488
4489 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4490 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4491
4492 let editor_a = workspace_a
4493 .update_in(cx_a, |workspace, window, cx| {
4494 workspace.open_path(
4495 (worktree_id, rel_path("src/main.rs")),
4496 None,
4497 true,
4498 window,
4499 cx,
4500 )
4501 })
4502 .await
4503 .unwrap()
4504 .downcast::<Editor>()
4505 .unwrap();
4506
4507 let editor_b = workspace_b
4508 .update_in(cx_b, |workspace, window, cx| {
4509 workspace.open_path(
4510 (worktree_id, rel_path("src/main.rs")),
4511 None,
4512 true,
4513 window,
4514 cx,
4515 )
4516 })
4517 .await
4518 .unwrap()
4519 .downcast::<Editor>()
4520 .unwrap();
4521
4522 cx_a.run_until_parked();
4523 cx_b.run_until_parked();
4524
4525 editor_a.update_in(cx_a, |editor, window, cx| {
4526 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4527 });
4528
4529 assert_eq!(
4530 cx_a.read_from_clipboard().and_then(|item| item.text()),
4531 Some("main".to_string())
4532 );
4533
4534 editor_b.update_in(cx_b, |editor, window, cx| {
4535 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4536 });
4537
4538 assert_eq!(
4539 cx_b.read_from_clipboard().and_then(|item| item.text()),
4540 Some("main".to_string())
4541 );
4542}
4543
4544#[gpui::test]
4545async fn test_copy_file_name(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4546 let mut server = TestServer::start(cx_a.executor()).await;
4547 let client_a = server.create_client(cx_a, "user_a").await;
4548 let client_b = server.create_client(cx_b, "user_b").await;
4549 server
4550 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4551 .await;
4552
4553 cx_b.update(editor::init);
4554
4555 client_a
4556 .fs()
4557 .insert_tree(
4558 path!("/root"),
4559 json!({
4560 "src": {
4561 "main.rs": indoc! {"
4562 fn main() {
4563 println!(\"Hello, world!\");
4564 }
4565 "},
4566 }
4567 }),
4568 )
4569 .await;
4570
4571 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4572 let active_call_a = cx_a.read(ActiveCall::global);
4573 let project_id = active_call_a
4574 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4575 .await
4576 .unwrap();
4577
4578 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4579
4580 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4581 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4582
4583 let editor_a = workspace_a
4584 .update_in(cx_a, |workspace, window, cx| {
4585 workspace.open_path(
4586 (worktree_id, rel_path("src/main.rs")),
4587 None,
4588 true,
4589 window,
4590 cx,
4591 )
4592 })
4593 .await
4594 .unwrap()
4595 .downcast::<Editor>()
4596 .unwrap();
4597
4598 let editor_b = workspace_b
4599 .update_in(cx_b, |workspace, window, cx| {
4600 workspace.open_path(
4601 (worktree_id, rel_path("src/main.rs")),
4602 None,
4603 true,
4604 window,
4605 cx,
4606 )
4607 })
4608 .await
4609 .unwrap()
4610 .downcast::<Editor>()
4611 .unwrap();
4612
4613 cx_a.run_until_parked();
4614 cx_b.run_until_parked();
4615
4616 editor_a.update_in(cx_a, |editor, window, cx| {
4617 editor.copy_file_name(&CopyFileName, window, cx);
4618 });
4619
4620 assert_eq!(
4621 cx_a.read_from_clipboard().and_then(|item| item.text()),
4622 Some("main.rs".to_string())
4623 );
4624
4625 editor_b.update_in(cx_b, |editor, window, cx| {
4626 editor.copy_file_name(&CopyFileName, window, cx);
4627 });
4628
4629 assert_eq!(
4630 cx_b.read_from_clipboard().and_then(|item| item.text()),
4631 Some("main.rs".to_string())
4632 );
4633}
4634
4635#[gpui::test]
4636async fn test_copy_file_location(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4637 let mut server = TestServer::start(cx_a.executor()).await;
4638 let client_a = server.create_client(cx_a, "user_a").await;
4639 let client_b = server.create_client(cx_b, "user_b").await;
4640 server
4641 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4642 .await;
4643
4644 cx_b.update(editor::init);
4645
4646 client_a
4647 .fs()
4648 .insert_tree(
4649 path!("/root"),
4650 json!({
4651 "src": {
4652 "main.rs": indoc! {"
4653 fn main() {
4654 println!(\"Hello, world!\");
4655 }
4656 "},
4657 }
4658 }),
4659 )
4660 .await;
4661
4662 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4663 let active_call_a = cx_a.read(ActiveCall::global);
4664 let project_id = active_call_a
4665 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4666 .await
4667 .unwrap();
4668
4669 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4670
4671 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4672 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4673
4674 let editor_a = workspace_a
4675 .update_in(cx_a, |workspace, window, cx| {
4676 workspace.open_path(
4677 (worktree_id, rel_path("src/main.rs")),
4678 None,
4679 true,
4680 window,
4681 cx,
4682 )
4683 })
4684 .await
4685 .unwrap()
4686 .downcast::<Editor>()
4687 .unwrap();
4688
4689 let editor_b = workspace_b
4690 .update_in(cx_b, |workspace, window, cx| {
4691 workspace.open_path(
4692 (worktree_id, rel_path("src/main.rs")),
4693 None,
4694 true,
4695 window,
4696 cx,
4697 )
4698 })
4699 .await
4700 .unwrap()
4701 .downcast::<Editor>()
4702 .unwrap();
4703
4704 cx_a.run_until_parked();
4705 cx_b.run_until_parked();
4706
4707 editor_a.update_in(cx_a, |editor, window, cx| {
4708 editor.change_selections(Default::default(), window, cx, |s| {
4709 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4710 });
4711 editor.copy_file_location(&CopyFileLocation, window, cx);
4712 });
4713
4714 assert_eq!(
4715 cx_a.read_from_clipboard().and_then(|item| item.text()),
4716 Some(format!("{}:2", path!("src/main.rs")))
4717 );
4718
4719 editor_b.update_in(cx_b, |editor, window, cx| {
4720 editor.change_selections(Default::default(), window, cx, |s| {
4721 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4722 });
4723 editor.copy_file_location(&CopyFileLocation, window, cx);
4724 });
4725
4726 assert_eq!(
4727 cx_b.read_from_clipboard().and_then(|item| item.text()),
4728 Some(format!("{}:2", path!("src/main.rs")))
4729 );
4730
4731 editor_a.update_in(cx_a, |editor, window, cx| {
4732 editor.change_selections(Default::default(), window, cx, |s| {
4733 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(44)]);
4734 });
4735 editor.copy_file_location(&CopyFileLocation, window, cx);
4736 });
4737
4738 assert_eq!(
4739 cx_a.read_from_clipboard().and_then(|item| item.text()),
4740 Some(format!("{}:2-3", path!("src/main.rs")))
4741 );
4742
4743 editor_b.update_in(cx_b, |editor, window, cx| {
4744 editor.change_selections(Default::default(), window, cx, |s| {
4745 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(44)]);
4746 });
4747 editor.copy_file_location(&CopyFileLocation, window, cx);
4748 });
4749
4750 assert_eq!(
4751 cx_b.read_from_clipboard().and_then(|item| item.text()),
4752 Some(format!("{}:2-3", path!("src/main.rs")))
4753 );
4754
4755 editor_a.update_in(cx_a, |editor, window, cx| {
4756 editor.change_selections(Default::default(), window, cx, |s| {
4757 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(43)]);
4758 });
4759 editor.copy_file_location(&CopyFileLocation, window, cx);
4760 });
4761
4762 assert_eq!(
4763 cx_a.read_from_clipboard().and_then(|item| item.text()),
4764 Some(format!("{}:2", path!("src/main.rs")))
4765 );
4766
4767 editor_b.update_in(cx_b, |editor, window, cx| {
4768 editor.change_selections(Default::default(), window, cx, |s| {
4769 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(43)]);
4770 });
4771 editor.copy_file_location(&CopyFileLocation, window, cx);
4772 });
4773
4774 assert_eq!(
4775 cx_b.read_from_clipboard().and_then(|item| item.text()),
4776 Some(format!("{}:2", path!("src/main.rs")))
4777 );
4778}
4779
4780#[track_caller]
4781fn tab_undo_assert(
4782 cx_a: &mut EditorTestContext,
4783 cx_b: &mut EditorTestContext,
4784 expected_initial: &str,
4785 expected_tabbed: &str,
4786 a_tabs: bool,
4787) {
4788 cx_a.assert_editor_state(expected_initial);
4789 cx_b.assert_editor_state(expected_initial);
4790
4791 if a_tabs {
4792 cx_a.update_editor(|editor, window, cx| {
4793 editor.tab(&editor::actions::Tab, window, cx);
4794 });
4795 } else {
4796 cx_b.update_editor(|editor, window, cx| {
4797 editor.tab(&editor::actions::Tab, window, cx);
4798 });
4799 }
4800
4801 cx_a.run_until_parked();
4802 cx_b.run_until_parked();
4803
4804 cx_a.assert_editor_state(expected_tabbed);
4805 cx_b.assert_editor_state(expected_tabbed);
4806
4807 if a_tabs {
4808 cx_a.update_editor(|editor, window, cx| {
4809 editor.undo(&editor::actions::Undo, window, cx);
4810 });
4811 } else {
4812 cx_b.update_editor(|editor, window, cx| {
4813 editor.undo(&editor::actions::Undo, window, cx);
4814 });
4815 }
4816 cx_a.run_until_parked();
4817 cx_b.run_until_parked();
4818 cx_a.assert_editor_state(expected_initial);
4819 cx_b.assert_editor_state(expected_initial);
4820}
4821
4822fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4823 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4824
4825 let mut all_cached_labels = Vec::new();
4826 let mut all_fetched_hints = Vec::new();
4827 for buffer in editor.buffer().read(cx).all_buffers() {
4828 lsp_store.update(cx, |lsp_store, cx| {
4829 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4830 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4831 let mut label = hint.text().to_string();
4832 if hint.padding_left {
4833 label.insert(0, ' ');
4834 }
4835 if hint.padding_right {
4836 label.push_str(" ");
4837 }
4838 label
4839 }));
4840 all_fetched_hints.extend(hints.all_fetched_hints());
4841 });
4842 }
4843
4844 assert!(
4845 all_fetched_hints.is_empty(),
4846 "Did not expect background hints fetch tasks, but got {} of them",
4847 all_fetched_hints.len()
4848 );
4849
4850 all_cached_labels
4851}
4852
4853#[track_caller]
4854fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
4855 editor
4856 .all_inlays(cx)
4857 .into_iter()
4858 .filter_map(|inlay| inlay.get_color())
4859 .map(Rgba::from)
4860 .collect()
4861}
4862
4863fn extract_semantic_token_ranges(editor: &Editor, cx: &App) -> Vec<Range<MultiBufferOffset>> {
4864 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
4865 editor
4866 .display_map
4867 .read(cx)
4868 .semantic_token_highlights
4869 .iter()
4870 .flat_map(|(_, (v, _))| v.iter())
4871 .map(|highlights| highlights.range.to_offset(&multi_buffer_snapshot))
4872 .collect()
4873}
4874
4875#[gpui::test(iterations = 10)]
4876async fn test_mutual_editor_semantic_token_cache_update(
4877 cx_a: &mut TestAppContext,
4878 cx_b: &mut TestAppContext,
4879) {
4880 let mut server = TestServer::start(cx_a.executor()).await;
4881 let executor = cx_a.executor();
4882 let client_a = server.create_client(cx_a, "user_a").await;
4883 let client_b = server.create_client(cx_b, "user_b").await;
4884 server
4885 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4886 .await;
4887 let active_call_a = cx_a.read(ActiveCall::global);
4888 let active_call_b = cx_b.read(ActiveCall::global);
4889
4890 cx_a.update(editor::init);
4891 cx_b.update(editor::init);
4892
4893 cx_a.update(|cx| {
4894 SettingsStore::update_global(cx, |store, cx| {
4895 store.update_user_settings(cx, |settings| {
4896 settings.project.all_languages.defaults.semantic_tokens =
4897 Some(SemanticTokens::Full);
4898 });
4899 });
4900 });
4901 cx_b.update(|cx| {
4902 SettingsStore::update_global(cx, |store, cx| {
4903 store.update_user_settings(cx, |settings| {
4904 settings.project.all_languages.defaults.semantic_tokens =
4905 Some(SemanticTokens::Full);
4906 });
4907 });
4908 });
4909
4910 let capabilities = lsp::ServerCapabilities {
4911 semantic_tokens_provider: Some(
4912 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
4913 lsp::SemanticTokensOptions {
4914 legend: lsp::SemanticTokensLegend {
4915 token_types: vec!["function".into()],
4916 token_modifiers: vec![],
4917 },
4918 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
4919 ..Default::default()
4920 },
4921 ),
4922 ),
4923 ..lsp::ServerCapabilities::default()
4924 };
4925 client_a.language_registry().add(rust_lang());
4926
4927 let edits_made = Arc::new(AtomicUsize::new(0));
4928 let closure_edits_made = Arc::clone(&edits_made);
4929 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4930 "Rust",
4931 FakeLspAdapter {
4932 capabilities: capabilities.clone(),
4933 initializer: Some(Box::new(move |fake_language_server| {
4934 let closure_edits_made = closure_edits_made.clone();
4935 fake_language_server
4936 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
4937 move |_, _| {
4938 let edits_made_2 = Arc::clone(&closure_edits_made);
4939 async move {
4940 let edits_made =
4941 AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
4942 Ok(Some(lsp::SemanticTokensResult::Tokens(
4943 lsp::SemanticTokens {
4944 data: vec![
4945 0, // delta_line
4946 3, // delta_start
4947 edits_made as u32 + 4, // length
4948 0, // token_type
4949 0, // token_modifiers_bitset
4950 ],
4951 result_id: None,
4952 },
4953 )))
4954 }
4955 },
4956 );
4957 })),
4958 ..FakeLspAdapter::default()
4959 },
4960 );
4961 client_b.language_registry().add(rust_lang());
4962 client_b.language_registry().register_fake_lsp_adapter(
4963 "Rust",
4964 FakeLspAdapter {
4965 capabilities,
4966 ..FakeLspAdapter::default()
4967 },
4968 );
4969
4970 client_a
4971 .fs()
4972 .insert_tree(
4973 path!("/a"),
4974 json!({
4975 "main.rs": "fn main() { a }",
4976 "other.rs": "// Test file",
4977 }),
4978 )
4979 .await;
4980 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4981 active_call_a
4982 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4983 .await
4984 .unwrap();
4985 let project_id = active_call_a
4986 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4987 .await
4988 .unwrap();
4989
4990 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4991 active_call_b
4992 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4993 .await
4994 .unwrap();
4995
4996 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4997
4998 let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
4999 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5000 });
5001 let _fake_language_server = fake_language_servers.next().await.unwrap();
5002 let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
5003 executor.advance_clock(Duration::from_millis(100));
5004 executor.run_until_parked();
5005
5006 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
5007 editor_a.update(cx_a, |editor, cx| {
5008 let ranges = extract_semantic_token_ranges(editor, cx);
5009 assert_eq!(
5010 ranges,
5011 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + initial_edit + 4)],
5012 "Host should get its first semantic tokens when opening an editor"
5013 );
5014 });
5015
5016 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5017 let editor_b = workspace_b
5018 .update_in(cx_b, |workspace, window, cx| {
5019 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5020 })
5021 .await
5022 .unwrap()
5023 .downcast::<Editor>()
5024 .unwrap();
5025
5026 executor.advance_clock(Duration::from_millis(100));
5027 executor.run_until_parked();
5028 editor_b.update(cx_b, |editor, cx| {
5029 let ranges = extract_semantic_token_ranges(editor, cx);
5030 assert_eq!(
5031 ranges,
5032 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + initial_edit + 4)],
5033 "Client should get its first semantic tokens when opening an editor"
5034 );
5035 });
5036
5037 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
5038 editor_b.update_in(cx_b, |editor, window, cx| {
5039 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5040 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
5041 });
5042 editor.handle_input(":", window, cx);
5043 });
5044 cx_b.focus(&editor_b);
5045
5046 executor.advance_clock(Duration::from_secs(1));
5047 executor.run_until_parked();
5048 editor_a.update(cx_a, |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 editor_b.update(cx_b, |editor, cx| {
5056 let ranges = extract_semantic_token_ranges(editor, cx);
5057 assert_eq!(
5058 ranges,
5059 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + after_client_edit + 4)],
5060 );
5061 });
5062
5063 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
5064 editor_a.update_in(cx_a, |editor, window, cx| {
5065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5066 s.select_ranges([MultiBufferOffset(14)..MultiBufferOffset(14)])
5067 });
5068 editor.handle_input("a change", window, cx);
5069 });
5070 cx_a.focus(&editor_a);
5071
5072 executor.advance_clock(Duration::from_secs(1));
5073 executor.run_until_parked();
5074 editor_a.update(cx_a, |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 editor_b.update(cx_b, |editor, cx| {
5082 let ranges = extract_semantic_token_ranges(editor, cx);
5083 assert_eq!(
5084 ranges,
5085 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + after_host_edit + 4)],
5086 );
5087 });
5088}
5089
5090#[gpui::test(iterations = 10)]
5091async fn test_semantic_token_refresh_is_forwarded(
5092 cx_a: &mut TestAppContext,
5093 cx_b: &mut TestAppContext,
5094) {
5095 let mut server = TestServer::start(cx_a.executor()).await;
5096 let executor = cx_a.executor();
5097 let client_a = server.create_client(cx_a, "user_a").await;
5098 let client_b = server.create_client(cx_b, "user_b").await;
5099 server
5100 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5101 .await;
5102 let active_call_a = cx_a.read(ActiveCall::global);
5103 let active_call_b = cx_b.read(ActiveCall::global);
5104
5105 cx_a.update(editor::init);
5106 cx_b.update(editor::init);
5107
5108 cx_a.update(|cx| {
5109 SettingsStore::update_global(cx, |store, cx| {
5110 store.update_user_settings(cx, |settings| {
5111 settings.project.all_languages.defaults.semantic_tokens = Some(SemanticTokens::Off);
5112 });
5113 });
5114 });
5115 cx_b.update(|cx| {
5116 SettingsStore::update_global(cx, |store, cx| {
5117 store.update_user_settings(cx, |settings| {
5118 settings.project.all_languages.defaults.semantic_tokens =
5119 Some(SemanticTokens::Full);
5120 });
5121 });
5122 });
5123
5124 let capabilities = lsp::ServerCapabilities {
5125 semantic_tokens_provider: Some(
5126 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
5127 lsp::SemanticTokensOptions {
5128 legend: lsp::SemanticTokensLegend {
5129 token_types: vec!["function".into()],
5130 token_modifiers: vec![],
5131 },
5132 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
5133 ..Default::default()
5134 },
5135 ),
5136 ),
5137 ..lsp::ServerCapabilities::default()
5138 };
5139 client_a.language_registry().add(rust_lang());
5140 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5141 "Rust",
5142 FakeLspAdapter {
5143 capabilities: capabilities.clone(),
5144 ..FakeLspAdapter::default()
5145 },
5146 );
5147 client_b.language_registry().add(rust_lang());
5148 client_b.language_registry().register_fake_lsp_adapter(
5149 "Rust",
5150 FakeLspAdapter {
5151 capabilities,
5152 ..FakeLspAdapter::default()
5153 },
5154 );
5155
5156 client_a
5157 .fs()
5158 .insert_tree(
5159 path!("/a"),
5160 json!({
5161 "main.rs": "fn main() { a }",
5162 "other.rs": "// Test file",
5163 }),
5164 )
5165 .await;
5166 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
5167 active_call_a
5168 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5169 .await
5170 .unwrap();
5171 let project_id = active_call_a
5172 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5173 .await
5174 .unwrap();
5175
5176 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5177 active_call_b
5178 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5179 .await
5180 .unwrap();
5181
5182 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
5183 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5184
5185 let editor_a = workspace_a
5186 .update_in(cx_a, |workspace, window, cx| {
5187 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5188 })
5189 .await
5190 .unwrap()
5191 .downcast::<Editor>()
5192 .unwrap();
5193
5194 let editor_b = workspace_b
5195 .update_in(cx_b, |workspace, window, cx| {
5196 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5197 })
5198 .await
5199 .unwrap()
5200 .downcast::<Editor>()
5201 .unwrap();
5202
5203 let other_tokens = Arc::new(AtomicBool::new(false));
5204 let fake_language_server = fake_language_servers.next().await.unwrap();
5205 let closure_other_tokens = Arc::clone(&other_tokens);
5206 fake_language_server
5207 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(move |params, _| {
5208 let task_other_tokens = Arc::clone(&closure_other_tokens);
5209 async move {
5210 assert_eq!(
5211 params.text_document.uri,
5212 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
5213 );
5214 let other_tokens = task_other_tokens.load(atomic::Ordering::Acquire);
5215 let (delta_start, length) = if other_tokens { (0, 2) } else { (3, 4) };
5216 Ok(Some(lsp::SemanticTokensResult::Tokens(
5217 lsp::SemanticTokens {
5218 data: vec![
5219 0, // delta_line
5220 delta_start,
5221 length,
5222 0, // token_type
5223 0, // token_modifiers_bitset
5224 ],
5225 result_id: None,
5226 },
5227 )))
5228 }
5229 })
5230 .next()
5231 .await
5232 .unwrap();
5233
5234 executor.run_until_parked();
5235 editor_a.update(cx_a, |editor, cx| {
5236 assert!(
5237 extract_semantic_token_ranges(editor, cx).is_empty(),
5238 "Host should get no semantic tokens due to them turned off"
5239 );
5240 });
5241
5242 executor.run_until_parked();
5243 editor_b.update(cx_b, |editor, cx| {
5244 assert_eq!(
5245 vec![MultiBufferOffset(3)..MultiBufferOffset(7)],
5246 extract_semantic_token_ranges(editor, cx),
5247 "Client should get its first semantic tokens when opening an editor"
5248 );
5249 });
5250
5251 other_tokens.fetch_or(true, atomic::Ordering::Release);
5252 fake_language_server
5253 .request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
5254 .await
5255 .into_response()
5256 .expect("semantic tokens refresh request failed");
5257 // wait out the debounce timeout
5258 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
5259 executor.run_until_parked();
5260 editor_a.update(cx_a, |editor, cx| {
5261 assert!(
5262 extract_semantic_token_ranges(editor, cx).is_empty(),
5263 "Host should get no semantic tokens due to them turned off, even after the /refresh"
5264 );
5265 });
5266
5267 executor.run_until_parked();
5268 editor_b.update(cx_b, |editor, cx| {
5269 assert_eq!(
5270 vec![MultiBufferOffset(0)..MultiBufferOffset(2)],
5271 extract_semantic_token_ranges(editor, cx),
5272 "Guest should get a /refresh LSP request propagated by host despite host tokens are off"
5273 );
5274 });
5275}
5276
5277#[gpui::test]
5278async fn test_document_folding_ranges(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5279 let mut server = TestServer::start(cx_a.executor()).await;
5280 let executor = cx_a.executor();
5281 let client_a = server.create_client(cx_a, "user_a").await;
5282 let client_b = server.create_client(cx_b, "user_b").await;
5283 server
5284 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5285 .await;
5286 let active_call_a = cx_a.read(ActiveCall::global);
5287 let active_call_b = cx_b.read(ActiveCall::global);
5288
5289 cx_a.update(editor::init);
5290 cx_b.update(editor::init);
5291
5292 let capabilities = lsp::ServerCapabilities {
5293 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
5294 ..lsp::ServerCapabilities::default()
5295 };
5296 client_a.language_registry().add(rust_lang());
5297 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5298 "Rust",
5299 FakeLspAdapter {
5300 capabilities: capabilities.clone(),
5301 ..FakeLspAdapter::default()
5302 },
5303 );
5304 client_b.language_registry().add(rust_lang());
5305 client_b.language_registry().register_fake_lsp_adapter(
5306 "Rust",
5307 FakeLspAdapter {
5308 capabilities,
5309 ..FakeLspAdapter::default()
5310 },
5311 );
5312
5313 client_a
5314 .fs()
5315 .insert_tree(
5316 path!("/a"),
5317 json!({
5318 "main.rs": "fn main() {\n if true {\n println!(\"hello\");\n }\n}\n",
5319 }),
5320 )
5321 .await;
5322 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
5323 active_call_a
5324 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5325 .await
5326 .unwrap();
5327 let project_id = active_call_a
5328 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5329 .await
5330 .unwrap();
5331
5332 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5333 active_call_b
5334 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5335 .await
5336 .unwrap();
5337
5338 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
5339
5340 let _buffer_a = project_a
5341 .update(cx_a, |project, cx| {
5342 project.open_local_buffer(path!("/a/main.rs"), cx)
5343 })
5344 .await
5345 .unwrap();
5346 let editor_a = workspace_a
5347 .update_in(cx_a, |workspace, window, cx| {
5348 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5349 })
5350 .await
5351 .unwrap()
5352 .downcast::<Editor>()
5353 .unwrap();
5354
5355 let fake_language_server = fake_language_servers.next().await.unwrap();
5356
5357 let folding_request_count = Arc::new(AtomicUsize::new(0));
5358 let closure_count = Arc::clone(&folding_request_count);
5359 let mut folding_request_handle = fake_language_server
5360 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(move |_, _| {
5361 let count = Arc::clone(&closure_count);
5362 async move {
5363 count.fetch_add(1, atomic::Ordering::Release);
5364 Ok(Some(vec![lsp::FoldingRange {
5365 start_line: 0,
5366 start_character: Some(10),
5367 end_line: 4,
5368 end_character: Some(1),
5369 kind: None,
5370 collapsed_text: None,
5371 }]))
5372 }
5373 });
5374
5375 executor.run_until_parked();
5376
5377 assert_eq!(
5378 0,
5379 folding_request_count.load(atomic::Ordering::Acquire),
5380 "LSP folding ranges are off by default, no request should have been made"
5381 );
5382 editor_a.update(cx_a, |editor, cx| {
5383 assert!(
5384 !editor.document_folding_ranges_enabled(cx),
5385 "Host should not have LSP folding ranges enabled"
5386 );
5387 });
5388
5389 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5390 let editor_b = workspace_b
5391 .update_in(cx_b, |workspace, window, cx| {
5392 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5393 })
5394 .await
5395 .unwrap()
5396 .downcast::<Editor>()
5397 .unwrap();
5398 executor.run_until_parked();
5399
5400 editor_b.update(cx_b, |editor, cx| {
5401 assert!(
5402 !editor.document_folding_ranges_enabled(cx),
5403 "Client should not have LSP folding ranges enabled by default"
5404 );
5405 });
5406
5407 cx_b.update(|_, cx| {
5408 SettingsStore::update_global(cx, |store, cx| {
5409 store.update_user_settings(cx, |settings| {
5410 settings
5411 .project
5412 .all_languages
5413 .defaults
5414 .document_folding_ranges = Some(DocumentFoldingRanges::On);
5415 });
5416 });
5417 });
5418 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
5419 folding_request_handle.next().await.unwrap();
5420 executor.run_until_parked();
5421
5422 assert!(
5423 folding_request_count.load(atomic::Ordering::Acquire) > 0,
5424 "After the client enables LSP folding ranges, a request should be made"
5425 );
5426 editor_b.update(cx_b, |editor, cx| {
5427 assert!(
5428 editor.document_folding_ranges_enabled(cx),
5429 "Client should have LSP folding ranges enabled after toggling the setting on"
5430 );
5431 });
5432 editor_a.update(cx_a, |editor, cx| {
5433 assert!(
5434 !editor.document_folding_ranges_enabled(cx),
5435 "Host should remain unaffected by the client's setting change"
5436 );
5437 });
5438
5439 editor_b.update_in(cx_b, |editor, window, cx| {
5440 let snapshot = editor.display_snapshot(cx);
5441 assert!(
5442 !snapshot.is_line_folded(MultiBufferRow(0)),
5443 "Line 0 should not be folded before fold_at"
5444 );
5445 editor.fold_at(MultiBufferRow(0), window, cx);
5446 });
5447 executor.run_until_parked();
5448
5449 editor_b.update(cx_b, |editor, cx| {
5450 let snapshot = editor.display_snapshot(cx);
5451 assert!(
5452 snapshot.is_line_folded(MultiBufferRow(0)),
5453 "Line 0 should be folded after fold_at using LSP folding range"
5454 );
5455 });
5456}
5457
5458#[gpui::test]
5459async fn test_remote_project_worktree_trust(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5460 let has_restricted_worktrees = |project: &gpui::Entity<project::Project>,
5461 cx: &mut VisualTestContext| {
5462 cx.update(|_, cx| {
5463 let worktree_store = project.read(cx).worktree_store();
5464 TrustedWorktrees::try_get_global(cx)
5465 .unwrap()
5466 .read(cx)
5467 .has_restricted_worktrees(&worktree_store, cx)
5468 })
5469 };
5470
5471 cx_a.update(|cx| {
5472 project::trusted_worktrees::init(HashMap::default(), cx);
5473 });
5474 cx_b.update(|cx| {
5475 project::trusted_worktrees::init(HashMap::default(), cx);
5476 });
5477
5478 let mut server = TestServer::start(cx_a.executor()).await;
5479 let client_a = server.create_client(cx_a, "user_a").await;
5480 let client_b = server.create_client(cx_b, "user_b").await;
5481 server
5482 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5483 .await;
5484
5485 client_a
5486 .fs()
5487 .insert_tree(
5488 path!("/a"),
5489 json!({
5490 "file.txt": "contents",
5491 }),
5492 )
5493 .await;
5494
5495 let (project_a, worktree_id) = client_a
5496 .build_local_project_with_trust(path!("/a"), cx_a)
5497 .await;
5498 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
5499 let active_call_a = cx_a.read(ActiveCall::global);
5500 let project_id = active_call_a
5501 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5502 .await
5503 .unwrap();
5504
5505 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5506 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5507
5508 let _editor_a = workspace_a
5509 .update_in(cx_a, |workspace, window, cx| {
5510 workspace.open_path(
5511 (worktree_id, rel_path("src/main.rs")),
5512 None,
5513 true,
5514 window,
5515 cx,
5516 )
5517 })
5518 .await
5519 .unwrap()
5520 .downcast::<Editor>()
5521 .unwrap();
5522
5523 let _editor_b = workspace_b
5524 .update_in(cx_b, |workspace, window, cx| {
5525 workspace.open_path(
5526 (worktree_id, rel_path("src/main.rs")),
5527 None,
5528 true,
5529 window,
5530 cx,
5531 )
5532 })
5533 .await
5534 .unwrap()
5535 .downcast::<Editor>()
5536 .unwrap();
5537
5538 cx_a.run_until_parked();
5539 cx_b.run_until_parked();
5540
5541 assert!(
5542 has_restricted_worktrees(&project_a, cx_a),
5543 "local client should have restricted worktrees after opening it"
5544 );
5545 assert!(
5546 !has_restricted_worktrees(&project_b, cx_b),
5547 "remote client joined a project should have no restricted worktrees"
5548 );
5549
5550 cx_a.update(|_, cx| {
5551 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
5552 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
5553 trusted_worktrees.trust(
5554 &project_a.read(cx).worktree_store(),
5555 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
5556 cx,
5557 );
5558 });
5559 }
5560 });
5561 assert!(
5562 !has_restricted_worktrees(&project_a, cx_a),
5563 "local client should have no worktrees after trusting those"
5564 );
5565 assert!(
5566 !has_restricted_worktrees(&project_b, cx_b),
5567 "remote client should still be trusted"
5568 );
5569}
5570
5571#[gpui::test]
5572async fn test_document_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5573 let mut server = TestServer::start(cx_a.executor()).await;
5574 let executor = cx_a.executor();
5575 let client_a = server.create_client(cx_a, "user_a").await;
5576 let client_b = server.create_client(cx_b, "user_b").await;
5577 server
5578 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5579 .await;
5580 let active_call_a = cx_a.read(ActiveCall::global);
5581 let active_call_b = cx_b.read(ActiveCall::global);
5582
5583 cx_a.update(editor::init);
5584 cx_b.update(editor::init);
5585
5586 let capabilities = lsp::ServerCapabilities {
5587 document_symbol_provider: Some(lsp::OneOf::Left(true)),
5588 ..lsp::ServerCapabilities::default()
5589 };
5590 client_a.language_registry().add(rust_lang());
5591 #[allow(deprecated)]
5592 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5593 "Rust",
5594 FakeLspAdapter {
5595 capabilities: capabilities.clone(),
5596 initializer: Some(Box::new(|fake_language_server| {
5597 #[allow(deprecated)]
5598 fake_language_server
5599 .set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>(
5600 move |_, _| async move {
5601 Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![
5602 lsp::DocumentSymbol {
5603 name: "Foo".to_string(),
5604 detail: None,
5605 kind: lsp::SymbolKind::STRUCT,
5606 tags: None,
5607 deprecated: None,
5608 range: lsp::Range::new(
5609 lsp::Position::new(0, 0),
5610 lsp::Position::new(2, 1),
5611 ),
5612 selection_range: lsp::Range::new(
5613 lsp::Position::new(0, 7),
5614 lsp::Position::new(0, 10),
5615 ),
5616 children: Some(vec![lsp::DocumentSymbol {
5617 name: "bar".to_string(),
5618 detail: None,
5619 kind: lsp::SymbolKind::FIELD,
5620 tags: None,
5621 deprecated: None,
5622 range: lsp::Range::new(
5623 lsp::Position::new(1, 4),
5624 lsp::Position::new(1, 13),
5625 ),
5626 selection_range: lsp::Range::new(
5627 lsp::Position::new(1, 4),
5628 lsp::Position::new(1, 7),
5629 ),
5630 children: None,
5631 }]),
5632 },
5633 ])))
5634 },
5635 );
5636 })),
5637 ..FakeLspAdapter::default()
5638 },
5639 );
5640 client_b.language_registry().add(rust_lang());
5641 client_b.language_registry().register_fake_lsp_adapter(
5642 "Rust",
5643 FakeLspAdapter {
5644 capabilities,
5645 ..FakeLspAdapter::default()
5646 },
5647 );
5648
5649 client_a
5650 .fs()
5651 .insert_tree(
5652 path!("/a"),
5653 json!({
5654 "main.rs": "struct Foo {\n bar: u32,\n}\n",
5655 }),
5656 )
5657 .await;
5658 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
5659 active_call_a
5660 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5661 .await
5662 .unwrap();
5663 let project_id = active_call_a
5664 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5665 .await
5666 .unwrap();
5667
5668 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5669 active_call_b
5670 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5671 .await
5672 .unwrap();
5673
5674 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
5675
5676 let editor_a = workspace_a
5677 .update_in(cx_a, |workspace, window, cx| {
5678 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5679 })
5680 .await
5681 .unwrap()
5682 .downcast::<Editor>()
5683 .unwrap();
5684
5685 let _fake_language_server = fake_language_servers.next().await.unwrap();
5686 executor.run_until_parked();
5687
5688 cx_a.update(|_, cx| {
5689 SettingsStore::update_global(cx, |store, cx| {
5690 store.update_user_settings(cx, |settings| {
5691 settings.project.all_languages.defaults.document_symbols =
5692 Some(DocumentSymbols::On);
5693 });
5694 });
5695 });
5696 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(100));
5697 executor.run_until_parked();
5698
5699 editor_a.update(cx_a, |editor, cx| {
5700 let (breadcrumbs, _) = editor
5701 .breadcrumbs(cx)
5702 .expect("Host should have breadcrumbs");
5703 let texts: Vec<_> = breadcrumbs.iter().map(|b| b.text.as_str()).collect();
5704 assert_eq!(
5705 texts,
5706 vec!["main.rs", "struct Foo"],
5707 "Host should see file path and LSP symbol 'Foo' in breadcrumbs"
5708 );
5709 });
5710
5711 cx_b.update(|cx| {
5712 SettingsStore::update_global(cx, |store, cx| {
5713 store.update_user_settings(cx, |settings| {
5714 settings.project.all_languages.defaults.document_symbols =
5715 Some(DocumentSymbols::On);
5716 });
5717 });
5718 });
5719 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5720 let editor_b = workspace_b
5721 .update_in(cx_b, |workspace, window, cx| {
5722 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5723 })
5724 .await
5725 .unwrap()
5726 .downcast::<Editor>()
5727 .unwrap();
5728 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(100));
5729 executor.run_until_parked();
5730
5731 editor_b.update(cx_b, |editor, cx| {
5732 assert_eq!(
5733 editor
5734 .breadcrumbs(cx)
5735 .expect("Client B should have breadcrumbs")
5736 .0
5737 .iter()
5738 .map(|b| b.text.as_str())
5739 .collect::<Vec<_>>(),
5740 vec!["main.rs", "struct Foo"],
5741 "Client B should see file path and LSP symbol 'Foo' via remote project"
5742 );
5743 });
5744}
5745
5746fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
5747 git::blame::BlameEntry {
5748 sha: sha.parse().unwrap(),
5749 range,
5750 ..Default::default()
5751 }
5752}