Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:42:05.675Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

dynamic_registration.rs

1881 lines · 64.5 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.