Skip to main content

omena_lsp_server/
lib.rs

1mod boundary;
2mod code_actions;
3mod color_provider;
4#[cfg(feature = "salsa-style-diagnostics")]
5mod deferred_notification;
6mod diagnostics_follow_up;
7mod diagnostics_scheduler;
8mod disk_cache;
9mod document_events;
10mod document_links;
11mod document_refresh;
12mod document_state;
13mod engine_input_params;
14mod explain;
15mod external_sif_loader;
16mod external_sif_symbols;
17mod foreign_style_identity;
18mod frame_aware_refresh;
19mod lsp_output;
20mod message_loop;
21mod occurrence_mapping;
22mod open_document_inputs;
23#[cfg(feature = "parallel-style-diagnostics")]
24mod parallel_style_wave;
25mod protocol;
26mod provider_tier_feedback;
27mod query_adapter;
28mod query_reuse;
29mod sdk_workflow;
30mod settings;
31mod source_completion;
32mod source_diagnostics;
33mod source_document_cache;
34mod source_domain_hover;
35mod source_occurrence_cache;
36mod source_selector_provider;
37mod source_syntax_index;
38mod source_type_fact_cache;
39mod source_type_facts;
40mod state;
41mod streaming_ifds_diagnostics;
42mod style_diagnostics;
43mod style_diagnostics_snapshot;
44mod style_hover_markdown;
45mod style_symbol_monikers;
46mod style_symbol_occurrence_cache;
47mod style_symbol_provider;
48pub mod tide;
49#[cfg(feature = "parallel-style-diagnostics")]
50mod tide_republish;
51mod workspace_index;
52mod workspace_occurrence_cache;
53mod workspace_occurrences;
54mod workspace_resolution;
55mod workspace_runtime_registry;
56mod workspace_symbols;
57
58pub use boundary::*;
59#[cfg(feature = "salsa-style-diagnostics")]
60pub use deferred_notification::{
61    resolve_deferred_diagnostics_notification,
62    resolve_deferred_diagnostics_notification_with_reverse_refresh,
63};
64pub use diagnostics_follow_up::*;
65pub(crate) use document_events::{
66    did_change_text_document, did_change_watched_files, did_change_workspace_folders,
67    did_close_text_document, did_open_text_document,
68};
69pub(crate) use document_refresh::{
70    StyleExternalDependencySnapshot, admit_foreign_style_dependencies_for_indexed_style_documents,
71    admit_foreign_style_dependencies_for_style_uri,
72    admit_foreign_style_dependencies_for_style_uris, ensure_style_document_loaded_from_disk,
73    is_resolution_config_document_uri, refresh_source_indexes_for_resolution_config_change,
74    refresh_source_indexes_for_resolution_settings_change,
75    refresh_source_indexes_for_style_document_change,
76    refresh_style_external_inputs_after_document_removal,
77    refresh_style_external_inputs_for_document_event, reload_indexed_source_document_from_disk,
78    reload_indexed_style_document_from_disk, style_external_dependency_snapshot,
79    summarize_style_document,
80};
81pub(crate) use document_state::{
82    lsp_text_document_state, lsp_text_document_state_with_source_syntax_index,
83};
84use engine_input_params::query_engine_input_from_params;
85pub(crate) use explain::{
86    EXPLAIN_REQUEST, project_hover_trace_through_explain_egress, resolve_lsp_explain,
87};
88pub use external_sif_loader::{
89    LspExternalSifRefreshJobV0, LspExternalSifRefreshResultV0,
90    apply_deferred_external_sif_refresh_result, collect_deferred_external_sif_refresh,
91    enable_deferred_external_sif_refresh, prepare_deferred_external_sif_refresh_job,
92};
93pub(crate) use external_sif_loader::{
94    bridge_sources_for_style_uris, refresh_external_sifs_for_bridge_source_delta,
95    refresh_external_sifs_for_state,
96};
97use external_sif_symbols::external_sif_sass_symbol_definition_location;
98pub(crate) use external_sif_symbols::{
99    ExternalSifSassSymbolTarget, external_sif_sass_symbol_target_for_candidate,
100};
101pub use frame_aware_refresh::*;
102pub use lsp_output::*;
103#[cfg(test)]
104pub(crate) use message_loop::current_time_millis;
105pub use message_loop::{
106    HOVER_SUBSTRATE_WARMUP_METHOD, LspLoopTurnV0, LspQueryDispatchV0,
107    complete_dispatched_query_response, dispatched_query_internal_error_response,
108    dispatched_query_is_heavy, handle_lsp_message, handle_lsp_message_outputs,
109    handle_lsp_message_scheduled_outputs, handle_lsp_message_scheduled_outputs_or_dispatch,
110    hover_substrate_warmup_dispatch, resolve_dispatched_query_response,
111    workspace_index_progress_end_output,
112};
113#[cfg(feature = "salsa-style-diagnostics")]
114use omena_query::summarize_omena_query_target_unresolved_sass_import_diagnostics_for_workspace_paths;
115use omena_query::{
116    OmenaParserStyleDialect, OmenaQueryCompletionCandidateV0, OmenaQueryCompletionItemV0,
117    OmenaQueryStyleDiagnosticV0, OmenaWorkspaceOccurrenceFamilyV0, OmenaWorkspaceOccurrenceIndexV0,
118    OmenaWorkspaceOccurrenceRoleV0, OmenaWorkspaceOccurrenceV0, ParserPositionV0,
119    is_omena_query_sass_symbol_candidate_kind as is_sass_symbol_candidate_kind,
120    is_omena_query_sass_symbol_declaration_kind as is_sass_symbol_declaration_kind,
121    is_omena_query_sass_symbol_reference_kind as is_sass_symbol_reference_kind,
122    occurrences_for_monikers,
123    omena_query_sass_symbol_kind_from_candidate_kind as sass_symbol_kind_from_candidate_kind,
124    read_omena_query_cascade_at_position_with_categorical_evidence,
125    read_omena_query_style_context_index, resolve_omena_query_sass_forward_sources,
126    resolve_omena_query_sass_module_use_sources_for_candidate,
127    resolve_omena_query_sass_symbol_declarations,
128    resolve_omena_query_source_candidate_selector_names,
129    resolve_omena_query_style_uri_for_specifier_with_resolution_inputs,
130    summarize_omena_query_omena_parser_style_facts,
131    summarize_omena_query_refs_for_class_from_occurrence_index,
132    summarize_omena_query_rename_plan_from_occurrence_index,
133    summarize_omena_query_source_completion_at_position,
134    summarize_omena_query_style_completion_candidate_documentation,
135    summarize_omena_query_style_completion_candidate_documentation_for_workspace_file_with_substrate,
136    summarize_omena_query_style_completion_for_workspace_file_with_substrate,
137    summarize_omena_query_style_diagnostics_for_file,
138    summarize_omena_query_style_diagnostics_for_file_with_deep_analysis,
139    summarize_omena_query_style_hover_render_parts_for_hover_position,
140    summarize_omena_query_style_hover_render_parts_for_workspace_file_hover_position_with_substrate,
141};
142#[cfg(not(feature = "salsa-style-diagnostics"))]
143use omena_query::{
144    OmenaQueryExternalModuleModeV0,
145    summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs_and_resolution_inputs,
146};
147#[cfg(test)]
148pub(crate) use omena_query::{
149    OmenaQueryExternalSifInputV0, OmenaQuerySourceImportedStyleBindingV0 as ImportedStyleBinding,
150    OmenaQuerySourceSelectorReferenceFactV0 as SourceSelectorReferenceFact,
151    OmenaQuerySourceSelectorReferenceMatchKindV0 as SourceSelectorReferenceMatchKind,
152    OmenaQuerySourceSyntaxIndexV0 as SourceSyntaxIndex, ParserByteSpanV0,
153};
154#[cfg(test)]
155pub(crate) use omena_tsgo_client::{TsgoResolvedTypeV0, TsgoTypeFactResultEntryV0};
156#[cfg(feature = "salsa-style-diagnostics")]
157pub(crate) use open_document_inputs::style_path_inputs_from_open_documents;
158pub(crate) use open_document_inputs::{
159    source_documents_from_open_documents, style_sources_for_hover_render,
160    style_sources_from_open_documents,
161};
162use protocol::*;
163use provider_tier_feedback::{
164    attach_provider_tier_feedback, current_provider_tier_feedback_data,
165    provider_tier_feedback_for_hover_definitions,
166};
167use query_adapter::*;
168use query_reuse::{
169    cascade_narrowing_substrate_for_style_sources, effective_style_package_manifests,
170    refresh_document_reusable_indexes,
171};
172use serde_json::{Value, json};
173pub(crate) use settings::{
174    apply_diagnostic_settings, apply_feature_settings, apply_resolution_settings,
175};
176use source_completion::{
177    source_completion_context_at_position, source_domain_option_completion_items,
178};
179#[cfg(feature = "salsa-style-diagnostics")]
180pub(crate) use source_diagnostics::finish_source_diagnostics_value;
181pub(crate) use source_diagnostics::{
182    prepare_deferred_source_diagnostics_for_uri, resolve_source_diagnostics_for_uri,
183};
184use source_domain_hover::{
185    source_domain_reference_hover_at_position, source_domain_reference_trace_at_position,
186};
187use source_occurrence_cache::store_source_selector_occurrence_sidecar;
188pub(crate) use source_selector_provider::{
189    collect_source_selector_reference_candidates, document_has_style_index,
190    first_style_document_for_workspace, resolve_source_provider_candidates,
191    source_selector_candidate_at_position, source_selector_candidate_for_params,
192    source_selector_candidates_at_position, style_selector_definitions_for_source_candidates,
193    style_selector_definitions_from_open_documents, style_selector_definitions_from_uri,
194};
195pub(crate) use source_syntax_index::{
196    build_source_syntax_index, collect_source_imports, source_selector_candidates_from_index,
197};
198#[cfg(test)]
199pub(crate) use source_type_facts::apply_source_type_fact_results_to_document;
200pub(crate) use source_type_facts::refresh_source_type_fact_candidates_for_document;
201pub use state::*;
202use std::{collections::BTreeSet, fs, sync::Arc};
203use streaming_ifds_diagnostics::summarize_cross_file_streaming_reachability_diagnostics_for_lsp;
204#[cfg(feature = "salsa-style-diagnostics")]
205pub(crate) use style_diagnostics_snapshot::LspStyleDiagnosticsRenderInputsV0;
206
207/// Apply an off-loop reverse-dependency refresh (produced by a worker's
208/// selector build, delivered through the completion channel) to the loop
209/// state's memo. The straight-line build has no memo; the refresh is a
210/// no-op there.
211pub fn apply_reverse_dependency_refresh(
212    state: &LspShellState,
213    refresh: &lsp_output::LspReverseDependencyRefreshV0,
214) {
215    #[cfg(feature = "salsa-style-diagnostics")]
216    diagnostics_scheduler::refresh_reverse_dependency_index_memo(
217        state,
218        refresh.revision,
219        &refresh.summary,
220        refresh.ledger_epoch,
221    );
222    #[cfg(not(feature = "salsa-style-diagnostics"))]
223    {
224        let _ = (state, refresh);
225    }
226}
227#[cfg(test)]
228pub(crate) use style_diagnostics::resolve_style_diagnostics_for_uri;
229pub(crate) use style_diagnostics::{
230    lsp_diagnostic_severity, prepare_deferred_style_diagnostics_for_uri,
231    resolve_document_diagnostics_for_uri, resolve_style_diagnostics,
232};
233use style_symbol_monikers::render_external_sif_sass_symbol_hover_markdown;
234pub(crate) use style_symbol_occurrence_cache::store_style_symbol_occurrence_sidecar;
235pub(crate) use style_symbol_provider::{
236    external_document_uri_for_query_uri, reference_lens_title,
237    render_style_hover_candidate_markdown_for_workspace, resolve_selector_rename,
238    resolve_style_symbol_rename, sass_forward_edges_for_document,
239    sass_symbol_definitions_for_candidate,
240    selector_reference_locations_by_name_from_open_documents,
241    selector_reference_locations_from_open_documents, source_candidate_selector_names,
242    style_symbol_definition_locations_from_documents,
243    style_symbol_reference_locations_from_documents,
244    style_symbol_workspace_occurrences_for_document, unapply_sass_forward_prefix,
245};
246#[cfg(feature = "parallel-style-diagnostics")]
247pub use tide_republish::{
248    TideWorkspaceRepublishItemV0, TideWorkspaceRepublishJobV0, TideWorkspaceRepublishResultV0,
249    apply_tide_workspace_republish_item, collect_tide_workspace_republish_streaming,
250    complete_tide_workspace_republish, prepare_tide_workspace_republish_job,
251};
252pub(crate) use workspace_index::index_workspace_style_files;
253pub(crate) use workspace_index::workspace_index_language_id_for_uri;
254pub use workspace_index::{
255    LspWorkspaceIndexJobV0, LspWorkspaceIndexResultV0, apply_background_workspace_index_result,
256    collect_background_workspace_index, prepare_background_workspace_index_continuation_job,
257    prepare_background_workspace_index_job,
258};
259#[cfg(test)]
260pub(crate) use workspace_index::{
261    WorkspaceStyleIndexBudget, index_workspace_style_files_with_budget,
262};
263#[cfg(test)]
264pub(crate) use workspace_resolution::load_lsp_workspace_style_resolution_inputs;
265pub(crate) use workspace_resolution::{
266    initialize_workspace_folders, insert_workspace_folder, refresh_document_workspace_owners,
267    refresh_workspace_resolution_inputs, refresh_workspace_resolution_inputs_for_uri,
268    resolution_inputs_for_workspace_uri, resolve_workspace_folder_uri,
269};
270
271pub const NODE_TEXT_DOCUMENT_SYNC_KIND: u8 = 2;
272
273pub fn omena_loop_trace_enabled() -> bool {
274    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
275    *ENABLED.get_or_init(|| std::env::var_os("OMENA_LOOP_TRACE").is_some())
276}
277
278#[macro_export]
279macro_rules! loop_trace {
280    ($($arg:tt)*) => {
281        if $crate::omena_loop_trace_enabled() {
282            eprintln!("[LOOPTRACE {:>10.3}] {}",
283                std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()%100000.0).unwrap_or(0.0),
284                format!($($arg)*));
285        }
286    };
287}
288
289pub const DEBUG_STATE_REQUEST: &str = "omena/rustLspState";
290pub const RUNTIME_LOOP_PROBE_REQUEST: &str = "omena/runtimeLoopProbe";
291pub const STYLE_HOVER_CANDIDATES_REQUEST: &str = "omena/rustStyleHoverCandidates";
292pub const STYLE_DIAGNOSTICS_REQUEST: &str = "omena/rustStyleDiagnostics";
293pub const SOURCE_DIAGNOSTICS_REQUEST: &str = "omena/rustSourceDiagnostics";
294pub const CASCADE_AT_POSITION_REQUEST: &str = "omena/rustCascadeAtPosition";
295pub const STYLE_CONTEXT_INDEX_REQUEST: &str = "omena/rustStyleContextIndex";
296pub const EXPLAIN_HOVER_TRACE_REQUEST: &str = "omena/explainHoverTrace";
297pub const SDK_WORKFLOW_REQUEST: &str = "omena/sdkWorkflow";
298const CANCEL_REQUEST_METHOD: &str = "$/cancelRequest";
299const REQUEST_CANCELLED_ERROR_CODE: i32 = -32800;
300// Cascade docs cost a whole-corpus narrowing analysis per completion item; only the
301// top-ranked items an editor list actually shows get them, so completion latency
302// stays independent of the workspace selector count.
303const SOURCE_COMPLETION_DOCUMENTATION_BUDGET: usize = 12;
304
305pub(crate) use code_actions::resolve_lsp_code_actions;
306
307#[cfg(feature = "test-support")]
308pub mod test_support {
309    use std::path::Path;
310
311    pub fn file_uri_equivalent(left: &str, right: &str) -> bool {
312        crate::protocol::file_uri_equivalent(left, right)
313    }
314
315    pub fn path_to_file_uri(path: &Path) -> String {
316        crate::protocol::path_to_file_uri(path)
317    }
318}
319
320pub fn resolve_style_hover_candidates(
321    state: &dyn LspQueryReadView,
322    params: Option<&Value>,
323) -> LspStyleHoverCandidatesResult {
324    let document_uri = document_uri_from_params(params);
325    let query_position = lsp_position_from_params(params);
326    let Some(document) = state.document(&document_uri) else {
327        return empty_style_hover_candidates_result(document_uri, None, query_position);
328    };
329
330    let Some((language, mut candidates)) = style_hover_candidates_for_document(document) else {
331        return empty_style_hover_candidates_result(
332            document_uri,
333            document.workspace_folder_uri.clone(),
334            query_position,
335        );
336    };
337
338    if let Some(position) = query_position {
339        candidates.retain(|candidate| parser_range_contains_position(&candidate.range, position));
340    }
341
342    LspStyleHoverCandidatesResult {
343        schema_version: "0",
344        product: "omena-lsp-server.style-hover-candidates",
345        document_uri,
346        workspace_folder_uri: document.workspace_folder_uri.clone(),
347        language: Some(language),
348        query_position,
349        candidate_count: candidates.len(),
350        candidates,
351    }
352}
353
354fn style_hover_candidates_for_document(
355    document: &LspTextDocumentState,
356) -> Option<(&'static str, Vec<LspStyleHoverCandidate>)> {
357    let summary = document.style_summary.as_ref()?;
358    Some((summary.language, document.style_candidates.clone()))
359}
360
361fn style_text_for_uri(state: &dyn LspQueryReadView, uri: &str) -> Option<String> {
362    state
363        .document(uri)
364        .map(|document| document.text.clone())
365        .or_else(|| fs::read_to_string(file_uri_to_path(uri)?).ok())
366}
367
368fn style_hover_candidates_for_uri(
369    state: &dyn LspQueryReadView,
370    uri: &str,
371) -> Option<(&'static str, Vec<LspStyleHoverCandidate>)> {
372    if let Some(document) = state.document(uri) {
373        return style_hover_candidates_for_document(document);
374    }
375    let text = style_text_for_uri(state, uri)?;
376    collect_style_hover_candidates(uri, text.as_str())
377}
378
379fn resolve_lsp_definition(state: &dyn LspQueryReadView, params: Option<&Value>) -> Value {
380    let document_uri = document_uri_from_params(params);
381    let Some(position) = lsp_position_from_params(params) else {
382        return Value::Null;
383    };
384    let Some(document) = state.document(&document_uri) else {
385        return Value::Null;
386    };
387    if !is_style_document_uri(document.uri.as_str()) {
388        return resolve_source_lsp_definition(state, document, position);
389    }
390
391    let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
392        return Value::Null;
393    };
394    let Some(candidate) = candidates
395        .iter()
396        .find(|candidate| parser_range_contains_position(&candidate.range, position))
397    else {
398        return Value::Null;
399    };
400    if is_sass_symbol_reference_kind(candidate.kind) {
401        let definitions = sass_symbol_definitions_for_candidate(state, document, candidate);
402        if !definitions.is_empty() {
403            return json!(
404                definitions
405                    .into_iter()
406                    .map(|(uri, definition)| json!({ "uri": uri, "range": definition.range }))
407                    .collect::<Vec<_>>()
408            );
409        }
410        if let Some(location) =
411            external_sif_sass_symbol_definition_location(state, document, candidate)
412        {
413            return json!([location]);
414        }
415        return Value::Null;
416    }
417    if candidate.kind == "customPropertyReference" {
418        let definitions =
419            style_symbol_definition_locations_from_documents(state, document, candidate);
420        if !definitions.is_empty() {
421            return json!(definitions);
422        }
423    }
424
425    let target = candidate;
426
427    json!([
428        {
429            "uri": document.uri.as_str(),
430            "range": target.range,
431        },
432    ])
433}
434
435fn resolve_lsp_references(state: &LspShellState, params: Option<&Value>) -> Value {
436    let document_uri = document_uri_from_params(params);
437    let Some(position) = lsp_position_from_params(params) else {
438        return Value::Null;
439    };
440    let Some(document) = state.document(&document_uri) else {
441        return Value::Null;
442    };
443    if !is_style_document_uri(document.uri.as_str()) {
444        return resolve_source_lsp_references(state, document, position, params);
445    }
446
447    let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
448        return Value::Null;
449    };
450    let Some(candidate) = candidates
451        .iter()
452        .find(|candidate| parser_range_contains_position(&candidate.range, position))
453    else {
454        return Value::Null;
455    };
456    let include_declaration = include_declaration_from_params(params);
457    let mut locations: Vec<Value> = if candidate.kind.starts_with("customProperty")
458        || is_sass_symbol_candidate_kind(candidate.kind)
459    {
460        style_symbol_reference_locations_from_documents(
461            state,
462            document,
463            candidate,
464            include_declaration,
465        )
466    } else if candidate.kind == "selector" {
467        let mut locations = if include_declaration {
468            vec![json!({ "uri": document.uri.as_str(), "range": candidate.range })]
469        } else {
470            Vec::new()
471        };
472        locations.extend(selector_reference_locations_from_open_documents(
473            state,
474            candidate.name.as_str(),
475            document.workspace_folder_uri.as_deref(),
476            Some(document.uri.as_str()),
477        ));
478        locations
479    } else if include_declaration {
480        vec![json!({ "uri": document.uri.as_str(), "range": candidate.range })]
481    } else {
482        Vec::new()
483    };
484
485    locations.sort_by_key(|location| {
486        let line = location
487            .pointer("/range/start/line")
488            .and_then(Value::as_u64)
489            .unwrap_or_default();
490        let character = location
491            .pointer("/range/start/character")
492            .and_then(Value::as_u64)
493            .unwrap_or_default();
494        (line, character)
495    });
496    json!(locations)
497}
498
499fn resolve_lsp_completion(state: &LspShellState, params: Option<&Value>) -> Value {
500    let document_uri = document_uri_from_params(params);
501    let Some(document) = state.document(&document_uri) else {
502        return Value::Null;
503    };
504    if !is_style_document_uri(document.uri.as_str()) {
505        return resolve_source_lsp_completion(state, document, params);
506    }
507
508    let Some(position) = lsp_position_from_params(params) else {
509        return Value::Null;
510    };
511    let style_sources = style_sources_from_open_documents(
512        state,
513        document.workspace_folder_uri.as_deref(),
514        Some(document.uri.as_str()),
515    );
516    let resolution_inputs =
517        resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref());
518    let package_manifests = effective_style_package_manifests(state, &resolution_inputs);
519    let narrowing_substrate = cascade_narrowing_substrate_for_style_sources(
520        state,
521        style_sources.as_slice(),
522        &resolution_inputs,
523    );
524    let completion = summarize_omena_query_style_completion_for_workspace_file_with_substrate(
525        document.uri.as_str(),
526        style_sources.as_slice(),
527        package_manifests.as_slice(),
528        state.resolution.external_sifs.as_slice(),
529        &resolution_inputs,
530        &narrowing_substrate,
531        position,
532    );
533    let provider_feedback =
534        current_provider_tier_feedback_data(document, "textDocument/completion");
535    let items: Vec<Value> = completion
536        .items
537        .into_iter()
538        .map(|item| {
539            lsp_completion_item_from_query(completion.file_kind, item, provider_feedback.as_ref())
540        })
541        .collect();
542
543    json!({
544        "isIncomplete": false,
545        "items": items,
546    })
547}
548
549fn lsp_completion_item_from_query(
550    file_kind: &str,
551    item: OmenaQueryCompletionItemV0,
552    provider_feedback: Option<&Value>,
553) -> Value {
554    let kind = match (file_kind, item.item_kind) {
555        ("style", "cssModuleSelector") => 7,
556        (_, "cssModuleSelector") | (_, "cssCustomProperty") => 10,
557        _ => 1,
558    };
559    let mut completion_item = json!({
560        "label": item.label,
561        "kind": kind,
562        "sortText": item.sort_text,
563        "detail": item.detail,
564        "insertText": item.insert_text,
565        "data": {
566            "source": item.source,
567            "rankingSource": item.ranking_source,
568        },
569    });
570    if let Some(documentation) = item.documentation {
571        completion_item["documentation"] = json!({
572            "kind": "markdown",
573            "value": documentation,
574        });
575    }
576    attach_provider_tier_feedback(&mut completion_item, provider_feedback);
577    completion_item
578}
579
580fn resolve_source_diagnostics(state: &LspShellState, params: Option<&Value>) -> Value {
581    let document_uri = document_uri_from_params(params);
582    resolve_source_diagnostics_for_uri(state, document_uri.as_str())
583}
584
585fn resolve_cascade_at_position(state: &LspShellState, params: Option<&Value>) -> Value {
586    let document_uri = document_uri_from_params(params);
587    let Some(position) = lsp_position_from_params(params) else {
588        return Value::Null;
589    };
590    let Some(document) = state.document(&document_uri) else {
591        return Value::Null;
592    };
593    if !is_style_document_uri(document.uri.as_str()) {
594        return Value::Null;
595    }
596    let Some(engine_input) = query_engine_input_from_params(params) else {
597        return Value::Null;
598    };
599
600    let include_categorical_evidence = params
601        .and_then(|value| value.get("context"))
602        .and_then(|value| value.get("includeCategoricalEvidence"))
603        .and_then(Value::as_bool)
604        .unwrap_or(false);
605
606    read_omena_query_cascade_at_position_with_categorical_evidence(
607        document.uri.as_str(),
608        document.text.as_str(),
609        &engine_input,
610        position,
611        include_categorical_evidence,
612    )
613    .map(|result| json!(result))
614    .unwrap_or(Value::Null)
615}
616
617pub(crate) fn resolve_style_context_index(state: &LspShellState, params: Option<&Value>) -> Value {
618    let document_uri = document_uri_from_params(params);
619    let Some(document) = state.document(&document_uri) else {
620        return Value::Null;
621    };
622    if !is_style_document_uri(document.uri.as_str()) {
623        return Value::Null;
624    }
625    let Some(engine_input) = query_engine_input_from_params(params) else {
626        return Value::Null;
627    };
628
629    read_omena_query_style_context_index(
630        document.uri.as_str(),
631        document.text.as_str(),
632        &engine_input,
633    )
634    .map(|result| json!(result))
635    .unwrap_or(Value::Null)
636}
637
638fn resolve_lsp_code_lens(state: &dyn LspQueryReadView, params: Option<&Value>) -> Value {
639    let document_uri = document_uri_from_params(params);
640    let Some(document) = state.document(document_uri.as_str()) else {
641        return Value::Null;
642    };
643    let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
644        return Value::Null;
645    };
646
647    let mut lenses = Vec::new();
648    let mut emitted_selectors = BTreeSet::new();
649    let reference_locations_by_name = selector_reference_locations_by_name_from_open_documents(
650        state,
651        document.workspace_folder_uri.as_deref(),
652        Some(document.uri.as_str()),
653    );
654    for candidate in candidates
655        .iter()
656        .filter(|candidate| candidate.kind == "selector")
657    {
658        if !emitted_selectors.insert(candidate.name.as_str()) {
659            continue;
660        }
661        let locations = reference_locations_by_name
662            .get(candidate.name.as_str())
663            .cloned()
664            .unwrap_or_default();
665        if locations.is_empty() {
666            continue;
667        }
668        let position = candidate.range.start;
669        lenses.push(json!({
670            "range": {
671                "start": position,
672                "end": position,
673            },
674            "command": {
675                "title": reference_lens_title(locations.len()),
676                "command": "editor.action.showReferences",
677                "arguments": [
678                    document.uri.as_str(),
679                    position,
680                    locations,
681                ],
682            },
683        }));
684    }
685    lenses.sort_by_key(lsp_range_start_sort_key);
686
687    if lenses.is_empty() {
688        Value::Null
689    } else {
690        json!(lenses)
691    }
692}
693
694pub(crate) fn query_style_dialect_for_uri(uri: &str) -> OmenaParserStyleDialect {
695    let lower = uri.to_ascii_lowercase();
696    if lower.ends_with(".sass") || lower.ends_with(".sass?module") {
697        OmenaParserStyleDialect::Sass
698    } else if lower.ends_with(".scss") || lower.ends_with(".module.scss") {
699        OmenaParserStyleDialect::Scss
700    } else if lower.ends_with(".less") || lower.ends_with(".module.less") {
701        OmenaParserStyleDialect::Less
702    } else {
703        OmenaParserStyleDialect::Css
704    }
705}
706
707pub(crate) fn resolve_lsp_style_uri_for_specifier(
708    state: &dyn LspQueryReadView,
709    document: &LspTextDocumentState,
710    specifier: &str,
711) -> Option<String> {
712    let resolution_inputs =
713        resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref());
714    resolve_omena_query_style_uri_for_specifier_with_resolution_inputs(
715        document.uri.as_str(),
716        document.workspace_folder_uri.as_deref(),
717        specifier,
718        &resolution_inputs,
719    )
720}
721
722fn resolve_lsp_prepare_rename(state: &LspShellState, params: Option<&Value>) -> Value {
723    if let Some((_, candidate)) = source_selector_candidate_for_params(state, params) {
724        return json!({
725            "range": candidate.range,
726            "placeholder": candidate.name,
727        });
728    }
729
730    let Some((_, candidate, _)) = style_candidates_for_params(state, params) else {
731        return Value::Null;
732    };
733
734    json!({
735        "range": candidate.range,
736        "placeholder": rename_placeholder(&candidate),
737    })
738}
739
740fn resolve_lsp_rename(state: &LspShellState, params: Option<&Value>) -> Value {
741    let Some(new_name) = params
742        .and_then(|value| value.get("newName"))
743        .and_then(Value::as_str)
744        .filter(|value| !value.is_empty())
745    else {
746        return Value::Null;
747    };
748    if let Some((document_uri, candidate)) = source_selector_candidate_for_params(state, params) {
749        let workspace_folder_uri = state
750            .document(document_uri.as_str())
751            .and_then(|document| document.workspace_folder_uri.as_deref());
752        return resolve_selector_rename(
753            state,
754            workspace_folder_uri,
755            candidate.target_style_uri.as_deref(),
756            candidate.name.as_str(),
757            new_name,
758        );
759    }
760
761    let Some((document_uri, candidate, _candidates)) = style_candidates_for_params(state, params)
762    else {
763        return Value::Null;
764    };
765
766    if candidate.kind == "selector" {
767        let workspace_folder_uri = state
768            .document(document_uri.as_str())
769            .and_then(|document| document.workspace_folder_uri.as_deref());
770        return resolve_selector_rename(
771            state,
772            workspace_folder_uri,
773            Some(document_uri.as_str()),
774            candidate.name.as_str(),
775            new_name,
776        );
777    }
778
779    if candidate.kind.starts_with("customProperty") || is_sass_symbol_candidate_kind(candidate.kind)
780    {
781        let Some(document) = state.document(document_uri.as_str()) else {
782            return Value::Null;
783        };
784        return resolve_style_symbol_rename(state, document, &candidate, new_name);
785    }
786
787    Value::Null
788}
789
790fn style_candidates_for_params(
791    state: &LspShellState,
792    params: Option<&Value>,
793) -> Option<(String, LspStyleHoverCandidate, Vec<LspStyleHoverCandidate>)> {
794    let document_uri = document_uri_from_params(params);
795    let position = lsp_position_from_params(params)?;
796    let document = state.document(document_uri.as_str())?;
797    let (_, candidates) = style_hover_candidates_for_document(document)?;
798    let candidate = candidates
799        .iter()
800        .find(|candidate| parser_range_contains_position(&candidate.range, position))?
801        .clone();
802    Some((document_uri, candidate, candidates))
803}
804
805fn rename_placeholder(candidate: &LspStyleHoverCandidate) -> &str {
806    candidate.name.as_str()
807}
808
809fn resolve_lsp_hover(state: &dyn LspQueryReadView, params: Option<&Value>) -> Value {
810    let document_uri = document_uri_from_params(params);
811    if let Some(document) = state.document(document_uri.as_str())
812        && !is_style_document_uri(document.uri.as_str())
813    {
814        return resolve_source_lsp_hover(state, document, params);
815    }
816
817    let candidates = resolve_style_hover_candidates(state, params);
818    let Some(candidate) = candidates.candidates.first() else {
819        return Value::Null;
820    };
821    let Some(document) = state.document(document_uri.as_str()) else {
822        return Value::Null;
823    };
824    if is_sass_symbol_reference_kind(candidate.kind)
825        && let Some((target_uri, target)) =
826            sass_symbol_definitions_for_candidate(state, document, candidate)
827                .into_iter()
828                .next()
829        && let Some(target_text) = style_text_for_uri(state, target_uri.as_str())
830    {
831        let mut response = json!({
832            "contents": {
833                "kind": "markdown",
834                "value": render_style_hover_candidate_markdown_for_workspace(
835                    state,
836                    target_uri.as_str(),
837                    target_text.as_str(),
838                    &target,
839                ),
840            },
841            "range": candidate.range,
842        });
843        if let Some(target_document) = state.document(target_uri.as_str()) {
844            let provider_feedback =
845                current_provider_tier_feedback_data(target_document, "textDocument/hover");
846            attach_provider_tier_feedback(&mut response, provider_feedback.as_ref());
847        }
848        return response;
849    }
850    if is_sass_symbol_reference_kind(candidate.kind)
851        && let Some(target) =
852            external_sif_sass_symbol_target_for_candidate(state, document, candidate)
853    {
854        return json!({
855            "contents": {
856                "kind": "markdown",
857                "value": render_external_sif_sass_symbol_hover_markdown(&target),
858            },
859            "range": candidate.range,
860        });
861    }
862
863    let mut response = json!({
864        "contents": {
865            "kind": "markdown",
866            "value": render_style_hover_candidate_markdown_for_workspace(
867                state,
868                document.uri.as_str(),
869                document.text.as_str(),
870                candidate,
871            ),
872        },
873        "range": candidate.range,
874    });
875    let provider_feedback = current_provider_tier_feedback_data(document, "textDocument/hover");
876    attach_provider_tier_feedback(&mut response, provider_feedback.as_ref());
877    response
878}
879
880fn resolve_lsp_hover_trace(state: &LspShellState, params: Option<&Value>) -> Value {
881    let document_uri = document_uri_from_params(params);
882    let mut trace = if let Some(position) = lsp_position_from_params(params) {
883        if let Some(document) = state.document(document_uri.as_str()) {
884            if is_style_document_uri(document.uri.as_str()) {
885                resolve_style_lsp_hover_trace(state, document, position)
886            } else {
887                resolve_source_lsp_hover_trace(state, document, position)
888            }
889        } else {
890            empty_hover_trace(
891                document_uri,
892                None,
893                "unknown",
894                Some(position),
895                "documentNotIndexed",
896            )
897        }
898    } else {
899        empty_hover_trace(document_uri, None, "unknown", None, "missingPosition")
900    };
901    project_hover_trace_through_explain_egress(&mut trace);
902    trace
903}
904
905fn resolve_style_lsp_hover_trace(
906    state: &LspShellState,
907    document: &LspTextDocumentState,
908    position: ParserPositionV0,
909) -> Value {
910    let Some((language, candidates)) = style_hover_candidates_for_document(document) else {
911        return empty_hover_trace(
912            document.uri.clone(),
913            document.workspace_folder_uri.clone(),
914            "style",
915            Some(position),
916            "styleDocumentNotIndexed",
917        );
918    };
919    let matched = candidates
920        .iter()
921        .filter(|candidate| parser_range_contains_position(&candidate.range, position))
922        .cloned()
923        .collect::<Vec<_>>();
924    let Some(candidate) = matched.first() else {
925        return json!({
926            "schemaVersion": "0",
927            "product": "omena-lsp-server.explain-hover-trace",
928            "documentUri": document.uri.as_str(),
929            "workspaceFolderUri": document.workspace_folder_uri.as_deref(),
930            "fileKind": "style",
931            "language": language,
932            "queryPosition": position,
933            "matched": false,
934            "reason": "noStyleCandidateAtPosition",
935            "candidateCount": 0,
936            "definitionCount": 0,
937            "candidates": [],
938            "definitions": [],
939            "resolutionPath": ["styleHoverCandidates"],
940            "readySurfaces": ["explainHoverTraceRpc", "styleHoverCandidates"],
941        });
942    };
943    let definitions =
944        style_hover_trace_definitions(state, document, candidate, candidates.as_slice());
945    let rendered_markdown =
946        render_source_hover_definitions_markdown(state, definitions.as_slice()).unwrap_or_default();
947
948    json!({
949        "schemaVersion": "0",
950        "product": "omena-lsp-server.explain-hover-trace",
951        "documentUri": document.uri.as_str(),
952        "workspaceFolderUri": document.workspace_folder_uri.as_deref(),
953        "fileKind": "style",
954        "language": language,
955        "queryPosition": position,
956        "matched": true,
957        "reason": "styleCandidateResolved",
958        "candidateCount": matched.len(),
959        "definitionCount": definitions.len(),
960        "candidates": matched,
961        "definitions": hover_trace_definition_values(definitions.as_slice()),
962        "renderedMarkdown": rendered_markdown,
963        "resolutionPath": ["styleHoverCandidates", "styleDefinitionResolver", "hoverMarkdownRenderer"],
964        "readySurfaces": ["explainHoverTraceRpc", "styleHoverCandidates", "hoverMarkdownRenderer"],
965    })
966}
967
968fn resolve_source_lsp_hover_trace(
969    state: &LspShellState,
970    document: &LspTextDocumentState,
971    position: ParserPositionV0,
972) -> Value {
973    if let Some(trace) = source_domain_reference_trace_at_position(state, document, position) {
974        return trace;
975    }
976
977    let resolution = resolve_source_provider_candidates(state, document);
978    let matched = resolution
979        .matched
980        .into_iter()
981        .filter(|candidate| parser_range_contains_position(&candidate.range, position))
982        .collect::<Vec<_>>();
983    let unresolved = resolution
984        .unresolved
985        .into_iter()
986        .filter(|candidate| parser_range_contains_position(&candidate.range, position))
987        .collect::<Vec<_>>();
988    if matched.is_empty() && unresolved.is_empty() {
989        return empty_hover_trace(
990            document.uri.clone(),
991            document.workspace_folder_uri.clone(),
992            "source",
993            Some(position),
994            "noSourceCandidateAtPosition",
995        );
996    }
997
998    let definitions = style_selector_definitions_for_source_candidates(
999        state,
1000        matched.as_slice(),
1001        document.workspace_folder_uri.as_deref(),
1002    );
1003    let rendered_markdown =
1004        render_source_hover_definitions_markdown(state, definitions.as_slice()).unwrap_or_default();
1005
1006    json!({
1007        "schemaVersion": "0",
1008        "product": "omena-lsp-server.explain-hover-trace",
1009        "documentUri": document.uri.as_str(),
1010        "workspaceFolderUri": document.workspace_folder_uri.as_deref(),
1011        "fileKind": "source",
1012        "languageId": document.language_id.as_str(),
1013        "queryPosition": position,
1014        "matched": !matched.is_empty(),
1015        "reason": if matched.is_empty() { "sourceCandidateUnresolved" } else { "sourceCandidateResolved" },
1016        "matchedCandidateCount": matched.len(),
1017        "unresolvedCandidateCount": unresolved.len(),
1018        "definitionCount": definitions.len(),
1019        "candidates": matched,
1020        "unresolvedCandidates": unresolved,
1021        "definitions": hover_trace_definition_values(definitions.as_slice()),
1022        "renderedMarkdown": rendered_markdown,
1023        "resolutionPath": ["sourceSyntaxIndex", "sourceProviderCandidateResolution", "styleSelectorDefinitionResolver", "hoverMarkdownRenderer"],
1024        "readySurfaces": ["explainHoverTraceRpc", "sourceSyntaxIndex", "sourceProviderCandidateResolution", "hoverMarkdownRenderer"],
1025    })
1026}
1027
1028fn empty_hover_trace(
1029    document_uri: String,
1030    workspace_folder_uri: Option<String>,
1031    file_kind: &'static str,
1032    query_position: Option<ParserPositionV0>,
1033    reason: &'static str,
1034) -> Value {
1035    json!({
1036        "schemaVersion": "0",
1037        "product": "omena-lsp-server.explain-hover-trace",
1038        "documentUri": document_uri,
1039        "workspaceFolderUri": workspace_folder_uri,
1040        "fileKind": file_kind,
1041        "queryPosition": query_position,
1042        "matched": false,
1043        "reason": reason,
1044        "candidateCount": 0,
1045        "definitionCount": 0,
1046        "candidates": [],
1047        "definitions": [],
1048        "resolutionPath": [],
1049        "readySurfaces": ["explainHoverTraceRpc"],
1050    })
1051}
1052
1053fn style_hover_trace_definitions(
1054    state: &LspShellState,
1055    document: &LspTextDocumentState,
1056    candidate: &LspStyleHoverCandidate,
1057    candidates: &[LspStyleHoverCandidate],
1058) -> Vec<(String, LspStyleHoverCandidate)> {
1059    if is_sass_symbol_reference_kind(candidate.kind) {
1060        return sass_symbol_definitions_for_candidate(state, document, candidate);
1061    }
1062    if candidate.kind == "customPropertyReference"
1063        && let Some(target) = candidates.iter().find(|target| {
1064            target.kind == "customPropertyDeclaration" && target.name == candidate.name
1065        })
1066    {
1067        return vec![(document.uri.clone(), target.clone())];
1068    }
1069    vec![(document.uri.clone(), candidate.clone())]
1070}
1071
1072fn hover_trace_definition_values(definitions: &[(String, LspStyleHoverCandidate)]) -> Vec<Value> {
1073    definitions
1074        .iter()
1075        .map(|(uri, definition)| {
1076            json!({
1077                "uri": uri,
1078                "kind": definition.kind,
1079                "name": definition.name,
1080                "range": definition.range,
1081                "source": definition.source,
1082                "targetStyleUri": definition.target_style_uri,
1083                "namespace": definition.namespace,
1084            })
1085        })
1086        .collect()
1087}
1088
1089fn resolve_source_lsp_hover(
1090    state: &dyn LspQueryReadView,
1091    document: &LspTextDocumentState,
1092    params: Option<&Value>,
1093) -> Value {
1094    let Some(position) = lsp_position_from_params(params) else {
1095        return Value::Null;
1096    };
1097    if let Some((range, value)) =
1098        source_domain_reference_hover_at_position(state, document, position)
1099    {
1100        return json!({
1101            "contents": {
1102                "kind": "markdown",
1103                "value": value,
1104            },
1105            "range": range,
1106        });
1107    }
1108    let candidates = source_selector_candidates_at_position(state, document, position);
1109    let Some(candidate) = candidates.first() else {
1110        return Value::Null;
1111    };
1112    let definitions = style_selector_definitions_for_source_candidates(
1113        state,
1114        candidates.as_slice(),
1115        document.workspace_folder_uri.as_deref(),
1116    );
1117    let value = render_source_hover_definitions_markdown(state, definitions.as_slice())
1118        .unwrap_or_else(|| format!("**`.{}`**", candidate.name));
1119
1120    let mut response = json!({
1121        "contents": {
1122            "kind": "markdown",
1123            "value": value,
1124        },
1125        "range": candidate.range,
1126    });
1127    let provider_feedback =
1128        provider_tier_feedback_for_hover_definitions(state, definitions.as_slice());
1129    attach_provider_tier_feedback(&mut response, provider_feedback.as_ref());
1130    response
1131}
1132
1133fn resolve_source_lsp_definition(
1134    state: &dyn LspQueryReadView,
1135    document: &LspTextDocumentState,
1136    position: ParserPositionV0,
1137) -> Value {
1138    let candidates = source_selector_candidates_at_position(state, document, position);
1139    if candidates.is_empty() {
1140        return Value::Null;
1141    };
1142    let definitions = style_selector_definitions_for_source_candidates(
1143        state,
1144        candidates.as_slice(),
1145        document.workspace_folder_uri.as_deref(),
1146    );
1147    if definitions.is_empty() {
1148        return Value::Null;
1149    }
1150
1151    json!(
1152        definitions
1153            .into_iter()
1154            .map(|(uri, definition)| json!({ "uri": uri, "range": definition.range }))
1155            .collect::<Vec<_>>()
1156    )
1157}
1158
1159fn resolve_source_lsp_references(
1160    state: &LspShellState,
1161    document: &LspTextDocumentState,
1162    position: ParserPositionV0,
1163    params: Option<&Value>,
1164) -> Value {
1165    let candidates = source_selector_candidates_at_position(state, document, position);
1166    if candidates.is_empty() {
1167        return Value::Null;
1168    };
1169    let include_declaration = include_declaration_from_params(params);
1170    let mut locations = Vec::new();
1171    if include_declaration {
1172        locations.extend(
1173            style_selector_definitions_for_source_candidates(
1174                state,
1175                candidates.as_slice(),
1176                document.workspace_folder_uri.as_deref(),
1177            )
1178            .into_iter()
1179            .map(|(uri, definition)| json!({ "uri": uri, "range": definition.range })),
1180        );
1181    }
1182    for candidate in candidates {
1183        if candidate.kind == "sourceSelectorPrefixReference" {
1184            let definitions = style_selector_definitions_from_open_documents(
1185                state,
1186                "",
1187                document.workspace_folder_uri.as_deref(),
1188            );
1189            for selector_name in source_candidate_selector_names(
1190                &candidate,
1191                definitions.as_slice(),
1192                candidate.target_style_uri.as_deref(),
1193            ) {
1194                locations.extend(selector_reference_locations_from_open_documents(
1195                    state,
1196                    selector_name.as_str(),
1197                    document.workspace_folder_uri.as_deref(),
1198                    candidate.target_style_uri.as_deref(),
1199                ));
1200            }
1201        } else {
1202            locations.extend(selector_reference_locations_from_open_documents(
1203                state,
1204                candidate.name.as_str(),
1205                document.workspace_folder_uri.as_deref(),
1206                candidate.target_style_uri.as_deref(),
1207            ));
1208        }
1209    }
1210    locations.sort_by_key(location_sort_key);
1211    locations.dedup();
1212
1213    if locations.is_empty() {
1214        Value::Null
1215    } else {
1216        json!(locations)
1217    }
1218}
1219
1220fn resolve_source_lsp_completion(
1221    state: &LspShellState,
1222    document: &LspTextDocumentState,
1223    params: Option<&Value>,
1224) -> Value {
1225    let Some(position) = lsp_position_from_params(params) else {
1226        return Value::Null;
1227    };
1228    let Some(context) = source_completion_context_at_position(state, document, position) else {
1229        return Value::Null;
1230    };
1231    if !context.domain_option_names.is_empty() {
1232        let items = source_domain_option_completion_items(
1233            context.domain_option_names.as_slice(),
1234            context.value_prefix.as_deref(),
1235        )
1236        .into_iter()
1237        .map(|item| lsp_completion_item_from_query("source", item, None))
1238        .collect::<Vec<_>>();
1239        return json!({
1240            "isIncomplete": false,
1241            "items": items,
1242        });
1243    }
1244    let inferred_target_style_uri = context.target_style_uri.clone().or_else(|| {
1245        source_selector_candidate_at_position(state, document, position)
1246            .and_then(|candidate| candidate.target_style_uri)
1247    });
1248    let target_style_uri = inferred_target_style_uri
1249        .as_deref()
1250        .map(|uri| external_document_uri_for_query_uri(state, uri));
1251    let style_sources = style_sources_from_open_documents(
1252        state,
1253        document.workspace_folder_uri.as_deref(),
1254        target_style_uri.as_deref(),
1255    );
1256    let resolution_inputs =
1257        resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref());
1258
1259    let definitions = style_selector_definitions_from_open_documents(
1260        state,
1261        "",
1262        document.workspace_folder_uri.as_deref(),
1263    )
1264    .into_iter()
1265    .filter(|(uri, _)| {
1266        target_style_uri
1267            .as_deref()
1268            .is_none_or(|target_uri| file_uri_equivalent(target_uri, uri))
1269    })
1270    .collect::<Vec<_>>();
1271    let candidates = definitions
1272        .iter()
1273        .map(|(uri, definition)| {
1274            let file_uri = target_style_uri
1275                .as_deref()
1276                .filter(|target_uri| file_uri_equivalent(target_uri, uri.as_str()))
1277                .map(ToString::to_string)
1278                .unwrap_or_else(|| uri.clone());
1279            OmenaQueryCompletionCandidateV0 {
1280                file_uri,
1281                name: definition.name.clone(),
1282                kind: definition.kind,
1283                range: definition.range,
1284                source: definition.source,
1285                documentation: None,
1286            }
1287        })
1288        .collect::<Vec<_>>();
1289    let mut completion = summarize_omena_query_source_completion_at_position(
1290        document.uri.as_str(),
1291        position,
1292        candidates.as_slice(),
1293        target_style_uri.as_deref(),
1294        context.value_prefix.as_deref(),
1295        context.preferred_selector_names.as_slice(),
1296    );
1297    // Cascade documentation is attached lazily AFTER ranking/dedup and only for the
1298    // top-ranked items a completion list actually surfaces. The name-independent
1299    // narrowing inputs come from the memoized substrate (rfcs#63 E-ii) — fetched once
1300    // per request, reused across requests while the corpus is unchanged — so the
1301    // per-candidate work is the cheap per-name filter, not a whole-corpus collection.
1302    let mut narrowing_substrate = None;
1303    for item in completion
1304        .items
1305        .iter_mut()
1306        .take(SOURCE_COMPLETION_DOCUMENTATION_BUDGET)
1307    {
1308        if item.item_kind != "cssModuleSelector" || item.documentation.is_some() {
1309            continue;
1310        }
1311        let Some((uri, definition)) = definitions
1312            .iter()
1313            .find(|(_, definition)| definition.kind == "selector" && definition.name == item.label)
1314        else {
1315            continue;
1316        };
1317        let narrowing_substrate = narrowing_substrate.get_or_insert_with(|| {
1318            cascade_narrowing_substrate_for_style_sources(
1319                state,
1320                style_sources.as_slice(),
1321                &resolution_inputs,
1322            )
1323        });
1324        item.documentation = style_text_for_uri(state, uri.as_str()).and_then(|style_text| {
1325            summarize_omena_query_style_completion_candidate_documentation_for_workspace_file_with_substrate(
1326                uri.as_str(),
1327                style_sources.as_slice(),
1328                narrowing_substrate,
1329                definition.kind,
1330                definition.name.as_str(),
1331                definition.range.start,
1332            )
1333            .or_else(|| {
1334                summarize_omena_query_style_completion_candidate_documentation(
1335                    style_text.as_str(),
1336                    definition.kind,
1337                    definition.name.as_str(),
1338                    definition.range.start,
1339                )
1340            })
1341        });
1342    }
1343    let completion = completion;
1344    let provider_feedback = target_style_uri
1345        .as_deref()
1346        .and_then(|uri| state.document(uri))
1347        .and_then(|target_document| {
1348            current_provider_tier_feedback_data(target_document, "textDocument/completion")
1349        });
1350    let items: Vec<Value> = completion
1351        .items
1352        .into_iter()
1353        .map(|item| {
1354            lsp_completion_item_from_query(completion.file_kind, item, provider_feedback.as_ref())
1355        })
1356        .collect();
1357
1358    json!({
1359        "isIncomplete": false,
1360        "items": items,
1361    })
1362}
1363
1364fn render_source_hover_definitions_markdown(
1365    state: &dyn LspQueryReadView,
1366    definitions: &[(String, LspStyleHoverCandidate)],
1367) -> Option<String> {
1368    let parts = definitions
1369        .iter()
1370        .filter_map(|(uri, definition)| {
1371            style_text_for_uri(state, uri).map(|text| {
1372                render_style_hover_candidate_markdown_for_workspace(
1373                    state,
1374                    uri.as_str(),
1375                    text.as_str(),
1376                    definition,
1377                )
1378            })
1379        })
1380        .collect::<Vec<_>>();
1381    if parts.is_empty() {
1382        None
1383    } else {
1384        Some(parts.join("\n\n---\n\n"))
1385    }
1386}
1387
1388#[cfg(test)]
1389mod tests;
1390
1391/// Compact workspace status for the client's status surface. Change-driven:
1392/// the loop sends `omena/status` only when this tuple moves, so the wire
1393/// cost is bounded by real state transitions, not by tick rate.
1394pub fn workspace_status_snapshot(state: &LspShellState) -> (usize, usize, bool, usize) {
1395    (
1396        state.workspace_index_pending_file_count,
1397        state.document_count(),
1398        diagnostics_follow_up::workspace_republish_frontier_passed(state),
1399        state.resolution.external_sifs.len(),
1400    )
1401}
1402
1403pub fn workspace_status_notification(
1404    (pending, indexed, settled, external_sifs): (usize, usize, bool, usize),
1405) -> Value {
1406    json!({
1407        "jsonrpc": "2.0",
1408        "method": "omena/status",
1409        "params": {
1410            "pendingFiles": pending,
1411            "indexedDocuments": indexed,
1412            "settled": settled,
1413            "externalTokenSources": external_sifs,
1414        },
1415    })
1416}