Skip to repository content1881 lines · 64.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:42:05.675Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
dynamic_registration.rs
1//! Tests for dynamic (un)registration of language server capabilities, handled in
2//! `project::lsp_store::dynamic_registration`.
3
4use pretty_assertions::assert_eq;
5
6use super::*;
7
8#[gpui::test]
9async fn test_dynamic_semantic_tokens_registration(cx: &mut gpui::TestAppContext) {
10 init_test(cx);
11
12 let fs = FakeFs::new(cx.executor());
13 fs.insert_tree(
14 path!("/the-root"),
15 json!({
16 "a.rs": "fn main() {}",
17 }),
18 )
19 .await;
20
21 let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
22 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23 language_registry.add(rust_lang());
24 let mut fake_servers = language_registry.register_fake_lsp(
25 "Rust",
26 FakeLspAdapter {
27 name: "the-language-server",
28 // Crucially, no `semantic_tokens_provider` is advertised statically; the
29 // server only offers it through dynamic registration (as Roslyn does).
30 ..FakeLspAdapter::default()
31 },
32 );
33
34 let _buffer = project
35 .update(cx, |project, cx| {
36 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
37 })
38 .await
39 .unwrap();
40
41 let fake_server = fake_servers.next().await.unwrap();
42 let server_id = fake_server.server.server_id();
43 cx.executor().run_until_parked();
44
45 let semantic_tokens_provider = |cx: &mut gpui::TestAppContext| {
46 project.read_with(cx, |project, cx| {
47 project
48 .lsp_store()
49 .read(cx)
50 .lsp_server_capabilities
51 .get(&server_id)
52 .and_then(|capabilities| capabilities.semantic_tokens_provider.clone())
53 })
54 };
55
56 assert!(
57 semantic_tokens_provider(cx).is_none(),
58 "server should not advertise semantic tokens before dynamic registration"
59 );
60
61 fake_server
62 .request::<lsp::request::RegisterCapability>(
63 lsp::RegistrationParams {
64 registrations: vec![lsp::Registration {
65 id: "semantic-tokens".to_string(),
66 method: "textDocument/semanticTokens".to_string(),
67 register_options: serde_json::to_value(
68 lsp::SemanticTokensRegistrationOptions {
69 text_document_registration_options:
70 lsp::TextDocumentRegistrationOptions {
71 document_selector: None,
72 },
73 semantic_tokens_options: lsp::SemanticTokensOptions {
74 legend: lsp::SemanticTokensLegend {
75 token_types: vec!["keyword".into(), "variable".into()],
76 token_modifiers: vec![],
77 },
78 full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
79 ..lsp::SemanticTokensOptions::default()
80 },
81 static_registration_options: lsp::StaticRegistrationOptions {
82 id: None,
83 },
84 },
85 )
86 .ok(),
87 }],
88 },
89 DEFAULT_LSP_REQUEST_TIMEOUT,
90 )
91 .await
92 .into_response()
93 .unwrap();
94 cx.executor().run_until_parked();
95
96 let provider = semantic_tokens_provider(cx)
97 .expect("semantic tokens provider should be set after dynamic registration");
98 // The capability round-trips through capability-sync serialization, which may
99 // normalize the registration options into plain options; either shape is fine
100 // as long as the legend survives.
101 let legend = match provider {
102 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(options) => options.legend,
103 lsp::SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(options) => {
104 options.semantic_tokens_options.legend
105 }
106 };
107 assert_eq!(
108 legend.token_types,
109 vec!["keyword".into(), "variable".into()],
110 );
111
112 fake_server
113 .request::<lsp::request::UnregisterCapability>(
114 lsp::UnregistrationParams {
115 unregisterations: vec![lsp::Unregistration {
116 id: "semantic-tokens".to_string(),
117 method: "textDocument/semanticTokens".to_string(),
118 }],
119 },
120 DEFAULT_LSP_REQUEST_TIMEOUT,
121 )
122 .await
123 .into_response()
124 .unwrap();
125 cx.executor().run_until_parked();
126
127 assert!(
128 semantic_tokens_provider(cx).is_none(),
129 "semantic tokens provider should be cleared after unregistration"
130 );
131}
132
133#[gpui::test]
134async fn test_multi_registration_inlay_hint(cx: &mut gpui::TestAppContext) {
135 init_test(cx);
136 let (project, fake_server) =
137 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
138 let server_id = fake_server.server.server_id();
139 let method = "textDocument/inlayHint";
140
141 let options_a = lsp::InlayHintOptions {
142 resolve_provider: Some(true),
143 ..lsp::InlayHintOptions::default()
144 };
145 let options_b = lsp::InlayHintOptions {
146 resolve_provider: Some(false),
147 ..lsp::InlayHintOptions::default()
148 };
149
150 assert_eq!(
151 server_capabilities(&project, server_id, cx).inlay_hint_provider,
152 None,
153 "expected no inlay hint provider before any registration",
154 );
155
156 register_capability(
157 &fake_server,
158 method,
159 "inlay-hint-a",
160 serde_json::to_value(&options_a).ok(),
161 )
162 .await;
163 cx.executor().run_until_parked();
164 assert_eq!(
165 server_capabilities(&project, server_id, cx).inlay_hint_provider,
166 Some(lsp::OneOf::Right(
167 lsp::InlayHintServerCapabilities::Options(options_a.clone())
168 )),
169 "expected the first registration's options after the first registration",
170 );
171
172 register_capability(
173 &fake_server,
174 method,
175 "inlay-hint-b",
176 serde_json::to_value(&options_b).ok(),
177 )
178 .await;
179 cx.executor().run_until_parked();
180 assert_eq!(
181 server_capabilities(&project, server_id, cx).inlay_hint_provider,
182 Some(lsp::OneOf::Right(
183 lsp::InlayHintServerCapabilities::Options(options_b)
184 )),
185 "expected the second registration's options after the second registration",
186 );
187
188 unregister_capabilities(&fake_server, method, &["inlay-hint-b"]).await;
189 cx.executor().run_until_parked();
190 assert_eq!(
191 server_capabilities(&project, server_id, cx).inlay_hint_provider,
192 Some(lsp::OneOf::Right(
193 lsp::InlayHintServerCapabilities::Options(options_a)
194 )),
195 "expected the remaining registration's options to be restored",
196 );
197
198 unregister_capabilities(&fake_server, method, &["inlay-hint-a"]).await;
199 cx.executor().run_until_parked();
200 assert_eq!(
201 server_capabilities(&project, server_id, cx).inlay_hint_provider,
202 None,
203 "expected inlay hint provider to be cleared after unregistering the last registration",
204 );
205}
206
207#[gpui::test]
208async fn test_multi_registration_code_lens(cx: &mut gpui::TestAppContext) {
209 init_test(cx);
210 let (project, fake_server) =
211 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
212 let server_id = fake_server.server.server_id();
213 let method = "textDocument/codeLens";
214
215 let options_a = lsp::CodeLensOptions {
216 resolve_provider: Some(true),
217 };
218 let options_b = lsp::CodeLensOptions {
219 resolve_provider: Some(false),
220 };
221
222 assert_eq!(
223 server_capabilities(&project, server_id, cx).code_lens_provider,
224 None,
225 "expected no code lens provider before any registration",
226 );
227
228 register_capability(
229 &fake_server,
230 method,
231 "code-lens-a",
232 serde_json::to_value(options_a).ok(),
233 )
234 .await;
235 register_capability(
236 &fake_server,
237 method,
238 "code-lens-b",
239 serde_json::to_value(options_b).ok(),
240 )
241 .await;
242 cx.executor().run_until_parked();
243 assert_eq!(
244 server_capabilities(&project, server_id, cx).code_lens_provider,
245 Some(options_b),
246 "expected the second registration's options after two registrations",
247 );
248
249 unregister_capabilities(&fake_server, method, &["code-lens-b"]).await;
250 cx.executor().run_until_parked();
251 assert_eq!(
252 server_capabilities(&project, server_id, cx).code_lens_provider,
253 Some(options_a),
254 "expected the remaining registration's options to be restored",
255 );
256
257 unregister_capabilities(&fake_server, method, &["code-lens-a"]).await;
258 cx.executor().run_until_parked();
259 assert_eq!(
260 server_capabilities(&project, server_id, cx).code_lens_provider,
261 None,
262 "expected code lens provider to be cleared after unregistering the last registration",
263 );
264}
265
266#[gpui::test]
267async fn test_multi_registration_document_symbol(cx: &mut gpui::TestAppContext) {
268 init_test(cx);
269 let (project, fake_server) =
270 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
271 let server_id = fake_server.server.server_id();
272 let method = "textDocument/documentSymbol";
273
274 let options_b = lsp::DocumentSymbolOptions {
275 label: Some("custom".to_string()),
276 work_done_progress_options: lsp::WorkDoneProgressOptions::default(),
277 };
278
279 assert_eq!(
280 server_capabilities(&project, server_id, cx).document_symbol_provider,
281 None,
282 "expected no document symbol provider before any registration",
283 );
284
285 register_capability(&fake_server, method, "document-symbol-a", None).await;
286 register_capability(
287 &fake_server,
288 method,
289 "document-symbol-b",
290 serde_json::to_value(&options_b).ok(),
291 )
292 .await;
293 cx.executor().run_until_parked();
294 assert_eq!(
295 server_capabilities(&project, server_id, cx).document_symbol_provider,
296 Some(lsp::OneOf::Right(options_b)),
297 "expected the second registration's options after two registrations",
298 );
299
300 unregister_capabilities(&fake_server, method, &["document-symbol-b"]).await;
301 cx.executor().run_until_parked();
302 assert_eq!(
303 server_capabilities(&project, server_id, cx).document_symbol_provider,
304 Some(lsp::OneOf::Left(true)),
305 "expected the remaining registration's options to be restored",
306 );
307
308 unregister_capabilities(&fake_server, method, &["document-symbol-a"]).await;
309 cx.executor().run_until_parked();
310 assert_eq!(
311 server_capabilities(&project, server_id, cx).document_symbol_provider,
312 None,
313 "expected document symbol provider to be cleared after unregistering the last registration",
314 );
315}
316
317#[gpui::test]
318async fn test_multi_registration_restores_static_capability(cx: &mut gpui::TestAppContext) {
319 init_test(cx);
320 let (project, fake_server) = setup_dynamic_registration_test(
321 cx,
322 lsp::ServerCapabilities {
323 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
324 ..lsp::ServerCapabilities::default()
325 },
326 )
327 .await;
328 let server_id = fake_server.server.server_id();
329 let method = "textDocument/inlayHint";
330
331 assert_eq!(
332 server_capabilities(&project, server_id, cx).inlay_hint_provider,
333 Some(lsp::OneOf::Left(true)),
334 "expected the statically declared inlay hint provider before any dynamic registration",
335 );
336
337 let dynamic_options = lsp::InlayHintOptions {
338 resolve_provider: Some(true),
339 ..lsp::InlayHintOptions::default()
340 };
341 register_capability(
342 &fake_server,
343 method,
344 "inlay-hint-dynamic",
345 serde_json::to_value(&dynamic_options).ok(),
346 )
347 .await;
348 cx.executor().run_until_parked();
349 assert_eq!(
350 server_capabilities(&project, server_id, cx).inlay_hint_provider,
351 Some(lsp::OneOf::Right(
352 lsp::InlayHintServerCapabilities::Options(dynamic_options)
353 )),
354 "expected the dynamic registration's options to override the static capability",
355 );
356
357 unregister_capabilities(&fake_server, method, &["unknown-id", "inlay-hint-dynamic"]).await;
358 cx.executor().run_until_parked();
359 assert_eq!(
360 server_capabilities(&project, server_id, cx).inlay_hint_provider,
361 Some(lsp::OneOf::Left(true)),
362 "expected the static capability to be restored after unregistering the last dynamic registration, despite an unknown ID earlier in the batch",
363 );
364}
365
366#[gpui::test]
367async fn test_multi_registration_duplicate_id_keeps_order(cx: &mut gpui::TestAppContext) {
368 init_test(cx);
369 let (project, fake_server) =
370 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
371 let server_id = fake_server.server.server_id();
372 let method = "textDocument/inlayHint";
373
374 let options_a = lsp::InlayHintOptions {
375 resolve_provider: Some(true),
376 ..lsp::InlayHintOptions::default()
377 };
378 let options_b = lsp::InlayHintOptions {
379 resolve_provider: Some(false),
380 ..lsp::InlayHintOptions::default()
381 };
382 let options_a_replacement = lsp::InlayHintOptions::default();
383
384 let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
385 register_capability(
386 &fake_server,
387 method,
388 "inlay-hint-a",
389 serde_json::to_value(&options_a).ok(),
390 )
391 .await;
392 register_capability(
393 &fake_server,
394 method,
395 "inlay-hint-b",
396 serde_json::to_value(&options_b).ok(),
397 )
398 .await;
399 cx.executor().run_until_parked();
400 assert_eq!(
401 refresh_events.lock().drain(..).collect::<Vec<_>>(),
402 vec![
403 format!("inlay_hints({server_id})"),
404 format!("inlay_hints({server_id})"),
405 ],
406 "expected both registrations to trigger a refresh",
407 );
408
409 register_capability(
410 &fake_server,
411 method,
412 "inlay-hint-a",
413 serde_json::to_value(&options_a_replacement).ok(),
414 )
415 .await;
416 cx.executor().run_until_parked();
417 assert_eq!(
418 server_capabilities(&project, server_id, cx).inlay_hint_provider,
419 Some(lsp::OneOf::Right(
420 lsp::InlayHintServerCapabilities::Options(options_b)
421 )),
422 "expected the latest distinct registration to stay active after a duplicate ID replaced an older one",
423 );
424 assert_eq!(
425 refresh_events.lock().as_slice(),
426 &[] as &[String],
427 "expected no refresh after a duplicate ID replaced an inactive registration",
428 );
429
430 unregister_capabilities(&fake_server, method, &["inlay-hint-b"]).await;
431 cx.executor().run_until_parked();
432 assert_eq!(
433 server_capabilities(&project, server_id, cx).inlay_hint_provider,
434 Some(lsp::OneOf::Right(
435 lsp::InlayHintServerCapabilities::Options(options_a_replacement)
436 )),
437 "expected the replaced registration's options to be restored",
438 );
439
440 unregister_capabilities(&fake_server, method, &["inlay-hint-a"]).await;
441 cx.executor().run_until_parked();
442 assert_eq!(
443 server_capabilities(&project, server_id, cx).inlay_hint_provider,
444 None,
445 "expected inlay hint provider to be cleared after unregistering the last registration",
446 );
447}
448
449#[gpui::test]
450async fn test_registration_with_unchanged_options_does_not_refresh(cx: &mut gpui::TestAppContext) {
451 init_test(cx);
452 let (project, fake_server) =
453 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
454 let server_id = fake_server.server.server_id();
455 let method = "textDocument/codeLens";
456
457 let options = lsp::CodeLensOptions {
458 resolve_provider: Some(true),
459 };
460
461 let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
462 register_capability(
463 &fake_server,
464 method,
465 "lens-a",
466 serde_json::to_value(options).ok(),
467 )
468 .await;
469 cx.executor().run_until_parked();
470 assert_eq!(
471 refresh_events.lock().drain(..).collect::<Vec<_>>(),
472 vec![format!("code_lens({server_id})")],
473 "expected the first registration to refresh",
474 );
475
476 register_capability(
477 &fake_server,
478 method,
479 "lens-b",
480 serde_json::to_value(options).ok(),
481 )
482 .await;
483 cx.executor().run_until_parked();
484 assert_eq!(
485 refresh_events.lock().as_slice(),
486 &[] as &[String],
487 "expected a registration with options identical to the active ones to not refresh",
488 );
489
490 unregister_capabilities(&fake_server, method, &["lens-b"]).await;
491 cx.executor().run_until_parked();
492 assert_eq!(
493 refresh_events.lock().as_slice(),
494 &[] as &[String],
495 "expected an unregistration that restores identical options to not refresh",
496 );
497 assert_eq!(
498 server_capabilities(&project, server_id, cx).code_lens_provider,
499 Some(options),
500 "expected the remaining registration's options to stay active",
501 );
502
503 unregister_capabilities(&fake_server, method, &["lens-a"]).await;
504 cx.executor().run_until_parked();
505 assert_eq!(
506 refresh_events.lock().drain(..).collect::<Vec<_>>(),
507 vec![format!("code_lens({server_id})")],
508 "expected the last unregistration to clear the capability and refresh",
509 );
510 assert_eq!(
511 server_capabilities(&project, server_id, cx).code_lens_provider,
512 None,
513 "expected the code lens provider to be cleared after unregistering the last registration",
514 );
515}
516
517#[gpui::test]
518async fn test_multi_registration_completion_triggers(cx: &mut gpui::TestAppContext) {
519 init_test(cx);
520 let (project, fake_server) =
521 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
522 let method = "textDocument/completion";
523
524 let (buffer, _lsp_handle) = project
525 .update(cx, |project, cx| {
526 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
527 })
528 .await
529 .unwrap();
530 let buffer_triggers = |cx: &mut gpui::TestAppContext| {
531 buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone())
532 };
533
534 let options_a = lsp::CompletionOptions {
535 trigger_characters: Some(vec![".".to_string()]),
536 ..lsp::CompletionOptions::default()
537 };
538 let options_b = lsp::CompletionOptions {
539 trigger_characters: Some(vec![":".to_string()]),
540 ..lsp::CompletionOptions::default()
541 };
542
543 register_capability(
544 &fake_server,
545 method,
546 "completion-a",
547 serde_json::to_value(&options_a).ok(),
548 )
549 .await;
550 cx.executor().run_until_parked();
551 assert_eq!(
552 buffer_triggers(cx),
553 BTreeSet::from([".".to_string()]),
554 "expected the first registration's triggers to be applied",
555 );
556
557 register_capability(
558 &fake_server,
559 method,
560 "completion-b",
561 serde_json::to_value(&options_b).ok(),
562 )
563 .await;
564 cx.executor().run_until_parked();
565 assert_eq!(
566 buffer_triggers(cx),
567 BTreeSet::from([":".to_string()]),
568 "expected the second registration's triggers to replace the first ones",
569 );
570
571 let options_a_replacement = lsp::CompletionOptions {
572 trigger_characters: Some(vec!["!".to_string()]),
573 ..lsp::CompletionOptions::default()
574 };
575 register_capability(
576 &fake_server,
577 method,
578 "completion-a",
579 serde_json::to_value(&options_a_replacement).ok(),
580 )
581 .await;
582 cx.executor().run_until_parked();
583 assert_eq!(
584 buffer_triggers(cx),
585 BTreeSet::from([":".to_string()]),
586 "expected the active registration's triggers to stay applied after a duplicate ID replaced an inactive one",
587 );
588
589 unregister_capabilities(&fake_server, method, &["completion-b"]).await;
590 cx.executor().run_until_parked();
591 assert_eq!(
592 buffer_triggers(cx),
593 BTreeSet::from(["!".to_string()]),
594 "expected the replaced registration's triggers to be restored",
595 );
596
597 unregister_capabilities(&fake_server, method, &["completion-a"]).await;
598 cx.executor().run_until_parked();
599 assert_eq!(
600 buffer_triggers(cx),
601 BTreeSet::new(),
602 "expected completion triggers to be cleared after unregistering the last registration",
603 );
604}
605
606#[gpui::test]
607async fn test_multi_registration_middle_removal(cx: &mut gpui::TestAppContext) {
608 init_test(cx);
609 let (project, fake_server) =
610 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
611 let server_id = fake_server.server.server_id();
612 let method = "textDocument/inlayHint";
613
614 let options_a = lsp::InlayHintOptions {
615 resolve_provider: Some(true),
616 ..lsp::InlayHintOptions::default()
617 };
618 let options_b = lsp::InlayHintOptions {
619 resolve_provider: Some(false),
620 ..lsp::InlayHintOptions::default()
621 };
622
623 register_capability(
624 &fake_server,
625 method,
626 "inlay-hint-a",
627 serde_json::to_value(&options_a).ok(),
628 )
629 .await;
630 register_capability(
631 &fake_server,
632 method,
633 "inlay-hint-b",
634 serde_json::to_value(&options_b).ok(),
635 )
636 .await;
637 cx.executor().run_until_parked();
638
639 let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
640 unregister_capabilities(&fake_server, method, &["inlay-hint-a"]).await;
641 cx.executor().run_until_parked();
642 assert_eq!(
643 server_capabilities(&project, server_id, cx).inlay_hint_provider,
644 Some(lsp::OneOf::Right(
645 lsp::InlayHintServerCapabilities::Options(options_b)
646 )),
647 "expected the latest registration to stay active after removing an older one from the middle",
648 );
649 assert_eq!(
650 refresh_events.lock().as_slice(),
651 &[] as &[String],
652 "expected no refresh after removing an inactive registration",
653 );
654
655 unregister_capabilities(&fake_server, method, &["inlay-hint-b"]).await;
656 cx.executor().run_until_parked();
657 assert_eq!(
658 server_capabilities(&project, server_id, cx).inlay_hint_provider,
659 None,
660 "expected inlay hint provider to be cleared after unregistering the last registration",
661 );
662}
663
664#[gpui::test]
665async fn test_refresh_during_code_lens_fetch_does_not_resurrect_stale_data(
666 cx: &mut gpui::TestAppContext,
667) {
668 init_test(cx);
669 let (project, fake_server) =
670 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
671 let method = "textDocument/codeLens";
672
673 let (buffer, _lsp_handle) = project
674 .update(cx, |project, cx| {
675 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
676 })
677 .await
678 .unwrap();
679
680 let lens_requests = Arc::new(atomic::AtomicUsize::new(0));
681 let (gate_tx, gate_rx) = futures::channel::oneshot::channel::<()>();
682 let gate_rx = Arc::new(Mutex::new(Some(gate_rx)));
683 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
684 let lens_requests = lens_requests.clone();
685 let gate_rx = gate_rx.clone();
686 move |_, _| {
687 lens_requests.fetch_add(1, atomic::Ordering::Release);
688 let gate = gate_rx.lock().take();
689 async move {
690 if let Some(gate) = gate {
691 gate.await.ok();
692 }
693 Ok(Some(vec![lsp::CodeLens {
694 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
695 command: Some(lsp::Command {
696 title: "stale lens".to_string(),
697 command: "lens_cmd".to_string(),
698 arguments: None,
699 }),
700 data: None,
701 }]))
702 }
703 }
704 });
705
706 register_capability(
707 &fake_server,
708 method,
709 "lens",
710 serde_json::to_value(lsp::CodeLensOptions {
711 resolve_provider: None,
712 })
713 .ok(),
714 )
715 .await;
716 cx.executor().run_until_parked();
717
718 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
719 let first_fetch =
720 lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx));
721 cx.executor().advance_clock(Duration::from_millis(50));
722 cx.executor().run_until_parked();
723 assert_eq!(
724 lens_requests.load(atomic::Ordering::Acquire),
725 1,
726 "expected the code lens request to be in flight before the unregistration",
727 );
728
729 unregister_capabilities(&fake_server, method, &["lens"]).await;
730 cx.executor().run_until_parked();
731
732 gate_tx.send(()).unwrap();
733 first_fetch.await.unwrap();
734 cx.executor().run_until_parked();
735
736 let actions = lsp_store
737 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
738 .await
739 .unwrap();
740 assert_eq!(
741 actions.map(|actions| actions.len()),
742 Some(0),
743 "expected no code lens data after the unregistration, even though a stale fetch completed after it",
744 );
745 assert_eq!(
746 lens_requests.load(atomic::Ordering::Acquire),
747 1,
748 "expected the unregistered server to not be queried again",
749 );
750}
751
752#[gpui::test]
753async fn test_multi_registration_unregister_with_static_only(cx: &mut gpui::TestAppContext) {
754 init_test(cx);
755 let (project, fake_server) = setup_dynamic_registration_test(
756 cx,
757 lsp::ServerCapabilities {
758 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
759 ..lsp::ServerCapabilities::default()
760 },
761 )
762 .await;
763 let server_id = fake_server.server.server_id();
764
765 unregister_capabilities(&fake_server, "textDocument/inlayHint", &["unknown-id"]).await;
766 cx.executor().run_until_parked();
767 assert_eq!(
768 server_capabilities(&project, server_id, cx).inlay_hint_provider,
769 Some(lsp::OneOf::Left(true)),
770 "expected the static capability to survive unregistering an unknown ID",
771 );
772}
773
774#[gpui::test]
775async fn test_multi_registration_completion_static_restore(cx: &mut gpui::TestAppContext) {
776 init_test(cx);
777 let static_options = lsp::CompletionOptions {
778 trigger_characters: Some(vec![".".to_string()]),
779 ..lsp::CompletionOptions::default()
780 };
781 let (project, fake_server) = setup_dynamic_registration_test(
782 cx,
783 lsp::ServerCapabilities {
784 completion_provider: Some(static_options.clone()),
785 ..lsp::ServerCapabilities::default()
786 },
787 )
788 .await;
789 let server_id = fake_server.server.server_id();
790 let method = "textDocument/completion";
791
792 let (buffer, _lsp_handle) = project
793 .update(cx, |project, cx| {
794 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
795 })
796 .await
797 .unwrap();
798 let buffer_triggers = |cx: &mut gpui::TestAppContext| {
799 buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone())
800 };
801 assert_eq!(
802 buffer_triggers(cx),
803 BTreeSet::from([".".to_string()]),
804 "expected the static trigger characters before any dynamic registration",
805 );
806
807 let dynamic_options = lsp::CompletionOptions {
808 trigger_characters: Some(vec![":".to_string()]),
809 ..lsp::CompletionOptions::default()
810 };
811 register_capability(
812 &fake_server,
813 method,
814 "completion-dynamic",
815 serde_json::to_value(&dynamic_options).ok(),
816 )
817 .await;
818 cx.executor().run_until_parked();
819 assert_eq!(
820 buffer_triggers(cx),
821 BTreeSet::from([":".to_string()]),
822 "expected the dynamic registration's triggers to override the static ones",
823 );
824
825 unregister_capabilities(&fake_server, method, &["completion-dynamic"]).await;
826 cx.executor().run_until_parked();
827 assert_eq!(
828 server_capabilities(&project, server_id, cx).completion_provider,
829 Some(static_options),
830 "expected the static completion provider to be restored",
831 );
832 assert_eq!(
833 buffer_triggers(cx),
834 BTreeSet::from([".".to_string()]),
835 "expected the static trigger characters to be restored",
836 );
837}
838
839#[gpui::test]
840async fn test_multi_registration_same_id_different_methods(cx: &mut gpui::TestAppContext) {
841 init_test(cx);
842 let (project, fake_server) =
843 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
844 let server_id = fake_server.server.server_id();
845
846 let code_lens_options = lsp::CodeLensOptions {
847 resolve_provider: Some(true),
848 };
849 register_capability(&fake_server, "textDocument/inlayHint", "shared-id", None).await;
850 register_capability(
851 &fake_server,
852 "textDocument/codeLens",
853 "shared-id",
854 serde_json::to_value(code_lens_options).ok(),
855 )
856 .await;
857 cx.executor().run_until_parked();
858
859 unregister_capabilities(&fake_server, "textDocument/codeLens", &["shared-id"]).await;
860 cx.executor().run_until_parked();
861 let capabilities = server_capabilities(&project, server_id, cx);
862 assert_eq!(
863 capabilities.code_lens_provider, None,
864 "expected the code lens registration to be removed",
865 );
866 assert_eq!(
867 capabilities.inlay_hint_provider,
868 Some(lsp::OneOf::Left(true)),
869 "expected the inlay hint registration with the same ID to stay intact",
870 );
871}
872
873#[gpui::test]
874async fn test_multi_registration_diagnostics(cx: &mut gpui::TestAppContext) {
875 init_test(cx);
876 let (project, fake_server) =
877 setup_dynamic_registration_test(cx, lsp::ServerCapabilities::default()).await;
878 let server_id = fake_server.server.server_id();
879 let method = "textDocument/diagnostic";
880 fake_server.set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(
881 move |_, _| async move {
882 Ok(lsp::DocumentDiagnosticReportResult::Report(
883 lsp::DocumentDiagnosticReport::Full(
884 lsp::RelatedFullDocumentDiagnosticReport::default(),
885 ),
886 ))
887 },
888 );
889
890 let options_a = lsp::DiagnosticServerCapabilities::Options(lsp::DiagnosticOptions {
891 identifier: Some("diagnostics-a".to_string()),
892 ..lsp::DiagnosticOptions::default()
893 });
894 let options_b = lsp::DiagnosticServerCapabilities::Options(lsp::DiagnosticOptions {
895 identifier: Some("diagnostics-b".to_string()),
896 ..lsp::DiagnosticOptions::default()
897 });
898
899 register_capability(
900 &fake_server,
901 method,
902 "diag-a",
903 serde_json::to_value(&options_a).ok(),
904 )
905 .await;
906 register_capability(
907 &fake_server,
908 method,
909 "diag-b",
910 serde_json::to_value(&options_b).ok(),
911 )
912 .await;
913 cx.executor().run_until_parked();
914 assert_eq!(
915 server_capabilities(&project, server_id, cx).diagnostic_provider,
916 Some(options_b),
917 "expected the latest diagnostic registration to be active",
918 );
919
920 unregister_capabilities(&fake_server, method, &["diag-b"]).await;
921 cx.executor().run_until_parked();
922 assert_eq!(
923 server_capabilities(&project, server_id, cx).diagnostic_provider,
924 Some(options_a.clone()),
925 "expected the remaining diagnostic registration to be restored",
926 );
927
928 unregister_capabilities(&fake_server, method, &["unknown-id"]).await;
929 cx.executor().run_until_parked();
930 assert_eq!(
931 server_capabilities(&project, server_id, cx).diagnostic_provider,
932 Some(options_a),
933 "expected an unknown unregistration ID to leave the diagnostic provider intact",
934 );
935
936 unregister_capabilities(&fake_server, method, &["diag-a"]).await;
937 cx.executor().run_until_parked();
938 assert_eq!(
939 server_capabilities(&project, server_id, cx).diagnostic_provider,
940 None,
941 "expected the diagnostic provider to be cleared after unregistering the last registration",
942 );
943}
944
945#[gpui::test]
946async fn test_dynamic_registration_refreshes_lsp_data(cx: &mut gpui::TestAppContext) {
947 init_test(cx);
948
949 let fs = FakeFs::new(cx.executor());
950 fs.insert_tree(path!("/the-root"), json!({ "a.rs": "fn main() {}" }))
951 .await;
952 let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await;
953 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
954 language_registry.add(rust_lang());
955
956 let mut static_servers = language_registry.register_fake_lsp(
957 "Rust",
958 FakeLspAdapter {
959 name: "static-server",
960 capabilities: lsp::ServerCapabilities {
961 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
962 document_link_provider: Some(lsp::DocumentLinkOptions {
963 resolve_provider: None,
964 work_done_progress_options: lsp::WorkDoneProgressOptions::default(),
965 }),
966 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
967 document_symbol_provider: Some(lsp::OneOf::Left(true)),
968 code_lens_provider: Some(lsp::CodeLensOptions {
969 resolve_provider: None,
970 }),
971 semantic_tokens_provider: Some(
972 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
973 lsp::SemanticTokensOptions {
974 full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
975 ..lsp::SemanticTokensOptions::default()
976 },
977 ),
978 ),
979 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
980 completion_provider: Some(lsp::CompletionOptions {
981 trigger_characters: Some(vec![".".to_string()]),
982 ..lsp::CompletionOptions::default()
983 }),
984 ..lsp::ServerCapabilities::default()
985 },
986 ..FakeLspAdapter::default()
987 },
988 );
989 let mut dynamic_servers = language_registry.register_fake_lsp(
990 "Rust",
991 FakeLspAdapter {
992 name: "dynamic-server",
993 ..FakeLspAdapter::default()
994 },
995 );
996 cx.executor().run_until_parked();
997
998 let (buffer, _lsp_handle) = project
999 .update(cx, |project, cx| {
1000 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
1001 })
1002 .await
1003 .unwrap();
1004 let static_server = static_servers.next().await.unwrap();
1005 let dynamic_server = dynamic_servers.next().await.unwrap();
1006 let dynamic_server_id = dynamic_server.server.server_id();
1007 cx.executor().run_until_parked();
1008
1009 let static_counters = count_lsp_requests(&static_server);
1010 let dynamic_counters = count_lsp_requests(&dynamic_server);
1011 let (refresh_events, _refresh_events_subscription) = observe_refresh_events(&project, cx);
1012 let buffer_triggers = |cx: &mut gpui::TestAppContext| {
1013 buffer.read_with(cx, |buffer, _| buffer.completion_triggers().clone())
1014 };
1015
1016 fetch_lsp_data(&project, &buffer, cx).await;
1017 assert_eq!(
1018 static_counters.snapshot(),
1019 LspRequestCounts {
1020 colors: 1,
1021 links: 1,
1022 folding_ranges: 1,
1023 document_symbols: 1,
1024 code_lens: 1,
1025 semantic_tokens: 1,
1026 },
1027 "expected the initial fetch to query the statically capable server",
1028 );
1029 assert_eq!(
1030 dynamic_counters.snapshot(),
1031 LspRequestCounts::default(),
1032 "expected the initial fetch to skip the server without capabilities",
1033 );
1034 assert_eq!(buffer_triggers(cx), BTreeSet::from([".".to_string()]));
1035 assert_eq!(refresh_events.lock().as_slice(), &[] as &[String]);
1036
1037 register_capability(
1038 &dynamic_server,
1039 "textDocument/documentColor",
1040 "colors",
1041 None,
1042 )
1043 .await;
1044 register_capability(
1045 &dynamic_server,
1046 "textDocument/documentLink",
1047 "links",
1048 serde_json::to_value(lsp::DocumentLinkOptions {
1049 resolve_provider: None,
1050 work_done_progress_options: lsp::WorkDoneProgressOptions::default(),
1051 })
1052 .ok(),
1053 )
1054 .await;
1055 register_capability(
1056 &dynamic_server,
1057 "textDocument/foldingRange",
1058 "folding",
1059 None,
1060 )
1061 .await;
1062 register_capability(
1063 &dynamic_server,
1064 "textDocument/documentSymbol",
1065 "symbols",
1066 None,
1067 )
1068 .await;
1069 register_capability(
1070 &dynamic_server,
1071 "textDocument/codeLens",
1072 "code-lens",
1073 serde_json::to_value(lsp::CodeLensOptions {
1074 resolve_provider: None,
1075 })
1076 .ok(),
1077 )
1078 .await;
1079 register_capability(
1080 &dynamic_server,
1081 "textDocument/semanticTokens",
1082 "tokens",
1083 serde_json::to_value(lsp::SemanticTokensRegistrationOptions {
1084 text_document_registration_options: lsp::TextDocumentRegistrationOptions {
1085 document_selector: None,
1086 },
1087 semantic_tokens_options: lsp::SemanticTokensOptions {
1088 full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
1089 ..lsp::SemanticTokensOptions::default()
1090 },
1091 static_registration_options: lsp::StaticRegistrationOptions::default(),
1092 })
1093 .ok(),
1094 )
1095 .await;
1096 register_capability(&dynamic_server, "textDocument/inlayHint", "hints", None).await;
1097 register_capability(
1098 &dynamic_server,
1099 "textDocument/completion",
1100 "completions",
1101 serde_json::to_value(lsp::CompletionOptions {
1102 trigger_characters: Some(vec![":".to_string()]),
1103 ..lsp::CompletionOptions::default()
1104 })
1105 .ok(),
1106 )
1107 .await;
1108 cx.executor().run_until_parked();
1109
1110 assert_eq!(
1111 sorted(refresh_events.lock().drain(..)),
1112 vec![
1113 format!("code_lens({dynamic_server_id})"),
1114 format!("document_colors({dynamic_server_id})"),
1115 format!("document_links({dynamic_server_id})"),
1116 format!("document_symbols({dynamic_server_id})"),
1117 format!("folding_ranges({dynamic_server_id})"),
1118 format!("inlay_hints({dynamic_server_id})"),
1119 format!("semantic_tokens({dynamic_server_id})"),
1120 ],
1121 "expected every dynamic registration to trigger the corresponding per-server refresh",
1122 );
1123 assert_eq!(
1124 buffer_triggers(cx),
1125 BTreeSet::from([".".to_string(), ":".to_string()]),
1126 "expected trigger characters from both servers to be combined",
1127 );
1128
1129 fetch_lsp_data(&project, &buffer, cx).await;
1130 assert_eq!(
1131 dynamic_counters.snapshot(),
1132 LspRequestCounts {
1133 colors: 1,
1134 links: 1,
1135 folding_ranges: 1,
1136 document_symbols: 1,
1137 code_lens: 1,
1138 semantic_tokens: 1,
1139 },
1140 "expected the newly registered server to be queried after the refreshes",
1141 );
1142 assert_eq!(
1143 static_counters.snapshot(),
1144 LspRequestCounts {
1145 colors: 1,
1146 links: 1,
1147 folding_ranges: 1,
1148 document_symbols: 1,
1149 code_lens: 1,
1150 semantic_tokens: 1,
1151 },
1152 "expected per-server refreshes to leave the static server's cached data untouched",
1153 );
1154
1155 unregister_capabilities(&dynamic_server, "textDocument/documentColor", &["colors"]).await;
1156 unregister_capabilities(&dynamic_server, "textDocument/documentLink", &["links"]).await;
1157 unregister_capabilities(&dynamic_server, "textDocument/foldingRange", &["folding"]).await;
1158 unregister_capabilities(&dynamic_server, "textDocument/documentSymbol", &["symbols"]).await;
1159 unregister_capabilities(&dynamic_server, "textDocument/codeLens", &["code-lens"]).await;
1160 unregister_capabilities(&dynamic_server, "textDocument/semanticTokens", &["tokens"]).await;
1161 unregister_capabilities(&dynamic_server, "textDocument/inlayHint", &["hints"]).await;
1162 unregister_capabilities(&dynamic_server, "textDocument/completion", &["completions"]).await;
1163 cx.executor().run_until_parked();
1164
1165 assert_eq!(
1166 sorted(refresh_events.lock().drain(..)),
1167 vec![
1168 format!("code_lens({dynamic_server_id})"),
1169 format!("document_colors({dynamic_server_id})"),
1170 format!("document_links({dynamic_server_id})"),
1171 format!("document_symbols({dynamic_server_id})"),
1172 format!("folding_ranges({dynamic_server_id})"),
1173 format!("inlay_hints({dynamic_server_id})"),
1174 format!("semantic_tokens({dynamic_server_id})"),
1175 ],
1176 "expected every unregistration to trigger the corresponding per-server refresh",
1177 );
1178 assert_eq!(
1179 buffer_triggers(cx),
1180 BTreeSet::from([".".to_string()]),
1181 "expected only the static server's trigger characters to remain",
1182 );
1183
1184 fetch_lsp_data(&project, &buffer, cx).await;
1185 assert_eq!(
1186 dynamic_counters.snapshot(),
1187 LspRequestCounts {
1188 colors: 1,
1189 links: 1,
1190 folding_ranges: 1,
1191 document_symbols: 1,
1192 code_lens: 1,
1193 semantic_tokens: 1,
1194 },
1195 "expected the unregistered server to not be queried anymore",
1196 );
1197 assert_eq!(
1198 static_counters.snapshot(),
1199 LspRequestCounts {
1200 colors: 1,
1201 links: 1,
1202 folding_ranges: 1,
1203 document_symbols: 1,
1204 code_lens: 1,
1205 semantic_tokens: 1,
1206 },
1207 "expected the static server to never be re-queried by another server's capability changes",
1208 );
1209}
1210
1211#[gpui::test]
1212async fn test_semantic_tokens_refresh_invalidates_only_the_refreshed_server(
1213 cx: &mut gpui::TestAppContext,
1214) {
1215 init_test(cx);
1216
1217 let fs = FakeFs::new(cx.executor());
1218 fs.insert_tree(path!("/the-root"), json!({ "a.rs": "fn main() {}" }))
1219 .await;
1220 let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await;
1221 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1222 language_registry.add(rust_lang());
1223
1224 let semantic_tokens_capabilities = lsp::ServerCapabilities {
1225 semantic_tokens_provider: Some(
1226 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1227 lsp::SemanticTokensOptions {
1228 full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
1229 ..lsp::SemanticTokensOptions::default()
1230 },
1231 ),
1232 ),
1233 ..lsp::ServerCapabilities::default()
1234 };
1235 let mut servers_a = language_registry.register_fake_lsp(
1236 "Rust",
1237 FakeLspAdapter {
1238 name: "server-a",
1239 capabilities: semantic_tokens_capabilities.clone(),
1240 ..FakeLspAdapter::default()
1241 },
1242 );
1243 let mut servers_b = language_registry.register_fake_lsp(
1244 "Rust",
1245 FakeLspAdapter {
1246 name: "server-b",
1247 capabilities: semantic_tokens_capabilities,
1248 ..FakeLspAdapter::default()
1249 },
1250 );
1251 cx.executor().run_until_parked();
1252
1253 let (buffer, _lsp_handle) = project
1254 .update(cx, |project, cx| {
1255 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
1256 })
1257 .await
1258 .unwrap();
1259 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
1260 let server_a = servers_a.next().await.unwrap();
1261 let server_b = servers_b.next().await.unwrap();
1262 for server in [&server_a, &server_b] {
1263 server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
1264 move |_, _| async move {
1265 Ok(Some(lsp::SemanticTokensResult::Tokens(
1266 lsp::SemanticTokens::default(),
1267 )))
1268 },
1269 );
1270 }
1271 let server_a_id = server_a.server.server_id();
1272 let server_b_id = server_b.server.server_id();
1273 cx.executor().run_until_parked();
1274
1275 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1276 let cached_token_servers = |cx: &mut gpui::TestAppContext| {
1277 lsp_store.read_with(cx, |lsp_store, _| {
1278 lsp_store.semantic_token_servers(buffer_id)
1279 })
1280 };
1281
1282 lsp_store
1283 .update(cx, |lsp_store, cx| {
1284 lsp_store.semantic_tokens(buffer.clone(), cx)
1285 })
1286 .await
1287 .unwrap();
1288 assert_eq!(
1289 cached_token_servers(cx),
1290 vec![server_a_id, server_b_id],
1291 "expected tokens from both servers after the initial fetch",
1292 );
1293
1294 for repetition in 0..2 {
1295 server_a
1296 .request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1297 .await
1298 .into_response()
1299 .unwrap();
1300 let refresh_task = lsp_store.update(cx, |lsp_store, cx| {
1301 lsp_store.semantic_tokens(buffer.clone(), cx)
1302 });
1303 assert_eq!(
1304 cached_token_servers(cx),
1305 vec![server_b_id],
1306 "expected refresh {repetition} to invalidate only the refreshed server's tokens",
1307 );
1308 let concurrent_task = lsp_store.update(cx, |lsp_store, cx| {
1309 lsp_store.semantic_tokens(buffer.clone(), cx)
1310 });
1311 assert_eq!(
1312 cached_token_servers(cx),
1313 vec![server_b_id],
1314 "expected a concurrent query {repetition} to not invalidate the data again",
1315 );
1316 refresh_task.await.unwrap();
1317 concurrent_task.await.unwrap();
1318 assert_eq!(
1319 cached_token_servers(cx),
1320 vec![server_a_id, server_b_id],
1321 "expected the refreshed server's tokens to be re-fetched after refresh {repetition}",
1322 );
1323 }
1324}
1325
1326#[gpui::test]
1327async fn test_semantic_tokens_refresh_during_fetch_does_not_resurrect_stale_data(
1328 cx: &mut gpui::TestAppContext,
1329) {
1330 init_test(cx);
1331 let (project, fake_server) = setup_dynamic_registration_test(
1332 cx,
1333 lsp::ServerCapabilities {
1334 semantic_tokens_provider: Some(
1335 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1336 lsp::SemanticTokensOptions {
1337 full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
1338 ..lsp::SemanticTokensOptions::default()
1339 },
1340 ),
1341 ),
1342 ..lsp::ServerCapabilities::default()
1343 },
1344 )
1345 .await;
1346 let server_id = fake_server.server.server_id();
1347
1348 let (buffer, _lsp_handle) = project
1349 .update(cx, |project, cx| {
1350 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
1351 })
1352 .await
1353 .unwrap();
1354 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
1355 cx.executor().run_until_parked();
1356
1357 let stale_data = vec![0, 0, 2, 0, 0];
1358 let fresh_data = vec![0, 0, 2, 0, 0, 0, 3, 4, 0, 0];
1359 let token_requests = Arc::new(atomic::AtomicUsize::new(0));
1360 let (gate_tx, gate_rx) = futures::channel::oneshot::channel::<()>();
1361 let gate_rx = Arc::new(Mutex::new(Some(gate_rx)));
1362 fake_server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>({
1363 let token_requests = token_requests.clone();
1364 let gate_rx = gate_rx.clone();
1365 let stale_data = stale_data.clone();
1366 let fresh_data = fresh_data.clone();
1367 move |_, _| {
1368 let request = token_requests.fetch_add(1, atomic::Ordering::Release);
1369 let gate = gate_rx.lock().take();
1370 let data = if request == 0 {
1371 stale_data.clone()
1372 } else {
1373 fresh_data.clone()
1374 };
1375 async move {
1376 if let Some(gate) = gate {
1377 gate.await.ok();
1378 }
1379 Ok(Some(lsp::SemanticTokensResult::Tokens(
1380 lsp::SemanticTokens {
1381 result_id: None,
1382 data,
1383 },
1384 )))
1385 }
1386 }
1387 });
1388
1389 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1390 let stale_fetch = lsp_store.update(cx, |lsp_store, cx| {
1391 lsp_store.semantic_tokens(buffer.clone(), cx)
1392 });
1393 cx.executor().run_until_parked();
1394 assert_eq!(
1395 token_requests.load(atomic::Ordering::Acquire),
1396 1,
1397 "expected the first fetch to be in flight before the refresh",
1398 );
1399
1400 fake_server
1401 .request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1402 .await
1403 .into_response()
1404 .unwrap();
1405 cx.executor().run_until_parked();
1406
1407 let fresh_fetch = lsp_store.update(cx, |lsp_store, cx| {
1408 lsp_store.semantic_tokens(buffer.clone(), cx)
1409 });
1410 fresh_fetch.await.unwrap();
1411
1412 gate_tx.send(()).unwrap();
1413 stale_fetch.await.unwrap();
1414 cx.executor().run_until_parked();
1415
1416 assert_eq!(
1417 lsp_store.read_with(cx, |lsp_store, _| lsp_store.semantic_token_data(buffer_id)),
1418 vec![(server_id, fresh_data)],
1419 "expected the stale fetch, completed after the refresh, to not overwrite the refreshed tokens",
1420 );
1421 assert_eq!(
1422 token_requests.load(atomic::Ordering::Acquire),
1423 2,
1424 "expected exactly the stale and the fresh fetches to have queried the server",
1425 );
1426}
1427
1428#[gpui::test]
1429async fn test_code_lens_concurrent_fetches_are_deduplicated(cx: &mut gpui::TestAppContext) {
1430 init_test(cx);
1431 let (project, fake_server) = setup_dynamic_registration_test(
1432 cx,
1433 lsp::ServerCapabilities {
1434 code_lens_provider: Some(lsp::CodeLensOptions {
1435 resolve_provider: None,
1436 }),
1437 ..lsp::ServerCapabilities::default()
1438 },
1439 )
1440 .await;
1441
1442 let (buffer, _lsp_handle) = project
1443 .update(cx, |project, cx| {
1444 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
1445 })
1446 .await
1447 .unwrap();
1448 cx.executor().run_until_parked();
1449
1450 let lens_requests = Arc::new(atomic::AtomicUsize::new(0));
1451 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
1452 let lens_requests = lens_requests.clone();
1453 move |_, _| {
1454 lens_requests.fetch_add(1, atomic::Ordering::Release);
1455 async move { Ok(Some(Vec::new())) }
1456 }
1457 });
1458
1459 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1460 let first_fetch =
1461 lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx));
1462 let second_fetch =
1463 lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx));
1464 cx.executor().advance_clock(Duration::from_millis(50));
1465 first_fetch.await.unwrap();
1466 second_fetch.await.unwrap();
1467 assert_eq!(
1468 lens_requests.load(atomic::Ordering::Acquire),
1469 1,
1470 "expected concurrent code lens fetches to share one LSP request",
1471 );
1472
1473 lsp_store
1474 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1475 .await
1476 .unwrap();
1477 assert_eq!(
1478 lens_requests.load(atomic::Ordering::Acquire),
1479 1,
1480 "expected a repeated fetch for the unchanged buffer to be served from the cache",
1481 );
1482}
1483
1484#[gpui::test]
1485async fn test_multiple_did_change_watched_files_registrations(cx: &mut gpui::TestAppContext) {
1486 init_test(cx);
1487
1488 let fs = FakeFs::new(cx.executor());
1489 fs.insert_tree(
1490 path!("/root"),
1491 json!({
1492 "src": {
1493 "a.rs": "",
1494 "b.rs": "",
1495 },
1496 "docs": {
1497 "readme.md": "",
1498 },
1499 }),
1500 )
1501 .await;
1502
1503 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1504 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1505 language_registry.add(rust_lang());
1506 let mut fake_servers = language_registry.register_fake_lsp(
1507 "Rust",
1508 FakeLspAdapter {
1509 name: "the-language-server",
1510 ..FakeLspAdapter::default()
1511 },
1512 );
1513
1514 cx.executor().run_until_parked();
1515
1516 project
1517 .update(cx, |project, cx| {
1518 project.open_local_buffer_with_lsp(path!("/root/src/a.rs"), cx)
1519 })
1520 .await
1521 .unwrap();
1522
1523 let fake_server = fake_servers.next().await.unwrap();
1524 cx.executor().run_until_parked();
1525
1526 let file_changes = Arc::new(Mutex::new(Vec::new()));
1527
1528 // Register two separate watched file registrations.
1529 register_capability(
1530 &fake_server,
1531 "workspace/didChangeWatchedFiles",
1532 "reg-1",
1533 serde_json::to_value(lsp::DidChangeWatchedFilesRegistrationOptions {
1534 watchers: vec![lsp::FileSystemWatcher {
1535 glob_pattern: lsp::GlobPattern::String(path!("/root/src/*.rs").to_string()),
1536 kind: None,
1537 }],
1538 })
1539 .ok(),
1540 )
1541 .await;
1542
1543 register_capability(
1544 &fake_server,
1545 "workspace/didChangeWatchedFiles",
1546 "reg-2",
1547 serde_json::to_value(lsp::DidChangeWatchedFilesRegistrationOptions {
1548 watchers: vec![lsp::FileSystemWatcher {
1549 glob_pattern: lsp::GlobPattern::String(path!("/root/docs/*.md").to_string()),
1550 kind: None,
1551 }],
1552 })
1553 .ok(),
1554 )
1555 .await;
1556
1557 fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
1558 let file_changes = file_changes.clone();
1559 move |params, _| {
1560 let mut file_changes = file_changes.lock();
1561 file_changes.extend(params.changes);
1562 file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
1563 }
1564 });
1565
1566 cx.executor().run_until_parked();
1567
1568 // Both registrations should match their respective patterns.
1569 fs.create_file(
1570 path!("/root/src/c.rs").as_ref(),
1571 fs::CreateOptions::default(),
1572 )
1573 .await
1574 .unwrap();
1575 fs.create_file(
1576 path!("/root/docs/guide.md").as_ref(),
1577 fs::CreateOptions::default(),
1578 )
1579 .await
1580 .unwrap();
1581 cx.executor().run_until_parked();
1582
1583 assert_eq!(
1584 &*file_changes.lock(),
1585 &[
1586 lsp::FileEvent {
1587 uri: lsp::Uri::from_file_path(path!("/root/docs/guide.md")).unwrap(),
1588 typ: lsp::FileChangeType::CREATED,
1589 },
1590 lsp::FileEvent {
1591 uri: lsp::Uri::from_file_path(path!("/root/src/c.rs")).unwrap(),
1592 typ: lsp::FileChangeType::CREATED,
1593 },
1594 ]
1595 );
1596 file_changes.lock().clear();
1597
1598 // Unregister the first registration.
1599 unregister_capabilities(&fake_server, "workspace/didChangeWatchedFiles", &["reg-1"]).await;
1600 cx.executor().run_until_parked();
1601
1602 // Only the second registration should still match.
1603 fs.create_file(
1604 path!("/root/src/d.rs").as_ref(),
1605 fs::CreateOptions::default(),
1606 )
1607 .await
1608 .unwrap();
1609 fs.create_file(
1610 path!("/root/docs/notes.md").as_ref(),
1611 fs::CreateOptions::default(),
1612 )
1613 .await
1614 .unwrap();
1615 cx.executor().run_until_parked();
1616
1617 assert_eq!(
1618 &*file_changes.lock(),
1619 &[lsp::FileEvent {
1620 uri: lsp::Uri::from_file_path(path!("/root/docs/notes.md")).unwrap(),
1621 typ: lsp::FileChangeType::CREATED,
1622 }]
1623 );
1624}
1625
1626async fn setup_dynamic_registration_test(
1627 cx: &mut gpui::TestAppContext,
1628 capabilities: lsp::ServerCapabilities,
1629) -> (Entity<Project>, lsp::FakeLanguageServer) {
1630 let fs = FakeFs::new(cx.executor());
1631 fs.insert_tree(path!("/the-root"), json!({ "a.rs": "" }))
1632 .await;
1633
1634 let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await;
1635 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1636 language_registry.add(rust_lang());
1637 let mut fake_servers = language_registry.register_fake_lsp(
1638 "Rust",
1639 FakeLspAdapter {
1640 name: "the-language-server",
1641 capabilities,
1642 ..FakeLspAdapter::default()
1643 },
1644 );
1645
1646 cx.executor().run_until_parked();
1647
1648 project
1649 .update(cx, |project, cx| {
1650 project.open_local_buffer_with_lsp(path!("/the-root/a.rs"), cx)
1651 })
1652 .await
1653 .unwrap();
1654
1655 let fake_server = fake_servers.next().await.unwrap();
1656 cx.executor().run_until_parked();
1657 (project, fake_server)
1658}
1659
1660async fn register_capability(
1661 fake_server: &lsp::FakeLanguageServer,
1662 method: &str,
1663 id: &str,
1664 register_options: Option<serde_json::Value>,
1665) {
1666 fake_server
1667 .request::<lsp::request::RegisterCapability>(
1668 lsp::RegistrationParams {
1669 registrations: vec![lsp::Registration {
1670 id: id.to_string(),
1671 method: method.to_string(),
1672 register_options,
1673 }],
1674 },
1675 DEFAULT_LSP_REQUEST_TIMEOUT,
1676 )
1677 .await
1678 .into_response()
1679 .unwrap();
1680}
1681
1682async fn unregister_capabilities(
1683 fake_server: &lsp::FakeLanguageServer,
1684 method: &str,
1685 ids: &[&str],
1686) {
1687 fake_server
1688 .request::<lsp::request::UnregisterCapability>(
1689 lsp::UnregistrationParams {
1690 unregisterations: ids
1691 .iter()
1692 .map(|id| lsp::Unregistration {
1693 id: id.to_string(),
1694 method: method.to_string(),
1695 })
1696 .collect(),
1697 },
1698 DEFAULT_LSP_REQUEST_TIMEOUT,
1699 )
1700 .await
1701 .into_response()
1702 .unwrap();
1703}
1704
1705fn server_capabilities(
1706 project: &Entity<Project>,
1707 server_id: LanguageServerId,
1708 cx: &mut gpui::TestAppContext,
1709) -> lsp::ServerCapabilities {
1710 project.read_with(cx, |project, cx| {
1711 project
1712 .lsp_store()
1713 .read(cx)
1714 .language_server_for_id(server_id)
1715 .unwrap()
1716 .capabilities()
1717 })
1718}
1719
1720#[derive(Default)]
1721struct LspRequestCounters {
1722 colors: Arc<atomic::AtomicUsize>,
1723 links: Arc<atomic::AtomicUsize>,
1724 folding_ranges: Arc<atomic::AtomicUsize>,
1725 document_symbols: Arc<atomic::AtomicUsize>,
1726 code_lens: Arc<atomic::AtomicUsize>,
1727 semantic_tokens: Arc<atomic::AtomicUsize>,
1728}
1729
1730#[derive(Debug, Default, PartialEq)]
1731struct LspRequestCounts {
1732 colors: usize,
1733 links: usize,
1734 folding_ranges: usize,
1735 document_symbols: usize,
1736 code_lens: usize,
1737 semantic_tokens: usize,
1738}
1739
1740impl LspRequestCounters {
1741 fn snapshot(&self) -> LspRequestCounts {
1742 LspRequestCounts {
1743 colors: self.colors.load(atomic::Ordering::Acquire),
1744 links: self.links.load(atomic::Ordering::Acquire),
1745 folding_ranges: self.folding_ranges.load(atomic::Ordering::Acquire),
1746 document_symbols: self.document_symbols.load(atomic::Ordering::Acquire),
1747 code_lens: self.code_lens.load(atomic::Ordering::Acquire),
1748 semantic_tokens: self.semantic_tokens.load(atomic::Ordering::Acquire),
1749 }
1750 }
1751}
1752
1753fn count_lsp_requests(fake_server: &lsp::FakeLanguageServer) -> LspRequestCounters {
1754 let counters = LspRequestCounters::default();
1755 fake_server.set_request_handler::<lsp::request::DocumentColor, _, _>({
1756 let count = counters.colors.clone();
1757 move |_, _| {
1758 count.fetch_add(1, atomic::Ordering::Release);
1759 async move { Ok(Vec::new()) }
1760 }
1761 });
1762 fake_server.set_request_handler::<lsp::request::DocumentLinkRequest, _, _>({
1763 let count = counters.links.clone();
1764 move |_, _| {
1765 count.fetch_add(1, atomic::Ordering::Release);
1766 async move { Ok(None) }
1767 }
1768 });
1769 fake_server.set_request_handler::<lsp::request::FoldingRangeRequest, _, _>({
1770 let count = counters.folding_ranges.clone();
1771 move |_, _| {
1772 count.fetch_add(1, atomic::Ordering::Release);
1773 async move { Ok(None) }
1774 }
1775 });
1776 fake_server.set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>({
1777 let count = counters.document_symbols.clone();
1778 move |_, _| {
1779 count.fetch_add(1, atomic::Ordering::Release);
1780 async move { Ok(None) }
1781 }
1782 });
1783 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
1784 let count = counters.code_lens.clone();
1785 move |_, _| {
1786 count.fetch_add(1, atomic::Ordering::Release);
1787 async move { Ok(None) }
1788 }
1789 });
1790 fake_server.set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>({
1791 let count = counters.semantic_tokens.clone();
1792 move |_, _| {
1793 count.fetch_add(1, atomic::Ordering::Release);
1794 async move {
1795 Ok(Some(lsp::SemanticTokensResult::Tokens(
1796 lsp::SemanticTokens::default(),
1797 )))
1798 }
1799 }
1800 });
1801 counters
1802}
1803
1804fn observe_refresh_events(
1805 project: &Entity<Project>,
1806 cx: &mut gpui::TestAppContext,
1807) -> (Arc<Mutex<Vec<String>>>, gpui::Subscription) {
1808 let events = Arc::new(Mutex::new(Vec::new()));
1809 let subscription = cx.update({
1810 let events = events.clone();
1811 let project = project.clone();
1812 move |cx| {
1813 cx.subscribe(&project, move |_, event, _| {
1814 let label = |name: &str, server_id: &Option<LanguageServerId>| {
1815 let server_id = server_id
1816 .map_or_else(|| "all".to_string(), |server_id| server_id.to_string());
1817 format!("{name}({server_id})")
1818 };
1819 let event = match event {
1820 Event::RefreshInlayHints { server_id, .. } => {
1821 format!("inlay_hints({server_id})")
1822 }
1823 Event::RefreshSemanticTokens { server_id, .. } => {
1824 format!("semantic_tokens({server_id})")
1825 }
1826 Event::RefreshCodeLens { server_id } => label("code_lens", server_id),
1827 Event::RefreshDocumentColors { server_id } => {
1828 label("document_colors", server_id)
1829 }
1830 Event::RefreshDocumentLinks { server_id } => label("document_links", server_id),
1831 Event::RefreshFoldingRanges { server_id } => label("folding_ranges", server_id),
1832 Event::RefreshDocumentSymbols { server_id } => {
1833 label("document_symbols", server_id)
1834 }
1835 _ => return,
1836 };
1837 events.lock().push(event);
1838 })
1839 }
1840 });
1841 (events, subscription)
1842}
1843
1844async fn fetch_lsp_data(
1845 project: &Entity<Project>,
1846 buffer: &Entity<Buffer>,
1847 cx: &mut gpui::TestAppContext,
1848) {
1849 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1850 let colors = lsp_store.update(cx, |lsp_store, cx| {
1851 lsp_store.document_colors(buffer.clone(), cx)
1852 });
1853 let links = lsp_store.update(cx, |lsp_store, cx| {
1854 lsp_store.fetch_document_links(buffer, cx)
1855 });
1856 let folding_ranges = lsp_store.update(cx, |lsp_store, cx| {
1857 lsp_store.fetch_folding_ranges(buffer, cx)
1858 });
1859 let document_symbols = lsp_store.update(cx, |lsp_store, cx| {
1860 lsp_store.fetch_document_symbols(buffer, cx)
1861 });
1862 let code_lens = lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
1863 let semantic_tokens = lsp_store.update(cx, |lsp_store, cx| {
1864 lsp_store.semantic_tokens(buffer.clone(), cx)
1865 });
1866 if let Some(colors) = colors {
1867 colors.await.unwrap();
1868 }
1869 links.await;
1870 folding_ranges.await;
1871 document_symbols.await;
1872 code_lens.await.unwrap();
1873 semantic_tokens.await.unwrap();
1874}
1875
1876fn sorted(events: impl IntoIterator<Item = String>) -> Vec<String> {
1877 let mut events = events.into_iter().collect::<Vec<_>>();
1878 events.sort();
1879 events
1880}
1881