1mod boundary;
2mod diagnostics_scheduler;
3mod frame_aware_refresh;
4mod message_loop;
5mod protocol;
6mod query_adapter;
7mod query_reuse;
8mod settings;
9mod source_type_facts;
10mod state;
11mod workspace_index;
12mod workspace_runtime_registry;
13
14pub use boundary::*;
15pub use frame_aware_refresh::*;
16#[cfg(test)]
17pub(crate) use message_loop::current_time_millis;
18pub use message_loop::{handle_lsp_message, handle_lsp_message_outputs};
19use omena_query::{
20 OmenaQueryCodeActionV0, OmenaQueryCompletionCandidateV0, OmenaQueryCompletionItemV0,
21 OmenaQueryEngineInputV2, OmenaQueryExternalModuleModeV0, OmenaQuerySourceDocumentInputV0,
22 OmenaQuerySourceImportedStyleBindingV0 as ImportedStyleBinding,
23 OmenaQuerySourceMissingSelectorDiagnosticCandidateV0,
24 OmenaQuerySourceSelectorReferenceFactV0 as SourceSelectorReferenceFact,
25 OmenaQuerySourceSelectorReferenceMatchKindV0 as SourceSelectorReferenceMatchKind,
26 OmenaQuerySourceSyntaxIndexV0 as SourceSyntaxIndex, OmenaQueryStyleSourceInputV0,
27 ParserByteSpanV0, ParserPositionV0, ParserRangeV0, StyleLanguage,
28 collect_omena_query_vue_style_module_bindings,
29 is_omena_query_sass_symbol_candidate_kind as is_sass_symbol_candidate_kind,
30 is_omena_query_sass_symbol_declaration_kind as is_sass_symbol_declaration_kind,
31 is_omena_query_sass_symbol_reference_kind as is_sass_symbol_reference_kind,
32 load_omena_query_workspace_style_resolution_inputs,
33 omena_query_sass_symbol_kind_from_candidate_kind as sass_symbol_kind_from_candidate_kind,
34 omena_query_sass_symbol_target_matches,
35 read_omena_query_cascade_at_position_with_categorical_evidence,
36 read_omena_query_style_context_index, resolve_omena_query_sass_forward_sources,
37 resolve_omena_query_sass_module_use_sources_for_candidate,
38 resolve_omena_query_sass_symbol_declarations,
39 resolve_omena_query_source_candidate_selector_names,
40 resolve_omena_query_source_provider_candidates,
41 resolve_omena_query_style_selector_definitions_for_source_candidate,
42 resolve_omena_query_style_uri_for_specifier_with_resolution_inputs,
43 summarize_omena_query_refs_for_class, summarize_omena_query_rename_plan,
44 summarize_omena_query_sass_module_sources, summarize_omena_query_source_completion_at_position,
45 summarize_omena_query_source_diagnostics_for_file,
46 summarize_omena_query_source_import_declarations_for_source_language,
47 summarize_omena_query_source_syntax_index_for_source_language,
48 summarize_omena_query_style_completion_at_position,
49 summarize_omena_query_style_diagnostics_for_file,
50 summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs_and_resolution_inputs,
51 summarize_omena_query_style_document, summarize_omena_query_style_extract_code_actions,
52 summarize_omena_query_style_hover_render_parts,
53 summarize_omena_query_style_inline_code_actions,
54};
55#[cfg(test)]
56pub(crate) use omena_tsgo_client::{TsgoResolvedTypeV0, TsgoTypeFactResultEntryV0};
57use protocol::*;
58use query_adapter::*;
59use query_reuse::refresh_document_reusable_indexes;
60use serde_json::{Value, json};
61pub(crate) use settings::{
62 apply_diagnostic_settings, apply_feature_settings, apply_resolution_settings,
63};
64#[cfg(test)]
65pub(crate) use source_type_facts::apply_source_type_fact_results_to_document;
66pub(crate) use source_type_facts::refresh_source_type_fact_candidates_for_document;
67pub use state::*;
68#[cfg(test)]
69use std::path::Path;
70use std::{
71 collections::{BTreeMap, BTreeSet},
72 fs,
73};
74pub(crate) use workspace_index::index_workspace_style_files;
75#[cfg(test)]
76pub(crate) use workspace_index::{
77 WorkspaceStyleIndexBudget, index_workspace_style_files_with_budget,
78};
79
80pub const NODE_TEXT_DOCUMENT_SYNC_KIND: u8 = 2;
81pub const DEBUG_STATE_REQUEST: &str = "omena/rustLspState";
82pub const RUNTIME_LOOP_PROBE_REQUEST: &str = "omena/runtimeLoopProbe";
83pub const STYLE_HOVER_CANDIDATES_REQUEST: &str = "omena/rustStyleHoverCandidates";
84pub const STYLE_DIAGNOSTICS_REQUEST: &str = "omena/rustStyleDiagnostics";
85pub const SOURCE_DIAGNOSTICS_REQUEST: &str = "omena/rustSourceDiagnostics";
86pub const CASCADE_AT_POSITION_REQUEST: &str = "omena/rustCascadeAtPosition";
87pub const STYLE_CONTEXT_INDEX_REQUEST: &str = "omena/rustStyleContextIndex";
88const CANCEL_REQUEST_METHOD: &str = "$/cancelRequest";
89const REQUEST_CANCELLED_ERROR_CODE: i32 = -32800;
90#[derive(Debug, Clone, PartialEq, Eq)]
91struct SourceProviderCandidateResolution {
92 matched: Vec<LspStyleHoverCandidate>,
93 unresolved: Vec<LspStyleHoverCandidate>,
94}
95
96fn initialize_workspace_folders(state: &mut LspShellState, params: Option<&Value>) {
97 state.workspace_runtime_registry.clear();
98 if let Some(folders) = params
99 .and_then(|value| value.get("workspaceFolders"))
100 .and_then(Value::as_array)
101 {
102 for folder in folders {
103 insert_workspace_folder(state, folder);
104 }
105 refresh_workspace_resolution_inputs(state);
106 return;
107 }
108
109 if let Some(root_uri) = params
110 .and_then(|value| value.get("rootUri"))
111 .and_then(Value::as_str)
112 {
113 state
114 .workspace_runtime_registry
115 .insert(root_uri.to_string(), root_uri.to_string());
116 }
117 refresh_workspace_resolution_inputs(state);
118}
119
120fn refresh_workspace_resolution_inputs(state: &mut LspShellState) {
121 let configured_package_manifests = state.resolution.package_manifests.clone();
122 let workspace_uris = state
123 .workspace_runtime_registry
124 .folder_snapshots()
125 .into_iter()
126 .map(|folder| folder.uri)
127 .collect::<BTreeSet<_>>();
128 state
129 .resolution
130 .workspace_style_resolution_inputs
131 .retain(|workspace_uri, _| workspace_uris.contains(workspace_uri));
132 for workspace_uri in workspace_uris {
133 let inputs = load_omena_query_workspace_style_resolution_inputs(
134 Some(workspace_uri.as_str()),
135 configured_package_manifests.as_slice(),
136 );
137 state
138 .resolution
139 .workspace_style_resolution_inputs
140 .insert(workspace_uri, inputs);
141 }
142}
143
144fn refresh_workspace_resolution_inputs_for_uri(state: &mut LspShellState, uri: &str) {
145 let Some(workspace_uri) = resolve_workspace_folder_uri(state, uri) else {
146 return;
147 };
148 let inputs = load_omena_query_workspace_style_resolution_inputs(
149 Some(workspace_uri.as_str()),
150 state.resolution.package_manifests.as_slice(),
151 );
152 state
153 .resolution
154 .workspace_style_resolution_inputs
155 .insert(workspace_uri, inputs);
156}
157
158fn resolution_inputs_for_workspace_uri(
159 state: &LspShellState,
160 workspace_folder_uri: Option<&str>,
161) -> omena_query::OmenaQueryStyleResolutionInputsV0 {
162 workspace_folder_uri
163 .and_then(|workspace_uri| {
164 state
165 .resolution
166 .workspace_style_resolution_inputs
167 .get(workspace_uri)
168 })
169 .cloned()
170 .unwrap_or_else(|| omena_query::OmenaQueryStyleResolutionInputsV0 {
171 package_manifests: state.resolution.package_manifests.clone(),
172 ..Default::default()
173 })
174}
175
176fn lsp_text_document_state(
177 uri: String,
178 workspace_folder_uri: Option<String>,
179 language_id: String,
180 version: i64,
181 text: String,
182 resolution_inputs: &omena_query::OmenaQueryStyleResolutionInputsV0,
183) -> LspTextDocumentState {
184 let mut document = LspTextDocumentState {
185 uri,
186 workspace_folder_uri,
187 language_id,
188 version,
189 text,
190 style_summary: None,
191 style_candidates: Vec::new(),
192 source_syntax_index: SourceSyntaxIndex::default(),
193 source_selector_candidates: Vec::new(),
194 };
195 refresh_document_reusable_indexes(&mut document, resolution_inputs);
196 document
197}
198
199fn did_open_text_document(state: &mut LspShellState, params: Option<&Value>) {
200 let Some(document) = params.and_then(|value| value.get("textDocument")) else {
201 return;
202 };
203 let Some(uri) = document.get("uri").and_then(Value::as_str) else {
204 return;
205 };
206
207 state.insert_open_document_uri(uri);
208 let workspace_folder_uri = resolve_workspace_folder_uri(state, uri);
209 let resolution_inputs =
210 resolution_inputs_for_workspace_uri(state, workspace_folder_uri.as_deref());
211 state.insert_document(
212 uri,
213 lsp_text_document_state(
214 uri.to_string(),
215 workspace_folder_uri,
216 document
217 .get("languageId")
218 .and_then(Value::as_str)
219 .unwrap_or("unknown")
220 .to_string(),
221 document.get("version").and_then(Value::as_i64).unwrap_or(0),
222 document
223 .get("text")
224 .and_then(Value::as_str)
225 .unwrap_or_default()
226 .to_string(),
227 &resolution_inputs,
228 ),
229 );
230 if is_style_document_uri(uri) {
231 refresh_source_type_fact_candidates_for_referencing_documents(state, uri);
232 } else {
233 refresh_source_type_fact_candidates_for_document(state, uri);
234 }
235}
236
237fn did_change_text_document(state: &mut LspShellState, params: Option<&Value>) {
238 let Some(text_document) = params.and_then(|value| value.get("textDocument")) else {
239 return;
240 };
241 let Some(uri) = text_document.get("uri").and_then(Value::as_str) else {
242 return;
243 };
244 let resolution_inputs = state
245 .document(uri)
246 .map(|document| {
247 resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref())
248 })
249 .unwrap_or_else(|| resolution_inputs_for_workspace_uri(state, None));
250 let Some(existing) = state.document_mut(uri) else {
251 return;
252 };
253
254 if let Some(version) = text_document.get("version").and_then(Value::as_i64) {
255 existing.version = version;
256 }
257 let Some(changes) = params
258 .and_then(|value| value.get("contentChanges"))
259 .and_then(Value::as_array)
260 else {
261 return;
262 };
263
264 let mut text_changed = false;
265 for change in changes {
266 if apply_text_document_content_change(existing, change) {
267 text_changed = true;
268 }
269 }
270 if text_changed {
271 refresh_document_reusable_indexes(existing, &resolution_inputs);
272 }
273 if text_changed {
274 if is_style_document_uri(uri) {
275 refresh_source_type_fact_candidates_for_referencing_documents(state, uri);
276 } else {
277 refresh_source_type_fact_candidates_for_document(state, uri);
278 }
279 }
280}
281
282fn apply_text_document_content_change(document: &mut LspTextDocumentState, change: &Value) -> bool {
283 let Some(next_text) = change.get("text").and_then(Value::as_str) else {
284 return false;
285 };
286 let Some(range) = change.get("range").and_then(lsp_range_from_value) else {
287 document.text = next_text.to_string();
288 return true;
289 };
290 let Some(start_offset) = byte_offset_for_parser_position(document.text.as_str(), range.start)
291 else {
292 return false;
293 };
294 let Some(end_offset) = byte_offset_for_parser_position(document.text.as_str(), range.end)
295 else {
296 return false;
297 };
298 if start_offset > end_offset {
299 return false;
300 }
301 document
302 .text
303 .replace_range(start_offset..end_offset, next_text);
304 true
305}
306
307fn did_close_text_document(state: &mut LspShellState, params: Option<&Value>) {
308 let Some(uri) = params
309 .and_then(|value| value.get("textDocument"))
310 .and_then(|value| value.get("uri"))
311 .and_then(Value::as_str)
312 else {
313 return;
314 };
315 state.remove_open_document_uri(uri);
316 if is_style_document_uri(uri) && reload_indexed_style_document_from_disk(state, uri) {
317 refresh_source_type_fact_candidates_for_referencing_documents(state, uri);
318 return;
319 }
320 state.remove_document_uri(uri);
321 if is_style_document_uri(uri) {
322 refresh_source_type_fact_candidates_for_referencing_documents(state, uri);
323 }
324}
325
326fn did_change_workspace_folders(state: &mut LspShellState, params: Option<&Value>) {
327 let event = params.and_then(|value| value.get("event"));
328 let mut removed_workspace_uris = Vec::new();
329 if let Some(removed) = event
330 .and_then(|value| value.get("removed"))
331 .and_then(Value::as_array)
332 {
333 for folder in removed {
334 if let Some(uri) = folder.get("uri").and_then(Value::as_str) {
335 state.workspace_runtime_registry.remove(uri);
336 removed_workspace_uris.push(uri.to_string());
337 }
338 }
339 }
340 let mut added_workspace_folder = false;
341 if let Some(added) = event
342 .and_then(|value| value.get("added"))
343 .and_then(Value::as_array)
344 {
345 for folder in added {
346 insert_workspace_folder(state, folder);
347 added_workspace_folder = true;
348 }
349 }
350 reconcile_documents_after_workspace_folder_changes(state, removed_workspace_uris.as_slice());
351 refresh_workspace_resolution_inputs(state);
352 if added_workspace_folder {
353 index_workspace_style_files(state);
354 }
355}
356
357fn reconcile_documents_after_workspace_folder_changes(
358 state: &mut LspShellState,
359 removed_workspace_uris: &[String],
360) {
361 remove_unowned_indexed_documents_for_removed_workspaces(state, removed_workspace_uris);
362 refresh_document_workspace_owners(state);
363}
364
365fn remove_unowned_indexed_documents_for_removed_workspaces(
366 state: &mut LspShellState,
367 removed_workspace_uris: &[String],
368) {
369 if removed_workspace_uris.is_empty() {
370 return;
371 }
372 let open_document_uris = state.open_document_uris.clone();
373 let workspace_runtime_registry = state.workspace_runtime_registry.clone();
374 state.documents.retain(|uri, document| {
375 if open_document_uris.contains(uri)
376 || open_document_uris
377 .iter()
378 .any(|open_uri| file_uri_equivalent(open_uri, uri))
379 {
380 return true;
381 }
382 let owned_by_removed_workspace =
383 document
384 .workspace_folder_uri
385 .as_deref()
386 .is_some_and(|workspace_uri| {
387 removed_workspace_uris
388 .iter()
389 .any(|removed_uri| removed_uri == workspace_uri)
390 });
391 !owned_by_removed_workspace
392 || workspace_runtime_registry
393 .resolve_owner_uri(uri.as_str())
394 .is_some()
395 });
396}
397
398fn did_change_watched_files(state: &mut LspShellState, params: Option<&Value>) {
399 let Some(changes) = params
400 .and_then(|value| value.get("changes"))
401 .and_then(Value::as_array)
402 else {
403 return;
404 };
405 for change in changes {
406 let Some(uri) = change.get("uri").and_then(Value::as_str) else {
407 continue;
408 };
409 let change_type = change.get("type").and_then(Value::as_u64).unwrap_or(0);
410 state.watched_file_changes.push(LspWatchedFileChangeState {
411 uri: uri.to_string(),
412 change_type,
413 });
414 apply_watched_file_change_to_index(state, uri, change_type);
415 }
416}
417
418fn apply_watched_file_change_to_index(state: &mut LspShellState, uri: &str, change_type: u64) {
419 if !is_style_document_uri(uri) {
420 if is_resolution_config_document_uri(uri) {
421 refresh_source_indexes_for_resolution_config_change(state, uri);
422 }
423 return;
424 }
425 if state.has_open_document_uri(uri) {
426 return;
427 }
428 if change_type == 3 {
429 state.remove_document_uri(uri);
430 refresh_source_type_fact_candidates_for_referencing_documents(state, uri);
431 return;
432 }
433
434 if reload_indexed_style_document_from_disk(state, uri) {
435 refresh_source_type_fact_candidates_for_referencing_documents(state, uri);
436 }
437}
438
439fn refresh_source_indexes_for_resolution_config_change(
440 state: &mut LspShellState,
441 config_uri: &str,
442) {
443 refresh_workspace_resolution_inputs_for_uri(state, config_uri);
444 let workspace_folder_uri = resolve_workspace_folder_uri(state, config_uri);
445 let source_uris = state
446 .documents
447 .values()
448 .filter(|document| !is_style_document_uri(document.uri.as_str()))
449 .filter(|document| {
450 workspace_folder_uri.as_deref().is_none_or(|workspace_uri| {
451 workspace_folder_compatible(Some(workspace_uri), document)
452 })
453 })
454 .map(|document| document.uri.clone())
455 .collect::<Vec<_>>();
456 for source_uri in source_uris {
457 let resolution_inputs = state
458 .document(source_uri.as_str())
459 .map(|document| {
460 resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref())
461 })
462 .unwrap_or_else(|| {
463 resolution_inputs_for_workspace_uri(state, workspace_folder_uri.as_deref())
464 });
465 if let Some(document) = state.document_mut(source_uri.as_str()) {
466 refresh_document_reusable_indexes(document, &resolution_inputs);
467 }
468 refresh_source_type_fact_candidates_for_document(state, source_uri.as_str());
469 }
470}
471
472pub(crate) fn refresh_source_indexes_for_resolution_settings_change(state: &mut LspShellState) {
473 refresh_workspace_resolution_inputs(state);
474 let source_uris = state
475 .documents
476 .values()
477 .filter(|document| !is_style_document_uri(document.uri.as_str()))
478 .map(|document| document.uri.clone())
479 .collect::<Vec<_>>();
480 for source_uri in source_uris {
481 let resolution_inputs = state
482 .document(source_uri.as_str())
483 .map(|document| {
484 resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref())
485 })
486 .unwrap_or_else(|| resolution_inputs_for_workspace_uri(state, None));
487 if let Some(document) = state.document_mut(source_uri.as_str()) {
488 refresh_document_reusable_indexes(document, &resolution_inputs);
489 }
490 refresh_source_type_fact_candidates_for_document(state, source_uri.as_str());
491 }
492}
493
494pub(crate) fn is_resolution_config_document_uri(uri: &str) -> bool {
495 let Some(path) = file_uri_to_path(uri) else {
496 return false;
497 };
498 let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
499 return false;
500 };
501 file_name == "package.json"
502 || file_name == "jsconfig.json"
503 || (file_name.starts_with("tsconfig") && file_name.ends_with(".json"))
504 || matches!(
505 file_name,
506 "vite.config.ts"
507 | "vite.config.mts"
508 | "vite.config.cts"
509 | "vite.config.js"
510 | "vite.config.mjs"
511 | "vite.config.cjs"
512 | "webpack.config.ts"
513 | "webpack.config.mts"
514 | "webpack.config.cts"
515 | "webpack.config.js"
516 | "webpack.config.mjs"
517 | "webpack.config.cjs"
518 )
519}
520
521pub(crate) fn ensure_style_document_loaded_from_disk(state: &mut LspShellState, uri: &str) -> bool {
522 if state.contains_document_uri(uri) {
523 return true;
524 }
525 reload_indexed_style_document_from_disk(state, uri)
526}
527
528fn reload_indexed_style_document_from_disk(state: &mut LspShellState, uri: &str) -> bool {
529 let Some(path) = file_uri_to_path(uri) else {
530 return false;
531 };
532 let Ok(text) = fs::read_to_string(path) else {
533 return false;
534 };
535 let workspace_folder_uri = resolve_workspace_folder_uri(state, uri);
536 let resolution_inputs =
537 resolution_inputs_for_workspace_uri(state, workspace_folder_uri.as_deref());
538 state.insert_document(
539 uri,
540 lsp_text_document_state(
541 uri.to_string(),
542 workspace_folder_uri,
543 StyleLanguage::from_module_path(uri)
544 .map(style_language_label)
545 .unwrap_or("unknown")
546 .to_string(),
547 0,
548 text,
549 &resolution_inputs,
550 ),
551 );
552 true
553}
554
555fn refresh_source_type_fact_candidates_for_referencing_documents(
556 state: &mut LspShellState,
557 style_uri: &str,
558) {
559 let source_uris = state
560 .documents
561 .values()
562 .filter(|document| !is_style_document_uri(document.uri.as_str()))
563 .filter(|document| document_references_style_uri(document, style_uri))
564 .map(|document| document.uri.clone())
565 .collect::<Vec<_>>();
566 for source_uri in source_uris {
567 refresh_source_type_fact_candidates_for_document(state, source_uri.as_str());
568 }
569}
570
571fn document_references_style_uri(document: &LspTextDocumentState, style_uri: &str) -> bool {
572 document
573 .source_syntax_index
574 .selector_references
575 .iter()
576 .any(|reference| reference.target_style_uri.as_deref() == Some(style_uri))
577 || document
578 .source_syntax_index
579 .type_fact_targets
580 .iter()
581 .any(|target| target.target_style_uri.as_deref() == Some(style_uri))
582}
583
584fn insert_workspace_folder(state: &mut LspShellState, folder: &Value) {
585 let Some(uri) = folder.get("uri").and_then(Value::as_str) else {
586 return;
587 };
588 state.workspace_runtime_registry.insert(
589 uri.to_string(),
590 folder
591 .get("name")
592 .and_then(Value::as_str)
593 .unwrap_or(uri)
594 .to_string(),
595 );
596}
597
598fn refresh_document_workspace_owners(state: &mut LspShellState) {
599 let workspace_runtime_registry = state.workspace_runtime_registry.clone();
600 for document in state.documents.values_mut() {
601 document.workspace_folder_uri =
602 workspace_runtime_registry.resolve_owner_uri(document.uri.as_str());
603 }
604}
605
606fn resolve_workspace_folder_uri(state: &LspShellState, document_uri: &str) -> Option<String> {
607 state
608 .workspace_runtime_registry
609 .resolve_owner_uri(document_uri)
610}
611
612fn summarize_style_document(uri: &str, text: Option<&str>) -> Option<LspStyleDocumentSummary> {
613 let text = text?;
614 let summary = summarize_omena_query_style_document(uri, text)?;
615 Some(LspStyleDocumentSummary {
616 language: summary.language,
617 selector_names: summary.selector_names,
618 custom_property_decl_names: summary.custom_property_decl_names,
619 custom_property_ref_names: summary.custom_property_ref_names,
620 sass_module_use_sources: summary.sass_module_use_sources,
621 sass_module_forward_sources: summary.sass_module_forward_sources,
622 diagnostic_count: summary.diagnostic_count,
623 })
624}
625
626pub fn resolve_style_hover_candidates(
627 state: &LspShellState,
628 params: Option<&Value>,
629) -> LspStyleHoverCandidatesResult {
630 let document_uri = document_uri_from_params(params);
631 let query_position = lsp_position_from_params(params);
632 let Some(document) = state.document(&document_uri) else {
633 return empty_style_hover_candidates_result(document_uri, None, query_position);
634 };
635
636 let Some((language, mut candidates)) = style_hover_candidates_for_document(document) else {
637 return empty_style_hover_candidates_result(
638 document_uri,
639 document.workspace_folder_uri.clone(),
640 query_position,
641 );
642 };
643
644 if let Some(position) = query_position {
645 candidates.retain(|candidate| parser_range_contains_position(&candidate.range, position));
646 }
647
648 LspStyleHoverCandidatesResult {
649 schema_version: "0",
650 product: "omena-lsp-server.style-hover-candidates",
651 document_uri,
652 workspace_folder_uri: document.workspace_folder_uri.clone(),
653 language: Some(language),
654 query_position,
655 candidate_count: candidates.len(),
656 candidates,
657 }
658}
659
660fn style_hover_candidates_for_document(
661 document: &LspTextDocumentState,
662) -> Option<(&'static str, Vec<LspStyleHoverCandidate>)> {
663 let summary = document.style_summary.as_ref()?;
664 Some((summary.language, document.style_candidates.clone()))
665}
666
667fn style_text_for_uri(state: &LspShellState, uri: &str) -> Option<String> {
668 state
669 .document(uri)
670 .map(|document| document.text.clone())
671 .or_else(|| fs::read_to_string(file_uri_to_path(uri)?).ok())
672}
673
674fn style_hover_candidates_for_uri(
675 state: &LspShellState,
676 uri: &str,
677) -> Option<(&'static str, Vec<LspStyleHoverCandidate>)> {
678 if let Some(document) = state.document(uri) {
679 return style_hover_candidates_for_document(document);
680 }
681 let text = style_text_for_uri(state, uri)?;
682 collect_style_hover_candidates(uri, text.as_str())
683}
684
685fn resolve_lsp_definition(state: &LspShellState, params: Option<&Value>) -> Value {
686 let document_uri = document_uri_from_params(params);
687 let Some(position) = lsp_position_from_params(params) else {
688 return Value::Null;
689 };
690 let Some(document) = state.document(&document_uri) else {
691 return Value::Null;
692 };
693 if !is_style_document_uri(document.uri.as_str()) {
694 return resolve_source_lsp_definition(state, document, position);
695 }
696
697 let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
698 return Value::Null;
699 };
700 let Some(candidate) = candidates
701 .iter()
702 .find(|candidate| parser_range_contains_position(&candidate.range, position))
703 else {
704 return Value::Null;
705 };
706 if is_sass_symbol_reference_kind(candidate.kind) {
707 let definitions = sass_symbol_definitions_for_candidate(state, document, candidate);
708 if definitions.is_empty() {
709 return Value::Null;
710 }
711 return json!(
712 definitions
713 .into_iter()
714 .map(|(uri, definition)| json!({ "uri": uri, "range": definition.range }))
715 .collect::<Vec<_>>()
716 );
717 }
718 let target = if candidate.kind == "customPropertyReference" {
719 candidates
720 .iter()
721 .find(|target| {
722 target.kind == "customPropertyDeclaration" && target.name == candidate.name
723 })
724 .unwrap_or(candidate)
725 } else {
726 candidate
727 };
728
729 json!([
730 {
731 "uri": document.uri.as_str(),
732 "range": target.range,
733 },
734 ])
735}
736
737fn resolve_lsp_references(state: &LspShellState, params: Option<&Value>) -> Value {
738 let document_uri = document_uri_from_params(params);
739 let Some(position) = lsp_position_from_params(params) else {
740 return Value::Null;
741 };
742 let Some(document) = state.document(&document_uri) else {
743 return Value::Null;
744 };
745 if !is_style_document_uri(document.uri.as_str()) {
746 return resolve_source_lsp_references(state, document, position, params);
747 }
748
749 let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
750 return Value::Null;
751 };
752 let Some(candidate) = candidates
753 .iter()
754 .find(|candidate| parser_range_contains_position(&candidate.range, position))
755 else {
756 return Value::Null;
757 };
758 let include_declaration = include_declaration_from_params(params);
759 let mut locations: Vec<Value> = if candidate.kind.starts_with("customProperty") {
760 candidates
761 .iter()
762 .filter(|target| {
763 target.name == candidate.name
764 && (target.kind == "customPropertyReference"
765 || (include_declaration && target.kind == "customPropertyDeclaration"))
766 })
767 .map(|target| json!({ "uri": document.uri.as_str(), "range": target.range }))
768 .collect()
769 } else if is_sass_symbol_candidate_kind(candidate.kind) {
770 let mut locations = Vec::new();
771 if include_declaration {
772 locations.extend(
773 sass_symbol_definitions_for_candidate(state, document, candidate)
774 .into_iter()
775 .map(|(uri, definition)| json!({ "uri": uri, "range": definition.range })),
776 );
777 }
778 locations.extend(
779 candidates
780 .iter()
781 .filter(|target| sass_symbol_reference_matches(candidate, target))
782 .map(|target| json!({ "uri": document.uri.as_str(), "range": target.range })),
783 );
784 locations
785 } else if candidate.kind == "selector" {
786 let mut locations = if include_declaration {
787 vec![json!({ "uri": document.uri.as_str(), "range": candidate.range })]
788 } else {
789 Vec::new()
790 };
791 locations.extend(selector_reference_locations_from_open_documents(
792 state,
793 candidate.name.as_str(),
794 document.workspace_folder_uri.as_deref(),
795 Some(document.uri.as_str()),
796 ));
797 locations
798 } else if include_declaration {
799 vec![json!({ "uri": document.uri.as_str(), "range": candidate.range })]
800 } else {
801 Vec::new()
802 };
803
804 locations.sort_by_key(|location| {
805 let line = location
806 .pointer("/range/start/line")
807 .and_then(Value::as_u64)
808 .unwrap_or_default();
809 let character = location
810 .pointer("/range/start/character")
811 .and_then(Value::as_u64)
812 .unwrap_or_default();
813 (line, character)
814 });
815 json!(locations)
816}
817
818fn resolve_lsp_completion(state: &LspShellState, params: Option<&Value>) -> Value {
819 let document_uri = document_uri_from_params(params);
820 let Some(document) = state.document(&document_uri) else {
821 return Value::Null;
822 };
823 if !is_style_document_uri(document.uri.as_str()) {
824 return resolve_source_lsp_completion(state, document, params);
825 }
826
827 let Some(position) = lsp_position_from_params(params) else {
828 return Value::Null;
829 };
830 let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
831 return Value::Null;
832 };
833
834 let query_candidates = candidates
835 .iter()
836 .map(query_style_hover_candidate_from_lsp)
837 .collect::<Vec<_>>();
838 let completion = summarize_omena_query_style_completion_at_position(
839 document.uri.as_str(),
840 document.text.as_str(),
841 position,
842 query_candidates.as_slice(),
843 );
844 let items: Vec<Value> = completion
845 .items
846 .into_iter()
847 .map(|item| lsp_completion_item_from_query(completion.file_kind, item))
848 .collect();
849
850 json!({
851 "isIncomplete": false,
852 "items": items,
853 })
854}
855
856fn lsp_completion_item_from_query(file_kind: &str, item: OmenaQueryCompletionItemV0) -> Value {
857 let kind = match (file_kind, item.item_kind) {
858 ("style", "cssModuleSelector") => 7,
859 (_, "cssModuleSelector") | (_, "cssCustomProperty") => 10,
860 _ => 1,
861 };
862 json!({
863 "label": item.label,
864 "kind": kind,
865 "sortText": item.sort_text,
866 "detail": item.detail,
867 "insertText": item.insert_text,
868 "data": {
869 "source": item.source,
870 "rankingSource": item.ranking_source,
871 },
872 })
873}
874
875fn resolve_style_diagnostics(state: &LspShellState, params: Option<&Value>) -> Value {
876 let document_uri = document_uri_from_params(params);
877 resolve_style_diagnostics_for_uri(state, document_uri.as_str())
878}
879
880fn resolve_source_diagnostics(state: &LspShellState, params: Option<&Value>) -> Value {
881 let document_uri = document_uri_from_params(params);
882 resolve_source_diagnostics_for_uri(state, document_uri.as_str())
883}
884
885fn resolve_cascade_at_position(state: &LspShellState, params: Option<&Value>) -> Value {
886 let document_uri = document_uri_from_params(params);
887 let Some(position) = lsp_position_from_params(params) else {
888 return Value::Null;
889 };
890 let Some(document) = state.document(&document_uri) else {
891 return Value::Null;
892 };
893 if !is_style_document_uri(document.uri.as_str()) {
894 return Value::Null;
895 }
896 let Some(engine_input) = query_engine_input_from_params(params) else {
897 return Value::Null;
898 };
899
900 let include_categorical_evidence = params
901 .and_then(|value| value.get("context"))
902 .and_then(|value| value.get("includeCategoricalEvidence"))
903 .and_then(Value::as_bool)
904 .unwrap_or(false);
905
906 read_omena_query_cascade_at_position_with_categorical_evidence(
907 document.uri.as_str(),
908 document.text.as_str(),
909 &engine_input,
910 position,
911 include_categorical_evidence,
912 )
913 .map(|result| json!(result))
914 .unwrap_or(Value::Null)
915}
916
917fn resolve_style_context_index(state: &LspShellState, params: Option<&Value>) -> Value {
918 let document_uri = document_uri_from_params(params);
919 let Some(document) = state.document(&document_uri) else {
920 return Value::Null;
921 };
922 if !is_style_document_uri(document.uri.as_str()) {
923 return Value::Null;
924 }
925 let Some(engine_input) = query_engine_input_from_params(params) else {
926 return Value::Null;
927 };
928
929 read_omena_query_style_context_index(
930 document.uri.as_str(),
931 document.text.as_str(),
932 &engine_input,
933 )
934 .map(|result| json!(result))
935 .unwrap_or(Value::Null)
936}
937
938fn query_engine_input_from_params(params: Option<&Value>) -> Option<OmenaQueryEngineInputV2> {
939 if let Some(engine_input) = params.and_then(|value| value.get("engineInput")) {
940 return serde_json::from_value(engine_input.clone()).ok();
941 }
942
943 serde_json::from_value(json!({
944 "version": "2",
945 "sources": [],
946 "styles": [],
947 "typeFacts": [],
948 }))
949 .ok()
950}
951
952fn resolve_document_diagnostics_for_uri(state: &LspShellState, document_uri: &str) -> Value {
953 if is_style_document_uri(document_uri) {
954 resolve_style_diagnostics_for_uri(state, document_uri)
955 } else {
956 resolve_source_diagnostics_for_uri(state, document_uri)
957 }
958}
959
960fn resolve_style_diagnostics_for_uri(state: &LspShellState, document_uri: &str) -> Value {
961 let Some(document) = state.document(document_uri) else {
962 return json!([]);
963 };
964 let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
965 return json!([]);
966 };
967
968 let query_candidates = candidates
969 .iter()
970 .map(query_style_hover_candidate_from_lsp)
971 .collect::<Vec<_>>();
972 let style_sources = style_sources_from_open_documents(
973 state,
974 document.workspace_folder_uri.as_deref(),
975 Some(document.uri.as_str()),
976 );
977 let source_documents =
978 source_documents_from_open_documents(state, document.workspace_folder_uri.as_deref());
979 let external_sifs = state.resolution.external_sifs.as_slice();
984 let external_mode = if external_sifs.is_empty() {
985 OmenaQueryExternalModuleModeV0::Ignored
986 } else {
987 OmenaQueryExternalModuleModeV0::Sif
988 };
989 let resolution_inputs =
993 resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref());
994 let diagnostics_summary =
995 summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs_and_resolution_inputs(
996 document.uri.as_str(),
997 style_sources.as_slice(),
998 source_documents.as_slice(),
999 state.resolution.package_manifests.as_slice(),
1000 None,
1001 external_mode,
1002 external_sifs,
1003 &resolution_inputs,
1004 )
1005 .unwrap_or_else(|| {
1006 summarize_omena_query_style_diagnostics_for_file(
1007 document.uri.as_str(),
1008 document.text.as_str(),
1009 query_candidates.as_slice(),
1010 )
1011 });
1012 let diagnostics = diagnostics_summary
1013 .diagnostics
1014 .into_iter()
1015 .map(|diagnostic| {
1016 let tags = diagnostic.tags;
1017 let query_severity = diagnostic.severity;
1018 let mut data = serde_json::Map::new();
1019 data.insert("querySeverity".to_string(), json!(query_severity));
1020 data.insert("provenance".to_string(), json!(diagnostic.provenance));
1021 if let Some(create_custom_property) = diagnostic.create_custom_property {
1022 data.insert(
1023 "createCustomProperty".to_string(),
1024 json!(create_custom_property),
1025 );
1026 }
1027
1028 let mut lsp_diagnostic = json!({
1029 "range": diagnostic.range,
1030 "severity": lsp_diagnostic_severity(query_severity, state.diagnostics.severity),
1031 "code": diagnostic.code,
1032 "source": "omena-css",
1033 "message": diagnostic.message,
1034 "data": Value::Object(data),
1035 });
1036 if !tags.is_empty() {
1037 lsp_diagnostic["tags"] = json!(tags);
1038 }
1039 lsp_diagnostic
1040 })
1041 .collect::<Vec<_>>();
1042
1043 json!(diagnostics)
1044}
1045
1046fn resolve_source_diagnostics_for_uri(state: &LspShellState, document_uri: &str) -> Value {
1047 let Some(document) = state.document(document_uri) else {
1048 return json!([]);
1049 };
1050 if is_style_document_uri(document.uri.as_str()) {
1051 return json!([]);
1052 }
1053
1054 let candidates = resolve_source_provider_candidates(state, document)
1055 .unresolved
1056 .into_iter()
1057 .filter(|candidate| candidate.kind == "sourceSelectorReference")
1058 .filter_map(|candidate| {
1059 let (target_style_uri, target_style_document) = source_selector_diagnostic_target(
1060 state,
1061 &candidate,
1062 document.workspace_folder_uri.as_deref(),
1063 )?;
1064 Some(OmenaQuerySourceMissingSelectorDiagnosticCandidateV0 {
1065 target_style_uri,
1066 target_style_source: target_style_document.text.clone(),
1067 selector_name: candidate.name,
1068 source_reference_range: candidate.range,
1069 })
1070 })
1071 .collect::<Vec<_>>();
1072 let diagnostics: Vec<Value> = summarize_omena_query_source_diagnostics_for_file(
1073 document.uri.as_str(),
1074 candidates.as_slice(),
1075 )
1076 .diagnostics
1077 .into_iter()
1078 .filter_map(|diagnostic| {
1079 let query_severity = diagnostic.severity;
1080 let create_selector = diagnostic.create_selector?;
1081 let mut data = serde_json::Map::new();
1082 data.insert("querySeverity".to_string(), json!(query_severity));
1083 data.insert("provenance".to_string(), json!(diagnostic.provenance));
1084 data.insert("createSelector".to_string(), json!(create_selector));
1085
1086 Some(json!({
1087 "range": diagnostic.range,
1088 "severity": lsp_diagnostic_severity(query_severity, state.diagnostics.severity),
1089 "code": diagnostic.code,
1090 "source": "omena-css",
1091 "message": diagnostic.message,
1092 "data": Value::Object(data),
1093 }))
1094 })
1095 .collect();
1096
1097 json!(diagnostics)
1098}
1099
1100fn lsp_diagnostic_severity(query_severity: &str, configured_severity: u8) -> u8 {
1101 if configured_severity != 2 {
1102 return configured_severity;
1103 }
1104 match query_severity {
1105 "error" => 1,
1106 "warning" => 2,
1107 "information" => 3,
1108 "hint" => 4,
1109 _ => configured_severity,
1110 }
1111}
1112
1113fn source_selector_diagnostic_target<'a>(
1114 state: &'a LspShellState,
1115 candidate: &LspStyleHoverCandidate,
1116 workspace_folder_uri: Option<&str>,
1117) -> Option<(String, &'a LspTextDocumentState)> {
1118 if let Some(target_style_uri) = candidate.target_style_uri.as_deref() {
1119 let target_document = state.document(target_style_uri)?;
1120 if !document_has_style_index(target_document)
1121 || !workspace_folder_compatible(workspace_folder_uri, target_document)
1122 {
1123 return None;
1124 }
1125 return Some((target_document.uri.clone(), target_document));
1126 }
1127
1128 first_style_document_for_workspace(state, workspace_folder_uri)
1129}
1130
1131fn resolve_lsp_code_actions(state: &LspShellState, params: Option<&Value>) -> Value {
1132 let diagnostics = params
1133 .and_then(|value| value.get("context"))
1134 .and_then(|value| value.get("diagnostics"))
1135 .and_then(Value::as_array)
1136 .map(Vec::as_slice)
1137 .unwrap_or(&[]);
1138
1139 let mut actions: Vec<Value> = diagnostics
1140 .iter()
1141 .enumerate()
1142 .filter_map(|(index, diagnostic)| {
1143 let payload = diagnostic
1144 .pointer("/data/createCustomProperty")
1145 .and_then(Value::as_object)?;
1146 let uri = payload.get("uri").and_then(Value::as_str)?;
1147 let range = payload.get("range")?;
1148 let new_text = payload.get("newText").and_then(Value::as_str)?;
1149 let property_name = payload.get("propertyName").and_then(Value::as_str)?;
1150 let mut changes = serde_json::Map::new();
1151 changes.insert(
1152 uri.to_string(),
1153 json!([
1154 {
1155 "range": range,
1156 "newText": new_text,
1157 },
1158 ]),
1159 );
1160
1161 Some(json!({
1162 "title": format!("Add '{}' to {}", property_name, file_label_from_uri(uri)),
1163 "kind": "quickfix",
1164 "diagnostics": [diagnostic],
1165 "edit": {
1166 "changes": Value::Object(changes),
1167 },
1168 "data": {
1169 "source": "omenaQueryStyleDiagnosticsForFile",
1170 "diagnosticIndex": index,
1171 },
1172 }))
1173 })
1174 .chain(diagnostics.iter().enumerate().filter_map(|(index, diagnostic)| {
1175 let payload = diagnostic
1176 .pointer("/data/createSelector")
1177 .and_then(Value::as_object)?;
1178 let uri = payload.get("uri").and_then(Value::as_str)?;
1179 let range = payload.get("range")?;
1180 let new_text = payload.get("newText").and_then(Value::as_str)?;
1181 let selector_name = payload.get("selectorName").and_then(Value::as_str)?;
1182 let mut changes = serde_json::Map::new();
1183 changes.insert(
1184 uri.to_string(),
1185 json!([
1186 {
1187 "range": range,
1188 "newText": new_text,
1189 },
1190 ]),
1191 );
1192
1193 Some(json!({
1194 "title": format!("Add '.{}' to {}", selector_name, file_label_from_uri(uri)),
1195 "kind": "quickfix",
1196 "diagnostics": [diagnostic],
1197 "edit": {
1198 "changes": Value::Object(changes),
1199 },
1200 "data": {
1201 "source": "omenaQuerySourceSyntaxIndex",
1202 "diagnosticIndex": index,
1203 },
1204 }))
1205 }))
1206 .collect();
1207
1208 if diagnostics.is_empty() {
1209 actions.extend(resolve_lsp_refactor_code_actions(state, params));
1210 }
1211
1212 if actions.is_empty() {
1213 Value::Null
1214 } else {
1215 json!(actions)
1216 }
1217}
1218
1219fn resolve_lsp_refactor_code_actions(state: &LspShellState, params: Option<&Value>) -> Vec<Value> {
1220 let document_uri = document_uri_from_params(params);
1221 let Some(document) = state.document(document_uri.as_str()) else {
1222 return Vec::new();
1223 };
1224 if !is_style_document_uri(document.uri.as_str()) {
1225 return Vec::new();
1226 }
1227 let Some(range) = params
1228 .and_then(|value| value.get("range"))
1229 .and_then(lsp_range_from_value)
1230 else {
1231 return Vec::new();
1232 };
1233
1234 let style_sources = style_sources_from_open_documents(
1235 state,
1236 document.workspace_folder_uri.as_deref(),
1237 Some(document.uri.as_str()),
1238 );
1239 let inline_actions = summarize_omena_query_style_inline_code_actions(
1240 document.uri.as_str(),
1241 style_sources.as_slice(),
1242 range,
1243 &[],
1244 )
1245 .actions;
1246 if !inline_actions.is_empty() {
1247 return render_omena_query_lsp_code_actions(inline_actions);
1248 }
1249
1250 let extract_actions = summarize_omena_query_style_extract_code_actions(
1251 document.uri.as_str(),
1252 document.text.as_str(),
1253 range,
1254 )
1255 .actions;
1256 render_omena_query_lsp_code_actions(extract_actions)
1257}
1258
1259fn style_sources_from_open_documents(
1260 state: &LspShellState,
1261 workspace_folder_uri: Option<&str>,
1262 required_document_uri: Option<&str>,
1263) -> Vec<OmenaQueryStyleSourceInputV0> {
1264 let mut sources = state
1265 .documents
1266 .values()
1267 .filter(|document| {
1268 is_style_document_uri(document.uri.as_str())
1269 && workspace_folder_compatible(workspace_folder_uri, document)
1270 })
1271 .map(|document| OmenaQueryStyleSourceInputV0 {
1272 style_path: document.uri.clone(),
1273 style_source: document.text.clone(),
1274 })
1275 .collect::<Vec<_>>();
1276 if let Some(required_document_uri) = required_document_uri
1277 && !sources
1278 .iter()
1279 .any(|source| source.style_path == required_document_uri)
1280 && let Some(document) = state.document(required_document_uri)
1281 {
1282 sources.push(OmenaQueryStyleSourceInputV0 {
1283 style_path: document.uri.clone(),
1284 style_source: document.text.clone(),
1285 });
1286 }
1287 sources
1288}
1289
1290fn source_documents_from_open_documents(
1291 state: &LspShellState,
1292 workspace_folder_uri: Option<&str>,
1293) -> Vec<OmenaQuerySourceDocumentInputV0> {
1294 state
1295 .documents
1296 .values()
1297 .filter(|document| {
1298 !is_style_document_uri(document.uri.as_str())
1299 && workspace_folder_compatible(workspace_folder_uri, document)
1300 })
1301 .map(|document| OmenaQuerySourceDocumentInputV0 {
1302 source_path: document.uri.clone(),
1303 source_source: document.text.clone(),
1304 })
1305 .collect()
1306}
1307
1308fn render_omena_query_lsp_code_actions(actions: Vec<OmenaQueryCodeActionV0>) -> Vec<Value> {
1309 actions
1310 .into_iter()
1311 .enumerate()
1312 .map(|(index, action)| {
1313 let mut changes_by_uri = BTreeMap::<String, Vec<Value>>::new();
1314 for edit in action.edits {
1315 changes_by_uri.entry(edit.uri).or_default().push(json!({
1316 "range": edit.range,
1317 "newText": edit.new_text,
1318 }));
1319 }
1320
1321 let changes = changes_by_uri
1322 .into_iter()
1323 .map(|(uri, edits)| (uri, Value::Array(edits)))
1324 .collect::<serde_json::Map<_, _>>();
1325
1326 json!({
1327 "title": action.title,
1328 "kind": action.kind,
1329 "edit": {
1330 "changes": Value::Object(changes),
1331 },
1332 "data": {
1333 "source": action.source,
1334 "actionIndex": index,
1335 },
1336 })
1337 })
1338 .collect()
1339}
1340
1341fn resolve_lsp_code_lens(state: &LspShellState, params: Option<&Value>) -> Value {
1342 let document_uri = document_uri_from_params(params);
1343 let Some(document) = state.document(document_uri.as_str()) else {
1344 return Value::Null;
1345 };
1346 let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
1347 return Value::Null;
1348 };
1349
1350 let mut lenses = Vec::new();
1351 let mut emitted_selectors = BTreeSet::new();
1352 let reference_locations_by_name = selector_reference_locations_by_name_from_open_documents(
1353 state,
1354 document.workspace_folder_uri.as_deref(),
1355 Some(document.uri.as_str()),
1356 );
1357 for candidate in candidates
1358 .iter()
1359 .filter(|candidate| candidate.kind == "selector")
1360 {
1361 if !emitted_selectors.insert(candidate.name.as_str()) {
1362 continue;
1363 }
1364 let locations = reference_locations_by_name
1365 .get(candidate.name.as_str())
1366 .cloned()
1367 .unwrap_or_default();
1368 if locations.is_empty() {
1369 continue;
1370 }
1371 let position = candidate.range.start;
1372 lenses.push(json!({
1373 "range": {
1374 "start": position,
1375 "end": position,
1376 },
1377 "command": {
1378 "title": reference_lens_title(locations.len()),
1379 "command": "editor.action.showReferences",
1380 "arguments": [
1381 document.uri.as_str(),
1382 position,
1383 locations,
1384 ],
1385 },
1386 }));
1387 }
1388 lenses.sort_by_key(lsp_range_start_sort_key);
1389
1390 if lenses.is_empty() {
1391 Value::Null
1392 } else {
1393 json!(lenses)
1394 }
1395}
1396
1397fn selector_reference_locations_from_open_documents(
1398 state: &LspShellState,
1399 selector_name: &str,
1400 workspace_folder_uri: Option<&str>,
1401 target_style_uri: Option<&str>,
1402) -> Vec<Value> {
1403 let definitions =
1404 style_selector_definitions_from_open_documents(state, "", workspace_folder_uri)
1405 .iter()
1406 .map(|(uri, definition)| query_style_selector_definition_for_matching(uri, definition))
1407 .collect::<Vec<_>>();
1408 let query_target_style_uri = query_target_style_uri_for_matching(target_style_uri);
1409 let mut references = Vec::new();
1410 for document in state.documents.values() {
1411 if is_style_document_uri(document.uri.as_str()) {
1412 continue;
1413 }
1414 if !workspace_folder_compatible(workspace_folder_uri, document) {
1415 continue;
1416 }
1417 references.extend(
1418 collect_source_selector_reference_candidates(state, document)
1419 .iter()
1420 .map(|candidate| {
1421 query_source_selector_reference_candidate_for_matching(document, candidate)
1422 }),
1423 );
1424 }
1425 summarize_omena_query_refs_for_class(
1426 selector_name,
1427 query_target_style_uri.as_deref(),
1428 false,
1429 definitions.as_slice(),
1430 references.as_slice(),
1431 )
1432 .locations
1433 .into_iter()
1434 .map(|location| json!({ "uri": location.uri, "range": location.range }))
1435 .collect()
1436}
1437
1438fn selector_reference_locations_by_name_from_open_documents(
1439 state: &LspShellState,
1440 workspace_folder_uri: Option<&str>,
1441 target_style_uri: Option<&str>,
1442) -> BTreeMap<String, Vec<Value>> {
1443 let mut locations_by_name: BTreeMap<String, Vec<Value>> = BTreeMap::new();
1444 let definitions =
1445 style_selector_definitions_from_open_documents(state, "", workspace_folder_uri);
1446 for document in state.documents.values() {
1447 if is_style_document_uri(document.uri.as_str()) {
1448 continue;
1449 }
1450 if !workspace_folder_compatible(workspace_folder_uri, document) {
1451 continue;
1452 }
1453 for candidate in collect_source_selector_reference_candidates(state, document) {
1454 if !source_candidate_matches_target_style(&candidate, target_style_uri) {
1455 continue;
1456 }
1457 for selector_name in source_candidate_selector_names(
1458 &candidate,
1459 definitions.as_slice(),
1460 target_style_uri,
1461 ) {
1462 locations_by_name
1463 .entry(selector_name)
1464 .or_default()
1465 .push(json!({
1466 "uri": document.uri.as_str(),
1467 "range": candidate.range,
1468 }));
1469 }
1470 }
1471 }
1472 for locations in locations_by_name.values_mut() {
1473 locations.sort_by_key(location_sort_key);
1474 locations
1475 .dedup_by(|left, right| location_identity_key(left) == location_identity_key(right));
1476 }
1477 locations_by_name
1478}
1479
1480fn source_candidate_selector_names(
1481 candidate: &LspStyleHoverCandidate,
1482 definitions: &[(String, LspStyleHoverCandidate)],
1483 target_style_uri: Option<&str>,
1484) -> Vec<String> {
1485 let query_definitions = definitions
1486 .iter()
1487 .map(|(uri, definition)| query_style_selector_definition_for_matching(uri, definition))
1488 .collect::<Vec<_>>();
1489 let query_target_style_uri = query_target_style_uri_for_matching(target_style_uri);
1490 resolve_omena_query_source_candidate_selector_names(
1491 &query_source_selector_candidate_for_matching(candidate),
1492 query_definitions.as_slice(),
1493 query_target_style_uri.as_deref(),
1494 )
1495}
1496
1497fn source_candidate_matches_target_style(
1498 candidate: &LspStyleHoverCandidate,
1499 target_style_uri: Option<&str>,
1500) -> bool {
1501 target_style_uri.is_none_or(|target_uri| {
1502 candidate
1503 .target_style_uri
1504 .as_deref()
1505 .is_none_or(|candidate_target_uri| {
1506 file_uri_equivalent(candidate_target_uri, target_uri)
1507 })
1508 })
1509}
1510
1511fn sass_symbol_definitions_for_candidate(
1512 state: &LspShellState,
1513 document: &LspTextDocumentState,
1514 candidate: &LspStyleHoverCandidate,
1515) -> Vec<(String, LspStyleHoverCandidate)> {
1516 let Some(symbol_kind) = sass_symbol_kind_from_candidate_kind(candidate.kind) else {
1517 return Vec::new();
1518 };
1519 if is_sass_symbol_declaration_kind(candidate.kind) {
1520 return vec![(document.uri.clone(), candidate.clone())];
1521 }
1522
1523 let mut definitions = if candidate.namespace.is_none() {
1524 sass_symbol_declarations_in_document(document, symbol_kind, candidate)
1525 } else {
1526 Vec::new()
1527 };
1528 if candidate.namespace.is_none() && !definitions.is_empty() {
1529 return definitions;
1530 }
1531
1532 for target_uri in sass_module_target_uris_for_candidate(state, document, candidate) {
1533 definitions.extend(sass_symbol_declarations_for_uri(
1534 state,
1535 target_uri.as_str(),
1536 symbol_kind,
1537 candidate,
1538 ));
1539 }
1540 definitions.sort_by_key(|(uri, target)| {
1541 (
1542 uri.clone(),
1543 target.range.start.line,
1544 target.range.start.character,
1545 )
1546 });
1547 definitions.dedup_by(|left, right| {
1548 left.0 == right.0
1549 && left.1.kind == right.1.kind
1550 && left.1.name == right.1.name
1551 && left.1.range == right.1.range
1552 });
1553 definitions
1554}
1555
1556fn sass_symbol_declarations_for_uri(
1557 state: &LspShellState,
1558 target_uri: &str,
1559 symbol_kind: &str,
1560 candidate: &LspStyleHoverCandidate,
1561) -> Vec<(String, LspStyleHoverCandidate)> {
1562 if let Some(target_document) = state.document(target_uri) {
1563 return sass_symbol_declarations_with_forwards(
1564 state,
1565 target_document,
1566 symbol_kind,
1567 candidate,
1568 &mut BTreeSet::new(),
1569 );
1570 }
1571 let Some((_, candidates)) = style_hover_candidates_for_uri(state, target_uri) else {
1572 return Vec::new();
1573 };
1574 let query_candidates = candidates
1575 .iter()
1576 .map(query_style_hover_candidate_from_lsp)
1577 .collect::<Vec<_>>();
1578 resolve_omena_query_sass_symbol_declarations(
1579 query_candidates.as_slice(),
1580 symbol_kind,
1581 candidate.name.as_str(),
1582 )
1583 .into_iter()
1584 .map(lsp_style_hover_candidate_from_query)
1585 .map(|target| (target_uri.to_string(), target))
1586 .collect()
1587}
1588
1589fn sass_symbol_declarations_in_document(
1590 document: &LspTextDocumentState,
1591 symbol_kind: &str,
1592 candidate: &LspStyleHoverCandidate,
1593) -> Vec<(String, LspStyleHoverCandidate)> {
1594 let query_candidates = document
1595 .style_candidates
1596 .iter()
1597 .map(query_style_hover_candidate_from_lsp)
1598 .collect::<Vec<_>>();
1599 resolve_omena_query_sass_symbol_declarations(
1600 query_candidates.as_slice(),
1601 symbol_kind,
1602 candidate.name.as_str(),
1603 )
1604 .into_iter()
1605 .map(lsp_style_hover_candidate_from_query)
1606 .map(|target| (document.uri.clone(), target))
1607 .collect()
1608}
1609
1610fn sass_module_target_uris_for_candidate(
1611 state: &LspShellState,
1612 document: &LspTextDocumentState,
1613 candidate: &LspStyleHoverCandidate,
1614) -> Vec<String> {
1615 let Some(sources) =
1616 summarize_omena_query_sass_module_sources(document.uri.as_str(), document.text.as_str())
1617 else {
1618 return Vec::new();
1619 };
1620 let mut uris = Vec::new();
1621 for source in resolve_omena_query_sass_module_use_sources_for_candidate(
1622 &sources,
1623 candidate.namespace.as_deref(),
1624 ) {
1625 if let Some(uri) = resolve_lsp_style_uri_for_specifier(state, document, source.as_str()) {
1626 uris.push(uri);
1627 }
1628 }
1629 for forward_source in resolve_omena_query_sass_forward_sources(&sources) {
1630 if let Some(uri) =
1631 resolve_lsp_style_uri_for_specifier(state, document, forward_source.as_str())
1632 {
1633 uris.push(uri.clone());
1634 if let Some(target_document) = state.document(uri.as_str()) {
1635 uris.extend(sass_forward_module_target_uris(
1636 state,
1637 target_document,
1638 &mut BTreeSet::new(),
1639 ));
1640 }
1641 }
1642 }
1643 uris.sort();
1644 uris.dedup();
1645 uris
1646}
1647
1648fn sass_symbol_declarations_with_forwards(
1649 state: &LspShellState,
1650 document: &LspTextDocumentState,
1651 symbol_kind: &str,
1652 candidate: &LspStyleHoverCandidate,
1653 visited: &mut BTreeSet<String>,
1654) -> Vec<(String, LspStyleHoverCandidate)> {
1655 if !visited.insert(document.uri.clone()) {
1656 return Vec::new();
1657 }
1658 let mut definitions = sass_symbol_declarations_in_document(document, symbol_kind, candidate);
1659 let Some(sources) =
1660 summarize_omena_query_sass_module_sources(document.uri.as_str(), document.text.as_str())
1661 else {
1662 return definitions;
1663 };
1664 for forward_source in resolve_omena_query_sass_forward_sources(&sources) {
1665 let Some(uri) =
1666 resolve_lsp_style_uri_for_specifier(state, document, forward_source.as_str())
1667 else {
1668 continue;
1669 };
1670 let Some(target_document) = state.document(uri.as_str()) else {
1671 continue;
1672 };
1673 definitions.extend(sass_symbol_declarations_with_forwards(
1674 state,
1675 target_document,
1676 symbol_kind,
1677 candidate,
1678 visited,
1679 ));
1680 }
1681 definitions
1682}
1683
1684fn sass_forward_module_target_uris(
1685 state: &LspShellState,
1686 document: &LspTextDocumentState,
1687 visited: &mut BTreeSet<String>,
1688) -> Vec<String> {
1689 if !visited.insert(document.uri.clone()) {
1690 return Vec::new();
1691 }
1692 let Some(sources) =
1693 summarize_omena_query_sass_module_sources(document.uri.as_str(), document.text.as_str())
1694 else {
1695 return Vec::new();
1696 };
1697 let mut uris = Vec::new();
1698 for forward_source in resolve_omena_query_sass_forward_sources(&sources) {
1699 if let Some(uri) =
1700 resolve_lsp_style_uri_for_specifier(state, document, forward_source.as_str())
1701 {
1702 uris.push(uri.clone());
1703 }
1704 }
1705 uris.sort();
1706 uris.dedup();
1707 uris
1708}
1709
1710fn resolve_lsp_style_uri_for_specifier(
1711 state: &LspShellState,
1712 document: &LspTextDocumentState,
1713 specifier: &str,
1714) -> Option<String> {
1715 let resolution_inputs =
1716 resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref());
1717 resolve_omena_query_style_uri_for_specifier_with_resolution_inputs(
1718 document.uri.as_str(),
1719 document.workspace_folder_uri.as_deref(),
1720 specifier,
1721 &resolution_inputs,
1722 )
1723}
1724
1725fn sass_symbol_reference_matches(
1726 candidate: &LspStyleHoverCandidate,
1727 target: &LspStyleHoverCandidate,
1728) -> bool {
1729 is_sass_symbol_reference_kind(target.kind) && sass_symbol_target_matches(candidate, target)
1730}
1731
1732fn sass_symbol_target_matches(
1733 candidate: &LspStyleHoverCandidate,
1734 target: &LspStyleHoverCandidate,
1735) -> bool {
1736 omena_query_sass_symbol_target_matches(
1737 candidate.kind,
1738 candidate.name.as_str(),
1739 candidate.namespace.as_deref(),
1740 target.kind,
1741 target.name.as_str(),
1742 target.namespace.as_deref(),
1743 )
1744}
1745
1746fn render_sass_symbol_label(candidate: &LspStyleHoverCandidate) -> String {
1747 let namespace_prefix = candidate
1748 .namespace
1749 .as_deref()
1750 .map(|namespace| format!("{namespace}."))
1751 .unwrap_or_default();
1752 match sass_symbol_kind_from_candidate_kind(candidate.kind) {
1753 Some("variable") => format!("{namespace_prefix}${}", candidate.name),
1754 Some("mixin") if is_sass_symbol_declaration_kind(candidate.kind) => {
1755 format!("@mixin {}", candidate.name)
1756 }
1757 Some("mixin") => format!("@include {namespace_prefix}{}", candidate.name),
1758 Some("function") => format!("{namespace_prefix}{}()", candidate.name),
1759 _ => candidate.name.clone(),
1760 }
1761}
1762
1763fn reference_lens_title(count: usize) -> String {
1764 if count == 1 {
1765 "1 reference".to_string()
1766 } else {
1767 format!("{count} references")
1768 }
1769}
1770
1771fn resolve_lsp_prepare_rename(state: &LspShellState, params: Option<&Value>) -> Value {
1772 if let Some((_, candidate)) = source_selector_candidate_for_params(state, params) {
1773 return json!({
1774 "range": candidate.range,
1775 "placeholder": candidate.name,
1776 });
1777 }
1778
1779 let Some((_, candidate, _)) = style_candidates_for_params(state, params) else {
1780 return Value::Null;
1781 };
1782
1783 json!({
1784 "range": candidate.range,
1785 "placeholder": rename_placeholder(&candidate),
1786 })
1787}
1788
1789fn resolve_lsp_rename(state: &LspShellState, params: Option<&Value>) -> Value {
1790 let Some(new_name) = params
1791 .and_then(|value| value.get("newName"))
1792 .and_then(Value::as_str)
1793 .filter(|value| !value.is_empty())
1794 else {
1795 return Value::Null;
1796 };
1797 if let Some((document_uri, candidate)) = source_selector_candidate_for_params(state, params) {
1798 let workspace_folder_uri = state
1799 .document(document_uri.as_str())
1800 .and_then(|document| document.workspace_folder_uri.as_deref());
1801 return resolve_selector_rename(
1802 state,
1803 workspace_folder_uri,
1804 candidate.target_style_uri.as_deref(),
1805 candidate.name.as_str(),
1806 new_name,
1807 );
1808 }
1809
1810 let Some((document_uri, candidate, candidates)) = style_candidates_for_params(state, params)
1811 else {
1812 return Value::Null;
1813 };
1814
1815 if candidate.kind == "selector" {
1816 let workspace_folder_uri = state
1817 .document(document_uri.as_str())
1818 .and_then(|document| document.workspace_folder_uri.as_deref());
1819 return resolve_selector_rename(
1820 state,
1821 workspace_folder_uri,
1822 Some(document_uri.as_str()),
1823 candidate.name.as_str(),
1824 new_name,
1825 );
1826 }
1827
1828 let replacement = match candidate.kind {
1829 "customPropertyReference" | "customPropertyDeclaration" => new_name.to_string(),
1830 _ => return Value::Null,
1831 };
1832 let mut targets: Vec<&LspStyleHoverCandidate> = candidates
1833 .iter()
1834 .filter(|target| rename_target_matches(&candidate, target))
1835 .collect();
1836 targets.sort_by_key(|target| {
1837 (
1838 target.range.start.line,
1839 target.range.start.character,
1840 target.range.end.line,
1841 target.range.end.character,
1842 )
1843 });
1844 let edits: Vec<Value> = targets
1845 .into_iter()
1846 .map(|target| {
1847 json!({
1848 "range": target.range,
1849 "newText": replacement,
1850 })
1851 })
1852 .collect();
1853 if edits.is_empty() {
1854 return Value::Null;
1855 }
1856
1857 let mut changes = serde_json::Map::new();
1858 changes.insert(document_uri, json!(edits));
1859 json!({
1860 "changes": Value::Object(changes),
1861 })
1862}
1863
1864fn style_candidates_for_params(
1865 state: &LspShellState,
1866 params: Option<&Value>,
1867) -> Option<(String, LspStyleHoverCandidate, Vec<LspStyleHoverCandidate>)> {
1868 let document_uri = document_uri_from_params(params);
1869 let position = lsp_position_from_params(params)?;
1870 let document = state.document(document_uri.as_str())?;
1871 let (_, candidates) = style_hover_candidates_for_document(document)?;
1872 let candidate = candidates
1873 .iter()
1874 .find(|candidate| parser_range_contains_position(&candidate.range, position))?
1875 .clone();
1876 Some((document_uri, candidate, candidates))
1877}
1878
1879fn rename_placeholder(candidate: &LspStyleHoverCandidate) -> &str {
1880 candidate.name.as_str()
1881}
1882
1883fn rename_target_matches(
1884 candidate: &LspStyleHoverCandidate,
1885 target: &LspStyleHoverCandidate,
1886) -> bool {
1887 match candidate.kind {
1888 "selector" => target.kind == "selector" && target.name == candidate.name,
1889 "customPropertyReference" | "customPropertyDeclaration" => {
1890 target.name == candidate.name && target.kind.starts_with("customProperty")
1891 }
1892 kind if is_sass_symbol_candidate_kind(kind) => {
1893 sass_symbol_target_matches(candidate, target)
1894 }
1895 _ => false,
1896 }
1897}
1898
1899fn resolve_lsp_hover(state: &LspShellState, params: Option<&Value>) -> Value {
1900 let document_uri = document_uri_from_params(params);
1901 if let Some(document) = state.document(document_uri.as_str())
1902 && !is_style_document_uri(document.uri.as_str())
1903 {
1904 return resolve_source_lsp_hover(state, document, params);
1905 }
1906
1907 let candidates = resolve_style_hover_candidates(state, params);
1908 let Some(candidate) = candidates.candidates.first() else {
1909 return Value::Null;
1910 };
1911 let Some(document) = state.document(document_uri.as_str()) else {
1912 return Value::Null;
1913 };
1914 if is_sass_symbol_reference_kind(candidate.kind)
1915 && let Some((target_uri, target)) =
1916 sass_symbol_definitions_for_candidate(state, document, candidate)
1917 .into_iter()
1918 .next()
1919 && let Some(target_text) = style_text_for_uri(state, target_uri.as_str())
1920 {
1921 return json!({
1922 "contents": {
1923 "kind": "markdown",
1924 "value": render_style_hover_candidate_markdown(
1925 target_uri.as_str(),
1926 target_text.as_str(),
1927 &target,
1928 ),
1929 },
1930 "range": candidate.range,
1931 });
1932 }
1933
1934 json!({
1935 "contents": {
1936 "kind": "markdown",
1937 "value": render_style_hover_candidate_markdown(
1938 document.uri.as_str(),
1939 document.text.as_str(),
1940 candidate,
1941 ),
1942 },
1943 "range": candidate.range,
1944 })
1945}
1946
1947fn resolve_source_lsp_hover(
1948 state: &LspShellState,
1949 document: &LspTextDocumentState,
1950 params: Option<&Value>,
1951) -> Value {
1952 let Some(position) = lsp_position_from_params(params) else {
1953 return Value::Null;
1954 };
1955 let candidates = source_selector_candidates_at_position(state, document, position);
1956 let Some(candidate) = candidates.first() else {
1957 return Value::Null;
1958 };
1959 let definitions = style_selector_definitions_for_source_candidates(
1960 state,
1961 candidates.as_slice(),
1962 document.workspace_folder_uri.as_deref(),
1963 );
1964 let value = render_source_hover_definitions_markdown(state, definitions.as_slice())
1965 .unwrap_or_else(|| format!("**`.{}`**", candidate.name));
1966
1967 json!({
1968 "contents": {
1969 "kind": "markdown",
1970 "value": value,
1971 },
1972 "range": candidate.range,
1973 })
1974}
1975
1976fn resolve_source_lsp_definition(
1977 state: &LspShellState,
1978 document: &LspTextDocumentState,
1979 position: ParserPositionV0,
1980) -> Value {
1981 let candidates = source_selector_candidates_at_position(state, document, position);
1982 if candidates.is_empty() {
1983 return Value::Null;
1984 };
1985 let definitions = style_selector_definitions_for_source_candidates(
1986 state,
1987 candidates.as_slice(),
1988 document.workspace_folder_uri.as_deref(),
1989 );
1990 if definitions.is_empty() {
1991 return Value::Null;
1992 }
1993
1994 json!(
1995 definitions
1996 .into_iter()
1997 .map(|(uri, definition)| json!({ "uri": uri, "range": definition.range }))
1998 .collect::<Vec<_>>()
1999 )
2000}
2001
2002fn resolve_source_lsp_references(
2003 state: &LspShellState,
2004 document: &LspTextDocumentState,
2005 position: ParserPositionV0,
2006 params: Option<&Value>,
2007) -> Value {
2008 let candidates = source_selector_candidates_at_position(state, document, position);
2009 if candidates.is_empty() {
2010 return Value::Null;
2011 };
2012 let include_declaration = include_declaration_from_params(params);
2013 let mut locations = Vec::new();
2014 if include_declaration {
2015 locations.extend(
2016 style_selector_definitions_for_source_candidates(
2017 state,
2018 candidates.as_slice(),
2019 document.workspace_folder_uri.as_deref(),
2020 )
2021 .into_iter()
2022 .map(|(uri, definition)| json!({ "uri": uri, "range": definition.range })),
2023 );
2024 }
2025 for candidate in candidates {
2026 if candidate.kind == "sourceSelectorPrefixReference" {
2027 let definitions = style_selector_definitions_from_open_documents(
2028 state,
2029 "",
2030 document.workspace_folder_uri.as_deref(),
2031 );
2032 for selector_name in source_candidate_selector_names(
2033 &candidate,
2034 definitions.as_slice(),
2035 candidate.target_style_uri.as_deref(),
2036 ) {
2037 locations.extend(selector_reference_locations_from_open_documents(
2038 state,
2039 selector_name.as_str(),
2040 document.workspace_folder_uri.as_deref(),
2041 candidate.target_style_uri.as_deref(),
2042 ));
2043 }
2044 } else {
2045 locations.extend(selector_reference_locations_from_open_documents(
2046 state,
2047 candidate.name.as_str(),
2048 document.workspace_folder_uri.as_deref(),
2049 candidate.target_style_uri.as_deref(),
2050 ));
2051 }
2052 }
2053 locations.sort_by_key(location_sort_key);
2054 locations.dedup();
2055
2056 if locations.is_empty() {
2057 Value::Null
2058 } else {
2059 json!(locations)
2060 }
2061}
2062
2063fn resolve_source_lsp_completion(
2064 state: &LspShellState,
2065 document: &LspTextDocumentState,
2066 params: Option<&Value>,
2067) -> Value {
2068 let Some(position) = lsp_position_from_params(params) else {
2069 return Value::Null;
2070 };
2071 let Some(context) = source_completion_context_at_position(state, document, position) else {
2072 return Value::Null;
2073 };
2074 let inferred_target_style_uri = context.target_style_uri.clone().or_else(|| {
2075 source_selector_candidate_at_position(state, document, position)
2076 .and_then(|candidate| candidate.target_style_uri)
2077 });
2078 let target_style_uri = inferred_target_style_uri
2079 .as_deref()
2080 .map(|uri| external_document_uri_for_query_uri(state, uri));
2081
2082 let candidates = style_selector_definitions_from_open_documents(
2083 state,
2084 "",
2085 document.workspace_folder_uri.as_deref(),
2086 )
2087 .into_iter()
2088 .filter(|(uri, _)| {
2089 target_style_uri
2090 .as_deref()
2091 .is_none_or(|target_uri| file_uri_equivalent(target_uri, uri))
2092 })
2093 .map(|(uri, definition)| {
2094 let file_uri = target_style_uri
2095 .as_deref()
2096 .filter(|target_uri| file_uri_equivalent(target_uri, uri.as_str()))
2097 .map(ToString::to_string)
2098 .unwrap_or(uri);
2099 OmenaQueryCompletionCandidateV0 {
2100 file_uri,
2101 name: definition.name,
2102 kind: definition.kind,
2103 range: definition.range,
2104 source: definition.source,
2105 }
2106 })
2107 .collect::<Vec<_>>();
2108 let completion = summarize_omena_query_source_completion_at_position(
2109 document.uri.as_str(),
2110 position,
2111 candidates.as_slice(),
2112 target_style_uri.as_deref(),
2113 context.value_prefix.as_deref(),
2114 context.preferred_selector_names.as_slice(),
2115 );
2116 let items: Vec<Value> = completion
2117 .items
2118 .into_iter()
2119 .map(|item| lsp_completion_item_from_query(completion.file_kind, item))
2120 .collect();
2121
2122 json!({
2123 "isIncomplete": false,
2124 "items": items,
2125 })
2126}
2127
2128struct SourceCompletionContext {
2129 target_style_uri: Option<String>,
2130 value_prefix: Option<String>,
2131 preferred_selector_names: Vec<String>,
2132}
2133
2134fn source_completion_context_at_position(
2135 state: &LspShellState,
2136 document: &LspTextDocumentState,
2137 position: ParserPositionV0,
2138) -> Option<SourceCompletionContext> {
2139 let offset = byte_offset_for_parser_position(document.text.as_str(), position)?;
2140 if let Some(target) = document
2141 .source_syntax_index
2142 .type_fact_targets
2143 .iter()
2144 .find(|target| offset >= target.byte_span.start && offset <= target.byte_span.end)
2145 {
2146 return Some(SourceCompletionContext {
2147 target_style_uri: target.target_style_uri.clone(),
2148 value_prefix: (!target.prefix.is_empty()).then(|| target.prefix.clone()),
2149 preferred_selector_names: source_completion_value_domain_selectors_for_target(
2150 document,
2151 target.byte_span,
2152 target.target_style_uri.as_deref(),
2153 ),
2154 });
2155 }
2156 if let Some(candidate) = source_selector_candidates_at_position(state, document, position)
2157 .into_iter()
2158 .find(|candidate| {
2159 candidate.kind == "sourceSelectorReference"
2160 || candidate.kind == "sourceSelectorPrefixReference"
2161 })
2162 && let Some(span) = byte_span_for_parser_range(document.text.as_str(), candidate.range)
2163 {
2164 return Some(SourceCompletionContext {
2165 target_style_uri: candidate.target_style_uri.clone(),
2166 value_prefix: source_completion_prefix_for_terminal_offset(
2167 document.text.as_str(),
2168 span,
2169 offset,
2170 ),
2171 preferred_selector_names: Vec::new(),
2172 });
2173 }
2174 if let Some(access) = document
2175 .source_syntax_index
2176 .style_property_accesses
2177 .iter()
2178 .find(|access| offset >= access.byte_span.start && offset <= access.byte_span.end)
2179 {
2180 let target_style_uri = access
2181 .target_style_uri
2182 .clone()
2183 .or_else(|| source_completion_target_uri_for_span(document, access.byte_span));
2184 return Some(SourceCompletionContext {
2185 target_style_uri,
2186 value_prefix: source_completion_prefix_for_terminal_offset(
2187 document.text.as_str(),
2188 access.byte_span,
2189 offset,
2190 ),
2191 preferred_selector_names: Vec::new(),
2192 });
2193 }
2194 if let Some(reference) = document
2195 .source_syntax_index
2196 .selector_references
2197 .iter()
2198 .find(|reference| offset >= reference.byte_span.start && offset <= reference.byte_span.end)
2199 {
2200 let target_style_uri = reference
2201 .target_style_uri
2202 .clone()
2203 .or_else(|| source_completion_target_uri_for_span(document, reference.byte_span));
2204 return Some(SourceCompletionContext {
2205 target_style_uri,
2206 value_prefix: source_completion_prefix_for_terminal_offset(
2207 document.text.as_str(),
2208 reference.byte_span,
2209 offset,
2210 ),
2211 preferred_selector_names: Vec::new(),
2212 });
2213 }
2214 if document
2215 .source_syntax_index
2216 .class_string_literals
2217 .iter()
2218 .any(|span| offset >= span.start && offset <= span.end)
2219 {
2220 let span = document
2221 .source_syntax_index
2222 .class_string_literals
2223 .iter()
2224 .find(|span| offset >= span.start && offset <= span.end)
2225 .copied()?;
2226 return Some(SourceCompletionContext {
2227 target_style_uri: None,
2228 value_prefix: source_completion_class_token_prefix_from_span(
2229 document.text.as_str(),
2230 span,
2231 offset,
2232 ),
2233 preferred_selector_names: Vec::new(),
2234 });
2235 }
2236 None
2237}
2238
2239fn source_completion_target_uri_for_span(
2240 document: &LspTextDocumentState,
2241 span: ParserByteSpanV0,
2242) -> Option<String> {
2243 let range = parser_range_for_byte_span(document.text.as_str(), span);
2244 document
2245 .source_selector_candidates
2246 .iter()
2247 .find(|candidate| {
2248 candidate.range == range
2249 || parser_range_contains_position(&candidate.range, range.start)
2250 || parser_range_contains_position(&candidate.range, range.end)
2251 })
2252 .and_then(|candidate| candidate.target_style_uri.clone())
2253}
2254
2255fn byte_span_for_parser_range(source: &str, range: ParserRangeV0) -> Option<ParserByteSpanV0> {
2256 Some(ParserByteSpanV0 {
2257 start: byte_offset_for_parser_position(source, range.start)?,
2258 end: byte_offset_for_parser_position(source, range.end)?,
2259 })
2260}
2261
2262fn source_completion_value_domain_selectors_for_target(
2263 document: &LspTextDocumentState,
2264 byte_span: ParserByteSpanV0,
2265 target_style_uri: Option<&str>,
2266) -> Vec<String> {
2267 let mut selectors = document
2268 .source_syntax_index
2269 .selector_references
2270 .iter()
2271 .filter(|reference| reference.byte_span == byte_span)
2272 .filter(|reference| reference.match_kind == SourceSelectorReferenceMatchKind::Exact)
2273 .filter(|reference| {
2274 target_style_uri.is_none_or(|target_uri| {
2275 reference
2276 .target_style_uri
2277 .as_deref()
2278 .is_some_and(|reference_uri| file_uri_equivalent(reference_uri, target_uri))
2279 })
2280 })
2281 .filter_map(|reference| reference.selector_name.clone())
2282 .collect::<Vec<_>>();
2283 selectors.sort();
2284 selectors.dedup();
2285 selectors
2286}
2287
2288fn source_completion_prefix_for_terminal_offset(
2289 source: &str,
2290 span: ParserByteSpanV0,
2291 offset: usize,
2292) -> Option<String> {
2293 (offset >= span.end).then(|| source_completion_prefix_from_span(source, span, offset))?
2294}
2295
2296fn source_completion_prefix_from_span(
2297 source: &str,
2298 span: ParserByteSpanV0,
2299 offset: usize,
2300) -> Option<String> {
2301 let end = offset.min(span.end);
2302 if end < span.start {
2303 return None;
2304 }
2305 let prefix = source.get(span.start..end)?;
2306 if prefix.is_empty() {
2307 return None;
2308 }
2309 if prefix.chars().all(is_css_identifier_continue) {
2310 Some(prefix.to_string())
2311 } else {
2312 None
2313 }
2314}
2315
2316fn source_completion_class_token_prefix_from_span(
2317 source: &str,
2318 span: ParserByteSpanV0,
2319 offset: usize,
2320) -> Option<String> {
2321 let end = offset.min(span.end);
2322 if end < span.start {
2323 return None;
2324 }
2325 let prefix = source.get(span.start..end)?;
2326 let token = prefix
2327 .rsplit(|ch: char| ch.is_ascii_whitespace())
2328 .next()
2329 .unwrap_or_default();
2330 if token.is_empty() {
2331 return None;
2332 }
2333 if token.chars().all(is_css_identifier_continue) {
2334 Some(token.to_string())
2335 } else {
2336 None
2337 }
2338}
2339
2340fn source_selector_candidate_for_params(
2341 state: &LspShellState,
2342 params: Option<&Value>,
2343) -> Option<(String, LspStyleHoverCandidate)> {
2344 let document_uri = document_uri_from_params(params);
2345 let position = lsp_position_from_params(params)?;
2346 let document = state.document(document_uri.as_str())?;
2347 if is_style_document_uri(document.uri.as_str()) {
2348 return None;
2349 }
2350 source_selector_candidate_at_position(state, document, position)
2351 .map(|candidate| (document_uri, candidate))
2352}
2353
2354fn source_selector_candidate_at_position(
2355 state: &LspShellState,
2356 document: &LspTextDocumentState,
2357 position: ParserPositionV0,
2358) -> Option<LspStyleHoverCandidate> {
2359 source_selector_candidates_at_position(state, document, position)
2360 .into_iter()
2361 .next()
2362}
2363
2364fn source_selector_candidates_at_position(
2365 state: &LspShellState,
2366 document: &LspTextDocumentState,
2367 position: ParserPositionV0,
2368) -> Vec<LspStyleHoverCandidate> {
2369 collect_source_selector_reference_candidates(state, document)
2370 .into_iter()
2371 .filter(|candidate| parser_range_contains_position(&candidate.range, position))
2372 .collect()
2373}
2374
2375fn collect_source_selector_reference_candidates(
2376 state: &LspShellState,
2377 document: &LspTextDocumentState,
2378) -> Vec<LspStyleHoverCandidate> {
2379 resolve_source_provider_candidates(state, document).matched
2380}
2381
2382fn resolve_source_provider_candidates(
2383 state: &LspShellState,
2384 document: &LspTextDocumentState,
2385) -> SourceProviderCandidateResolution {
2386 let source_candidates = collect_source_class_reference_candidates(document);
2387 let mut definitions = style_selector_definitions_from_open_documents(
2388 state,
2389 "",
2390 document.workspace_folder_uri.as_deref(),
2391 );
2392 for candidate in &source_candidates {
2393 if let Some(target_uri) = candidate.target_style_uri.as_deref()
2394 && !definitions
2395 .iter()
2396 .any(|(uri, _)| file_uri_equivalent(uri, target_uri))
2397 {
2398 definitions.extend(style_selector_definitions_from_uri(state, target_uri));
2399 }
2400 }
2401 let query_definitions = definitions
2402 .iter()
2403 .map(|(uri, definition)| query_style_selector_definition_for_matching(uri, definition))
2404 .collect::<Vec<_>>();
2405 let resolution = resolve_omena_query_source_provider_candidates(
2406 source_candidates
2407 .iter()
2408 .map(query_source_selector_candidate_for_matching)
2409 .collect(),
2410 query_definitions.as_slice(),
2411 );
2412
2413 SourceProviderCandidateResolution {
2414 matched: resolution
2415 .matched
2416 .into_iter()
2417 .map(lsp_source_selector_candidate_from_query)
2418 .collect(),
2419 unresolved: resolution
2420 .unresolved
2421 .into_iter()
2422 .map(lsp_source_selector_candidate_from_query)
2423 .collect(),
2424 }
2425}
2426
2427fn collect_source_class_reference_candidates(
2428 document: &LspTextDocumentState,
2429) -> Vec<LspStyleHoverCandidate> {
2430 document.source_selector_candidates.clone()
2431}
2432
2433fn source_selector_candidates_from_index(
2434 document: &LspTextDocumentState,
2435 index: &SourceSyntaxIndex,
2436) -> Vec<LspStyleHoverCandidate> {
2437 let mut candidates: Vec<LspStyleHoverCandidate> = index
2438 .selector_references
2439 .iter()
2440 .map(|reference| source_reference_candidate(document, reference))
2441 .collect();
2442 candidates.sort();
2443 candidates.dedup();
2444 candidates
2445}
2446
2447fn build_source_syntax_index(
2448 document: &LspTextDocumentState,
2449 resolution_inputs: &omena_query::OmenaQueryStyleResolutionInputsV0,
2450) -> SourceSyntaxIndex {
2451 if is_style_document_uri(document.uri.as_str()) {
2452 return SourceSyntaxIndex::default();
2453 }
2454
2455 let imports = collect_source_imports(document, resolution_inputs);
2456 summarize_omena_query_source_syntax_index_for_source_language(
2457 document.uri.as_str(),
2458 document.text.as_str(),
2459 Some(document.language_id.as_str()),
2460 imports.imported_style_bindings,
2461 imports.classnames_bind_bindings,
2462 )
2463}
2464
2465#[derive(Debug, Clone, PartialEq, Eq)]
2466struct SourceImportIndex {
2467 imported_style_bindings: Vec<ImportedStyleBinding>,
2468 classnames_bind_bindings: Vec<String>,
2469}
2470
2471fn collect_source_imports(
2472 document: &LspTextDocumentState,
2473 resolution_inputs: &omena_query::OmenaQueryStyleResolutionInputsV0,
2474) -> SourceImportIndex {
2475 let source = document.text.as_str();
2476 let mut imports = SourceImportIndex {
2477 imported_style_bindings: Vec::new(),
2478 classnames_bind_bindings: Vec::new(),
2479 };
2480 let summary = summarize_omena_query_source_import_declarations_for_source_language(
2481 document.uri.as_str(),
2482 source,
2483 Some(document.language_id.as_str()),
2484 );
2485 for import in summary.imports {
2486 if import.specifier == "classnames/bind" {
2487 imports.classnames_bind_bindings.push(import.binding);
2488 } else if StyleLanguage::from_module_path(import.specifier.as_str()).is_some()
2489 && let Some(style_uri) =
2490 resolve_omena_query_style_uri_for_specifier_with_resolution_inputs(
2491 document.uri.as_str(),
2492 document.workspace_folder_uri.as_deref(),
2493 import.specifier.as_str(),
2494 resolution_inputs,
2495 )
2496 {
2497 imports.imported_style_bindings.push(ImportedStyleBinding {
2498 binding: import.binding,
2499 style_uri,
2500 });
2501 }
2502 }
2503 if is_vue_document(document) && has_vue_module_style_block(source) {
2504 for binding in collect_omena_query_vue_style_module_bindings(
2505 document.uri.as_str(),
2506 source,
2507 Some(document.language_id.as_str()),
2508 ) {
2509 imports.imported_style_bindings.push(ImportedStyleBinding {
2510 binding,
2511 style_uri: document.uri.clone(),
2512 });
2513 }
2514 }
2515 imports
2516 .imported_style_bindings
2517 .sort_by(|left, right| left.binding.cmp(&right.binding));
2518 imports
2519 .imported_style_bindings
2520 .dedup_by(|left, right| left.binding == right.binding && left.style_uri == right.style_uri);
2521 imports.classnames_bind_bindings.sort();
2522 imports.classnames_bind_bindings.dedup();
2523 imports
2524}
2525
2526fn is_vue_document(document: &LspTextDocumentState) -> bool {
2527 document.language_id == "vue" || document.uri.ends_with(".vue")
2528}
2529
2530fn document_has_style_index(document: &LspTextDocumentState) -> bool {
2531 is_style_document_uri(document.uri.as_str()) || document.style_summary.is_some()
2532}
2533
2534fn has_vue_module_style_block(source: &str) -> bool {
2535 let lower = source.to_ascii_lowercase();
2536 let mut cursor = 0usize;
2537 while let Some(relative_start) = lower[cursor..].find("<style") {
2538 let tag_start = cursor + relative_start;
2539 let Some(relative_tag_end) = lower[tag_start..].find('>') else {
2540 break;
2541 };
2542 let tag = &lower[tag_start..tag_start + relative_tag_end + 1];
2543 if tag.contains("module") {
2544 return true;
2545 }
2546 cursor = tag_start + relative_tag_end + 1;
2547 }
2548 false
2549}
2550
2551fn source_reference_candidate(
2552 document: &LspTextDocumentState,
2553 reference: &SourceSelectorReferenceFact,
2554) -> LspStyleHoverCandidate {
2555 let name = reference.selector_name.clone().unwrap_or_else(|| {
2556 document.text[reference.byte_span.start..reference.byte_span.end].to_string()
2557 });
2558 LspStyleHoverCandidate {
2559 kind: match reference.match_kind {
2560 SourceSelectorReferenceMatchKind::Exact => "sourceSelectorReference",
2561 SourceSelectorReferenceMatchKind::Prefix => "sourceSelectorPrefixReference",
2562 },
2563 name,
2564 range: parser_range_for_byte_span(document.text.as_str(), reference.byte_span),
2565 source: "omenaQuerySourceSyntaxIndex",
2566 target_style_uri: reference.target_style_uri.clone(),
2567 namespace: None,
2568 }
2569}
2570
2571fn style_selector_definitions_from_open_documents(
2572 state: &LspShellState,
2573 selector_name: &str,
2574 workspace_folder_uri: Option<&str>,
2575) -> Vec<(String, LspStyleHoverCandidate)> {
2576 let mut definitions = Vec::new();
2577 for document in state.documents.values() {
2578 if !document_has_style_index(document)
2579 || !workspace_folder_compatible(workspace_folder_uri, document)
2580 {
2581 continue;
2582 }
2583 let Some((_, candidates)) = style_hover_candidates_for_document(document) else {
2584 continue;
2585 };
2586 definitions.extend(
2587 candidates
2588 .into_iter()
2589 .filter(|candidate| {
2590 candidate.kind == "selector"
2591 && (selector_name.is_empty() || candidate.name == selector_name)
2592 })
2593 .map(|candidate| (document.uri.clone(), candidate)),
2594 );
2595 }
2596 definitions.sort_by_key(|(uri, candidate)| {
2597 (
2598 uri.clone(),
2599 candidate.range.start.line,
2600 candidate.range.start.character,
2601 )
2602 });
2603 definitions
2604}
2605
2606fn style_selector_definitions_from_uri(
2607 state: &LspShellState,
2608 uri: &str,
2609) -> Vec<(String, LspStyleHoverCandidate)> {
2610 style_hover_candidates_for_uri(state, uri)
2611 .map(|(_, candidates)| {
2612 candidates
2613 .into_iter()
2614 .filter(|candidate| candidate.kind == "selector")
2615 .map(|candidate| (uri.to_string(), candidate))
2616 .collect()
2617 })
2618 .unwrap_or_default()
2619}
2620
2621fn style_selector_definitions_for_source_candidate(
2622 state: &LspShellState,
2623 candidate: &LspStyleHoverCandidate,
2624 workspace_folder_uri: Option<&str>,
2625) -> Vec<(String, LspStyleHoverCandidate)> {
2626 let mut definitions = style_selector_definitions_from_open_documents(
2627 state,
2628 source_candidate_definition_lookup_name(candidate),
2629 workspace_folder_uri,
2630 );
2631 if let Some(target_uri) = candidate.target_style_uri.as_deref()
2632 && !definitions
2633 .iter()
2634 .any(|(uri, _)| file_uri_equivalent(uri, target_uri))
2635 {
2636 definitions.extend(style_selector_definitions_from_uri(state, target_uri));
2637 }
2638 let query_definitions = definitions
2639 .iter()
2640 .map(|(uri, definition)| query_style_selector_definition_for_matching(uri, definition))
2641 .collect::<Vec<_>>();
2642 let matched_identities = resolve_omena_query_style_selector_definitions_for_source_candidate(
2643 &query_source_selector_candidate_for_matching(candidate),
2644 query_definitions.as_slice(),
2645 )
2646 .into_iter()
2647 .map(|definition| {
2648 query_definition_identity(
2649 definition.uri.as_str(),
2650 definition.name.as_str(),
2651 definition.range,
2652 )
2653 })
2654 .collect::<BTreeSet<_>>();
2655
2656 definitions
2657 .into_iter()
2658 .filter(|(uri, definition)| {
2659 matched_identities.contains(&query_definition_identity(
2660 uri.as_str(),
2661 definition.name.as_str(),
2662 definition.range,
2663 ))
2664 })
2665 .collect()
2666}
2667
2668fn style_selector_definitions_for_source_candidates(
2669 state: &LspShellState,
2670 candidates: &[LspStyleHoverCandidate],
2671 workspace_folder_uri: Option<&str>,
2672) -> Vec<(String, LspStyleHoverCandidate)> {
2673 let mut definitions = candidates
2674 .iter()
2675 .flat_map(|candidate| {
2676 style_selector_definitions_for_source_candidate(state, candidate, workspace_folder_uri)
2677 })
2678 .collect::<Vec<_>>();
2679 definitions.sort_by_key(|(uri, definition)| {
2680 (
2681 uri.clone(),
2682 definition.range.start.line,
2683 definition.range.start.character,
2684 definition.name.clone(),
2685 )
2686 });
2687 definitions.dedup_by(|left, right| {
2688 left.0 == right.0 && left.1.name == right.1.name && left.1.range == right.1.range
2689 });
2690 definitions
2691}
2692
2693fn render_source_hover_definitions_markdown(
2694 state: &LspShellState,
2695 definitions: &[(String, LspStyleHoverCandidate)],
2696) -> Option<String> {
2697 let parts = definitions
2698 .iter()
2699 .filter_map(|(uri, definition)| {
2700 style_text_for_uri(state, uri).map(|text| {
2701 render_style_hover_candidate_markdown(uri.as_str(), text.as_str(), definition)
2702 })
2703 })
2704 .collect::<Vec<_>>();
2705 if parts.is_empty() {
2706 None
2707 } else {
2708 Some(parts.join("\n\n---\n\n"))
2709 }
2710}
2711
2712fn source_candidate_definition_lookup_name(candidate: &LspStyleHoverCandidate) -> &str {
2713 if candidate.kind == "sourceSelectorPrefixReference" {
2714 ""
2715 } else {
2716 candidate.name.as_str()
2717 }
2718}
2719
2720fn first_style_document_for_workspace<'a>(
2721 state: &'a LspShellState,
2722 workspace_folder_uri: Option<&str>,
2723) -> Option<(String, &'a LspTextDocumentState)> {
2724 state
2725 .documents
2726 .values()
2727 .filter(|document| document_has_style_index(document))
2728 .filter(|document| workspace_folder_compatible(workspace_folder_uri, document))
2729 .map(|document| (document.uri.clone(), document))
2730 .next()
2731}
2732
2733fn resolve_selector_rename(
2734 state: &LspShellState,
2735 workspace_folder_uri: Option<&str>,
2736 target_style_uri: Option<&str>,
2737 selector_name: &str,
2738 new_name: &str,
2739) -> Value {
2740 let query_definitions =
2741 style_selector_definitions_from_open_documents(state, selector_name, workspace_folder_uri)
2742 .iter()
2743 .map(|(uri, definition)| query_style_selector_definition_for_matching(uri, definition))
2744 .collect::<Vec<_>>();
2745 let query_target_style_uri = query_target_style_uri_for_matching(target_style_uri);
2746 let mut query_references = Vec::new();
2747 for document in state.documents.values() {
2748 if is_style_document_uri(document.uri.as_str()) {
2749 continue;
2750 }
2751 if !workspace_folder_compatible(workspace_folder_uri, document) {
2752 continue;
2753 }
2754 query_references.extend(
2755 collect_source_selector_reference_candidates(state, document)
2756 .iter()
2757 .map(|candidate| {
2758 query_source_selector_reference_edit_target_for_matching(document, candidate)
2759 }),
2760 );
2761 }
2762 let rename_plan = summarize_omena_query_rename_plan(
2763 selector_name,
2764 new_name,
2765 query_target_style_uri.as_deref(),
2766 query_definitions.as_slice(),
2767 query_references.as_slice(),
2768 );
2769 if rename_plan.edits.is_empty() {
2770 return Value::Null;
2771 }
2772
2773 let mut changes: BTreeMap<String, Vec<Value>> = BTreeMap::new();
2774 for edit in rename_plan.edits {
2775 let edit_uri = external_document_uri_for_query_uri(state, edit.uri.as_str());
2776 changes.entry(edit_uri).or_default().push(json!({
2777 "range": edit.range,
2778 "newText": edit.new_text,
2779 }));
2780 }
2781 for edits in changes.values_mut() {
2782 edits.sort_by_key(|edit| {
2783 let line = edit
2784 .pointer("/range/start/line")
2785 .and_then(Value::as_u64)
2786 .unwrap_or_default();
2787 let character = edit
2788 .pointer("/range/start/character")
2789 .and_then(Value::as_u64)
2790 .unwrap_or_default();
2791 (line, character)
2792 });
2793 }
2794
2795 let mut response_changes = serde_json::Map::new();
2796 for (uri, edits) in changes {
2797 response_changes.insert(uri, json!(edits));
2798 }
2799 json!({
2800 "changes": Value::Object(response_changes),
2801 })
2802}
2803
2804fn external_document_uri_for_query_uri(state: &LspShellState, uri: &str) -> String {
2805 state
2806 .document(uri)
2807 .map(|document| document.uri.clone())
2808 .unwrap_or_else(|| uri.to_string())
2809}
2810
2811fn render_style_hover_candidate_markdown(
2812 document_uri: &str,
2813 source: &str,
2814 candidate: &LspStyleHoverCandidate,
2815) -> String {
2816 let location = format!(
2817 "{}:{}",
2818 file_label_from_uri(document_uri),
2819 candidate.range.start.line + 1
2820 );
2821 let render_parts = summarize_omena_query_style_hover_render_parts(
2822 source,
2823 candidate.kind,
2824 candidate.name.as_str(),
2825 candidate.range.start,
2826 );
2827 match candidate.kind {
2828 "selector" => {
2829 format!(
2830 "**`.{}`** - _{}_\n\n```scss\n{}\n```",
2831 candidate.name, location, render_parts.snippet
2832 )
2833 }
2834 "customPropertyReference" => {
2835 format!(
2836 "**`var({})`** - _{}_\n\n```scss\n{}\n```",
2837 candidate.name, location, render_parts.snippet
2838 )
2839 }
2840 "customPropertyDeclaration" => {
2841 format!(
2842 "**`{}`** - _{}_\n\n```scss\n{}\n```",
2843 candidate.name, location, render_parts.snippet
2844 )
2845 }
2846 kind if is_sass_symbol_candidate_kind(kind) => {
2847 render_sass_symbol_hover_markdown(candidate, location.as_str(), &render_parts)
2848 }
2849 _ => candidate.name.clone(),
2850 }
2851}
2852
2853fn render_sass_symbol_hover_markdown(
2854 candidate: &LspStyleHoverCandidate,
2855 location: &str,
2856 render_parts: &omena_query::OmenaQueryStyleHoverRenderPartsV0,
2857) -> String {
2858 let label = render_sass_symbol_label(candidate);
2859 match sass_symbol_kind_from_candidate_kind(candidate.kind) {
2860 Some("variable") if is_sass_symbol_declaration_kind(candidate.kind) => {
2861 if let Some(value) = render_parts.value.as_deref() {
2862 return format!(
2863 "**`{}`** - _{}_\n\nValue: `{}`\n\n```scss\n{}\n```",
2864 label, location, value, render_parts.snippet
2865 );
2866 }
2867 format!(
2868 "**`{}`** - _{}_\n\n```scss\n{}\n```",
2869 label, location, render_parts.snippet
2870 )
2871 }
2872 Some("mixin" | "function") if is_sass_symbol_declaration_kind(candidate.kind) => {
2873 let rendered_label = render_parts.signature.as_deref().unwrap_or(label.as_str());
2874 format!(
2875 "**`{}`** - _{}_\n\n```scss\n{}\n```",
2876 rendered_label, location, render_parts.snippet
2877 )
2878 }
2879 _ => {
2880 format!(
2881 "**`{}`** - _{}_\n\n```scss\n{}\n```",
2882 label, location, render_parts.snippet
2883 )
2884 }
2885 }
2886}
2887
2888#[cfg(test)]
2889mod tests;