Skip to main content

omena_lsp_server/
state.rs

1use crate::disk_cache::DiskDiagnosticsCacheSessionV0;
2use crate::lsp_output::DiagnosticsPublishDigestRegistryV0;
3use crate::workspace_runtime_registry::WorkspaceRuntimeRegistry;
4use omena_incremental::IncrementalCancellationRegistryV0;
5#[cfg(feature = "salsa-style-diagnostics")]
6use omena_query::ReverseDependencyIndexV0;
7use omena_query::{
8    AnalyzedGraphV0, OmenaQueryExternalSifInputV0, OmenaQuerySourceSelectorOccurrenceIndexV0,
9    OmenaQuerySourceSelectorReferenceFactV0 as SourceSelectorReferenceFact,
10    OmenaQuerySourceSyntaxIndexV0 as SourceSyntaxIndex,
11    OmenaQuerySourceTypeFactLexicalAttemptV0 as SourceTypeFactLexicalAttempt,
12    OmenaQueryStyleCascadeNarrowingSubstrateV0, OmenaQueryStylePackageManifestV0,
13    OmenaQueryStyleResolutionInputsV0, OmenaQueryStyleSelectorDefinitionV0,
14    OmenaQueryStyleSourceInputV0, OmenaWorkspaceOccurrenceFamilyV0,
15    OmenaWorkspaceOccurrenceIndexV0, OmenaWorkspaceOccurrenceKindV0,
16    OmenaWorkspaceOccurrenceRoleV0, OmenaWorkspaceOccurrenceV0, ParserPositionV0, ParserRangeV0,
17};
18#[cfg(feature = "parallel-style-diagnostics")]
19use omena_query::{
20    OmenaResolverStyleModuleConfirmationIdentityIndexV0,
21    OmenaResolverStyleModuleDiskCandidateIdentityV0,
22};
23use omena_syntax::ident::{
24    AuthoredPropertyTextV0, CanonicalClassKeyV0, CanonicalCustomPropertyNameV0, PropertyNameV0,
25};
26use omena_tsgo_client::{TsgoTypeFactResultEntryV0, TsgoWorkspaceProcessPoolV0};
27use serde::Serialize;
28use std::cell::RefCell;
29use std::cmp::Ordering as CmpOrdering;
30use std::collections::{BTreeMap, BTreeSet};
31use std::path::PathBuf;
32use std::sync::{
33    Arc, Mutex, MutexGuard,
34    atomic::{AtomicU8, AtomicU64, Ordering},
35};
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub enum LspDocumentOrigin {
40    Local,
41    Foreign,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct LspTextDocumentState {
47    pub uri: String,
48    #[serde(skip)]
49    pub origin: LspDocumentOrigin,
50    pub workspace_folder_uri: Option<String>,
51    pub language_id: String,
52    pub version: i64,
53    pub text: String,
54    #[serde(skip)]
55    pub(crate) text_hash: String,
56    pub style_summary: Option<LspStyleDocumentSummary>,
57    pub diagnostics_schedule_count: usize,
58    pub optimizing_tier_feedback: Option<LspOptimizingTierFeedback>,
59    #[serde(skip)]
60    pub style_candidates: Vec<LspStyleHoverCandidate>,
61    #[serde(skip)]
62    pub(crate) source_syntax_index: SourceSyntaxIndex,
63    #[serde(skip)]
64    pub(crate) source_module_specifiers: Vec<String>,
65    #[serde(skip)]
66    pub(crate) source_module_specifier_index_complete: bool,
67    #[serde(skip)]
68    pub(crate) source_type_fact_lexical_attempts: Vec<SourceTypeFactLexicalAttempt>,
69    #[serde(skip)]
70    pub(crate) source_type_fact_tier_attempts: Vec<LspSourceTypeFactTierAttemptV0>,
71    #[serde(skip)]
72    pub(crate) has_unresolved_style_import: bool,
73    #[serde(skip)]
74    pub source_selector_candidates: Vec<LspStyleHoverCandidate>,
75    #[serde(skip)]
76    pub(crate) source_type_fact_selector_references: Vec<SourceSelectorReferenceFact>,
77    #[serde(skip)]
78    pub(crate) source_type_fact_retired_prefix_references: Vec<SourceSelectorReferenceFact>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
82#[non_exhaustive]
83#[serde(rename_all = "camelCase")]
84pub struct LspSourceTypeFactTierAttemptV0 {
85    pub expression_id: String,
86    pub tier: &'static str,
87    pub outcome: &'static str,
88    pub reason: Option<&'static str>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct LspOptimizingTierFeedback {
94    pub schema_version: &'static str,
95    pub product: &'static str,
96    pub document_version: i64,
97    pub policy: &'static str,
98    pub consumer: &'static str,
99    pub analyzed_graph: AnalyzedGraphV0,
100}
101
102#[derive(Debug, Clone, Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct LspStyleDocumentSummary {
105    pub language: &'static str,
106    pub selector_names: Vec<String>,
107    pub custom_property_decl_names: Vec<AuthoredPropertyTextV0>,
108    pub custom_property_ref_names: Vec<AuthoredPropertyTextV0>,
109    pub sass_module_use_sources: Vec<String>,
110    pub sass_module_forward_sources: Vec<String>,
111    pub diagnostic_count: usize,
112}
113
114impl PartialEq for LspStyleDocumentSummary {
115    fn eq(&self, other: &Self) -> bool {
116        self.language == other.language
117            && self.selector_names == other.selector_names
118            && authored_custom_property_sequences_same(
119                &self.custom_property_decl_names,
120                &other.custom_property_decl_names,
121            )
122            && authored_custom_property_sequences_same(
123                &self.custom_property_ref_names,
124                &other.custom_property_ref_names,
125            )
126            && self.sass_module_use_sources == other.sass_module_use_sources
127            && self.sass_module_forward_sources == other.sass_module_forward_sources
128            && self.diagnostic_count == other.diagnostic_count
129    }
130}
131
132impl Eq for LspStyleDocumentSummary {}
133
134fn authored_custom_property_sequences_same(
135    left: &[AuthoredPropertyTextV0],
136    right: &[AuthoredPropertyTextV0],
137) -> bool {
138    left.len() == right.len()
139        && left
140            .iter()
141            .zip(right)
142            .all(|(left, right)| left.to_custom_key() == right.to_custom_key())
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
146#[serde(rename_all = "camelCase")]
147pub struct LspStyleHoverCandidatesResult {
148    pub schema_version: &'static str,
149    pub product: &'static str,
150    pub document_uri: String,
151    pub workspace_folder_uri: Option<String>,
152    pub language: Option<&'static str>,
153    pub query_position: Option<ParserPositionV0>,
154    pub candidate_count: usize,
155    pub candidates: Vec<LspStyleHoverCandidate>,
156}
157
158#[derive(Debug, Clone, Serialize)]
159#[serde(rename_all = "camelCase")]
160pub struct LspStyleHoverCandidate {
161    pub kind: &'static str,
162    pub name: AuthoredPropertyTextV0,
163    #[serde(skip)]
164    pub selector_key: Option<CanonicalClassKeyV0>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub property_key: Option<CanonicalCustomPropertyNameV0>,
167    pub range: ParserRangeV0,
168    pub source: &'static str,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub target_style_uri: Option<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub namespace: Option<String>,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
176pub(crate) enum LspStyleHoverCandidateIdentityRefV0<'candidate> {
177    Selector(Option<&'candidate CanonicalClassKeyV0>),
178    CustomProperty(Option<&'candidate CanonicalCustomPropertyNameV0>),
179    Other,
180}
181
182impl LspStyleHoverCandidate {
183    pub(crate) fn identity_name(&self) -> String {
184        if matches!(
185            self.kind,
186            "customPropertyDeclaration" | "customPropertyReference"
187        ) {
188            self.property_key
189                .as_ref()
190                .map(CanonicalCustomPropertyNameV0::as_str)
191                .unwrap_or_default()
192                .to_string()
193        } else {
194            let mut name = String::new();
195            let _ = omena_syntax::ident::render_authored(&self.name, &mut name);
196            name
197        }
198    }
199
200    pub(crate) fn identity(&self) -> LspStyleHoverCandidateIdentityRefV0<'_> {
201        if matches!(
202            self.kind,
203            "selector" | "sourceSelectorReference" | "sourceSelectorPrefixReference"
204        ) {
205            LspStyleHoverCandidateIdentityRefV0::Selector(self.selector_key.as_ref())
206        } else if matches!(
207            self.kind,
208            "customPropertyDeclaration" | "customPropertyReference"
209        ) {
210            LspStyleHoverCandidateIdentityRefV0::CustomProperty(self.property_key.as_ref())
211        } else {
212            LspStyleHoverCandidateIdentityRefV0::Other
213        }
214    }
215}
216
217impl PartialEq for LspStyleHoverCandidate {
218    fn eq(&self, other: &Self) -> bool {
219        self.kind == other.kind
220            && self.identity() == other.identity()
221            && self.range == other.range
222            && self.source == other.source
223            && self.target_style_uri == other.target_style_uri
224            && self.namespace == other.namespace
225    }
226}
227
228impl Eq for LspStyleHoverCandidate {}
229
230impl Ord for LspStyleHoverCandidate {
231    fn cmp(&self, other: &Self) -> CmpOrdering {
232        (
233            self.kind,
234            self.identity(),
235            self.range,
236            self.source,
237            &self.target_style_uri,
238            &self.namespace,
239        )
240            .cmp(&(
241                other.kind,
242                other.identity(),
243                other.range,
244                other.source,
245                &other.target_style_uri,
246                &other.namespace,
247            ))
248    }
249}
250
251impl PartialOrd for LspStyleHoverCandidate {
252    fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
253        Some(self.cmp(other))
254    }
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct LspWorkspaceFolderState {
260    pub uri: String,
261    pub name: String,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
265#[serde(rename_all = "camelCase")]
266pub struct LspWatchedFileChangeState {
267    pub uri: String,
268    pub change_type: u64,
269}
270
271#[derive(Debug, Clone)]
272pub(crate) struct LspSourceTypeFactCacheEntryV0 {
273    pub(crate) entries: Vec<TsgoTypeFactResultEntryV0>,
274    pub(crate) last_used: u64,
275}
276
277#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
278#[serde(rename_all = "camelCase")]
279pub struct LspSourceTypeFactCacheTelemetryV0 {
280    pub hit_count: u64,
281    pub miss_count: u64,
282    pub sidecar_hit_count: u64,
283    pub sidecar_refused_by_reason: BTreeMap<String, u64>,
284    pub closure_incomplete_by_reason: BTreeMap<String, u64>,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
288pub struct LspFileId(u32);
289
290impl LspFileId {
291    #[cfg(test)]
292    pub(crate) fn incremental_key(self) -> u32 {
293        self.0
294    }
295
296    #[cfg(test)]
297    pub(crate) const fn fixture(value: u32) -> Self {
298        Self(value)
299    }
300}
301
302#[derive(Debug, Clone, Default)]
303pub(crate) struct LspFileIdentityInterner {
304    next_id: u32,
305    ids_by_uri_alias: BTreeMap<String, LspFileId>,
306    ids_by_storage_uri: BTreeMap<String, LspFileId>,
307    storage_uris_by_id: BTreeMap<LspFileId, String>,
308}
309
310impl LspFileIdentityInterner {
311    fn intern_uri(&mut self, uri: &str) -> (LspFileId, String) {
312        if let Some(file_id) = self.ids_by_uri_alias.get(uri) {
313            let storage_uri = self
314                .storage_uris_by_id
315                .get(file_id)
316                .cloned()
317                .unwrap_or_else(|| uri.to_string());
318            return (*file_id, storage_uri);
319        }
320        let storage_uri = Self::storage_uri(uri);
321        if let Some(file_id) = self.ids_by_storage_uri.get(storage_uri.as_str()) {
322            self.ids_by_uri_alias.insert(uri.to_string(), *file_id);
323            return (*file_id, storage_uri);
324        }
325        let file_id = LspFileId(self.next_id);
326        self.next_id = self.next_id.saturating_add(1);
327        self.ids_by_uri_alias.insert(uri.to_string(), file_id);
328        self.ids_by_uri_alias.insert(storage_uri.clone(), file_id);
329        self.ids_by_storage_uri.insert(storage_uri.clone(), file_id);
330        self.storage_uris_by_id.insert(file_id, storage_uri.clone());
331        (file_id, storage_uri)
332    }
333
334    fn file_id_for_uri(&self, uri: &str) -> Option<LspFileId> {
335        if let Some(file_id) = self.ids_by_uri_alias.get(uri) {
336            return Some(*file_id);
337        }
338        let storage_uri = Self::storage_uri(uri);
339        self.ids_by_storage_uri.get(storage_uri.as_str()).copied()
340    }
341
342    pub(crate) fn storage_uri_for_file_id(&self, file_id: LspFileId) -> Option<&str> {
343        self.storage_uris_by_id.get(&file_id).map(String::as_str)
344    }
345
346    fn storage_uri(uri: &str) -> String {
347        crate::protocol::canonical_file_uri(uri).unwrap_or_else(|| uri.to_string())
348    }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
352#[serde(rename_all = "camelCase")]
353pub struct LspShellStateSnapshot {
354    pub shutdown_requested: bool,
355    pub should_exit: bool,
356    pub features: LspFeatureSettings,
357    pub diagnostics: LspDiagnosticSettings,
358    pub resolution: LspResolutionSettings,
359    pub cancelled_request_count: usize,
360    pub suppressed_dispatched_result_count: u64,
361    pub workspace_style_index_exhausted_count: usize,
362    pub workspace_index_pending_file_count: usize,
363    /// Wire-compatibility sentinel for the retired automatic workspace-lock
364    /// reader. It is always zero.
365    pub external_sif_lock_read_count: usize,
366    pub external_sif_bridge_generation_count: usize,
367    pub document_count: usize,
368    pub workspace_folder_count: usize,
369    pub configuration_change_count: usize,
370    pub watched_file_event_count: usize,
371    pub cached_workspace_resolution_input_count: usize,
372    pub source_type_fact_cache_telemetry: LspSourceTypeFactCacheTelemetryV0,
373    /// Tide observability (rfcs#111 §11.4): the ledger epoch and the state
374    /// of both settle-gated lanes, so #110-style loop debugging is a debug
375    /// request instead of ad-hoc instrumentation.
376    pub tide_epoch: u64,
377    pub tide_sif_lane_generation: u64,
378    pub tide_sif_lane_in_flight: bool,
379    pub tide_sif_lane_has_demand: bool,
380    pub tide_republish_lane_generation: u64,
381    pub tide_republish_lane_in_flight: bool,
382    pub tide_republish_lane_has_demand: bool,
383    pub tide_starvation_alarm_count: u64,
384    /// Republish tides actually disowned, partitioned by the semantic input
385    /// kind that reopened the settle window.
386    pub tide_disowns_total: BTreeMap<String, u64>,
387    /// The subset of `tide_disowns_total` whose triggering document set did
388    /// not intersect the frozen tide's concrete target cone.
389    pub tide_disowns_out_of_cone: BTreeMap<String, u64>,
390    /// Current backlog ages (ticks since the oldest un-flushed deposit),
391    /// `None` while the lane is at bottom — the alarm count says starvation
392    /// HAPPENED, these say how far behind each lane is NOW.
393    pub tide_sif_lane_oldest_deposit_age_ticks: Option<u64>,
394    pub tide_republish_lane_oldest_deposit_age_ticks: Option<u64>,
395    pub documents: Vec<LspTextDocumentState>,
396    pub workspace_folders: Vec<LspWorkspaceFolderState>,
397    pub watched_file_changes: Vec<LspWatchedFileChangeState>,
398}
399
400fn tide_disown_counts_by_driver(
401    counts: &[u64; crate::tide::ledger::TIDE_INPUT_KIND_COUNT],
402) -> BTreeMap<String, u64> {
403    crate::tide::TideInputKindV0::ALL
404        .into_iter()
405        .map(|kind| (kind.wire_name().to_string(), counts[kind as usize]))
406        .collect()
407}
408
409const DISPATCHED_REQUEST_PENDING: u8 = 0;
410const DISPATCHED_REQUEST_CANCELLED: u8 = 1;
411const DISPATCHED_REQUEST_COMPLETED: u8 = 2;
412
413#[derive(Debug, Default)]
414struct LspInFlightRequestRegistryInner {
415    next_generation: u64,
416    requests: BTreeMap<String, LspInFlightRequestEntry>,
417}
418
419#[derive(Debug, Clone)]
420struct LspInFlightRequestEntry {
421    generation: u64,
422    status: Arc<AtomicU8>,
423}
424
425/// Shared request-lifecycle registry for dispatched JSON-RPC queries.
426///
427/// The loop marks the current generation cancelled; the worker atomically
428/// chooses either the computed result or a cancellation response at completion.
429#[derive(Debug, Clone, Default)]
430pub(crate) struct LspInFlightRequestRegistry {
431    inner: Arc<Mutex<LspInFlightRequestRegistryInner>>,
432    suppressed_result_count: Arc<AtomicU64>,
433}
434
435#[derive(Debug, Clone)]
436pub(crate) struct LspDispatchedRequestToken {
437    request_key: String,
438    generation: u64,
439    status: Arc<AtomicU8>,
440    registry: LspInFlightRequestRegistry,
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub(crate) enum LspDispatchedRequestCompletion {
445    Result,
446    Cancelled,
447    AlreadyCompleted,
448}
449
450impl LspInFlightRequestRegistry {
451    pub(crate) fn register(&self, request_key: String) -> LspDispatchedRequestToken {
452        let mut inner = self.inner.lock().unwrap_or_else(|error| error.into_inner());
453        inner.next_generation = inner.next_generation.saturating_add(1).max(1);
454        let generation = inner.next_generation;
455        let status = Arc::new(AtomicU8::new(DISPATCHED_REQUEST_PENDING));
456        inner.requests.insert(
457            request_key.clone(),
458            LspInFlightRequestEntry {
459                generation,
460                status: Arc::clone(&status),
461            },
462        );
463        LspDispatchedRequestToken {
464            request_key,
465            generation,
466            status,
467            registry: self.clone(),
468        }
469    }
470
471    pub(crate) fn cancel(&self, request_key: &str) -> bool {
472        let inner = self.inner.lock().unwrap_or_else(|error| error.into_inner());
473        let Some(entry) = inner.requests.get(request_key) else {
474            return false;
475        };
476        let _ = entry.status.compare_exchange(
477            DISPATCHED_REQUEST_PENDING,
478            DISPATCHED_REQUEST_CANCELLED,
479            Ordering::AcqRel,
480            Ordering::Acquire,
481        );
482        true
483    }
484
485    pub(crate) fn suppressed_result_count(&self) -> u64 {
486        self.suppressed_result_count.load(Ordering::Acquire)
487    }
488
489    fn remove_if_current(&self, request_key: &str, generation: u64, status: &Arc<AtomicU8>) {
490        let mut inner = self.inner.lock().unwrap_or_else(|error| error.into_inner());
491        let is_current = inner.requests.get(request_key).is_some_and(|entry| {
492            entry.generation == generation && Arc::ptr_eq(&entry.status, status)
493        });
494        if is_current {
495            inner.requests.remove(request_key);
496        }
497    }
498}
499
500impl LspDispatchedRequestToken {
501    pub(crate) fn complete(&self) -> LspDispatchedRequestCompletion {
502        let completion = match self.status.compare_exchange(
503            DISPATCHED_REQUEST_PENDING,
504            DISPATCHED_REQUEST_COMPLETED,
505            Ordering::AcqRel,
506            Ordering::Acquire,
507        ) {
508            Ok(_) => LspDispatchedRequestCompletion::Result,
509            Err(DISPATCHED_REQUEST_CANCELLED) => {
510                if self
511                    .status
512                    .compare_exchange(
513                        DISPATCHED_REQUEST_CANCELLED,
514                        DISPATCHED_REQUEST_COMPLETED,
515                        Ordering::AcqRel,
516                        Ordering::Acquire,
517                    )
518                    .is_ok()
519                {
520                    self.registry
521                        .suppressed_result_count
522                        .fetch_add(1, Ordering::AcqRel);
523                    LspDispatchedRequestCompletion::Cancelled
524                } else {
525                    LspDispatchedRequestCompletion::AlreadyCompleted
526                }
527            }
528            Err(_) => LspDispatchedRequestCompletion::AlreadyCompleted,
529        };
530        self.registry
531            .remove_if_current(self.request_key.as_str(), self.generation, &self.status);
532        completion
533    }
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
537#[serde(rename_all = "camelCase")]
538pub struct LspFeatureSettings {
539    pub definition: bool,
540    pub hover: bool,
541    pub completion: bool,
542    pub references: bool,
543    pub rename: bool,
544}
545
546impl Default for LspFeatureSettings {
547    fn default() -> Self {
548        Self {
549            definition: true,
550            hover: true,
551            completion: true,
552            references: true,
553            rename: true,
554        }
555    }
556}
557
558#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
559#[serde(rename_all = "camelCase")]
560pub struct LspDiagnosticSettings {
561    pub severity: u8,
562    pub deep_analysis: bool,
563}
564
565impl Default for LspDiagnosticSettings {
566    fn default() -> Self {
567        Self {
568            severity: 2,
569            deep_analysis: false,
570        }
571    }
572}
573
574#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
575#[serde(rename_all = "camelCase")]
576pub struct LspResolutionSettings {
577    pub package_manifest_paths: Vec<String>,
578    #[serde(skip)]
579    pub package_manifests: Vec<OmenaQueryStylePackageManifestV0>,
580    #[serde(skip)]
581    pub workspace_style_resolution_inputs: BTreeMap<String, OmenaQueryStyleResolutionInputsV0>,
582    /// External Sass-module SIF artifacts sourced from workspace locks and bridge generation.
583    /// The diagnostics path runs in Auto mode, so source-available edges stay local while
584    /// SIF-backed and unresolved foreign edges are classified per import edge.
585    #[serde(skip)]
586    pub external_sifs: Vec<OmenaQueryExternalSifInputV0>,
587    #[serde(skip)]
588    pub(crate) external_sif_trust_records:
589        BTreeMap<String, omena_query::OmenaQueryExternalSifTrustV1>,
590    #[serde(skip)]
591    pub(crate) bridge_external_sif_urls: BTreeSet<String>,
592    #[serde(skip)]
593    pub(crate) cache_storage: crate::cache_root::LspCacheStorageConfigV0,
594}
595
596/// Workspace-revision memo for the cascade-narrowing substrate (rfcs#63 E-ii).
597/// Self-validating: the key is the exact narrowing input set (ordered style sources +
598/// package manifests + external SIFs + resolution mappings), so any document
599/// open/close/edit, disk reload, or resolution-config change misses by comparison and
600/// rebuilds — there is no eviction site to keep in sync.
601#[derive(Debug)]
602pub(crate) struct LspCascadeNarrowingSubstrateMemo {
603    pub(crate) style_sources: Vec<OmenaQueryStyleSourceInputV0>,
604    pub(crate) package_manifests: Vec<OmenaQueryStylePackageManifestV0>,
605    pub(crate) external_sifs: Vec<OmenaQueryExternalSifInputV0>,
606    pub(crate) resolution_inputs: OmenaQueryStyleResolutionInputsV0,
607    pub(crate) substrate: Arc<OmenaQueryStyleCascadeNarrowingSubstrateV0>,
608}
609
610#[cfg(feature = "parallel-style-diagnostics")]
611#[derive(Debug, Clone)]
612pub(crate) struct LspResolverIdentityIndexMemo {
613    pub(crate) available_style_paths: Vec<String>,
614    pub(crate) disk_style_path_identities: Vec<OmenaResolverStyleModuleDiskCandidateIdentityV0>,
615    pub(crate) index: Arc<OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
619#[serde(rename_all = "camelCase")]
620pub(crate) struct LspSourceSelectorOccurrenceDocumentKey {
621    pub(crate) uri: String,
622    pub(crate) workspace_folder_uri: Option<String>,
623    pub(crate) language_id: String,
624    pub(crate) version: i64,
625    pub(crate) text_hash: String,
626}
627
628#[derive(Debug, Clone)]
629pub(crate) struct LspWorkspaceOccurrenceIndexMemo {
630    pub(crate) workspace_folder_uri: Option<String>,
631    /// Digest over the NON-document inputs the build reads (external SIFs +
632    /// workspace resolution inputs). Document keys alone cannot see an SIF
633    /// or resolver-config move — the eviction on SIF refresh raced a
634    /// worker's in-flight store, reviving a stale index (review finding);
635    /// putting the environment IN the key makes the memo self-validating
636    /// for real instead of eviction-dependent.
637    pub(crate) environment_digest: Option<String>,
638    pub(crate) source_document_keys: Vec<LspSourceSelectorOccurrenceDocumentKey>,
639    pub(crate) style_document_keys: Vec<LspSourceSelectorOccurrenceDocumentKey>,
640    pub(crate) document_entries: BTreeMap<LspFileId, LspWorkspaceOccurrenceDocumentMemoEntry>,
641    /// The aggregate input revision whose reusable entries were already
642    /// byte-checked by the RAM value oracle. Sampling one revision in sixteen
643    /// keeps the exact-hit fast path intact instead of walking every entry on
644    /// every serve.
645    pub(crate) shadow_verified_revision_digest: Option<String>,
646    pub(crate) definitions: Vec<OmenaQueryStyleSelectorDefinitionV0>,
647    pub(crate) source_selector_index: Arc<OmenaQuerySourceSelectorOccurrenceIndexV0>,
648    pub(crate) workspace_index: Arc<OmenaWorkspaceOccurrenceIndexV0>,
649    /// Session-owned production shadow telemetry. Keeping the counter on the
650    /// shared memo prevents process-global test leakage and preserves one count
651    /// across query-snapshot rebuilds. No current product wire reads this value.
652    pub(crate) shadow_mismatch_count: Arc<AtomicU64>,
653}
654
655#[derive(Debug, Clone)]
656pub(crate) struct LspWorkspaceOccurrenceDocumentMemoEntry {
657    pub(crate) document_key: LspSourceSelectorOccurrenceDocumentKey,
658    pub(crate) dependency_document_uris: BTreeSet<String>,
659    pub(crate) dependency_digest: Option<String>,
660    pub(crate) occurrences: Vec<OmenaWorkspaceOccurrenceV0>,
661}
662
663/// documentColor cache rows: uri -> (freshness key, rendered informations).
664pub(crate) type LspDocumentColorCacheV0 = BTreeMap<String, ((i64, u64, u64), serde_json::Value)>;
665
666#[cfg(feature = "salsa-style-diagnostics")]
667#[derive(Debug, Clone, PartialEq, Eq)]
668pub(crate) struct LspReverseDependencyIndexMemo {
669    pub(crate) revision: u64,
670    pub(crate) summary_hash: String,
671    /// Tide-ledger epoch at the last refresh: consumers that would GUESS
672    /// from a stale graph (cone seeding) compare this against the corpus
673    /// input marks and widen instead.
674    pub(crate) ledger_epoch: u64,
675    pub(crate) index: ReverseDependencyIndexV0,
676    /// Session-id mirror of local document edges. It is rebuilt when the
677    /// committed string graph changes, so republish closure walks compact
678    /// identities without deriving URI aliases at every hop.
679    pub(crate) file_id_rev: BTreeMap<LspFileId, BTreeSet<LspFileId>>,
680}
681
682#[derive(Debug, Clone, Serialize)]
683#[serde(rename_all = "camelCase")]
684pub(crate) struct LspStyleSymbolOccurrenceV0 {
685    pub(crate) moniker: String,
686    pub(crate) uri: String,
687    pub(crate) kind: OmenaWorkspaceOccurrenceKindV0,
688    pub(crate) family: OmenaWorkspaceOccurrenceFamilyV0,
689    pub(crate) name: String,
690    pub(crate) range: ParserRangeV0,
691    pub(crate) role: OmenaWorkspaceOccurrenceRoleV0,
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub(crate) namespace: Option<String>,
694}
695
696impl PartialEq for LspStyleSymbolOccurrenceV0 {
697    fn eq(&self, other: &Self) -> bool {
698        self.cmp(other) == CmpOrdering::Equal
699    }
700}
701
702impl Eq for LspStyleSymbolOccurrenceV0 {}
703
704impl Ord for LspStyleSymbolOccurrenceV0 {
705    fn cmp(&self, other: &Self) -> CmpOrdering {
706        self.moniker
707            .cmp(&other.moniker)
708            .then_with(|| self.uri.cmp(&other.uri))
709            .then_with(|| self.kind.cmp(&other.kind))
710            .then_with(|| self.family.cmp(&other.family))
711            .then_with(|| style_symbol_occurrence_name_cmp(self, other))
712            .then_with(|| self.range.cmp(&other.range))
713            .then_with(|| self.role.cmp(&other.role))
714            .then_with(|| self.namespace.cmp(&other.namespace))
715    }
716}
717
718impl PartialOrd for LspStyleSymbolOccurrenceV0 {
719    fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
720        Some(self.cmp(other))
721    }
722}
723
724fn style_symbol_occurrence_name_cmp(
725    left: &LspStyleSymbolOccurrenceV0,
726    right: &LspStyleSymbolOccurrenceV0,
727) -> CmpOrdering {
728    if left.kind.family() == OmenaWorkspaceOccurrenceFamilyV0::CustomProperty
729        && right.kind.family() == OmenaWorkspaceOccurrenceFamilyV0::CustomProperty
730    {
731        return PropertyNameV0::canonical_custom_key(left.name.clone())
732            .cmp(&PropertyNameV0::canonical_custom_key(right.name.clone()));
733    }
734    left.name.cmp(&right.name)
735}
736
737/// Read-only state surface available to off-loop query work.
738///
739/// The required methods mirror the fields copied by [`LspShellState::query_snapshot`].
740/// Loop-owned state is intentionally absent, so a resolver cannot accidentally
741/// observe a default-filled cache, process pool, scheduler, or cancellation registry.
742///
743/// ```compile_fail
744/// # fn forbidden<T: omena_lsp_server::LspQueryReadView>(view: &T) {
745/// let _ = view.shutdown_requested();
746/// # }
747/// ```
748#[allow(private_interfaces)]
749pub trait LspQueryReadView {
750    #[doc(hidden)]
751    fn query_features(&self) -> &LspFeatureSettings;
752
753    #[doc(hidden)]
754    fn query_diagnostics(&self) -> &LspDiagnosticSettings;
755
756    #[doc(hidden)]
757    fn query_resolution(&self) -> &LspResolutionSettings;
758
759    #[doc(hidden)]
760    fn query_file_identity(&self) -> &LspFileIdentityInterner;
761
762    #[doc(hidden)]
763    fn query_documents(&self) -> &BTreeMap<LspFileId, Arc<LspTextDocumentState>>;
764
765    #[doc(hidden)]
766    fn query_open_document_uris(&self) -> &BTreeSet<LspFileId>;
767
768    #[doc(hidden)]
769    fn query_workspace_runtime_registry(&self) -> &WorkspaceRuntimeRegistry;
770
771    #[doc(hidden)]
772    fn query_tide_ledger(&self) -> &crate::tide::TideEpochLedgerV0;
773
774    #[cfg(feature = "salsa-style-diagnostics")]
775    #[doc(hidden)]
776    fn query_style_workspace_snapshot_revision_hint(&self) -> u64;
777
778    #[doc(hidden)]
779    fn query_document_color_cache(&self) -> &Arc<Mutex<LspDocumentColorCacheV0>>;
780
781    #[doc(hidden)]
782    fn query_cascade_narrowing_substrate_memo(
783        &self,
784    ) -> &Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>>;
785
786    #[doc(hidden)]
787    fn query_workspace_occurrence_index_memo(
788        &self,
789    ) -> &Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>>;
790
791    #[cfg(feature = "parallel-style-diagnostics")]
792    #[doc(hidden)]
793    fn query_resolver_identity_index_memo(
794        &self,
795    ) -> &Arc<Mutex<Option<LspResolverIdentityIndexMemo>>>;
796
797    fn document(&self, uri: &str) -> Option<&LspTextDocumentState> {
798        let file_id = self.query_file_identity().file_id_for_uri(uri)?;
799        self.query_documents().get(&file_id).map(Arc::as_ref)
800    }
801
802    fn document_for_file_id(&self, file_id: LspFileId) -> Option<&LspTextDocumentState> {
803        self.query_documents().get(&file_id).map(Arc::as_ref)
804    }
805
806    fn workspace_folder(&self, uri: &str) -> Option<&LspWorkspaceFolderState> {
807        self.query_workspace_runtime_registry().get(uri)
808    }
809
810    fn cascade_narrowing_substrate_memo_lock(
811        &self,
812    ) -> MutexGuard<'_, Option<LspCascadeNarrowingSubstrateMemo>> {
813        self.query_cascade_narrowing_substrate_memo()
814            .lock()
815            .unwrap_or_else(|error| error.into_inner())
816    }
817
818    fn workspace_occurrence_index_memo_lock(
819        &self,
820    ) -> MutexGuard<'_, Option<LspWorkspaceOccurrenceIndexMemo>> {
821        self.query_workspace_occurrence_index_memo()
822            .lock()
823            .unwrap_or_else(|error| error.into_inner())
824    }
825
826    #[cfg(feature = "parallel-style-diagnostics")]
827    fn resolver_identity_index_memo_lock(
828        &self,
829    ) -> MutexGuard<'_, Option<LspResolverIdentityIndexMemo>> {
830        self.query_resolver_identity_index_memo()
831            .lock()
832            .unwrap_or_else(|error| error.into_inner())
833    }
834
835    #[cfg(feature = "salsa-style-diagnostics")]
836    fn style_workspace_snapshot_revision_hint(&self) -> omena_query::IncrementalRevisionV0 {
837        omena_query::IncrementalRevisionV0 {
838            value: self.query_style_workspace_snapshot_revision_hint().max(1),
839        }
840    }
841}
842
843#[derive(Debug, Default)]
844pub struct LspShellState {
845    pub shutdown_requested: bool,
846    pub should_exit: bool,
847    pub(crate) features: LspFeatureSettings,
848    pub(crate) diagnostics: LspDiagnosticSettings,
849    pub(crate) resolution: LspResolutionSettings,
850    pub(crate) cancelled_request_ids: IncrementalCancellationRegistryV0,
851    pub(crate) in_flight_requests: LspInFlightRequestRegistry,
852    pub(crate) workspace_style_index_exhausted_count: usize,
853    pub(crate) source_type_fact_workspace_index_incomplete: bool,
854    pub(crate) source_type_fact_watched_files_observed: bool,
855    pub(crate) workspace_index_pending_file_count: usize,
856    pub(crate) external_sif_bridge_generation_count: usize,
857    pub(crate) external_sif_refresh_deferred: bool,
858    /// Tide kernel (rfcs#111): the epoch ledger with per-input high-water
859    /// marks, and the two settle-gated demand lanes. Trigger sites deposit
860    /// demands; the gates decide when a flush happens. These replace the
861    /// dirty/owed flags and the per-subsystem refresh revision.
862    pub(crate) tide_ledger: crate::tide::TideEpochLedgerV0,
863    pub(crate) tide_sif_lane: crate::tide::TideLaneV0<crate::tide::TideSifDemandV0>,
864    pub(crate) tide_republish_lane: crate::tide::TideLaneV0<crate::tide::TideRepublishDemandV0>,
865    pub(crate) tide_disowns_total: [u64; crate::tide::ledger::TIDE_INPUT_KIND_COUNT],
866    pub(crate) tide_disowns_out_of_cone: [u64; crate::tide::ledger::TIDE_INPUT_KIND_COUNT],
867    /// Executor-visible generation watch for the republish lane: flushes
868    /// store their generation, window reopens bump it, and the off-loop wave
869    /// compares it at item boundaries to abort disowned tides (rfcs#111).
870    pub(crate) tide_republish_gen_watch: std::sync::Arc<std::sync::atomic::AtomicU64>,
871    /// Loop tick counter consumed by lane aging; advanced once per runtime
872    /// loop iteration, stays 0 under test drivers.
873    pub(crate) tide_tick: u64,
874    pub(crate) workspace_index_revision: u64,
875    #[cfg(feature = "salsa-style-diagnostics")]
876    pub(crate) style_workspace_snapshot_revision_hint: u64,
877    pub(crate) configuration_change_count: usize,
878    /// RFC 0009 Pillar A (rfcs#67, slice A-min): documents are `Arc` entries so a
879    /// query snapshot clones pointers instead of the corpus; mutation paths go
880    /// through `document_mut`/`insert_document`, which copy-on-write via
881    /// `Arc::make_mut`/`Arc::new` (a worker holding a snapshot of a document
882    /// forces at most a one-document deep clone on that document's next edit).
883    pub(crate) file_identity: LspFileIdentityInterner,
884    pub(crate) documents: BTreeMap<LspFileId, Arc<LspTextDocumentState>>,
885    pub(crate) open_document_uris: BTreeSet<LspFileId>,
886    pub(crate) workspace_runtime_registry: WorkspaceRuntimeRegistry,
887    pub(crate) tsgo_workspace_process_pool: TsgoWorkspaceProcessPoolV0,
888    pub(crate) watched_file_changes: Vec<LspWatchedFileChangeState>,
889    pub(crate) swept_legacy_cache_roots: BTreeSet<PathBuf>,
890    pub(crate) client_supports_work_done_progress: bool,
891    pub(crate) next_server_progress_request_id: u64,
892    pub(crate) pending_server_progress_request_tokens: BTreeMap<String, String>,
893    /// Shared (not per-state) since RFC 0009 Pillar A: the loop and dispatched
894    /// query snapshots reuse ONE memo slot so a substrate built on either side is
895    /// visible to both. The memo is self-validating by exact input compare, so
896    /// last-writer-wins is safe; lock only to compare and to store — never across
897    /// the substrate collection (see `cascade_narrowing_substrate_for_style_sources`).
898    pub(crate) cascade_narrowing_substrate_memo:
899        Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>>,
900    #[cfg(feature = "parallel-style-diagnostics")]
901    pub(crate) resolver_identity_index_memo: Arc<Mutex<Option<LspResolverIdentityIndexMemo>>>,
902    /// Shared into query snapshots (`Arc`) like the cascade memo: codeLens
903    /// resolves on the dispatched query lane, so the occurrence index a
904    /// worker builds must be visible to the loop and the next worker —
905    /// otherwise every dispatched codeLens rebuilds the workspace index.
906    /// Self-validating by document-key compare; last-writer-wins is safe.
907    pub(crate) workspace_occurrence_index_memo: Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>>,
908    /// documentColor cross-request cache, keyed by (document version, corpus
909    /// text mark, corpus set mark) — shared into query snapshots (`Arc`) so
910    /// dispatched requests hit it too.
911    pub(crate) document_color_cache: Arc<Mutex<LspDocumentColorCacheV0>>,
912    #[cfg(feature = "salsa-style-diagnostics")]
913    pub(crate) reverse_dependency_index_memo: RefCell<Option<LspReverseDependencyIndexMemo>>,
914    /// Module-interface projection of the LAST text the source and style-peer
915    /// fan-outs saw. A didChange whose projection compares equal is an
916    /// interface-preserving edit, while a disk-backed close compares the
917    /// restored on-disk projection against the retained open-buffer projection.
918    /// Entries are retained across disk-backed close, removed with documents
919    /// that leave the state, and capped by the diagnostics scheduler.
920    pub(crate) style_module_interface_memo:
921        RefCell<BTreeMap<String, omena_query::OmenaQueryModuleInterfaceChangeProjectionV0>>,
922    /// Shared with delayed diagnostics workers and updated only after a payload
923    /// reaches the client writer, so stale or failed work cannot suppress a retry.
924    pub(crate) diagnostics_publish_digest_registry: DiagnosticsPublishDigestRegistryV0,
925    pub(crate) source_type_fact_cache: BTreeMap<String, LspSourceTypeFactCacheEntryV0>,
926    pub(crate) source_type_fact_cache_next_use: u64,
927    pub(crate) source_type_fact_cache_telemetry: LspSourceTypeFactCacheTelemetryV0,
928    /// RFC 0009 Pillar C (rfcs#66): fail-soft write breaker for the disk
929    /// diagnostics shard cache. Interior mutability because the write-behind
930    /// runs on the immutable resolve path; owned by the single loop thread.
931    pub(crate) disk_diagnostics_cache_session: RefCell<DiskDiagnosticsCacheSessionV0>,
932    /// RFC 0009 Pillar B (rfcs#65): the long-lived salsa-memoized
933    /// style-diagnostics host. Owned by the loop thread; the host diff-syncs
934    /// its inputs on every resolve, so it never serves a stale revision.
935    #[cfg(feature = "salsa-style-diagnostics")]
936    pub(crate) style_memo_host: RefCell<Option<omena_query::OmenaQueryStyleMemoHostV0>>,
937}
938
939impl LspShellState {
940    pub fn configure_standalone_cache_storage(&mut self, cache_dir: Option<PathBuf>) {
941        self.resolution.cache_storage =
942            crate::cache_root::LspCacheStorageConfigV0::standalone(cache_dir);
943    }
944
945    pub fn document_count(&self) -> usize {
946        self.documents.len()
947    }
948
949    pub fn workspace_folder_count(&self) -> usize {
950        self.workspace_runtime_registry.len()
951    }
952
953    pub(crate) fn allocate_work_done_progress_request(&mut self) -> (String, String) {
954        self.next_server_progress_request_id += 1;
955        let id = format!(
956            "omena-work-done-progress-create-{}",
957            self.next_server_progress_request_id
958        );
959        let token = format!(
960            "omena-workspace-index-{}",
961            self.next_server_progress_request_id
962        );
963        self.pending_server_progress_request_tokens
964            .insert(id.clone(), token.clone());
965        (id, token)
966    }
967
968    pub(crate) fn take_server_progress_response(&mut self, id: &str) -> bool {
969        self.pending_server_progress_request_tokens
970            .remove(id)
971            .is_some()
972    }
973
974    pub fn document(&self, uri: &str) -> Option<&LspTextDocumentState> {
975        let file_id = self.file_identity.file_id_for_uri(uri)?;
976        self.documents.get(&file_id).map(Arc::as_ref)
977    }
978
979    #[cfg(feature = "test-support")]
980    pub fn evict_document_for_test(&mut self, uri: &str) -> Option<LspTextDocumentState> {
981        self.remove_document_uri(uri)
982    }
983
984    #[cfg(feature = "test-support")]
985    pub fn clear_workspace_occurrence_index_memo_for_test(&self) {
986        *self.workspace_occurrence_index_memo_lock() = None;
987    }
988
989    pub(crate) fn workspace_occurrence_index_memo_lock(
990        &self,
991    ) -> MutexGuard<'_, Option<LspWorkspaceOccurrenceIndexMemo>> {
992        self.workspace_occurrence_index_memo
993            .lock()
994            .unwrap_or_else(|error| error.into_inner())
995    }
996
997    pub(crate) fn document_mut(&mut self, uri: &str) -> Option<&mut LspTextDocumentState> {
998        let file_id = self.file_identity.file_id_for_uri(uri)?;
999        let document = self.documents.get_mut(&file_id)?;
1000        if document.origin == LspDocumentOrigin::Foreign {
1001            return None;
1002        }
1003        Some(Arc::make_mut(document))
1004    }
1005
1006    #[cfg(test)]
1007    pub(crate) fn document_storage_uri(uri: &str) -> String {
1008        LspFileIdentityInterner::storage_uri(uri)
1009    }
1010
1011    pub(crate) fn document_file_id(&self, uri: &str) -> Option<LspFileId> {
1012        self.file_identity.file_id_for_uri(uri)
1013    }
1014
1015    #[cfg(test)]
1016    pub(crate) fn intern_file_uri(&mut self, uri: &str) -> LspFileId {
1017        self.file_identity.intern_uri(uri).0
1018    }
1019
1020    pub(crate) fn document_storage_uri_for_file_id(&self, file_id: LspFileId) -> Option<&str> {
1021        self.file_identity.storage_uri_for_file_id(file_id)
1022    }
1023
1024    pub(crate) fn document_for_file_id(&self, file_id: LspFileId) -> Option<&LspTextDocumentState> {
1025        self.documents.get(&file_id).map(Arc::as_ref)
1026    }
1027
1028    pub(crate) fn insert_open_document_uri(&mut self, uri: &str) -> String {
1029        let (file_id, storage_uri) = self.file_identity.intern_uri(uri);
1030        self.open_document_uris.insert(file_id);
1031        storage_uri
1032    }
1033
1034    pub(crate) fn remove_open_document_uri(&mut self, uri: &str) {
1035        if let Some(file_id) = self.file_identity.file_id_for_uri(uri) {
1036            self.open_document_uris.remove(&file_id);
1037        }
1038    }
1039
1040    pub(crate) fn has_open_document_uri(&self, uri: &str) -> bool {
1041        self.file_identity
1042            .file_id_for_uri(uri)
1043            .is_some_and(|file_id| self.open_document_uris.contains(&file_id))
1044    }
1045
1046    pub(crate) fn insert_document(&mut self, uri: &str, document: LspTextDocumentState) {
1047        let (file_id, _) = self.file_identity.intern_uri(uri);
1048        self.documents.insert(file_id, Arc::new(document));
1049    }
1050
1051    pub(crate) fn remove_document_uri(&mut self, uri: &str) -> Option<LspTextDocumentState> {
1052        let file_id = self.file_identity.file_id_for_uri(uri)?;
1053        let document = self.documents.remove(&file_id).map(Arc::unwrap_or_clone)?;
1054        let memo = self.style_module_interface_memo.get_mut();
1055        memo.remove(uri);
1056        memo.remove(document.uri.as_str());
1057        self.diagnostics_publish_digest_registry
1058            .forget_reactive_shadow_module_interface(uri);
1059        self.diagnostics_publish_digest_registry
1060            .forget_reactive_shadow_module_interface(document.uri.as_str());
1061        Some(document)
1062    }
1063
1064    pub(crate) fn contains_document_uri(&self, uri: &str) -> bool {
1065        self.document(uri).is_some()
1066    }
1067
1068    pub fn workspace_folder(&self, uri: &str) -> Option<&LspWorkspaceFolderState> {
1069        self.workspace_runtime_registry.get(uri)
1070    }
1071
1072    pub fn snapshot(&self) -> LspShellStateSnapshot {
1073        LspShellStateSnapshot {
1074            shutdown_requested: self.shutdown_requested,
1075            should_exit: self.should_exit,
1076            features: self.features.clone(),
1077            diagnostics: self.diagnostics.clone(),
1078            resolution: self.resolution.clone(),
1079            cancelled_request_count: self.cancelled_request_ids.len(),
1080            suppressed_dispatched_result_count: self.in_flight_requests.suppressed_result_count(),
1081            workspace_style_index_exhausted_count: self.workspace_style_index_exhausted_count,
1082            workspace_index_pending_file_count: self.workspace_index_pending_file_count,
1083            external_sif_lock_read_count: 0,
1084            external_sif_bridge_generation_count: self.external_sif_bridge_generation_count,
1085            document_count: self.document_count(),
1086            workspace_folder_count: self.workspace_folder_count(),
1087            configuration_change_count: self.configuration_change_count,
1088            watched_file_event_count: self.watched_file_changes.len(),
1089            cached_workspace_resolution_input_count: self
1090                .resolution
1091                .workspace_style_resolution_inputs
1092                .len(),
1093            source_type_fact_cache_telemetry: self.source_type_fact_cache_telemetry.clone(),
1094            tide_epoch: self.tide_ledger.epoch(),
1095            tide_sif_lane_generation: self.tide_sif_lane.generation(),
1096            tide_sif_lane_in_flight: self.tide_sif_lane.in_flight(),
1097            tide_sif_lane_has_demand: self.tide_sif_lane.has_demand(),
1098            tide_republish_lane_generation: self.tide_republish_lane.generation(),
1099            tide_republish_lane_in_flight: self.tide_republish_lane.in_flight(),
1100            tide_republish_lane_has_demand: self.tide_republish_lane.has_demand(),
1101            tide_starvation_alarm_count: self.tide_sif_lane.starvation_alarm_count()
1102                + self.tide_republish_lane.starvation_alarm_count(),
1103            tide_disowns_total: tide_disown_counts_by_driver(&self.tide_disowns_total),
1104            tide_disowns_out_of_cone: tide_disown_counts_by_driver(&self.tide_disowns_out_of_cone),
1105            tide_sif_lane_oldest_deposit_age_ticks: self
1106                .tide_sif_lane
1107                .oldest_deposit_age_ticks(self.tide_tick),
1108            tide_republish_lane_oldest_deposit_age_ticks: self
1109                .tide_republish_lane
1110                .oldest_deposit_age_ticks(self.tide_tick),
1111            documents: {
1112                let mut documents = self
1113                    .documents
1114                    .values()
1115                    .map(|document| (**document).clone())
1116                    .collect::<Vec<_>>();
1117                documents.sort_by(|left, right| left.uri.cmp(&right.uri));
1118                documents
1119            },
1120            workspace_folders: self.workspace_runtime_registry.folder_snapshots(),
1121            watched_file_changes: self.watched_file_changes.clone(),
1122        }
1123    }
1124
1125    #[cfg(feature = "parallel-style-diagnostics")]
1126    pub(crate) fn resolver_identity_index_memo_lock(
1127        &self,
1128    ) -> MutexGuard<'_, Option<LspResolverIdentityIndexMemo>> {
1129        self.resolver_identity_index_memo
1130            .lock()
1131            .unwrap_or_else(|error| error.into_inner())
1132    }
1133
1134    /// Current republish-lane generation — the runtime loop compares queued
1135    /// apply batches against it to drop disowned tides (rfcs#111 §9.4).
1136    pub fn tide_republish_lane_generation(&self) -> u64 {
1137        self.tide_republish_lane.generation()
1138    }
1139
1140    /// Whether a republish tide is in flight — the runtime loop's pump must
1141    /// keep this held until the stream's FINAL chunk drains (completing on a
1142    /// momentarily-empty queue would disable disown/abort/carry-over).
1143    pub fn tide_republish_lane_in_flight(&self) -> bool {
1144        self.tide_republish_lane.in_flight()
1145    }
1146
1147    /// Advance the Tide tick — called once per runtime loop iteration; the
1148    /// tick feeds lane aging (courtesy-layer override, never correctness).
1149    pub fn advance_tide_tick(&mut self) {
1150        self.tide_tick = self.tide_tick.saturating_add(1);
1151    }
1152
1153    /// Reopen the republish settle window: bump the lane generation (a
1154    /// running tide is disowned) and publish it to the executor watch.
1155    pub(crate) fn tide_reopen_republish_window(&mut self, cause: crate::tide::TideDisownCauseV0) {
1156        let in_flight_demand = self.tide_republish_lane.in_flight_demand().cloned();
1157        let out_of_cone = in_flight_demand
1158            .as_ref()
1159            .is_some_and(|demand| self.tide_disown_cause_is_out_of_cone(demand, &cause));
1160        let reopened = self.tide_republish_lane.reopen_window_for_cause(cause);
1161        if let Some(disowned_cause) = reopened.disowned_cause {
1162            let index = disowned_cause.kind as usize;
1163            self.tide_disowns_total[index] = self.tide_disowns_total[index].saturating_add(1);
1164            if out_of_cone {
1165                self.tide_disowns_out_of_cone[index] =
1166                    self.tide_disowns_out_of_cone[index].saturating_add(1);
1167            }
1168            crate::loop_trace!(
1169                "republish-tide disowned kind={} disposition={}",
1170                disowned_cause.kind.wire_name(),
1171                if out_of_cone {
1172                    "out-of-cone"
1173                } else {
1174                    "in-cone"
1175                }
1176            );
1177        }
1178        self.tide_republish_gen_watch
1179            .store(reopened.generation, std::sync::atomic::Ordering::Relaxed);
1180    }
1181
1182    fn tide_disown_cause_is_out_of_cone(
1183        &self,
1184        in_flight: &crate::tide::TideRepublishDemandV0,
1185        cause: &crate::tide::TideDisownCauseV0,
1186    ) -> bool {
1187        use crate::tide::TideRepublishDemandV0;
1188
1189        match (&cause.affected, in_flight) {
1190            (TideRepublishDemandV0::All, _)
1191            | (_, TideRepublishDemandV0::All)
1192            | (_, TideRepublishDemandV0::None) => false,
1193            (TideRepublishDemandV0::None, TideRepublishDemandV0::Cone(_)) => true,
1194            (TideRepublishDemandV0::Cone(affected), TideRepublishDemandV0::Cone(_)) => {
1195                let targets =
1196                    crate::diagnostics_follow_up::tide_republish_target_file_ids(self, in_flight);
1197                !targets
1198                    .iter()
1199                    .any(|target| affected.members.contains(target))
1200            }
1201        }
1202    }
1203
1204    #[cfg(feature = "salsa-style-diagnostics")]
1205    pub(crate) fn mark_style_workspace_snapshot_changed(
1206        &mut self,
1207    ) -> omena_query::IncrementalRevisionV0 {
1208        let committed = self
1209            .style_memo_host
1210            .borrow()
1211            .as_ref()
1212            .map(|host| host.committed_revision().value)
1213            .unwrap_or_default();
1214        let next = self
1215            .style_workspace_snapshot_revision_hint
1216            .max(committed)
1217            .saturating_add(1)
1218            .max(1);
1219        self.style_workspace_snapshot_revision_hint = next;
1220        omena_query::IncrementalRevisionV0 { value: next }
1221    }
1222
1223    /// Build the copy-on-write read model used by off-loop query work.
1224    ///
1225    /// The copied fields are the complete storage surface exposed through
1226    /// [`LspQueryReadView`]. All remaining shell fields stay loop-owned and are
1227    /// unreachable through that interface. Document values are shared through
1228    /// `Arc`, while settings and workspace registries are copied at dispatch.
1229    pub fn query_snapshot(&self) -> LspQuerySnapshotV0 {
1230        LspQuerySnapshotV0 {
1231            state: LspShellState {
1232                features: self.features.clone(),
1233                diagnostics: self.diagnostics.clone(),
1234                resolution: self.resolution.clone(),
1235                file_identity: self.file_identity.clone(),
1236                documents: self.documents.clone(),
1237                open_document_uris: self.open_document_uris.clone(),
1238                workspace_runtime_registry: self.workspace_runtime_registry.clone(),
1239                tide_ledger: self.tide_ledger.clone(),
1240                #[cfg(feature = "salsa-style-diagnostics")]
1241                style_workspace_snapshot_revision_hint: self.style_workspace_snapshot_revision_hint,
1242                document_color_cache: Arc::clone(&self.document_color_cache),
1243                cascade_narrowing_substrate_memo: Arc::clone(
1244                    &self.cascade_narrowing_substrate_memo,
1245                ),
1246                workspace_occurrence_index_memo: Arc::clone(&self.workspace_occurrence_index_memo),
1247                #[cfg(feature = "parallel-style-diagnostics")]
1248                resolver_identity_index_memo: Arc::clone(&self.resolver_identity_index_memo),
1249                ..LspShellState::default()
1250            },
1251        }
1252    }
1253}
1254
1255#[allow(private_interfaces)]
1256impl LspQueryReadView for LspShellState {
1257    fn query_features(&self) -> &LspFeatureSettings {
1258        &self.features
1259    }
1260
1261    fn query_diagnostics(&self) -> &LspDiagnosticSettings {
1262        &self.diagnostics
1263    }
1264
1265    fn query_resolution(&self) -> &LspResolutionSettings {
1266        &self.resolution
1267    }
1268
1269    fn query_file_identity(&self) -> &LspFileIdentityInterner {
1270        &self.file_identity
1271    }
1272
1273    fn query_documents(&self) -> &BTreeMap<LspFileId, Arc<LspTextDocumentState>> {
1274        &self.documents
1275    }
1276
1277    fn query_open_document_uris(&self) -> &BTreeSet<LspFileId> {
1278        &self.open_document_uris
1279    }
1280
1281    fn query_workspace_runtime_registry(&self) -> &WorkspaceRuntimeRegistry {
1282        &self.workspace_runtime_registry
1283    }
1284
1285    fn query_tide_ledger(&self) -> &crate::tide::TideEpochLedgerV0 {
1286        &self.tide_ledger
1287    }
1288
1289    #[cfg(feature = "salsa-style-diagnostics")]
1290    fn query_style_workspace_snapshot_revision_hint(&self) -> u64 {
1291        self.style_workspace_snapshot_revision_hint
1292    }
1293
1294    fn query_document_color_cache(&self) -> &Arc<Mutex<LspDocumentColorCacheV0>> {
1295        &self.document_color_cache
1296    }
1297
1298    fn query_cascade_narrowing_substrate_memo(
1299        &self,
1300    ) -> &Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>> {
1301        &self.cascade_narrowing_substrate_memo
1302    }
1303
1304    fn query_workspace_occurrence_index_memo(
1305        &self,
1306    ) -> &Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>> {
1307        &self.workspace_occurrence_index_memo
1308    }
1309
1310    #[cfg(feature = "parallel-style-diagnostics")]
1311    fn query_resolver_identity_index_memo(
1312        &self,
1313    ) -> &Arc<Mutex<Option<LspResolverIdentityIndexMemo>>> {
1314        &self.resolver_identity_index_memo
1315    }
1316}
1317
1318/// Copy-on-write read model for dispatched queries and deferred read workers.
1319///
1320/// The partial shell remains a private storage detail. Consumers compile only
1321/// against [`LspQueryReadView`], which prevents access to default-filled
1322/// loop-owned machinery such as process pools, schedulers, cache breakers, and
1323/// mutation-side memo hosts.
1324#[derive(Debug)]
1325pub struct LspQuerySnapshotV0 {
1326    pub(crate) state: LspShellState,
1327}
1328
1329impl LspQuerySnapshotV0 {
1330    #[cfg(test)]
1331    pub(crate) fn shell_state_for_test(&self) -> &LspShellState {
1332        &self.state
1333    }
1334}
1335
1336#[allow(private_interfaces)]
1337impl LspQueryReadView for LspQuerySnapshotV0 {
1338    fn query_features(&self) -> &LspFeatureSettings {
1339        &self.state.features
1340    }
1341
1342    fn query_diagnostics(&self) -> &LspDiagnosticSettings {
1343        &self.state.diagnostics
1344    }
1345
1346    fn query_resolution(&self) -> &LspResolutionSettings {
1347        &self.state.resolution
1348    }
1349
1350    fn query_file_identity(&self) -> &LspFileIdentityInterner {
1351        &self.state.file_identity
1352    }
1353
1354    fn query_documents(&self) -> &BTreeMap<LspFileId, Arc<LspTextDocumentState>> {
1355        &self.state.documents
1356    }
1357
1358    fn query_open_document_uris(&self) -> &BTreeSet<LspFileId> {
1359        &self.state.open_document_uris
1360    }
1361
1362    fn query_workspace_runtime_registry(&self) -> &WorkspaceRuntimeRegistry {
1363        &self.state.workspace_runtime_registry
1364    }
1365
1366    fn query_tide_ledger(&self) -> &crate::tide::TideEpochLedgerV0 {
1367        &self.state.tide_ledger
1368    }
1369
1370    #[cfg(feature = "salsa-style-diagnostics")]
1371    fn query_style_workspace_snapshot_revision_hint(&self) -> u64 {
1372        self.state.style_workspace_snapshot_revision_hint
1373    }
1374
1375    fn query_document_color_cache(&self) -> &Arc<Mutex<LspDocumentColorCacheV0>> {
1376        &self.state.document_color_cache
1377    }
1378
1379    fn query_cascade_narrowing_substrate_memo(
1380        &self,
1381    ) -> &Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>> {
1382        &self.state.cascade_narrowing_substrate_memo
1383    }
1384
1385    fn query_workspace_occurrence_index_memo(
1386        &self,
1387    ) -> &Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>> {
1388        &self.state.workspace_occurrence_index_memo
1389    }
1390
1391    #[cfg(feature = "parallel-style-diagnostics")]
1392    fn query_resolver_identity_index_memo(
1393        &self,
1394    ) -> &Arc<Mutex<Option<LspResolverIdentityIndexMemo>>> {
1395        &self.state.resolver_identity_index_memo
1396    }
1397}
1398
1399// The dispatched query lane moves snapshots onto the worker thread; keep that
1400// property checked at compile time independent of the worker code shape.
1401const _: () = {
1402    const fn assert_send<T: Send>() {}
1403    assert_send::<LspQuerySnapshotV0>();
1404};