Skip to main content

omena_lsp_server/
state.rs

1use crate::disk_cache::DiskDiagnosticsCacheSessionV0;
2use crate::workspace_runtime_registry::WorkspaceRuntimeRegistry;
3use omena_incremental::IncrementalCancellationRegistryV0;
4#[cfg(feature = "salsa-style-diagnostics")]
5use omena_query::ReverseDependencyIndexV0;
6use omena_query::{
7    AnalyzedGraphV0, OmenaQueryExternalSifInputV0, OmenaQuerySourceSelectorOccurrenceIndexV0,
8    OmenaQuerySourceSelectorReferenceFactV0 as SourceSelectorReferenceFact,
9    OmenaQuerySourceSyntaxIndexV0 as SourceSyntaxIndex, OmenaQueryStyleCascadeNarrowingSubstrateV0,
10    OmenaQueryStylePackageManifestV0, OmenaQueryStyleResolutionInputsV0,
11    OmenaQueryStyleSelectorDefinitionV0, OmenaQueryStyleSourceInputV0,
12    OmenaWorkspaceOccurrenceFamilyV0, OmenaWorkspaceOccurrenceIndexV0,
13    OmenaWorkspaceOccurrenceKindV0, OmenaWorkspaceOccurrenceRoleV0, ParserPositionV0,
14    ParserRangeV0,
15};
16#[cfg(feature = "parallel-style-diagnostics")]
17use omena_query::{
18    OmenaResolverStyleModuleConfirmationIdentityIndexV0,
19    OmenaResolverStyleModuleDiskCandidateIdentityV0,
20};
21use omena_tsgo_client::{TsgoTypeFactResultEntryV0, TsgoWorkspaceProcessPoolV0};
22use serde::Serialize;
23use std::cell::RefCell;
24use std::collections::{BTreeMap, BTreeSet};
25use std::sync::{
26    Arc, Mutex, MutexGuard,
27    atomic::{AtomicU8, AtomicU64, Ordering},
28};
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub enum LspDocumentOrigin {
33    Local,
34    Foreign,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct LspTextDocumentState {
40    pub uri: String,
41    #[serde(skip)]
42    pub origin: LspDocumentOrigin,
43    pub workspace_folder_uri: Option<String>,
44    pub language_id: String,
45    pub version: i64,
46    pub text: String,
47    #[serde(skip)]
48    pub(crate) text_hash: String,
49    pub style_summary: Option<LspStyleDocumentSummary>,
50    pub diagnostics_schedule_count: usize,
51    pub optimizing_tier_feedback: Option<LspOptimizingTierFeedback>,
52    #[serde(skip)]
53    pub style_candidates: Vec<LspStyleHoverCandidate>,
54    #[serde(skip)]
55    pub(crate) source_syntax_index: SourceSyntaxIndex,
56    #[serde(skip)]
57    pub(crate) has_unresolved_style_import: bool,
58    #[serde(skip)]
59    pub source_selector_candidates: Vec<LspStyleHoverCandidate>,
60    #[serde(skip)]
61    pub(crate) source_type_fact_selector_references: Vec<SourceSelectorReferenceFact>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct LspOptimizingTierFeedback {
67    pub schema_version: &'static str,
68    pub product: &'static str,
69    pub document_version: i64,
70    pub policy: &'static str,
71    pub consumer: &'static str,
72    pub analyzed_graph: AnalyzedGraphV0,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct LspStyleDocumentSummary {
78    pub language: &'static str,
79    pub selector_names: Vec<String>,
80    pub custom_property_decl_names: Vec<String>,
81    pub custom_property_ref_names: Vec<String>,
82    pub sass_module_use_sources: Vec<String>,
83    pub sass_module_forward_sources: Vec<String>,
84    pub diagnostic_count: usize,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct LspStyleHoverCandidatesResult {
90    pub schema_version: &'static str,
91    pub product: &'static str,
92    pub document_uri: String,
93    pub workspace_folder_uri: Option<String>,
94    pub language: Option<&'static str>,
95    pub query_position: Option<ParserPositionV0>,
96    pub candidate_count: usize,
97    pub candidates: Vec<LspStyleHoverCandidate>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct LspStyleHoverCandidate {
103    pub kind: &'static str,
104    pub name: String,
105    pub range: ParserRangeV0,
106    pub source: &'static str,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub target_style_uri: Option<String>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub namespace: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
114#[serde(rename_all = "camelCase")]
115pub struct LspWorkspaceFolderState {
116    pub uri: String,
117    pub name: String,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct LspWatchedFileChangeState {
123    pub uri: String,
124    pub change_type: u64,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
128pub(crate) struct LspFileId(u32);
129
130impl LspFileId {
131    #[cfg(test)]
132    pub(crate) fn incremental_key(self) -> u32 {
133        self.0
134    }
135}
136
137#[derive(Debug, Clone, Default)]
138pub(crate) struct LspFileIdentityInterner {
139    next_id: u32,
140    ids_by_storage_uri: BTreeMap<String, LspFileId>,
141    storage_uris_by_id: BTreeMap<LspFileId, String>,
142}
143
144impl LspFileIdentityInterner {
145    fn intern_uri(&mut self, uri: &str) -> (LspFileId, String) {
146        let storage_uri = Self::storage_uri(uri);
147        if let Some(file_id) = self.ids_by_storage_uri.get(storage_uri.as_str()) {
148            return (*file_id, storage_uri);
149        }
150        let file_id = LspFileId(self.next_id);
151        self.next_id = self.next_id.saturating_add(1);
152        self.ids_by_storage_uri.insert(storage_uri.clone(), file_id);
153        self.storage_uris_by_id.insert(file_id, storage_uri.clone());
154        (file_id, storage_uri)
155    }
156
157    fn file_id_for_uri(&self, uri: &str) -> Option<LspFileId> {
158        let storage_uri = Self::storage_uri(uri);
159        self.ids_by_storage_uri.get(storage_uri.as_str()).copied()
160    }
161
162    pub(crate) fn storage_uri_for_file_id(&self, file_id: LspFileId) -> Option<&str> {
163        self.storage_uris_by_id.get(&file_id).map(String::as_str)
164    }
165
166    fn storage_uri(uri: &str) -> String {
167        crate::protocol::canonical_file_uri(uri).unwrap_or_else(|| uri.to_string())
168    }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct LspShellStateSnapshot {
174    pub shutdown_requested: bool,
175    pub should_exit: bool,
176    pub features: LspFeatureSettings,
177    pub diagnostics: LspDiagnosticSettings,
178    pub resolution: LspResolutionSettings,
179    pub cancelled_request_count: usize,
180    pub suppressed_dispatched_result_count: u64,
181    pub workspace_style_index_exhausted_count: usize,
182    pub workspace_index_pending_file_count: usize,
183    pub external_sif_lock_read_count: usize,
184    pub external_sif_bridge_generation_count: usize,
185    pub document_count: usize,
186    pub workspace_folder_count: usize,
187    pub configuration_change_count: usize,
188    pub watched_file_event_count: usize,
189    pub cached_workspace_resolution_input_count: usize,
190    /// Tide observability (rfcs#111 §11.4): the ledger epoch and the state
191    /// of both settle-gated lanes, so #110-style loop debugging is a debug
192    /// request instead of ad-hoc instrumentation.
193    pub tide_epoch: u64,
194    pub tide_sif_lane_generation: u64,
195    pub tide_sif_lane_in_flight: bool,
196    pub tide_sif_lane_has_demand: bool,
197    pub tide_republish_lane_generation: u64,
198    pub tide_republish_lane_in_flight: bool,
199    pub tide_republish_lane_has_demand: bool,
200    pub tide_starvation_alarm_count: u64,
201    /// Current backlog ages (ticks since the oldest un-flushed deposit),
202    /// `None` while the lane is at bottom — the alarm count says starvation
203    /// HAPPENED, these say how far behind each lane is NOW.
204    pub tide_sif_lane_oldest_deposit_age_ticks: Option<u64>,
205    pub tide_republish_lane_oldest_deposit_age_ticks: Option<u64>,
206    pub documents: Vec<LspTextDocumentState>,
207    pub workspace_folders: Vec<LspWorkspaceFolderState>,
208    pub watched_file_changes: Vec<LspWatchedFileChangeState>,
209}
210
211const DISPATCHED_REQUEST_PENDING: u8 = 0;
212const DISPATCHED_REQUEST_CANCELLED: u8 = 1;
213const DISPATCHED_REQUEST_COMPLETED: u8 = 2;
214
215#[derive(Debug, Default)]
216struct LspInFlightRequestRegistryInner {
217    next_generation: u64,
218    requests: BTreeMap<String, LspInFlightRequestEntry>,
219}
220
221#[derive(Debug, Clone)]
222struct LspInFlightRequestEntry {
223    generation: u64,
224    status: Arc<AtomicU8>,
225}
226
227/// Shared request-lifecycle registry for dispatched JSON-RPC queries.
228///
229/// The loop marks the current generation cancelled; the worker atomically
230/// chooses either the computed result or a cancellation response at completion.
231#[derive(Debug, Clone, Default)]
232pub(crate) struct LspInFlightRequestRegistry {
233    inner: Arc<Mutex<LspInFlightRequestRegistryInner>>,
234    suppressed_result_count: Arc<AtomicU64>,
235}
236
237#[derive(Debug, Clone)]
238pub(crate) struct LspDispatchedRequestToken {
239    request_key: String,
240    generation: u64,
241    status: Arc<AtomicU8>,
242    registry: LspInFlightRequestRegistry,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub(crate) enum LspDispatchedRequestCompletion {
247    Result,
248    Cancelled,
249    AlreadyCompleted,
250}
251
252impl LspInFlightRequestRegistry {
253    pub(crate) fn register(&self, request_key: String) -> LspDispatchedRequestToken {
254        let mut inner = self.inner.lock().unwrap_or_else(|error| error.into_inner());
255        inner.next_generation = inner.next_generation.saturating_add(1).max(1);
256        let generation = inner.next_generation;
257        let status = Arc::new(AtomicU8::new(DISPATCHED_REQUEST_PENDING));
258        inner.requests.insert(
259            request_key.clone(),
260            LspInFlightRequestEntry {
261                generation,
262                status: Arc::clone(&status),
263            },
264        );
265        LspDispatchedRequestToken {
266            request_key,
267            generation,
268            status,
269            registry: self.clone(),
270        }
271    }
272
273    pub(crate) fn cancel(&self, request_key: &str) -> bool {
274        let inner = self.inner.lock().unwrap_or_else(|error| error.into_inner());
275        let Some(entry) = inner.requests.get(request_key) else {
276            return false;
277        };
278        let _ = entry.status.compare_exchange(
279            DISPATCHED_REQUEST_PENDING,
280            DISPATCHED_REQUEST_CANCELLED,
281            Ordering::AcqRel,
282            Ordering::Acquire,
283        );
284        true
285    }
286
287    pub(crate) fn suppressed_result_count(&self) -> u64 {
288        self.suppressed_result_count.load(Ordering::Acquire)
289    }
290
291    fn remove_if_current(&self, request_key: &str, generation: u64, status: &Arc<AtomicU8>) {
292        let mut inner = self.inner.lock().unwrap_or_else(|error| error.into_inner());
293        let is_current = inner.requests.get(request_key).is_some_and(|entry| {
294            entry.generation == generation && Arc::ptr_eq(&entry.status, status)
295        });
296        if is_current {
297            inner.requests.remove(request_key);
298        }
299    }
300}
301
302impl LspDispatchedRequestToken {
303    pub(crate) fn complete(&self) -> LspDispatchedRequestCompletion {
304        let completion = match self.status.compare_exchange(
305            DISPATCHED_REQUEST_PENDING,
306            DISPATCHED_REQUEST_COMPLETED,
307            Ordering::AcqRel,
308            Ordering::Acquire,
309        ) {
310            Ok(_) => LspDispatchedRequestCompletion::Result,
311            Err(DISPATCHED_REQUEST_CANCELLED) => {
312                if self
313                    .status
314                    .compare_exchange(
315                        DISPATCHED_REQUEST_CANCELLED,
316                        DISPATCHED_REQUEST_COMPLETED,
317                        Ordering::AcqRel,
318                        Ordering::Acquire,
319                    )
320                    .is_ok()
321                {
322                    self.registry
323                        .suppressed_result_count
324                        .fetch_add(1, Ordering::AcqRel);
325                    LspDispatchedRequestCompletion::Cancelled
326                } else {
327                    LspDispatchedRequestCompletion::AlreadyCompleted
328                }
329            }
330            Err(_) => LspDispatchedRequestCompletion::AlreadyCompleted,
331        };
332        self.registry
333            .remove_if_current(self.request_key.as_str(), self.generation, &self.status);
334        completion
335    }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
339#[serde(rename_all = "camelCase")]
340pub struct LspFeatureSettings {
341    pub definition: bool,
342    pub hover: bool,
343    pub completion: bool,
344    pub references: bool,
345    pub rename: bool,
346}
347
348impl Default for LspFeatureSettings {
349    fn default() -> Self {
350        Self {
351            definition: true,
352            hover: true,
353            completion: true,
354            references: true,
355            rename: true,
356        }
357    }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
361#[serde(rename_all = "camelCase")]
362pub struct LspDiagnosticSettings {
363    pub severity: u8,
364    pub deep_analysis: bool,
365}
366
367impl Default for LspDiagnosticSettings {
368    fn default() -> Self {
369        Self {
370            severity: 2,
371            deep_analysis: false,
372        }
373    }
374}
375
376#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
377#[serde(rename_all = "camelCase")]
378pub struct LspResolutionSettings {
379    pub package_manifest_paths: Vec<String>,
380    #[serde(skip)]
381    pub package_manifests: Vec<OmenaQueryStylePackageManifestV0>,
382    #[serde(skip)]
383    pub workspace_style_resolution_inputs: BTreeMap<String, OmenaQueryStyleResolutionInputsV0>,
384    /// External Sass-module SIF artifacts sourced from workspace locks and bridge generation.
385    /// The diagnostics path runs in Auto mode, so source-available edges stay local while
386    /// SIF-backed and unresolved foreign edges are classified per import edge.
387    #[serde(skip)]
388    pub external_sifs: Vec<OmenaQueryExternalSifInputV0>,
389    #[serde(skip)]
390    pub(crate) bridge_external_sif_urls: BTreeSet<String>,
391}
392
393/// Workspace-revision memo for the cascade-narrowing substrate (rfcs#63 E-ii).
394/// Self-validating: the key is the exact narrowing input set (ordered style sources +
395/// package manifests + external SIFs + resolution mappings), so any document
396/// open/close/edit, disk reload, or resolution-config change misses by comparison and
397/// rebuilds — there is no eviction site to keep in sync.
398#[derive(Debug)]
399pub(crate) struct LspCascadeNarrowingSubstrateMemo {
400    pub(crate) style_sources: Vec<OmenaQueryStyleSourceInputV0>,
401    pub(crate) package_manifests: Vec<OmenaQueryStylePackageManifestV0>,
402    pub(crate) external_sifs: Vec<OmenaQueryExternalSifInputV0>,
403    pub(crate) resolution_inputs: OmenaQueryStyleResolutionInputsV0,
404    pub(crate) substrate: Arc<OmenaQueryStyleCascadeNarrowingSubstrateV0>,
405}
406
407#[cfg(feature = "parallel-style-diagnostics")]
408#[derive(Debug, Clone)]
409pub(crate) struct LspResolverIdentityIndexMemo {
410    pub(crate) available_style_paths: Vec<String>,
411    pub(crate) disk_style_path_identities: Vec<OmenaResolverStyleModuleDiskCandidateIdentityV0>,
412    pub(crate) index: Arc<OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
416#[serde(rename_all = "camelCase")]
417pub(crate) struct LspSourceSelectorOccurrenceDocumentKey {
418    pub(crate) uri: String,
419    pub(crate) workspace_folder_uri: Option<String>,
420    pub(crate) language_id: String,
421    pub(crate) version: i64,
422    pub(crate) text_hash: String,
423}
424
425#[derive(Debug, Clone)]
426pub(crate) struct LspWorkspaceOccurrenceIndexMemo {
427    pub(crate) workspace_folder_uri: Option<String>,
428    /// Digest over the NON-document inputs the build reads (external SIFs +
429    /// workspace resolution inputs). Document keys alone cannot see an SIF
430    /// or resolver-config move — the eviction on SIF refresh raced a
431    /// worker's in-flight store, reviving a stale index (review finding);
432    /// putting the environment IN the key makes the memo self-validating
433    /// for real instead of eviction-dependent.
434    pub(crate) environment_digest: Option<String>,
435    pub(crate) source_document_keys: Vec<LspSourceSelectorOccurrenceDocumentKey>,
436    pub(crate) style_document_keys: Vec<LspSourceSelectorOccurrenceDocumentKey>,
437    pub(crate) definitions: Vec<OmenaQueryStyleSelectorDefinitionV0>,
438    pub(crate) source_selector_index: Arc<OmenaQuerySourceSelectorOccurrenceIndexV0>,
439    pub(crate) workspace_index: Arc<OmenaWorkspaceOccurrenceIndexV0>,
440}
441
442/// documentColor cache rows: uri -> (freshness key, rendered informations).
443pub(crate) type LspDocumentColorCacheV0 = BTreeMap<String, ((i64, u64, u64), serde_json::Value)>;
444
445#[cfg(feature = "salsa-style-diagnostics")]
446#[derive(Debug, Clone, PartialEq, Eq)]
447pub(crate) struct LspReverseDependencyIndexMemo {
448    pub(crate) revision: u64,
449    pub(crate) summary_hash: String,
450    /// Tide-ledger epoch at the last refresh: consumers that would GUESS
451    /// from a stale graph (cone seeding) compare this against the corpus
452    /// input marks and widen instead.
453    pub(crate) ledger_epoch: u64,
454    pub(crate) index: ReverseDependencyIndexV0,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
458#[serde(rename_all = "camelCase")]
459pub(crate) struct LspStyleSymbolOccurrenceV0 {
460    pub(crate) moniker: String,
461    pub(crate) uri: String,
462    pub(crate) kind: OmenaWorkspaceOccurrenceKindV0,
463    pub(crate) family: OmenaWorkspaceOccurrenceFamilyV0,
464    pub(crate) name: String,
465    pub(crate) range: ParserRangeV0,
466    pub(crate) role: OmenaWorkspaceOccurrenceRoleV0,
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub(crate) namespace: Option<String>,
469}
470
471/// Read-only state surface available to off-loop query work.
472///
473/// The required methods mirror the fields copied by [`LspShellState::query_snapshot`].
474/// Loop-owned state is intentionally absent, so a resolver cannot accidentally
475/// observe a default-filled cache, process pool, scheduler, or cancellation registry.
476///
477/// ```compile_fail
478/// # fn forbidden<T: omena_lsp_server::LspQueryReadView>(view: &T) {
479/// let _ = view.shutdown_requested();
480/// # }
481/// ```
482#[allow(private_interfaces)]
483pub trait LspQueryReadView {
484    #[doc(hidden)]
485    fn query_features(&self) -> &LspFeatureSettings;
486
487    #[doc(hidden)]
488    fn query_diagnostics(&self) -> &LspDiagnosticSettings;
489
490    #[doc(hidden)]
491    fn query_resolution(&self) -> &LspResolutionSettings;
492
493    #[doc(hidden)]
494    fn query_file_identity(&self) -> &LspFileIdentityInterner;
495
496    #[doc(hidden)]
497    fn query_documents(&self) -> &BTreeMap<LspFileId, Arc<LspTextDocumentState>>;
498
499    #[doc(hidden)]
500    fn query_open_document_uris(&self) -> &BTreeSet<LspFileId>;
501
502    #[doc(hidden)]
503    fn query_workspace_runtime_registry(&self) -> &WorkspaceRuntimeRegistry;
504
505    #[doc(hidden)]
506    fn query_tide_ledger(&self) -> &crate::tide::TideEpochLedgerV0;
507
508    #[cfg(feature = "salsa-style-diagnostics")]
509    #[doc(hidden)]
510    fn query_style_workspace_snapshot_revision_hint(&self) -> u64;
511
512    #[doc(hidden)]
513    fn query_document_color_cache(&self) -> &Arc<Mutex<LspDocumentColorCacheV0>>;
514
515    #[doc(hidden)]
516    fn query_cascade_narrowing_substrate_memo(
517        &self,
518    ) -> &Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>>;
519
520    #[doc(hidden)]
521    fn query_workspace_occurrence_index_memo(
522        &self,
523    ) -> &Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>>;
524
525    #[cfg(feature = "parallel-style-diagnostics")]
526    #[doc(hidden)]
527    fn query_resolver_identity_index_memo(
528        &self,
529    ) -> &Arc<Mutex<Option<LspResolverIdentityIndexMemo>>>;
530
531    fn document(&self, uri: &str) -> Option<&LspTextDocumentState> {
532        let file_id = self.query_file_identity().file_id_for_uri(uri)?;
533        self.query_documents().get(&file_id).map(Arc::as_ref)
534    }
535
536    fn document_for_file_id(&self, file_id: LspFileId) -> Option<&LspTextDocumentState> {
537        self.query_documents().get(&file_id).map(Arc::as_ref)
538    }
539
540    fn workspace_folder(&self, uri: &str) -> Option<&LspWorkspaceFolderState> {
541        self.query_workspace_runtime_registry().get(uri)
542    }
543
544    fn cascade_narrowing_substrate_memo_lock(
545        &self,
546    ) -> MutexGuard<'_, Option<LspCascadeNarrowingSubstrateMemo>> {
547        self.query_cascade_narrowing_substrate_memo()
548            .lock()
549            .unwrap_or_else(|error| error.into_inner())
550    }
551
552    fn workspace_occurrence_index_memo_lock(
553        &self,
554    ) -> MutexGuard<'_, Option<LspWorkspaceOccurrenceIndexMemo>> {
555        self.query_workspace_occurrence_index_memo()
556            .lock()
557            .unwrap_or_else(|error| error.into_inner())
558    }
559
560    #[cfg(feature = "parallel-style-diagnostics")]
561    fn resolver_identity_index_memo_lock(
562        &self,
563    ) -> MutexGuard<'_, Option<LspResolverIdentityIndexMemo>> {
564        self.query_resolver_identity_index_memo()
565            .lock()
566            .unwrap_or_else(|error| error.into_inner())
567    }
568
569    #[cfg(feature = "salsa-style-diagnostics")]
570    fn style_workspace_snapshot_revision_hint(&self) -> omena_query::IncrementalRevisionV0 {
571        omena_query::IncrementalRevisionV0 {
572            value: self.query_style_workspace_snapshot_revision_hint().max(1),
573        }
574    }
575}
576
577#[derive(Debug, Default)]
578pub struct LspShellState {
579    pub shutdown_requested: bool,
580    pub should_exit: bool,
581    pub(crate) features: LspFeatureSettings,
582    pub(crate) diagnostics: LspDiagnosticSettings,
583    pub(crate) resolution: LspResolutionSettings,
584    pub(crate) cancelled_request_ids: IncrementalCancellationRegistryV0,
585    pub(crate) in_flight_requests: LspInFlightRequestRegistry,
586    pub(crate) workspace_style_index_exhausted_count: usize,
587    pub(crate) workspace_index_pending_file_count: usize,
588    pub(crate) external_sif_lock_read_count: usize,
589    pub(crate) external_sif_bridge_generation_count: usize,
590    pub(crate) external_sif_refresh_deferred: bool,
591    /// Tide kernel (rfcs#111): the epoch ledger with per-input high-water
592    /// marks, and the two settle-gated demand lanes. Trigger sites deposit
593    /// demands; the gates decide when a flush happens. These replace the
594    /// dirty/owed flags and the per-subsystem refresh revision.
595    pub(crate) tide_ledger: crate::tide::TideEpochLedgerV0,
596    pub(crate) tide_sif_lane: crate::tide::TideLaneV0<crate::tide::TideSifDemandV0>,
597    pub(crate) tide_republish_lane: crate::tide::TideLaneV0<crate::tide::TideRepublishDemandV0>,
598    /// Executor-visible generation watch for the republish lane: flushes
599    /// store their generation, window reopens bump it, and the off-loop wave
600    /// compares it at item boundaries to abort disowned tides (rfcs#111).
601    pub(crate) tide_republish_gen_watch: std::sync::Arc<std::sync::atomic::AtomicU64>,
602    /// Loop tick counter consumed by lane aging; advanced once per runtime
603    /// loop iteration, stays 0 under test drivers.
604    pub(crate) tide_tick: u64,
605    pub(crate) workspace_index_revision: u64,
606    #[cfg(feature = "salsa-style-diagnostics")]
607    pub(crate) style_workspace_snapshot_revision_hint: u64,
608    pub(crate) configuration_change_count: usize,
609    /// RFC 0009 Pillar A (rfcs#67, slice A-min): documents are `Arc` entries so a
610    /// query snapshot clones pointers instead of the corpus; mutation paths go
611    /// through `document_mut`/`insert_document`, which copy-on-write via
612    /// `Arc::make_mut`/`Arc::new` (a worker holding a snapshot of a document
613    /// forces at most a one-document deep clone on that document's next edit).
614    pub(crate) file_identity: LspFileIdentityInterner,
615    pub(crate) documents: BTreeMap<LspFileId, Arc<LspTextDocumentState>>,
616    pub(crate) open_document_uris: BTreeSet<LspFileId>,
617    pub(crate) workspace_runtime_registry: WorkspaceRuntimeRegistry,
618    pub(crate) tsgo_workspace_process_pool: TsgoWorkspaceProcessPoolV0,
619    pub(crate) watched_file_changes: Vec<LspWatchedFileChangeState>,
620    pub(crate) client_supports_work_done_progress: bool,
621    pub(crate) next_server_progress_request_id: u64,
622    pub(crate) pending_server_progress_request_tokens: BTreeMap<String, String>,
623    /// Shared (not per-state) since RFC 0009 Pillar A: the loop and dispatched
624    /// query snapshots reuse ONE memo slot so a substrate built on either side is
625    /// visible to both. The memo is self-validating by exact input compare, so
626    /// last-writer-wins is safe; lock only to compare and to store — never across
627    /// the substrate collection (see `cascade_narrowing_substrate_for_style_sources`).
628    pub(crate) cascade_narrowing_substrate_memo:
629        Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>>,
630    #[cfg(feature = "parallel-style-diagnostics")]
631    pub(crate) resolver_identity_index_memo: Arc<Mutex<Option<LspResolverIdentityIndexMemo>>>,
632    /// Shared into query snapshots (`Arc`) like the cascade memo: codeLens
633    /// resolves on the dispatched query lane, so the occurrence index a
634    /// worker builds must be visible to the loop and the next worker —
635    /// otherwise every dispatched codeLens rebuilds the workspace index.
636    /// Self-validating by document-key compare; last-writer-wins is safe.
637    pub(crate) workspace_occurrence_index_memo: Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>>,
638    /// documentColor cross-request cache, keyed by (document version, corpus
639    /// text mark, corpus set mark) — shared into query snapshots (`Arc`) so
640    /// dispatched requests hit it too.
641    pub(crate) document_color_cache: Arc<Mutex<LspDocumentColorCacheV0>>,
642    #[cfg(feature = "salsa-style-diagnostics")]
643    pub(crate) reverse_dependency_index_memo: RefCell<Option<LspReverseDependencyIndexMemo>>,
644    /// Module-interface projection of the LAST text the source fan-out saw,
645    /// per open style URI. A didChange whose projection compares equal is an
646    /// interface-preserving edit — no open source document's diagnostics can
647    /// move, so the fan-out is skipped from ONE single-file parse instead of
648    /// a workspace selector build. Evicted on didClose; loop-owned.
649    pub(crate) style_module_interface_memo:
650        RefCell<BTreeMap<String, omena_query::OmenaQueryModuleInterfaceProjectionV0>>,
651    pub(crate) source_type_fact_cache: BTreeMap<String, Vec<TsgoTypeFactResultEntryV0>>,
652    /// RFC 0009 Pillar C (rfcs#66): fail-soft write breaker for the disk
653    /// diagnostics shard cache. Interior mutability because the write-behind
654    /// runs on the immutable resolve path; owned by the single loop thread.
655    pub(crate) disk_diagnostics_cache_session: RefCell<DiskDiagnosticsCacheSessionV0>,
656    /// RFC 0009 Pillar B (rfcs#65): the long-lived salsa-memoized
657    /// style-diagnostics host. Owned by the loop thread; the host diff-syncs
658    /// its inputs on every resolve, so it never serves a stale revision.
659    #[cfg(feature = "salsa-style-diagnostics")]
660    pub(crate) style_memo_host: RefCell<Option<omena_query::OmenaQueryStyleMemoHostV0>>,
661}
662
663impl LspShellState {
664    pub fn document_count(&self) -> usize {
665        self.documents.len()
666    }
667
668    pub fn workspace_folder_count(&self) -> usize {
669        self.workspace_runtime_registry.len()
670    }
671
672    pub(crate) fn allocate_work_done_progress_request(&mut self) -> (String, String) {
673        self.next_server_progress_request_id += 1;
674        let id = format!(
675            "omena-work-done-progress-create-{}",
676            self.next_server_progress_request_id
677        );
678        let token = format!(
679            "omena-workspace-index-{}",
680            self.next_server_progress_request_id
681        );
682        self.pending_server_progress_request_tokens
683            .insert(id.clone(), token.clone());
684        (id, token)
685    }
686
687    pub(crate) fn take_server_progress_response(&mut self, id: &str) -> bool {
688        self.pending_server_progress_request_tokens
689            .remove(id)
690            .is_some()
691    }
692
693    pub fn document(&self, uri: &str) -> Option<&LspTextDocumentState> {
694        let file_id = self.file_identity.file_id_for_uri(uri)?;
695        self.documents.get(&file_id).map(Arc::as_ref)
696    }
697
698    #[cfg(feature = "test-support")]
699    pub fn evict_document_for_test(&mut self, uri: &str) -> Option<LspTextDocumentState> {
700        self.remove_document_uri(uri)
701    }
702
703    #[cfg(feature = "test-support")]
704    pub fn clear_workspace_occurrence_index_memo_for_test(&self) {
705        *self.workspace_occurrence_index_memo_lock() = None;
706    }
707
708    pub(crate) fn workspace_occurrence_index_memo_lock(
709        &self,
710    ) -> MutexGuard<'_, Option<LspWorkspaceOccurrenceIndexMemo>> {
711        self.workspace_occurrence_index_memo
712            .lock()
713            .unwrap_or_else(|error| error.into_inner())
714    }
715
716    pub(crate) fn document_mut(&mut self, uri: &str) -> Option<&mut LspTextDocumentState> {
717        let file_id = self.file_identity.file_id_for_uri(uri)?;
718        let document = self.documents.get_mut(&file_id)?;
719        if document.origin == LspDocumentOrigin::Foreign {
720            return None;
721        }
722        Some(Arc::make_mut(document))
723    }
724
725    #[cfg(test)]
726    pub(crate) fn document_storage_uri(uri: &str) -> String {
727        LspFileIdentityInterner::storage_uri(uri)
728    }
729
730    #[cfg(test)]
731    pub(crate) fn document_file_id(&self, uri: &str) -> Option<LspFileId> {
732        self.file_identity.file_id_for_uri(uri)
733    }
734
735    pub(crate) fn document_storage_uri_for_file_id(&self, file_id: LspFileId) -> Option<&str> {
736        self.file_identity.storage_uri_for_file_id(file_id)
737    }
738
739    pub(crate) fn document_for_file_id(&self, file_id: LspFileId) -> Option<&LspTextDocumentState> {
740        self.documents.get(&file_id).map(Arc::as_ref)
741    }
742
743    pub(crate) fn insert_open_document_uri(&mut self, uri: &str) -> String {
744        let (file_id, storage_uri) = self.file_identity.intern_uri(uri);
745        self.open_document_uris.insert(file_id);
746        storage_uri
747    }
748
749    pub(crate) fn remove_open_document_uri(&mut self, uri: &str) {
750        if let Some(file_id) = self.file_identity.file_id_for_uri(uri) {
751            self.open_document_uris.remove(&file_id);
752        }
753    }
754
755    pub(crate) fn has_open_document_uri(&self, uri: &str) -> bool {
756        self.file_identity
757            .file_id_for_uri(uri)
758            .is_some_and(|file_id| self.open_document_uris.contains(&file_id))
759    }
760
761    pub(crate) fn insert_document(&mut self, uri: &str, document: LspTextDocumentState) {
762        let (file_id, _) = self.file_identity.intern_uri(uri);
763        self.documents.insert(file_id, Arc::new(document));
764    }
765
766    pub(crate) fn remove_document_uri(&mut self, uri: &str) -> Option<LspTextDocumentState> {
767        let file_id = self.file_identity.file_id_for_uri(uri)?;
768        self.documents.remove(&file_id).map(Arc::unwrap_or_clone)
769    }
770
771    pub(crate) fn contains_document_uri(&self, uri: &str) -> bool {
772        self.document(uri).is_some()
773    }
774
775    pub fn workspace_folder(&self, uri: &str) -> Option<&LspWorkspaceFolderState> {
776        self.workspace_runtime_registry.get(uri)
777    }
778
779    pub fn snapshot(&self) -> LspShellStateSnapshot {
780        LspShellStateSnapshot {
781            shutdown_requested: self.shutdown_requested,
782            should_exit: self.should_exit,
783            features: self.features.clone(),
784            diagnostics: self.diagnostics.clone(),
785            resolution: self.resolution.clone(),
786            cancelled_request_count: self.cancelled_request_ids.len(),
787            suppressed_dispatched_result_count: self.in_flight_requests.suppressed_result_count(),
788            workspace_style_index_exhausted_count: self.workspace_style_index_exhausted_count,
789            workspace_index_pending_file_count: self.workspace_index_pending_file_count,
790            external_sif_lock_read_count: self.external_sif_lock_read_count,
791            external_sif_bridge_generation_count: self.external_sif_bridge_generation_count,
792            document_count: self.document_count(),
793            workspace_folder_count: self.workspace_folder_count(),
794            configuration_change_count: self.configuration_change_count,
795            watched_file_event_count: self.watched_file_changes.len(),
796            cached_workspace_resolution_input_count: self
797                .resolution
798                .workspace_style_resolution_inputs
799                .len(),
800            tide_epoch: self.tide_ledger.epoch(),
801            tide_sif_lane_generation: self.tide_sif_lane.generation(),
802            tide_sif_lane_in_flight: self.tide_sif_lane.in_flight(),
803            tide_sif_lane_has_demand: self.tide_sif_lane.has_demand(),
804            tide_republish_lane_generation: self.tide_republish_lane.generation(),
805            tide_republish_lane_in_flight: self.tide_republish_lane.in_flight(),
806            tide_republish_lane_has_demand: self.tide_republish_lane.has_demand(),
807            tide_starvation_alarm_count: self.tide_sif_lane.starvation_alarm_count()
808                + self.tide_republish_lane.starvation_alarm_count(),
809            tide_sif_lane_oldest_deposit_age_ticks: self
810                .tide_sif_lane
811                .oldest_deposit_age_ticks(self.tide_tick),
812            tide_republish_lane_oldest_deposit_age_ticks: self
813                .tide_republish_lane
814                .oldest_deposit_age_ticks(self.tide_tick),
815            documents: {
816                let mut documents = self
817                    .documents
818                    .values()
819                    .map(|document| (**document).clone())
820                    .collect::<Vec<_>>();
821                documents.sort_by(|left, right| left.uri.cmp(&right.uri));
822                documents
823            },
824            workspace_folders: self.workspace_runtime_registry.folder_snapshots(),
825            watched_file_changes: self.watched_file_changes.clone(),
826        }
827    }
828
829    #[cfg(feature = "parallel-style-diagnostics")]
830    pub(crate) fn resolver_identity_index_memo_lock(
831        &self,
832    ) -> MutexGuard<'_, Option<LspResolverIdentityIndexMemo>> {
833        self.resolver_identity_index_memo
834            .lock()
835            .unwrap_or_else(|error| error.into_inner())
836    }
837
838    /// Current republish-lane generation — the runtime loop compares queued
839    /// apply batches against it to drop disowned tides (rfcs#111 §9.4).
840    pub fn tide_republish_lane_generation(&self) -> u64 {
841        self.tide_republish_lane.generation()
842    }
843
844    /// Whether a republish tide is in flight — the runtime loop's pump must
845    /// keep this held until the stream's FINAL chunk drains (completing on a
846    /// momentarily-empty queue would disable disown/abort/carry-over).
847    pub fn tide_republish_lane_in_flight(&self) -> bool {
848        self.tide_republish_lane.in_flight()
849    }
850
851    /// Advance the Tide tick — called once per runtime loop iteration; the
852    /// tick feeds lane aging (courtesy-layer override, never correctness).
853    pub fn advance_tide_tick(&mut self) {
854        self.tide_tick = self.tide_tick.saturating_add(1);
855    }
856
857    /// Reopen the republish settle window: bump the lane generation (a
858    /// running tide is disowned) and publish it to the executor watch.
859    pub(crate) fn tide_reopen_republish_window(&mut self) {
860        let generation = self.tide_republish_lane.reopen_window();
861        self.tide_republish_gen_watch
862            .store(generation, std::sync::atomic::Ordering::Relaxed);
863    }
864
865    #[cfg(feature = "salsa-style-diagnostics")]
866    pub(crate) fn mark_style_workspace_snapshot_changed(
867        &mut self,
868    ) -> omena_query::IncrementalRevisionV0 {
869        let committed = self
870            .style_memo_host
871            .borrow()
872            .as_ref()
873            .map(|host| host.committed_revision().value)
874            .unwrap_or_default();
875        let next = self
876            .style_workspace_snapshot_revision_hint
877            .max(committed)
878            .saturating_add(1)
879            .max(1);
880        self.style_workspace_snapshot_revision_hint = next;
881        omena_query::IncrementalRevisionV0 { value: next }
882    }
883
884    /// Build the copy-on-write read model used by off-loop query work.
885    ///
886    /// The copied fields are the complete storage surface exposed through
887    /// [`LspQueryReadView`]. All remaining shell fields stay loop-owned and are
888    /// unreachable through that interface. Document values are shared through
889    /// `Arc`, while settings and workspace registries are copied at dispatch.
890    pub fn query_snapshot(&self) -> LspQuerySnapshotV0 {
891        LspQuerySnapshotV0 {
892            state: LspShellState {
893                features: self.features.clone(),
894                diagnostics: self.diagnostics.clone(),
895                resolution: self.resolution.clone(),
896                file_identity: self.file_identity.clone(),
897                documents: self.documents.clone(),
898                open_document_uris: self.open_document_uris.clone(),
899                workspace_runtime_registry: self.workspace_runtime_registry.clone(),
900                tide_ledger: self.tide_ledger.clone(),
901                #[cfg(feature = "salsa-style-diagnostics")]
902                style_workspace_snapshot_revision_hint: self.style_workspace_snapshot_revision_hint,
903                document_color_cache: Arc::clone(&self.document_color_cache),
904                cascade_narrowing_substrate_memo: Arc::clone(
905                    &self.cascade_narrowing_substrate_memo,
906                ),
907                workspace_occurrence_index_memo: Arc::clone(&self.workspace_occurrence_index_memo),
908                #[cfg(feature = "parallel-style-diagnostics")]
909                resolver_identity_index_memo: Arc::clone(&self.resolver_identity_index_memo),
910                ..LspShellState::default()
911            },
912        }
913    }
914}
915
916#[allow(private_interfaces)]
917impl LspQueryReadView for LspShellState {
918    fn query_features(&self) -> &LspFeatureSettings {
919        &self.features
920    }
921
922    fn query_diagnostics(&self) -> &LspDiagnosticSettings {
923        &self.diagnostics
924    }
925
926    fn query_resolution(&self) -> &LspResolutionSettings {
927        &self.resolution
928    }
929
930    fn query_file_identity(&self) -> &LspFileIdentityInterner {
931        &self.file_identity
932    }
933
934    fn query_documents(&self) -> &BTreeMap<LspFileId, Arc<LspTextDocumentState>> {
935        &self.documents
936    }
937
938    fn query_open_document_uris(&self) -> &BTreeSet<LspFileId> {
939        &self.open_document_uris
940    }
941
942    fn query_workspace_runtime_registry(&self) -> &WorkspaceRuntimeRegistry {
943        &self.workspace_runtime_registry
944    }
945
946    fn query_tide_ledger(&self) -> &crate::tide::TideEpochLedgerV0 {
947        &self.tide_ledger
948    }
949
950    #[cfg(feature = "salsa-style-diagnostics")]
951    fn query_style_workspace_snapshot_revision_hint(&self) -> u64 {
952        self.style_workspace_snapshot_revision_hint
953    }
954
955    fn query_document_color_cache(&self) -> &Arc<Mutex<LspDocumentColorCacheV0>> {
956        &self.document_color_cache
957    }
958
959    fn query_cascade_narrowing_substrate_memo(
960        &self,
961    ) -> &Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>> {
962        &self.cascade_narrowing_substrate_memo
963    }
964
965    fn query_workspace_occurrence_index_memo(
966        &self,
967    ) -> &Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>> {
968        &self.workspace_occurrence_index_memo
969    }
970
971    #[cfg(feature = "parallel-style-diagnostics")]
972    fn query_resolver_identity_index_memo(
973        &self,
974    ) -> &Arc<Mutex<Option<LspResolverIdentityIndexMemo>>> {
975        &self.resolver_identity_index_memo
976    }
977}
978
979/// Copy-on-write read model for dispatched queries and deferred read workers.
980///
981/// The partial shell remains a private storage detail. Consumers compile only
982/// against [`LspQueryReadView`], which prevents access to default-filled
983/// loop-owned machinery such as process pools, schedulers, cache breakers, and
984/// mutation-side memo hosts.
985#[derive(Debug)]
986pub struct LspQuerySnapshotV0 {
987    pub(crate) state: LspShellState,
988}
989
990impl LspQuerySnapshotV0 {
991    #[cfg(test)]
992    pub(crate) fn shell_state_for_test(&self) -> &LspShellState {
993        &self.state
994    }
995}
996
997#[allow(private_interfaces)]
998impl LspQueryReadView for LspQuerySnapshotV0 {
999    fn query_features(&self) -> &LspFeatureSettings {
1000        &self.state.features
1001    }
1002
1003    fn query_diagnostics(&self) -> &LspDiagnosticSettings {
1004        &self.state.diagnostics
1005    }
1006
1007    fn query_resolution(&self) -> &LspResolutionSettings {
1008        &self.state.resolution
1009    }
1010
1011    fn query_file_identity(&self) -> &LspFileIdentityInterner {
1012        &self.state.file_identity
1013    }
1014
1015    fn query_documents(&self) -> &BTreeMap<LspFileId, Arc<LspTextDocumentState>> {
1016        &self.state.documents
1017    }
1018
1019    fn query_open_document_uris(&self) -> &BTreeSet<LspFileId> {
1020        &self.state.open_document_uris
1021    }
1022
1023    fn query_workspace_runtime_registry(&self) -> &WorkspaceRuntimeRegistry {
1024        &self.state.workspace_runtime_registry
1025    }
1026
1027    fn query_tide_ledger(&self) -> &crate::tide::TideEpochLedgerV0 {
1028        &self.state.tide_ledger
1029    }
1030
1031    #[cfg(feature = "salsa-style-diagnostics")]
1032    fn query_style_workspace_snapshot_revision_hint(&self) -> u64 {
1033        self.state.style_workspace_snapshot_revision_hint
1034    }
1035
1036    fn query_document_color_cache(&self) -> &Arc<Mutex<LspDocumentColorCacheV0>> {
1037        &self.state.document_color_cache
1038    }
1039
1040    fn query_cascade_narrowing_substrate_memo(
1041        &self,
1042    ) -> &Arc<Mutex<Option<LspCascadeNarrowingSubstrateMemo>>> {
1043        &self.state.cascade_narrowing_substrate_memo
1044    }
1045
1046    fn query_workspace_occurrence_index_memo(
1047        &self,
1048    ) -> &Arc<Mutex<Option<LspWorkspaceOccurrenceIndexMemo>>> {
1049        &self.state.workspace_occurrence_index_memo
1050    }
1051
1052    #[cfg(feature = "parallel-style-diagnostics")]
1053    fn query_resolver_identity_index_memo(
1054        &self,
1055    ) -> &Arc<Mutex<Option<LspResolverIdentityIndexMemo>>> {
1056        &self.state.resolver_identity_index_memo
1057    }
1058}
1059
1060// The dispatched query lane moves snapshots onto the worker thread; keep that
1061// property checked at compile time independent of the worker code shape.
1062const _: () = {
1063    const fn assert_send<T: Send>() {}
1064    assert_send::<LspQuerySnapshotV0>();
1065};