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