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
207pub 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;
300const 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 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
1391pub 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}