1use std::collections::HashMap;
4use std::sync::Arc;
5
6use styx_cst::{Parse, parse};
7use styx_tree::Value;
8use tokio::sync::RwLock;
9use tower_lsp::jsonrpc::Result;
10use tower_lsp::lsp_types::*;
11use tower_lsp::{Client, LanguageServer, LspService, Server};
12
13use crate::extensions::{ExtensionManager, ExtensionResult, get_extension_info};
14use crate::schema_hints::find_matching_hint;
15use crate::schema_validation::{
16 find_object_at_offset, find_schema_declaration, find_tagged_context_at_offset,
17 get_document_fields, get_error_span, get_schema_fields, get_schema_fields_at_path,
18 load_document_schema, resolve_schema, validate_against_schema,
19};
20use crate::semantic_tokens::{compute_semantic_tokens, semantic_token_legend};
21use styx_lsp_ext as ext;
22
23pub struct DocumentState {
25 pub content: String,
27 pub parse: Parse,
29 pub tree: Option<Value>,
31 #[allow(dead_code)]
33 pub version: i32,
34}
35
36pub type DocumentMap = Arc<RwLock<HashMap<Url, DocumentState>>>;
38
39struct BlockedExtensionInfo {
41 schema_id: String,
43 command: String,
45}
46
47pub struct StyxLanguageServer {
49 client: Client,
51 documents: DocumentMap,
53 extensions: Arc<ExtensionManager>,
55}
56
57impl StyxLanguageServer {
58 pub fn new(client: Client) -> Self {
59 let documents: DocumentMap = Arc::new(RwLock::new(HashMap::new()));
60 Self {
61 client,
62 documents: documents.clone(),
63 extensions: Arc::new(ExtensionManager::new(documents)),
64 }
65 }
66
67 async fn check_for_extension(&self, tree: &Value, uri: &Url) -> Option<BlockedExtensionInfo> {
71 let Ok(schema) = load_document_schema(tree, uri) else {
73 return None;
74 };
75
76 let ext_info = get_extension_info(&schema)?;
78
79 tracing::info!(
80 schema_id = %ext_info.schema_id,
81 launch = ?ext_info.config.launch,
82 "Document schema has LSP extension"
83 );
84
85 match self
87 .extensions
88 .get_or_spawn(&ext_info.schema_id, &ext_info.config, uri.as_str())
89 .await
90 {
91 ExtensionResult::Running => None,
92 ExtensionResult::NotAllowed { command } => Some(BlockedExtensionInfo {
93 schema_id: ext_info.schema_id,
94 command,
95 }),
96 ExtensionResult::Failed => None,
97 }
98 }
99
100 #[allow(clippy::too_many_arguments)]
102 async fn publish_diagnostics(
103 &self,
104 uri: Url,
105 content: &str,
106 parsed: &Parse,
107 tree: Option<&Value>,
108 tree_error: Option<&styx_tree::BuildError>,
109 version: i32,
110 blocked_extension: Option<BlockedExtensionInfo>,
111 ) {
112 let mut diagnostics = self.compute_diagnostics(&uri, content, parsed, tree, tree_error);
113
114 if let Some(blocked) = blocked_extension
116 && let Some(tree) = tree
117 && let Some(range) = find_schema_declaration_range(tree, content)
118 {
119 diagnostics.push(Diagnostic {
120 range,
121 severity: Some(DiagnosticSeverity::INFORMATION),
122 code: None,
123 code_description: None,
124 source: Some("styx-extension".to_string()),
125 message: format!(
126 "LSP extension '{}' is not allowed. Use the code action to allow it.",
127 blocked.command
128 ),
129 related_information: None,
130 tags: None,
131 data: Some(serde_json::json!({
132 "type": "allow_extension",
133 "schema_id": blocked.schema_id,
134 "command": blocked.command,
135 })),
136 });
137 }
138
139 if let Some(tree) = tree
141 && let Ok(schema_file) = load_document_schema(tree, &uri)
142 {
143 let schema_id = &schema_file.meta.id;
144 if let Some(client) = self.extensions.get_client(schema_id).await {
145 let ext_params = ext::DiagnosticParams {
146 document_uri: uri.to_string(),
147 tree: tree.clone(),
148 content: content.to_string(),
149 };
150
151 match client.diagnostics(ext_params).await {
152 Ok(ext_diagnostics) => {
153 tracing::debug!(
154 count = ext_diagnostics.len(),
155 "Got diagnostics from extension"
156 );
157 for diag in ext_diagnostics {
158 let severity = match diag.severity {
159 ext::DiagnosticSeverity::Error => DiagnosticSeverity::ERROR,
160 ext::DiagnosticSeverity::Warning => DiagnosticSeverity::WARNING,
161 ext::DiagnosticSeverity::Info => DiagnosticSeverity::INFORMATION,
162 ext::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT,
163 };
164 diagnostics.push(Diagnostic {
165 range: Range {
166 start: offset_to_position(content, diag.span.start as usize),
167 end: offset_to_position(content, diag.span.end as usize),
168 },
169 severity: Some(severity),
170 code: diag.code.map(NumberOrString::String),
171 code_description: None,
172 source: diag.source,
173 message: diag.message,
174 related_information: None,
175 tags: None,
176 data: diag.data.map(|v| convert_styx_value_to_json(&v)),
177 });
178 }
179 }
180 Err(e) => {
181 tracing::warn!(error = ?e, "Extension diagnostics failed");
182 }
183 }
184 }
185 }
186
187 self.client
188 .publish_diagnostics(uri, diagnostics, Some(version))
189 .await;
190 }
191
192 fn compute_diagnostics(
194 &self,
195 uri: &Url,
196 content: &str,
197 parsed: &Parse,
198 tree: Option<&Value>,
199 tree_error: Option<&styx_tree::BuildError>,
200 ) -> Vec<Diagnostic> {
201 let mut diagnostics = Vec::new();
202
203 for error in parsed.errors() {
205 let range = Range {
206 start: offset_to_position(content, error.offset as usize),
207 end: offset_to_position(content, error.offset as usize + 1),
208 };
209
210 diagnostics.push(Diagnostic {
211 range,
212 severity: Some(DiagnosticSeverity::ERROR),
213 code: None,
214 code_description: None,
215 source: Some("styx".to_string()),
216 message: error.message.clone(),
217 related_information: None,
218 tags: None,
219 data: None,
220 });
221 }
222
223 if let Some(err) = tree_error {
225 let range = match err {
226 styx_tree::BuildError::Parse(_, span) => Range {
227 start: offset_to_position(content, span.start as usize),
228 end: offset_to_position(content, span.end as usize),
229 },
230 _ => Range {
231 start: Position::new(0, 0),
232 end: Position::new(0, 1),
233 },
234 };
235
236 diagnostics.push(Diagnostic {
237 range,
238 severity: Some(DiagnosticSeverity::ERROR),
239 code: None,
240 code_description: None,
241 source: Some("styx".to_string()),
242 message: err.to_string(),
243 related_information: None,
244 tags: None,
245 data: None,
246 });
247 }
248
249 let validation_diagnostics = styx_cst::validate(&parsed.syntax());
251 for diag in validation_diagnostics {
252 let range = Range {
253 start: offset_to_position(content, diag.range.start().into()),
254 end: offset_to_position(content, diag.range.end().into()),
255 };
256
257 let severity = match diag.severity {
258 styx_cst::Severity::Error => DiagnosticSeverity::ERROR,
259 styx_cst::Severity::Warning => DiagnosticSeverity::WARNING,
260 styx_cst::Severity::Hint => DiagnosticSeverity::HINT,
261 };
262
263 diagnostics.push(Diagnostic {
264 range,
265 severity: Some(severity),
266 code: None,
267 code_description: None,
268 source: Some("styx".to_string()),
269 message: diag.message,
270 related_information: None,
271 tags: None,
272 data: None,
273 });
274 }
275
276 if let Some(tree) = tree {
278 if let Ok(schema) = resolve_schema(tree, uri) {
280 let schema_location = Some(DiagnosticRelatedInformation {
282 location: Location {
283 uri: schema.uri.clone(),
284 range: Range::default(),
285 },
286 message: format!("schema: {}", schema.uri),
287 });
288
289 match validate_against_schema(tree, uri) {
290 Ok(result) => {
291 for error in &result.errors {
293 let range = if let Some(span) = error.span {
295 Range {
296 start: offset_to_position(content, span.start as usize),
297 end: offset_to_position(content, span.end as usize),
298 }
299 } else {
300 if let Some((start, end)) = get_error_span(tree, &error.path) {
302 Range {
303 start: offset_to_position(content, start),
304 end: offset_to_position(content, end),
305 }
306 } else {
307 Range {
308 start: Position::new(0, 0),
309 end: Position::new(0, 1),
310 }
311 }
312 };
313
314 let data = error.quickfix_data();
316
317 diagnostics.push(Diagnostic {
318 range,
319 severity: Some(DiagnosticSeverity::ERROR),
320 code: None,
321 code_description: None,
322 source: Some("styx-schema".to_string()),
323 message: error.diagnostic_message(),
324 related_information: schema_location.clone().map(|loc| vec![loc]),
325 tags: None,
326 data,
327 });
328 }
329
330 for warning in &result.warnings {
332 let range = if let Some(span) = warning.span {
334 Range {
335 start: offset_to_position(content, span.start as usize),
336 end: offset_to_position(content, span.end as usize),
337 }
338 } else {
339 Range {
340 start: Position::new(0, 0),
341 end: Position::new(0, 1),
342 }
343 };
344
345 diagnostics.push(Diagnostic {
346 range,
347 severity: Some(DiagnosticSeverity::WARNING),
348 code: None,
349 code_description: None,
350 source: Some("styx-schema".to_string()),
351 message: warning.message.clone(),
352 related_information: schema_location.clone().map(|loc| vec![loc]),
353 tags: None,
354 data: None,
355 });
356 }
357 }
358 Err(e) => {
359 diagnostics.push(Diagnostic {
361 range: Range {
362 start: Position::new(0, 0),
363 end: Position::new(0, 1),
364 },
365 severity: Some(DiagnosticSeverity::ERROR),
366 code: None,
367 code_description: None,
368 source: Some("styx-schema".to_string()),
369 message: e,
370 related_information: None,
371 tags: None,
372 data: None,
373 });
374 }
375 }
376 }
377 }
378
379 if let Some(tree) = tree
382 && find_schema_declaration(tree).is_none()
383 && let Some(hint_match) = find_matching_hint(uri)
384 {
385 let data = serde_json::json!({
387 "type": "add_schema",
388 "declaration": hint_match.schema_declaration(),
389 "tool": hint_match.tool_name,
390 });
391
392 diagnostics.push(Diagnostic {
393 range: Range {
394 start: Position::new(0, 0),
395 end: Position::new(0, 0),
396 },
397 severity: Some(DiagnosticSeverity::HINT),
398 code: Some(NumberOrString::String("missing-schema".to_string())),
399 code_description: None,
400 source: Some("styx-hints".to_string()),
401 message: format!(
402 "This file matches the {} pattern. Add @schema declaration?",
403 hint_match.description()
404 ),
405 related_information: None,
406 tags: None,
407 data: Some(data),
408 });
409 }
410
411 diagnostics
412 }
413}
414
415#[tower_lsp::async_trait]
416impl LanguageServer for StyxLanguageServer {
417 async fn initialize(&self, _params: InitializeParams) -> Result<InitializeResult> {
418 Ok(InitializeResult {
419 capabilities: ServerCapabilities {
420 text_document_sync: Some(TextDocumentSyncCapability::Kind(
422 TextDocumentSyncKind::FULL,
423 )),
424 semantic_tokens_provider: Some(
426 SemanticTokensServerCapabilities::SemanticTokensOptions(
427 SemanticTokensOptions {
428 work_done_progress_options: WorkDoneProgressOptions::default(),
429 legend: semantic_token_legend(),
430 range: Some(false),
431 full: Some(SemanticTokensFullOptions::Bool(true)),
432 },
433 ),
434 ),
435 document_link_provider: Some(DocumentLinkOptions {
437 resolve_provider: Some(false),
438 work_done_progress_options: WorkDoneProgressOptions::default(),
439 }),
440 definition_provider: Some(OneOf::Left(true)),
442 hover_provider: Some(HoverProviderCapability::Simple(true)),
444 completion_provider: Some(CompletionOptions {
446 trigger_characters: Some(vec![" ".into(), "\n".into()]),
447 resolve_provider: Some(false),
448 ..Default::default()
449 }),
450 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
452 references_provider: Some(OneOf::Left(true)),
454 inlay_hint_provider: Some(OneOf::Left(true)),
456 document_formatting_provider: Some(OneOf::Left(true)),
458 document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
460 first_trigger_character: "\n".to_string(),
461 more_trigger_character: None,
462 }),
463 document_symbol_provider: Some(OneOf::Left(true)),
465 execute_command_provider: Some(ExecuteCommandOptions {
467 commands: vec!["styx.allowExtension".to_string()],
468 work_done_progress_options: WorkDoneProgressOptions::default(),
469 }),
470 ..Default::default()
471 },
472 server_info: Some(ServerInfo {
473 name: "styx-lsp".to_string(),
474 version: Some(env!("CARGO_PKG_VERSION").to_string()),
475 }),
476 })
477 }
478
479 async fn initialized(&self, _: InitializedParams) {
480 self.client
481 .log_message(
482 MessageType::INFO,
483 format!(
484 "Styx language server initialized (PID: {})",
485 std::process::id()
486 ),
487 )
488 .await;
489 }
490
491 async fn shutdown(&self) -> Result<()> {
492 Ok(())
493 }
494
495 async fn did_open(&self, params: DidOpenTextDocumentParams) {
496 let uri = params.text_document.uri;
497 let content = params.text_document.text;
498 let version = params.text_document.version;
499
500 let parsed = parse(&content);
502
503 let (tree, tree_error) = match styx_tree::parse(&content) {
505 Ok(tree) => (Some(tree), None),
506 Err(e) => (None, Some(e)),
507 };
508
509 let blocked_extension = if let Some(ref tree) = tree {
511 self.check_for_extension(tree, &uri).await
512 } else {
513 None
514 };
515
516 self.publish_diagnostics(
518 uri.clone(),
519 &content,
520 &parsed,
521 tree.as_ref(),
522 tree_error.as_ref(),
523 version,
524 blocked_extension,
525 )
526 .await;
527
528 {
530 let mut docs = self.documents.write().await;
531 docs.insert(
532 uri,
533 DocumentState {
534 content,
535 parse: parsed,
536 tree,
537 version,
538 },
539 );
540 }
541 }
542
543 async fn did_change(&self, params: DidChangeTextDocumentParams) {
544 let uri = params.text_document.uri;
545 let version = params.text_document.version;
546
547 if let Some(change) = params.content_changes.into_iter().next() {
549 let content = change.text;
550
551 let parsed = parse(&content);
553
554 let (tree, tree_error) = match styx_tree::parse(&content) {
556 Ok(tree) => (Some(tree), None),
557 Err(e) => (None, Some(e)),
558 };
559
560 let blocked_extension = if let Some(ref tree) = tree {
562 self.check_for_extension(tree, &uri).await
563 } else {
564 None
565 };
566
567 self.publish_diagnostics(
569 uri.clone(),
570 &content,
571 &parsed,
572 tree.as_ref(),
573 tree_error.as_ref(),
574 version,
575 blocked_extension,
576 )
577 .await;
578
579 {
581 let mut docs = self.documents.write().await;
582 docs.insert(
583 uri,
584 DocumentState {
585 content,
586 parse: parsed,
587 tree,
588 version,
589 },
590 );
591 }
592 }
593 }
594
595 async fn did_close(&self, params: DidCloseTextDocumentParams) {
596 let uri = params.text_document.uri;
597
598 {
600 let mut docs = self.documents.write().await;
601 docs.remove(&uri);
602 }
603
604 self.client.publish_diagnostics(uri, vec![], None).await;
606 }
607
608 async fn semantic_tokens_full(
609 &self,
610 params: SemanticTokensParams,
611 ) -> Result<Option<SemanticTokensResult>> {
612 let uri = params.text_document.uri;
613
614 let docs = self.documents.read().await;
615 let Some(doc) = docs.get(&uri) else {
616 return Ok(None);
617 };
618
619 let tokens = compute_semantic_tokens(&doc.parse);
620
621 Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
622 result_id: None,
623 data: tokens,
624 })))
625 }
626
627 async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
628 let uri = params.text_document.uri;
629
630 let docs = self.documents.read().await;
631 let Some(doc) = docs.get(&uri) else {
632 return Ok(None);
633 };
634
635 let Some(tree) = &doc.tree else {
636 return Ok(None);
637 };
638
639 let mut links = Vec::new();
640
641 if let Some(range) = find_schema_declaration_range(tree, &doc.content)
643 && let Ok(schema) = resolve_schema(tree, &uri)
644 {
645 links.push(DocumentLink {
646 range,
647 target: Some(schema.uri.clone()),
648 tooltip: Some(format!("Open schema: {}", schema.uri)),
649 data: None,
650 });
651 }
652
653 Ok(Some(links))
654 }
655
656 async fn goto_definition(
657 &self,
658 params: GotoDefinitionParams,
659 ) -> Result<Option<GotoDefinitionResponse>> {
660 let uri = params.text_document_position_params.text_document.uri;
661 let position = params.text_document_position_params.position;
662
663 let docs = self.documents.read().await;
664 let Some(doc) = docs.get(&uri) else {
665 return Ok(None);
666 };
667
668 let Some(tree) = &doc.tree else {
669 return Ok(None);
670 };
671
672 let offset = position_to_offset(&doc.content, position);
673
674 let resolved = resolve_schema(tree, &uri).ok();
676
677 if let Some(range) = find_schema_declaration_range(tree, &doc.content)
679 && position >= range.start
680 && position <= range.end
681 && let Some(ref schema) = resolved
682 {
683 return Ok(Some(GotoDefinitionResponse::Scalar(Location {
684 uri: schema.uri.clone(),
685 range: Range::default(),
686 })));
687 }
688
689 if let Some(field_name) = find_field_key_at_offset(tree, offset)
691 && let Some(ref schema) = resolved
692 && let Some(field_range) = find_field_in_schema_source(&schema.source, &field_name)
693 {
694 return Ok(Some(GotoDefinitionResponse::Scalar(Location {
695 uri: schema.uri.clone(),
696 range: field_range,
697 })));
698 }
699
700 if is_schema_file(tree)
703 && let Some(field_name) = find_field_key_at_offset(tree, offset)
704 {
705 for (doc_uri, doc_state) in docs.iter() {
707 if doc_uri == &uri {
708 continue; }
710 if let Some(ref doc_tree) = doc_state.tree {
711 if let Ok(doc_schema) = resolve_schema(doc_tree, doc_uri)
713 && doc_schema.uri == uri
714 {
715 if let Some(field_range) =
717 find_field_in_doc(doc_tree, &field_name, &doc_state.content)
718 {
719 return Ok(Some(GotoDefinitionResponse::Scalar(Location {
720 uri: doc_uri.clone(),
721 range: field_range,
722 })));
723 }
724 }
725 }
726 }
727 }
728
729 if let Ok(schema_file) = load_document_schema(tree, &uri) {
731 let schema_id = &schema_file.meta.id;
732 tracing::debug!(%schema_id, "Trying extension for definition");
733 if let Some(client) = self.extensions.get_client(schema_id).await {
734 tracing::debug!("Got extension client, calling definition");
735 let field_path = find_field_path_at_offset(tree, offset).unwrap_or_default();
736 let context_obj = find_object_at_offset(tree, offset);
737 let tagged_context = find_tagged_context_at_offset(tree, offset);
738
739 let ext_params = ext::DefinitionParams {
740 document_uri: uri.to_string(),
741 cursor: ext::Cursor {
742 line: position.line,
743 character: position.character,
744 offset: offset as u32,
745 },
746 path: field_path,
747 context: context_obj.map(|c| Value {
748 tag: None,
749 payload: Some(styx_tree::Payload::Object(c.object)),
750 span: None,
751 }),
752 tagged_context,
753 };
754
755 match client.definition(ext_params).await {
756 Ok(locations) if !locations.is_empty() => {
757 tracing::debug!(count = locations.len(), "Extension returned definitions");
758 let lsp_locations: Vec<Location> = locations
759 .into_iter()
760 .filter_map(|loc| {
761 let target_uri =
762 Url::parse(&loc.uri).unwrap_or_else(|_| uri.clone());
763 let content = if target_uri == uri {
765 &doc.content
766 } else if let Some(target_doc) = docs.get(&target_uri) {
767 &target_doc.content
768 } else {
769 tracing::warn!(
771 uri = %target_uri,
772 "Cannot convert span to range: document not open"
773 );
774 return None;
775 };
776 Some(Location {
777 uri: target_uri,
778 range: Range {
779 start: offset_to_position(content, loc.span.start as usize),
780 end: offset_to_position(content, loc.span.end as usize),
781 },
782 })
783 })
784 .collect();
785 if lsp_locations.is_empty() {
786 return Ok(None);
787 } else if lsp_locations.len() == 1 {
788 return Ok(Some(GotoDefinitionResponse::Scalar(
789 lsp_locations.into_iter().next().unwrap(),
790 )));
791 } else {
792 return Ok(Some(GotoDefinitionResponse::Array(lsp_locations)));
793 }
794 }
795 Ok(_) => {
796 tracing::debug!("Extension returned empty definitions");
797 }
798 Err(e) => {
799 tracing::warn!(error = ?e, "Extension definition failed");
800 }
801 }
802 }
803 }
804
805 Ok(None)
806 }
807
808 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
809 let uri = params.text_document_position_params.text_document.uri;
810 let position = params.text_document_position_params.position;
811
812 let docs = self.documents.read().await;
813 let Some(doc) = docs.get(&uri) else {
814 return Ok(None);
815 };
816
817 let Some(tree) = &doc.tree else {
818 return Ok(None);
819 };
820
821 let offset = position_to_offset(&doc.content, position);
822
823 let resolved = resolve_schema(tree, &uri).ok();
825
826 if let Some(range) = find_schema_declaration_range(tree, &doc.content)
828 && position >= range.start
829 && position <= range.end
830 && let Some(ref schema) = resolved
831 {
832 let content = format!(
833 "**Schema**: `{}`\n\nClick to open schema.",
834 schema.uri.as_str()
835 );
836 return Ok(Some(Hover {
837 contents: HoverContents::Markup(MarkupContent {
838 kind: MarkupKind::Markdown,
839 value: content,
840 }),
841 range: Some(range),
842 }));
843 }
844
845 if let Ok(schema_file) = load_document_schema(tree, &uri) {
847 let schema_id = &schema_file.meta.id;
848 tracing::debug!(%schema_id, "Trying extension for hover");
849 if let Some(client) = self.extensions.get_client(schema_id).await {
850 tracing::debug!("Got extension client, calling hover");
851 let field_path = find_field_path_at_offset(tree, offset).unwrap_or_default();
852 let context_obj = find_object_at_offset(tree, offset);
853 let tagged_context = find_tagged_context_at_offset(tree, offset);
854
855 let ext_params = ext::HoverParams {
856 document_uri: uri.to_string(),
857 cursor: ext::Cursor {
858 line: position.line,
859 character: position.character,
860 offset: offset as u32,
861 },
862 path: field_path,
863 context: context_obj.map(|c| Value {
864 tag: None,
865 payload: Some(styx_tree::Payload::Object(c.object)),
866 span: None,
867 }),
868 tagged_context,
869 };
870
871 match client.hover(ext_params).await {
872 Ok(Some(result)) => {
873 tracing::debug!(contents = %result.contents, "Extension returned hover");
874 let range = result.range.map(|r| Range {
875 start: Position {
876 line: r.start.line,
877 character: r.start.character,
878 },
879 end: Position {
880 line: r.end.line,
881 character: r.end.character,
882 },
883 });
884 return Ok(Some(Hover {
885 contents: HoverContents::Markup(MarkupContent {
886 kind: MarkupKind::Markdown,
887 value: result.contents,
888 }),
889 range,
890 }));
891 }
892 Ok(None) => {
893 tracing::debug!("Extension returned None for hover");
894 }
896 Err(e) => {
897 tracing::warn!(error = ?e, "Extension hover failed");
898 }
899 }
900 }
901 }
902
903 if let Some(field_path) = find_field_path_at_offset(tree, offset)
905 && let Some(ref schema) = resolved
906 {
907 let path_refs: Vec<&str> = field_path.iter().map(|s| s.as_str()).collect();
908
909 if let Some(field_info) = get_field_info_from_schema(&schema.source, &path_refs) {
910 let field_name = field_path.last().map(|s| s.as_str()).unwrap_or("");
912 let field_range = find_field_in_schema_source(&schema.source, field_name);
913 let schema_link = {
914 let mut link_uri = schema.uri.clone();
915 if let Some(range) = field_range {
916 let line = range.start.line + 1;
917 let col = range.start.character + 1;
918 link_uri.set_fragment(Some(&format!("L{}:{}", line, col)));
919 }
920 Some(link_uri)
921 };
922
923 let content = format_field_hover(
924 &field_path,
925 &field_info,
926 schema.uri.as_str(),
927 schema_link.as_ref(),
928 );
929 return Ok(Some(Hover {
930 contents: HoverContents::Markup(MarkupContent {
931 kind: MarkupKind::Markdown,
932 value: content,
933 }),
934 range: None,
935 }));
936 }
937 }
938
939 Ok(None)
940 }
941
942 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
943 let uri = params.text_document_position.text_document.uri;
944 let position = params.text_document_position.position;
945
946 let docs = self.documents.read().await;
947 let Some(doc) = docs.get(&uri) else {
948 return Ok(None);
949 };
950
951 let Some(tree) = &doc.tree else {
952 return Ok(None);
953 };
954
955 let Ok(schema) = resolve_schema(tree, &uri) else {
957 return Ok(None);
958 };
959
960 let offset = position_to_offset(&doc.content, position);
962 let context = find_object_at_offset(tree, offset);
963 let path = context.as_ref().map(|c| c.path.as_slice()).unwrap_or(&[]);
964
965 tracing::debug!(?offset, ?path, "completion: finding fields at path");
966
967 let schema_fields: Vec<(String, String)> = if let Ok(schema_file) =
969 facet_styx::from_str::<facet_styx::SchemaFile>(&schema.source)
970 {
971 let fields = get_schema_fields_at_path(&schema_file, path);
972 tracing::debug!(
973 count = fields.len(),
974 ?path,
975 "schema fields from parsed SchemaFile"
976 );
977 fields
978 .into_iter()
979 .map(|f| {
980 let type_str = if f.optional {
982 format!("@optional({})", schema_to_type_str(&f.schema))
983 } else if let Some(default) = &f.default_value {
984 format!("@default({} {})", default, schema_to_type_str(&f.schema))
985 } else {
986 schema_to_type_str(&f.schema)
987 };
988 (f.name, type_str)
989 })
990 .collect()
991 } else {
992 tracing::debug!("Failed to parse schema, falling back to source-based lookup");
993 get_schema_fields_from_source_at_path(&schema.source, path)
995 };
996 let existing_fields = context
997 .as_ref()
998 .map(|c| {
999 c.object
1000 .entries
1001 .iter()
1002 .filter_map(|e| e.key.as_str().map(|s| s.to_string()))
1003 .collect()
1004 })
1005 .unwrap_or_else(|| get_existing_fields(tree));
1006
1007 let word_info = get_word_range_at_position(&doc.content, position);
1009 let current_word = word_info.as_ref().map(|(w, _)| w.clone());
1010 let edit_range = word_info.map(|(_, r)| r).unwrap_or_else(|| Range {
1012 start: position,
1013 end: position,
1014 });
1015
1016 let available_fields: Vec<_> = schema_fields
1018 .into_iter()
1019 .filter(|(name, _)| !existing_fields.contains(name))
1020 .collect();
1021
1022 const MAX_COMPLETIONS: usize = 50;
1024 let filtered_fields = if available_fields.len() > MAX_COMPLETIONS {
1025 if let Some(ref word) = current_word {
1026 let mut scored: Vec<_> = available_fields
1028 .into_iter()
1029 .filter_map(|(name, type_str)| {
1030 let name_lower = name.to_lowercase();
1031 let word_lower = word.to_lowercase();
1032
1033 if name_lower.starts_with(&word_lower) {
1035 return Some((name, type_str, 0));
1036 }
1037
1038 if name_lower.contains(&word_lower) {
1040 return Some((name, type_str, 1));
1041 }
1042
1043 let dist = levenshtein(&word_lower, &name_lower);
1045 if dist <= 3 && dist < word.len().max(2) {
1046 return Some((name, type_str, 2 + dist));
1047 }
1048
1049 None
1050 })
1051 .collect();
1052
1053 scored.sort_by_key(|(_, _, score)| *score);
1055 scored
1056 .into_iter()
1057 .take(MAX_COMPLETIONS)
1058 .map(|(name, type_str, _)| (name, type_str))
1059 .collect()
1060 } else {
1061 let mut sorted = available_fields;
1063 sorted.sort_by_key(|(_, type_str)| {
1064 if type_str.starts_with("@optional") || type_str.starts_with("@default") {
1065 1
1066 } else {
1067 0
1068 }
1069 });
1070 sorted.into_iter().take(MAX_COMPLETIONS).collect()
1071 }
1072 } else {
1073 available_fields
1074 };
1075
1076 let mut items: Vec<CompletionItem> = filtered_fields
1078 .into_iter()
1079 .map(|(name, type_str)| {
1080 let is_optional =
1081 type_str.starts_with("@optional") || type_str.starts_with("@default");
1082
1083 let label_details = current_word.as_ref().and_then(|word| {
1085 let word_lower = word.to_lowercase();
1086 let name_lower = name.to_lowercase();
1087 if !name_lower.starts_with(&word_lower) && !word.is_empty() {
1088 let dist = levenshtein(&word_lower, &name_lower);
1089 if dist <= 3 && dist > 0 {
1090 return Some(CompletionItemLabelDetails {
1091 detail: Some(" (did you mean?)".to_string()),
1092 description: None,
1093 });
1094 }
1095 }
1096 None
1097 });
1098
1099 CompletionItem {
1100 label: name.clone(),
1101 label_details,
1102 kind: Some(CompletionItemKind::FIELD),
1103 detail: Some(type_str),
1104 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
1105 range: edit_range,
1106 new_text: format!("{} ", name),
1107 })),
1108 filter_text: Some(name.clone()),
1109 sort_text: Some(if is_optional {
1110 format!("1{}", name) } else {
1112 format!("0{}", name) }),
1114 ..Default::default()
1115 }
1116 })
1117 .collect();
1118
1119 if let Ok(schema_file) = load_document_schema(tree, &uri) {
1121 let schema_id = &schema_file.meta.id;
1122 if let Some(client) = self.extensions.get_client(schema_id).await {
1123 let tagged_context = find_tagged_context_at_offset(tree, offset);
1124 let ext_params = ext::CompletionParams {
1125 document_uri: uri.to_string(),
1126 cursor: ext::Cursor {
1127 line: position.line,
1128 character: position.character,
1129 offset: offset as u32,
1130 },
1131 path: path.iter().map(|s| s.to_string()).collect(),
1132 prefix: current_word.clone().unwrap_or_default(),
1133 context: context.map(|c| Value {
1134 tag: None,
1135 payload: Some(styx_tree::Payload::Object(c.object.clone())),
1136 span: None,
1137 }),
1138 tagged_context,
1139 };
1140
1141 match client.completions(ext_params).await {
1142 Ok(ext_items) => {
1143 tracing::debug!(count = ext_items.len(), "Got completions from extension");
1144 for item in ext_items {
1145 items.push(convert_ext_completion(item, edit_range));
1146 }
1147 }
1148 Err(e) => {
1149 tracing::warn!(error = ?e, "Extension completion failed");
1150 }
1151 }
1152 }
1153 }
1154
1155 Ok(Some(CompletionResponse::Array(items)))
1156 }
1157
1158 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
1159 let uri = params.text_document.uri;
1160 let mut actions = Vec::new();
1161
1162 for diag in ¶ms.context.diagnostics {
1164 if diag.source.as_deref() == Some("styx-hints") {
1166 if let Some(data) = &diag.data
1167 && let Some(fix_type) = data.get("type").and_then(|v| v.as_str())
1168 && fix_type == "add_schema"
1169 && let Some(declaration) = data.get("declaration").and_then(|v| v.as_str())
1170 {
1171 let tool_name = data
1172 .get("tool")
1173 .and_then(|v| v.as_str())
1174 .unwrap_or("schema");
1175
1176 {
1178 let edit = TextEdit {
1179 range: Range {
1180 start: Position::new(0, 0),
1181 end: Position::new(0, 0),
1182 },
1183 new_text: format!("{}\n\n", declaration),
1184 };
1185
1186 let mut changes = std::collections::HashMap::new();
1187 changes.insert(uri.clone(), vec![edit]);
1188
1189 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1190 title: format!("Add {} schema declaration", tool_name),
1191 kind: Some(CodeActionKind::QUICKFIX),
1192 diagnostics: Some(vec![diag.clone()]),
1193 edit: Some(WorkspaceEdit {
1194 changes: Some(changes),
1195 ..Default::default()
1196 }),
1197 is_preferred: Some(true),
1198 ..Default::default()
1199 }));
1200 }
1201
1202 {
1204 let edit = TextEdit {
1205 range: Range {
1206 start: Position::new(0, 0),
1207 end: Position::new(0, 0),
1208 },
1209 new_text: "@schema @\n\n".to_string(),
1210 };
1211
1212 let mut changes = std::collections::HashMap::new();
1213 changes.insert(uri.clone(), vec![edit]);
1214
1215 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1216 title: "Don't use a schema for this file".to_string(),
1217 kind: Some(CodeActionKind::QUICKFIX),
1218 diagnostics: Some(vec![diag.clone()]),
1219 edit: Some(WorkspaceEdit {
1220 changes: Some(changes),
1221 ..Default::default()
1222 }),
1223 is_preferred: Some(false),
1224 ..Default::default()
1225 }));
1226 }
1227 }
1228 continue;
1229 }
1230
1231 if diag.source.as_deref() == Some("styx-extension") {
1233 if let Some(data) = &diag.data
1234 && let Some(fix_type) = data.get("type").and_then(|v| v.as_str())
1235 && fix_type == "allow_extension"
1236 && let Some(command) = data.get("command").and_then(|v| v.as_str())
1237 {
1238 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1240 title: format!("Allow LSP extension '{}'", command),
1241 kind: Some(CodeActionKind::QUICKFIX),
1242 diagnostics: Some(vec![diag.clone()]),
1243 command: Some(Command {
1244 title: format!("Allow LSP extension '{}'", command),
1245 command: "styx.allowExtension".to_string(),
1246 arguments: Some(vec![serde_json::json!({
1247 "command": command,
1248 })]),
1249 }),
1250 is_preferred: Some(true),
1251 ..Default::default()
1252 }));
1253 }
1254 continue;
1255 }
1256
1257 if diag.source.as_deref() != Some("styx-schema") {
1259 continue;
1260 }
1261
1262 if let Some(data) = &diag.data
1264 && let Some(fix_type) = data.get("type").and_then(|v| v.as_str())
1265 && fix_type == "rename_field"
1266 && let (Some(from), Some(to)) = (
1267 data.get("from").and_then(|v| v.as_str()),
1268 data.get("to").and_then(|v| v.as_str()),
1269 )
1270 {
1271 let edit = TextEdit {
1273 range: diag.range,
1274 new_text: to.to_string(),
1275 };
1276
1277 let mut changes = std::collections::HashMap::new();
1278 changes.insert(uri.clone(), vec![edit]);
1279
1280 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1281 title: format!("Rename '{}' to '{}'", from, to),
1282 kind: Some(CodeActionKind::QUICKFIX),
1283 diagnostics: Some(vec![diag.clone()]),
1284 edit: Some(WorkspaceEdit {
1285 changes: Some(changes),
1286 ..Default::default()
1287 }),
1288 is_preferred: Some(true),
1289 ..Default::default()
1290 }));
1291 }
1292 }
1293
1294 let docs = self.documents.read().await;
1296 if let Some(doc) = docs.get(&uri)
1297 && let Some(ref tree) = doc.tree
1298 && let Ok(schema_file) = load_document_schema(tree, &uri)
1299 {
1300 let schema_id = &schema_file.meta.id;
1301 if let Some(client) = self.extensions.get_client(schema_id).await {
1302 let ext_diagnostics: Vec<ext::Diagnostic> = params
1304 .context
1305 .diagnostics
1306 .iter()
1307 .map(|d| ext::Diagnostic {
1308 span: styx_tree::Span {
1309 start: position_to_offset(&doc.content, d.range.start) as u32,
1310 end: position_to_offset(&doc.content, d.range.end) as u32,
1311 },
1312 severity: match d.severity {
1313 Some(DiagnosticSeverity::ERROR) => ext::DiagnosticSeverity::Error,
1314 Some(DiagnosticSeverity::WARNING) => ext::DiagnosticSeverity::Warning,
1315 Some(DiagnosticSeverity::INFORMATION) => ext::DiagnosticSeverity::Info,
1316 Some(DiagnosticSeverity::HINT) | None => ext::DiagnosticSeverity::Hint,
1317 Some(_) => ext::DiagnosticSeverity::Info,
1318 },
1319 message: d.message.clone(),
1320 source: d.source.clone(),
1321 code: d.code.as_ref().map(|c| match c {
1322 NumberOrString::String(s) => s.clone(),
1323 NumberOrString::Number(n) => n.to_string(),
1324 }),
1325 data: d.data.as_ref().map(|json_val| {
1326 json_to_styx_value(json_val)
1328 }),
1329 })
1330 .collect();
1331
1332 let ext_params = ext::CodeActionParams {
1333 document_uri: uri.to_string(),
1334 span: styx_tree::Span {
1335 start: position_to_offset(&doc.content, params.range.start) as u32,
1336 end: position_to_offset(&doc.content, params.range.end) as u32,
1337 },
1338 diagnostics: ext_diagnostics,
1339 };
1340
1341 match client.code_actions(ext_params).await {
1342 Ok(ext_actions) => {
1343 tracing::debug!(
1344 count = ext_actions.len(),
1345 "Got code actions from extension"
1346 );
1347 for action in ext_actions {
1348 let kind = action.kind.map(|k| match k {
1349 ext::CodeActionKind::QuickFix => CodeActionKind::QUICKFIX,
1350 ext::CodeActionKind::Refactor => CodeActionKind::REFACTOR,
1351 ext::CodeActionKind::Source => CodeActionKind::SOURCE,
1352 });
1353
1354 let edit = action.edit.map(|we| {
1355 let changes: std::collections::HashMap<_, _> = we
1356 .changes
1357 .into_iter()
1358 .filter_map(|doc_edit| {
1359 let doc_uri = Url::parse(&doc_edit.uri)
1360 .unwrap_or_else(|_| uri.clone());
1361 let content = if doc_uri == uri {
1363 &doc.content
1364 } else if let Some(target_doc) = docs.get(&doc_uri) {
1365 &target_doc.content
1366 } else {
1367 tracing::warn!(
1368 uri = %doc_uri,
1369 "Cannot convert span to range: document not open"
1370 );
1371 return None;
1372 };
1373 let edits: Vec<TextEdit> = doc_edit
1374 .edits
1375 .into_iter()
1376 .map(|e| TextEdit {
1377 range: Range {
1378 start: offset_to_position(
1379 content,
1380 e.span.start as usize,
1381 ),
1382 end: offset_to_position(
1383 content,
1384 e.span.end as usize,
1385 ),
1386 },
1387 new_text: e.new_text,
1388 })
1389 .collect();
1390 Some((doc_uri, edits))
1391 })
1392 .collect();
1393 WorkspaceEdit {
1394 changes: Some(changes),
1395 ..Default::default()
1396 }
1397 });
1398
1399 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1400 title: action.title,
1401 kind,
1402 edit,
1403 is_preferred: Some(action.is_preferred),
1404 ..Default::default()
1405 }));
1406 }
1407 }
1408 Err(e) => {
1409 tracing::warn!(error = ?e, "Extension code actions failed");
1410 }
1411 }
1412 }
1413 }
1414 drop(docs);
1415
1416 let docs = self.documents.read().await;
1418 if let Some(doc) = docs.get(&uri)
1419 && let Some(ref tree) = doc.tree
1420 {
1421 if let Ok(schema_file) = load_document_schema(tree, &uri) {
1423 let cursor_offset = position_to_offset(&doc.content, params.range.start);
1425 let object_ctx = find_object_at_offset(tree, cursor_offset);
1426
1427 let (schema_fields, existing_fields, context_name) =
1429 if let Some(ref ctx) = object_ctx {
1430 let fields = get_schema_fields_at_path(&schema_file, &ctx.path);
1431 let existing: Vec<String> = ctx
1432 .object
1433 .entries
1434 .iter()
1435 .filter_map(|e| e.key.as_str().map(String::from))
1436 .collect();
1437 let name = if ctx.path.is_empty() {
1438 String::new()
1439 } else {
1440 format!(" in '{}'", ctx.path.join("."))
1441 };
1442 (fields, existing, name)
1443 } else {
1444 let fields = get_schema_fields(&schema_file);
1445 let existing = get_document_fields(tree);
1446 (fields, existing, String::new())
1447 };
1448
1449 let missing_required: Vec<_> = schema_fields
1451 .iter()
1452 .filter(|f| !f.optional && !existing_fields.contains(&f.name))
1453 .collect();
1454
1455 let missing_optional: Vec<_> = schema_fields
1456 .iter()
1457 .filter(|f| f.optional && !existing_fields.contains(&f.name))
1458 .collect();
1459
1460 let insert_info = if let Some(ref ctx) = object_ctx {
1462 if let Some(span) = ctx.span {
1463 let obj_content = &doc.content[span.start as usize..span.end as usize];
1465 let obj_insert = find_field_insert_position(obj_content);
1466 let obj_start_pos = offset_to_position(&doc.content, span.start as usize);
1468 InsertPosition {
1469 position: Position {
1470 line: obj_start_pos.line + obj_insert.position.line,
1471 character: if obj_insert.position.line == 0 {
1472 obj_start_pos.character + obj_insert.position.character
1473 } else {
1474 obj_insert.position.character
1475 },
1476 },
1477 indent: obj_insert.indent,
1478 }
1479 } else {
1480 find_field_insert_position(&doc.content)
1481 }
1482 } else {
1483 find_field_insert_position(&doc.content)
1484 };
1485
1486 if !missing_required.is_empty() {
1488 let new_text = generate_fields_text(&missing_required, &insert_info.indent);
1489 let edit = TextEdit {
1490 range: Range {
1491 start: insert_info.position,
1492 end: insert_info.position,
1493 },
1494 new_text,
1495 };
1496
1497 let mut changes = std::collections::HashMap::new();
1498 changes.insert(uri.clone(), vec![edit]);
1499
1500 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1501 title: format!(
1502 "Fill {} required field{}{}",
1503 missing_required.len(),
1504 if missing_required.len() == 1 { "" } else { "s" },
1505 context_name
1506 ),
1507 kind: Some(CodeActionKind::REFACTOR),
1508 edit: Some(WorkspaceEdit {
1509 changes: Some(changes),
1510 ..Default::default()
1511 }),
1512 ..Default::default()
1513 }));
1514 }
1515
1516 let all_missing: Vec<_> = schema_fields
1518 .iter()
1519 .filter(|f| !existing_fields.contains(&f.name))
1520 .collect();
1521
1522 if !all_missing.is_empty() && !missing_optional.is_empty() {
1523 let new_text = generate_fields_text(&all_missing, &insert_info.indent);
1524 let edit = TextEdit {
1525 range: Range {
1526 start: insert_info.position,
1527 end: insert_info.position,
1528 },
1529 new_text,
1530 };
1531
1532 let mut changes = std::collections::HashMap::new();
1533 changes.insert(uri.clone(), vec![edit]);
1534
1535 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1536 title: format!(
1537 "Fill all {} field{}{}",
1538 all_missing.len(),
1539 if all_missing.len() == 1 { "" } else { "s" },
1540 context_name
1541 ),
1542 kind: Some(CodeActionKind::REFACTOR),
1543 edit: Some(WorkspaceEdit {
1544 changes: Some(changes),
1545 ..Default::default()
1546 }),
1547 ..Default::default()
1548 }));
1549 }
1550
1551 if object_ctx
1553 .as_ref()
1554 .map(|c| c.path.is_empty())
1555 .unwrap_or(true)
1556 {
1557 let root_fields = get_schema_fields(&schema_file);
1558 if let Some(reorder_edit) =
1559 generate_reorder_edit(tree, &root_fields, &doc.content)
1560 {
1561 let mut changes = std::collections::HashMap::new();
1562 changes.insert(uri.clone(), vec![reorder_edit]);
1563
1564 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
1565 title: "Reorder fields to match schema".to_string(),
1566 kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
1567 edit: Some(WorkspaceEdit {
1568 changes: Some(changes),
1569 ..Default::default()
1570 }),
1571 ..Default::default()
1572 }));
1573 }
1574 }
1575 }
1576 }
1577
1578 if actions.is_empty() {
1579 Ok(None)
1580 } else {
1581 Ok(Some(actions))
1582 }
1583 }
1584
1585 async fn execute_command(
1586 &self,
1587 params: ExecuteCommandParams,
1588 ) -> Result<Option<serde_json::Value>> {
1589 match params.command.as_str() {
1590 "styx.allowExtension" => {
1591 if let Some(arg) = params.arguments.first()
1593 && let Some(command) = arg.get("command").and_then(|v| v.as_str())
1594 {
1595 tracing::info!(command, "Allowing LSP extension");
1596 self.extensions.allow(command.to_string()).await;
1597
1598 self.client
1600 .log_message(
1601 MessageType::INFO,
1602 format!("Allowed LSP extension: {}", command),
1603 )
1604 .await;
1605
1606 let docs = self.documents.read().await;
1609 for (uri, doc) in docs.iter() {
1610 let blocked_extension = if let Some(ref tree) = doc.tree {
1611 self.check_for_extension(tree, uri).await
1612 } else {
1613 None
1614 };
1615 self.publish_diagnostics(
1616 uri.clone(),
1617 &doc.content,
1618 &doc.parse,
1619 doc.tree.as_ref(),
1620 None, doc.version,
1622 blocked_extension,
1623 )
1624 .await;
1625 }
1626
1627 let _ = self.client.inlay_hint_refresh().await;
1629 }
1630 Ok(None)
1631 }
1632 _ => {
1633 tracing::warn!(command = %params.command, "Unknown command");
1634 Ok(None)
1635 }
1636 }
1637 }
1638
1639 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
1640 let uri = params.text_document_position.text_document.uri;
1641 let position = params.text_document_position.position;
1642
1643 let docs = self.documents.read().await;
1644 let doc = match docs.get(&uri) {
1645 Some(doc) => doc,
1646 None => return Ok(None),
1647 };
1648
1649 let Some(tree) = &doc.tree else {
1650 return Ok(None);
1651 };
1652
1653 let offset = position_to_offset(&doc.content, position);
1654
1655 let Some(field_name) = find_field_key_at_offset(tree, offset) else {
1657 return Ok(None);
1658 };
1659
1660 let mut locations = Vec::new();
1661
1662 if is_schema_file(tree) {
1664 for (doc_uri, doc_state) in docs.iter() {
1666 if doc_uri == &uri {
1667 continue;
1668 }
1669 if let Some(ref doc_tree) = doc_state.tree {
1670 if let Ok(doc_schema) = resolve_schema(doc_tree, doc_uri)
1672 && doc_schema.uri == uri
1673 {
1674 if let Some(range) =
1676 find_field_in_doc(doc_tree, &field_name, &doc_state.content)
1677 {
1678 locations.push(Location {
1679 uri: doc_uri.clone(),
1680 range,
1681 });
1682 }
1683 }
1684 }
1685 }
1686 } else {
1687 if let Ok(schema) = resolve_schema(tree, &uri) {
1689 if let Some(field_range) = find_field_in_schema_source(&schema.source, &field_name)
1691 {
1692 locations.push(Location {
1693 uri: schema.uri.clone(),
1694 range: field_range,
1695 });
1696
1697 for (doc_uri, doc_state) in docs.iter() {
1699 if let Some(ref doc_tree) = doc_state.tree
1700 && let Ok(doc_schema) = resolve_schema(doc_tree, doc_uri)
1701 && doc_schema.uri == schema.uri
1702 {
1703 if let Some(range) =
1705 find_field_in_doc(doc_tree, &field_name, &doc_state.content)
1706 {
1707 locations.push(Location {
1708 uri: doc_uri.clone(),
1709 range,
1710 });
1711 }
1712 }
1713 }
1714 }
1715 }
1716 }
1717
1718 if locations.is_empty() {
1719 Ok(None)
1720 } else {
1721 Ok(Some(locations))
1722 }
1723 }
1724
1725 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
1726 let uri = params.text_document.uri;
1727
1728 let (tree, content) = {
1731 let docs = self.documents.read().await;
1732 let Some(doc) = docs.get(&uri) else {
1733 return Ok(None);
1734 };
1735 let Some(tree) = &doc.tree else {
1736 return Ok(None);
1737 };
1738 (tree.clone(), doc.content.clone())
1739 };
1740
1741 let mut hints = Vec::new();
1742
1743 if let Some(range) = find_schema_declaration_range(&tree, &content)
1745 && let Ok(schema) = resolve_schema(&tree, &uri)
1746 {
1747 if let Some(meta) = get_schema_meta(&schema.source) {
1749 let short_desc = meta
1752 .description
1753 .as_ref()
1754 .and_then(|d| d.lines().next().map(|s| s.trim().to_string()));
1755
1756 let label = match (&short_desc, &meta.version) {
1758 (Some(desc), Some(ver)) => format!(" — {} ({})", desc, ver),
1759 (Some(desc), None) => format!(" — {}", desc),
1760 (None, Some(ver)) => format!(" — v{}", ver),
1761 (None, None) => String::new(),
1762 };
1763
1764 if !label.is_empty() {
1765 let tooltip = {
1767 let mut parts = Vec::new();
1768 if let Some(id) = &meta.id {
1769 parts.push(format!("Schema ID: {}", id));
1770 }
1771 if let Some(desc) = &meta.description
1773 && desc.contains('\n')
1774 {
1775 parts.push(String::new()); parts.push(desc.clone());
1777 }
1778 if parts.is_empty() {
1779 None
1780 } else {
1781 Some(InlayHintTooltip::String(parts.join("\n")))
1782 }
1783 };
1784
1785 hints.push(InlayHint {
1786 position: range.end,
1787 label: InlayHintLabel::String(label),
1788 kind: Some(InlayHintKind::TYPE),
1789 text_edits: None,
1790 tooltip,
1791 padding_left: Some(false),
1792 padding_right: Some(true),
1793 data: None,
1794 });
1795 }
1796 }
1797 }
1798
1799 if let Ok(schema_file) = load_document_schema(&tree, &uri) {
1801 let schema_id = &schema_file.meta.id;
1802 tracing::debug!(%schema_id, "Trying extension for inlay hints");
1803 if let Some(client) = self.extensions.get_client(schema_id).await {
1804 tracing::debug!("Got extension client, calling inlay_hints");
1805 let ext_params = ext::InlayHintParams {
1806 document_uri: uri.to_string(),
1807 range: ext::Range {
1808 start: ext::Position {
1809 line: params.range.start.line,
1810 character: params.range.start.character,
1811 },
1812 end: ext::Position {
1813 line: params.range.end.line,
1814 character: params.range.end.character,
1815 },
1816 },
1817 context: Some(tree.clone()),
1818 };
1819
1820 match client.inlay_hints(ext_params).await {
1821 Ok(ext_hints) => {
1822 tracing::debug!(count = ext_hints.len(), "Got inlay hints from extension");
1823 for hint in ext_hints {
1824 let position = Position {
1827 line: hint.position.line,
1828 character: hint.position.character,
1829 };
1830 hints.push(InlayHint {
1831 position,
1832 label: InlayHintLabel::String(hint.label),
1833 kind: hint.kind.map(|k| match k {
1834 ext::InlayHintKind::Type => InlayHintKind::TYPE,
1835 ext::InlayHintKind::Parameter => InlayHintKind::PARAMETER,
1836 }),
1837 text_edits: None,
1838 tooltip: None,
1839 padding_left: Some(hint.padding_left),
1840 padding_right: Some(hint.padding_right),
1841 data: None,
1842 });
1843 }
1844 }
1845 Err(e) => {
1846 tracing::warn!(error = ?e, "Extension inlay hints failed");
1847 }
1848 }
1849 }
1850 }
1851
1852 if hints.is_empty() {
1853 Ok(None)
1854 } else {
1855 Ok(Some(hints))
1856 }
1857 }
1858
1859 async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1860 let uri = params.text_document.uri;
1861
1862 let docs = self.documents.read().await;
1863 let Some(doc) = docs.get(&uri) else {
1864 return Ok(None);
1865 };
1866
1867 if doc.tree.is_none() {
1869 return Ok(None);
1870 }
1871
1872 let indent = if params.options.insert_spaces {
1874 " ".repeat(params.options.tab_size as usize)
1875 } else {
1876 "\t".to_string()
1877 };
1878
1879 let options = styx_format::FormatOptions::default().indent(
1881 Box::leak(indent.into_boxed_str()),
1884 );
1885
1886 let formatted = styx_format::format_source(&doc.content, options);
1887
1888 if formatted == doc.content {
1890 return Ok(None);
1891 }
1892
1893 let lines: Vec<&str> = doc.content.lines().collect();
1895 let last_line = lines.len().saturating_sub(1);
1896 let last_char = lines.last().map(|l| l.len()).unwrap_or(0);
1897
1898 Ok(Some(vec![TextEdit {
1899 range: Range {
1900 start: Position {
1901 line: 0,
1902 character: 0,
1903 },
1904 end: Position {
1905 line: last_line as u32,
1906 character: last_char as u32,
1907 },
1908 },
1909 new_text: formatted,
1910 }]))
1911 }
1912
1913 async fn document_symbol(
1914 &self,
1915 params: DocumentSymbolParams,
1916 ) -> Result<Option<DocumentSymbolResponse>> {
1917 let uri = params.text_document.uri;
1918
1919 let docs = self.documents.read().await;
1920 let Some(doc) = docs.get(&uri) else {
1921 return Ok(None);
1922 };
1923
1924 let Some(tree) = &doc.tree else {
1925 return Ok(None);
1926 };
1927
1928 let symbols = collect_document_symbols(tree, &doc.content);
1929
1930 if symbols.is_empty() {
1931 Ok(None)
1932 } else {
1933 Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1934 }
1935 }
1936
1937 async fn on_type_formatting(
1938 &self,
1939 params: DocumentOnTypeFormattingParams,
1940 ) -> Result<Option<Vec<TextEdit>>> {
1941 let uri = params.text_document_position.text_document.uri;
1942 let position = params.text_document_position.position;
1943
1944 let docs = self.documents.read().await;
1945 let Some(doc) = docs.get(&uri) else {
1946 return Ok(None);
1947 };
1948
1949 if params.ch != "\n" {
1951 return Ok(None);
1952 }
1953
1954 let offset = position_to_offset(&doc.content, position);
1956
1957 let depth = find_nesting_depth_cst(&doc.parse, offset);
1959
1960 let indent_unit = if params.options.insert_spaces {
1962 " ".repeat(params.options.tab_size as usize)
1963 } else {
1964 "\t".to_string()
1965 };
1966
1967 let indent_str = indent_unit.repeat(depth);
1969
1970 Ok(Some(vec![TextEdit {
1972 range: Range {
1973 start: Position {
1974 line: position.line,
1975 character: 0,
1976 },
1977 end: Position {
1978 line: position.line,
1979 character: 0,
1980 },
1981 },
1982 new_text: indent_str,
1983 }]))
1984 }
1985}
1986
1987fn find_nesting_depth_cst(parse: &styx_cst::Parse, offset: usize) -> usize {
1992 use styx_cst::{SyntaxKind, TextSize, TokenAtOffset};
1993
1994 let root = parse.syntax();
1995 let offset = TextSize::new(offset as u32);
1996
1997 let token = match root.token_at_offset(offset) {
1999 TokenAtOffset::None => return 0,
2000 TokenAtOffset::Single(t) => {
2001 if matches!(t.kind(), SyntaxKind::R_BRACE | SyntaxKind::R_PAREN)
2003 && t.text_range().end() == offset
2004 {
2005 let mut depth: usize = 0;
2007 let mut node = t.parent();
2008 while let Some(n) = node {
2009 if matches!(n.kind(), SyntaxKind::OBJECT | SyntaxKind::SEQUENCE) {
2010 depth += 1;
2011 }
2012 node = n.parent();
2013 }
2014 return depth.saturating_sub(1);
2016 }
2017 t
2018 }
2019 TokenAtOffset::Between(left, right) => {
2020 match left.kind() {
2024 SyntaxKind::R_BRACE | SyntaxKind::R_PAREN => right,
2025 _ => left,
2026 }
2027 }
2028 };
2029
2030 let mut depth = 0;
2032 let mut node = token.parent();
2033
2034 while let Some(n) = node {
2035 match n.kind() {
2036 SyntaxKind::OBJECT | SyntaxKind::SEQUENCE => {
2037 depth += 1;
2038 }
2039 _ => {}
2040 }
2041 node = n.parent();
2042 }
2043
2044 depth
2045}
2046
2047fn collect_document_symbols(value: &Value, content: &str) -> Vec<DocumentSymbol> {
2049 let mut symbols = Vec::new();
2050
2051 let Some(obj) = value.as_object() else {
2052 return symbols;
2053 };
2054
2055 for entry in &obj.entries {
2056 if entry.key.is_unit() {
2058 continue;
2059 }
2060
2061 let Some(name) = entry.key.as_str() else {
2062 continue;
2063 };
2064
2065 let Some(key_span) = entry.key.span else {
2066 continue;
2067 };
2068
2069 let Some(val_span) = entry.value.span else {
2070 continue;
2071 };
2072
2073 let val_text = &content[val_span.start as usize..val_span.end as usize];
2075
2076 let (kind, detail, children): (SymbolKind, Option<String>, Vec<DocumentSymbol>) =
2078 if let Some(nested_obj) = entry.value.as_object() {
2079 let nested_value = Value {
2081 tag: entry.value.tag.clone(),
2082 payload: Some(styx_tree::Payload::Object(nested_obj.clone())),
2083 span: entry.value.span,
2084 };
2085 let children = collect_document_symbols(&nested_value, content);
2086 (SymbolKind::OBJECT, None, children)
2087 } else if entry.value.as_sequence().is_some() {
2088 (SymbolKind::ARRAY, Some("array".to_string()), Vec::new())
2089 } else if entry.value.as_str().is_some() {
2090 (SymbolKind::STRING, Some(val_text.to_string()), Vec::new())
2091 } else if entry.value.is_unit() {
2092 (SymbolKind::NULL, Some("@".to_string()), Vec::new())
2093 } else if entry.value.tag.is_some() {
2094 (SymbolKind::VARIABLE, Some(val_text.to_string()), Vec::new())
2096 } else {
2097 (SymbolKind::CONSTANT, Some(val_text.to_string()), Vec::new())
2099 };
2100
2101 let selection_range = Range {
2102 start: offset_to_position(content, key_span.start as usize),
2103 end: offset_to_position(content, key_span.end as usize),
2104 };
2105
2106 let range = Range {
2107 start: offset_to_position(content, key_span.start as usize),
2108 end: offset_to_position(content, val_span.end as usize),
2109 };
2110
2111 #[allow(deprecated)]
2112 symbols.push(DocumentSymbol {
2113 name: name.to_string(),
2114 detail,
2115 kind,
2116 tags: None,
2117 deprecated: None,
2118 range,
2119 selection_range,
2120 children: if children.is_empty() {
2121 None
2122 } else {
2123 Some(children)
2124 },
2125 });
2126 }
2127
2128 symbols
2129}
2130
2131fn find_schema_declaration_range(tree: &Value, content: &str) -> Option<Range> {
2133 let obj = tree.as_object()?;
2134
2135 for entry in &obj.entries {
2136 if entry.key.is_schema_tag() {
2137 let span = entry.value.span?;
2138 return Some(Range {
2139 start: offset_to_position(content, span.start as usize),
2140 end: offset_to_position(content, span.end as usize),
2141 });
2142 }
2143 }
2144
2145 None
2146}
2147
2148fn offset_to_position(content: &str, offset: usize) -> Position {
2150 let mut line = 0u32;
2151 let mut col = 0u32;
2152
2153 for (i, ch) in content.char_indices() {
2154 if i >= offset {
2155 break;
2156 }
2157 if ch == '\n' {
2158 line += 1;
2159 col = 0;
2160 } else {
2161 col += 1;
2162 }
2163 }
2164
2165 Position::new(line, col)
2166}
2167
2168fn position_to_offset(content: &str, position: Position) -> usize {
2170 let mut current_line = 0u32;
2171 let mut current_col = 0u32;
2172
2173 for (i, ch) in content.char_indices() {
2174 if current_line == position.line && current_col == position.character {
2175 return i;
2176 }
2177 if ch == '\n' {
2178 if current_line == position.line {
2179 return i;
2181 }
2182 current_line += 1;
2183 current_col = 0;
2184 } else {
2185 current_col += 1;
2186 }
2187 }
2188
2189 content.len()
2190}
2191
2192fn find_field_key_at_offset(tree: &Value, offset: usize) -> Option<String> {
2194 find_field_path_at_offset(tree, offset).and_then(|path| path.last().cloned())
2196}
2197
2198#[derive(Debug, Clone)]
2200enum PathSegment {
2201 Field(String),
2202 Index(usize),
2203}
2204
2205impl PathSegment {
2206 fn as_str(&self) -> String {
2207 match self {
2208 PathSegment::Field(name) => name.clone(),
2209 PathSegment::Index(i) => i.to_string(),
2210 }
2211 }
2212}
2213
2214fn find_field_path_at_offset(tree: &Value, offset: usize) -> Option<Vec<String>> {
2216 find_path_segments_at_offset(tree, offset)
2217 .map(|segments| segments.iter().map(|s| s.as_str()).collect())
2218}
2219
2220fn find_path_segments_at_offset(tree: &Value, offset: usize) -> Option<Vec<PathSegment>> {
2222 find_path_in_value(tree, offset)
2223}
2224
2225fn find_path_in_value(value: &Value, offset: usize) -> Option<Vec<PathSegment>> {
2227 if let Some(obj) = value.as_object() {
2229 for entry in &obj.entries {
2230 if let Some(span) = entry.key.span {
2232 let start = span.start as usize;
2233 let end = span.end as usize;
2234 if offset >= start
2235 && offset < end
2236 && let Some(key) = entry.key.as_str()
2237 {
2238 return Some(vec![PathSegment::Field(key.to_string())]);
2239 }
2240 }
2241 if let Some(span) = entry.value.span {
2243 let start = span.start as usize;
2244 let end = span.end as usize;
2245 if offset >= start && offset < end {
2246 if let Some(mut nested_path) = find_path_in_value(&entry.value, offset) {
2248 if let Some(key) = entry.key.as_str() {
2250 nested_path.insert(0, PathSegment::Field(key.to_string()));
2251 }
2252 return Some(nested_path);
2253 }
2254 if let Some(key) = entry.key.as_str() {
2256 return Some(vec![PathSegment::Field(key.to_string())]);
2257 }
2258 }
2259 }
2260 }
2261 }
2262
2263 if let Some(seq) = value.as_sequence() {
2265 for (index, item) in seq.items.iter().enumerate() {
2266 if let Some(span) = item.span {
2267 let start = span.start as usize;
2268 let end = span.end as usize;
2269 if offset >= start && offset < end {
2270 if let Some(mut nested_path) = find_path_in_value(item, offset) {
2272 nested_path.insert(0, PathSegment::Index(index));
2273 return Some(nested_path);
2274 }
2275 return Some(vec![PathSegment::Index(index)]);
2277 }
2278 }
2279 }
2280 }
2281
2282 None
2283}
2284
2285fn find_field_in_schema_source(schema_source: &str, field_name: &str) -> Option<Range> {
2287 let tree = styx_tree::parse(schema_source).ok()?;
2289
2290 let obj = tree.as_object()?;
2292
2293 for entry in &obj.entries {
2294 if entry.key.as_str() == Some("schema") {
2295 if let Some(schema_obj) = entry.value.as_object() {
2301 if let Some(unit_value) = schema_obj.get_unit() {
2303 if let Some(inner_obj) = unit_value.as_object() {
2305 for inner_entry in &inner_obj.entries {
2307 if inner_entry.key.tag_name() == Some("object") {
2308 return find_field_in_object(
2310 &inner_entry.value,
2311 schema_source,
2312 field_name,
2313 );
2314 }
2315 }
2316 } else if unit_value.tag_name() == Some("object") {
2317 return find_field_in_object(unit_value, schema_source, field_name);
2319 }
2320 }
2321 }
2322 return find_field_in_object(&entry.value, schema_source, field_name);
2324 }
2325 }
2326
2327 None
2328}
2329
2330fn find_field_in_object(value: &Value, source: &str, field_name: &str) -> Option<Range> {
2332 let obj = if let Some(obj) = value.as_object() {
2334 obj
2335 } else if value.tag.is_some() {
2336 match &value.payload {
2338 Some(styx_tree::Payload::Object(obj)) => obj,
2339 _ => return None,
2340 }
2341 } else {
2342 return None;
2343 };
2344
2345 for entry in &obj.entries {
2346 if entry.key.as_str() == Some(field_name) {
2347 if let Some(span) = entry.key.span {
2349 return Some(Range {
2350 start: offset_to_position(source, span.start as usize),
2351 end: offset_to_position(source, span.end as usize),
2352 });
2353 }
2354 }
2355
2356 if entry.key.is_unit()
2358 && let Some(found) = find_field_in_object(&entry.value, source, field_name)
2359 {
2360 return Some(found);
2361 }
2362
2363 if let Some(found) = find_field_in_object(&entry.value, source, field_name) {
2365 return Some(found);
2366 }
2367 }
2368
2369 None
2370}
2371
2372struct SchemaMeta {
2374 id: Option<String>,
2375 version: Option<String>,
2376 description: Option<String>,
2377}
2378
2379fn get_schema_meta(schema_source: &str) -> Option<SchemaMeta> {
2381 let tree = styx_tree::parse(schema_source).ok()?;
2382 let obj = tree.as_object()?;
2383
2384 for entry in &obj.entries {
2385 if entry.key.as_str() == Some("meta") {
2386 let meta_obj = entry.value.as_object()?;
2387
2388 let id = meta_obj
2389 .get("id")
2390 .and_then(|v| v.as_str())
2391 .map(String::from);
2392 let version = meta_obj
2393 .get("version")
2394 .and_then(|v| v.as_str())
2395 .map(String::from);
2396 let description = meta_obj
2397 .get("description")
2398 .and_then(|v| v.as_str())
2399 .map(String::from);
2400
2401 return Some(SchemaMeta {
2402 id,
2403 version,
2404 description,
2405 });
2406 }
2407 }
2408
2409 None
2410}
2411
2412struct FieldInfo {
2415 type_str: String,
2417 doc_comment: Option<String>,
2419}
2420
2421fn get_field_info_from_schema(schema_source: &str, field_path: &[&str]) -> Option<FieldInfo> {
2422 let tree = styx_tree::parse(schema_source).ok()?;
2423 let obj = tree.as_object()?;
2424
2425 let schema_value = obj
2427 .entries
2428 .iter()
2429 .find(|e| e.key.as_str() == Some("schema"))
2430 .map(|e| &e.value)?;
2431
2432 get_field_info_in_schema(schema_value, field_path)
2433}
2434
2435fn get_field_info_in_schema(schema_block: &Value, path: &[&str]) -> Option<FieldInfo> {
2438 let schema_obj = schema_block.as_object()?;
2439
2440 let root = schema_obj
2442 .entries
2443 .iter()
2444 .find(|e| e.key.is_unit())
2445 .map(|e| &e.value)?;
2446
2447 get_field_info_in_type(root, path, schema_obj)
2448}
2449
2450fn get_field_info_in_type(
2453 value: &Value,
2454 path: &[&str],
2455 schema_defs: &styx_tree::Object,
2456) -> Option<FieldInfo> {
2457 if path.is_empty() {
2458 return None;
2459 }
2460
2461 let field_name = path[0];
2462 let remaining_path = &path[1..];
2463
2464 if let Some(seq_element_type) = extract_seq_element_type(value) {
2466 if field_name.parse::<usize>().is_ok() {
2468 let resolved = resolve_type_reference(seq_element_type, schema_defs);
2470 return get_field_info_in_type(resolved, remaining_path, schema_defs);
2471 }
2472 }
2473
2474 if let Some(map_value_type) = extract_map_value_type(value) {
2476 if remaining_path.is_empty() {
2477 return Some(FieldInfo {
2479 type_str: format_type_concise(map_value_type),
2480 doc_comment: None, });
2482 } else {
2483 let resolved = resolve_type_reference(map_value_type, schema_defs);
2486 return get_field_info_in_type(resolved, remaining_path, schema_defs);
2487 }
2488 }
2489
2490 if let Some(catchall_value_type) = extract_object_catchall_value_type(value) {
2493 if remaining_path.is_empty() {
2494 return Some(FieldInfo {
2496 type_str: format_type_concise(catchall_value_type),
2497 doc_comment: None, });
2499 } else {
2500 let resolved = resolve_type_reference(catchall_value_type, schema_defs);
2502 return get_field_info_in_type(resolved, remaining_path, schema_defs);
2503 }
2504 }
2505
2506 if let Some(enum_obj) = extract_enum_object(value) {
2510 for entry in &enum_obj.entries {
2512 let resolved = resolve_type_reference(&entry.value, schema_defs);
2514 if let Some(info) = get_field_info_in_type(resolved, path, schema_defs) {
2515 return Some(info);
2516 }
2517 }
2518 return None;
2519 }
2520
2521 let obj = extract_object_from_value_with_defs(value, schema_defs)?;
2523
2524 for entry in &obj.entries {
2525 if entry.key.as_str() == Some(field_name) {
2526 if remaining_path.is_empty() {
2527 return Some(FieldInfo {
2529 type_str: format_type_concise(&entry.value),
2530 doc_comment: entry.doc_comment.clone(),
2531 });
2532 } else {
2533 return get_field_info_in_type(&entry.value, remaining_path, schema_defs);
2535 }
2536 }
2537
2538 if entry.key.is_unit()
2540 && let Some(found) = get_field_info_in_type(&entry.value, path, schema_defs)
2541 {
2542 return Some(found);
2543 }
2544 }
2545
2546 None
2547}
2548
2549fn resolve_type_reference<'a>(value: &'a Value, schema_defs: &'a styx_tree::Object) -> &'a Value {
2552 if let Some(tag) = &value.tag {
2554 let is_type_ref = match &value.payload {
2555 None => true,
2556 Some(styx_tree::Payload::Scalar(s)) if s.text.is_empty() => true,
2557 _ => false,
2558 };
2559
2560 if is_type_ref {
2561 for entry in &schema_defs.entries {
2563 if entry.key.as_str() == Some(&tag.name) {
2564 return &entry.value;
2565 }
2566 }
2567 }
2568 }
2569 value
2570}
2571
2572fn extract_seq_element_type(value: &Value) -> Option<&Value> {
2576 let tag = value.tag.as_ref()?;
2577
2578 if tag.name == "seq" {
2580 let seq = value.as_sequence()?;
2581 return seq.items.first();
2582 }
2583
2584 if (tag.name == "optional" || tag.name == "default")
2586 && let Some(seq) = value.as_sequence()
2587 {
2588 for item in &seq.items {
2589 if let Some(element_type) = extract_seq_element_type(item) {
2590 return Some(element_type);
2591 }
2592 }
2593 }
2594
2595 None
2596}
2597
2598fn extract_map_value_type(value: &Value) -> Option<&Value> {
2602 let tag = value.tag.as_ref()?;
2603 if tag.name != "map" {
2604 return None;
2605 }
2606
2607 let seq = value.as_sequence()?;
2609 match seq.items.len() {
2610 1 => Some(&seq.items[0]), 2 => Some(&seq.items[1]), _ => None,
2613 }
2614}
2615
2616fn extract_enum_object(value: &Value) -> Option<&styx_tree::Object> {
2623 let tag = value.tag.as_ref()?;
2624 if tag.name != "enum" {
2625 return None;
2626 }
2627 value.as_object()
2628}
2629
2630fn extract_object_catchall_value_type(value: &Value) -> Option<&Value> {
2639 let tag = value.tag.as_ref()?;
2641 if tag.name != "object" {
2642 return None;
2643 }
2644
2645 let obj = value.as_object()?;
2646
2647 if obj.entries.len() == 1 {
2649 let entry = &obj.entries[0];
2650 if entry.key.tag.is_some() && entry.key.payload.is_none() {
2652 return Some(&entry.value);
2653 }
2654 }
2655
2656 None
2657}
2658
2659fn extract_object_from_value_with_defs<'a>(
2662 value: &'a Value,
2663 schema_defs: &'a styx_tree::Object,
2664) -> Option<&'a styx_tree::Object> {
2665 let resolved = resolve_type_reference(value, schema_defs);
2667
2668 if let Some(obj) = resolved.as_object() {
2670 return Some(obj);
2671 }
2672
2673 if resolved.tag.is_some() {
2675 match &resolved.payload {
2676 Some(styx_tree::Payload::Object(obj)) => return Some(obj),
2677 Some(styx_tree::Payload::Sequence(seq)) => {
2679 for item in &seq.items {
2681 if let Some(obj) = extract_object_from_value_with_defs(item, schema_defs) {
2682 return Some(obj);
2683 }
2684 }
2685 }
2686 _ => {}
2687 }
2688 }
2689
2690 None
2691}
2692
2693fn extract_object_from_value(value: &Value) -> Option<&styx_tree::Object> {
2696 if let Some(obj) = value.as_object() {
2698 return Some(obj);
2699 }
2700
2701 if value.tag.is_some() {
2703 match &value.payload {
2704 Some(styx_tree::Payload::Object(obj)) => return Some(obj),
2705 Some(styx_tree::Payload::Sequence(seq)) => {
2707 for item in &seq.items {
2709 if let Some(obj) = extract_object_from_value(item) {
2710 return Some(obj);
2711 }
2712 }
2713 }
2714 _ => {}
2715 }
2716 }
2717
2718 None
2719}
2720
2721fn format_type_concise(value: &Value) -> String {
2723 let mut result = String::new();
2724
2725 if let Some(tag) = &value.tag {
2726 result.push('@');
2727 result.push_str(&tag.name);
2728 }
2729
2730 match &value.payload {
2731 None => {}
2732 Some(styx_tree::Payload::Scalar(s)) => {
2733 if value.tag.is_some() {
2734 result.push('(');
2735 result.push_str(&s.text);
2736 result.push(')');
2737 } else {
2738 result.push_str(&s.text);
2739 }
2740 }
2741 Some(styx_tree::Payload::Object(obj)) => {
2742 if value.tag.is_some() {
2743 result.push_str("{...}");
2745 } else {
2746 let field_count = obj.entries.len();
2748 result.push_str(&format!("{{...}} ({} fields)", field_count));
2749 }
2750 }
2751 Some(styx_tree::Payload::Sequence(seq)) => {
2752 result.push('(');
2753 for (i, item) in seq.items.iter().enumerate() {
2754 if i > 0 {
2755 result.push(' ');
2756 }
2757 result.push_str(&format_type_concise(item));
2758 }
2759 result.push(')');
2760 }
2761 }
2762
2763 if result.is_empty() {
2764 "@".to_string() } else {
2766 result
2767 }
2768}
2769
2770fn format_breadcrumb(path: &[String], schema_link: Option<&str>) -> String {
2773 let root = match schema_link {
2774 Some(uri) => format!("[@]({})", uri),
2775 None => "@".to_string(),
2776 };
2777 let mut result = root;
2778 for segment in path {
2779 result.push_str(" › ");
2780 result.push_str(segment);
2781 }
2782 result
2783}
2784
2785fn format_field_hover(
2787 field_path: &[String],
2788 field_info: &FieldInfo,
2789 _schema_path: &str,
2790 schema_uri: Option<&Url>,
2791) -> String {
2792 let mut content = String::new();
2793
2794 if let Some(doc) = &field_info.doc_comment {
2796 content.push_str(doc);
2797 content.push_str("\n\n");
2798 }
2799
2800 let schema_link = schema_uri.map(|u| u.as_str());
2803 let breadcrumb = format_breadcrumb(field_path, schema_link);
2804 content.push_str(&format!("{}: `{}`", breadcrumb, field_info.type_str));
2805
2806 content
2807}
2808
2809fn schema_to_type_str(schema: &facet_styx::Schema) -> String {
2811 use facet_styx::Schema;
2812 match schema {
2813 Schema::String(_) => "@string".to_string(),
2814 Schema::Int(_) => "@int".to_string(),
2815 Schema::Float(_) => "@float".to_string(),
2816 Schema::Bool => "@bool".to_string(),
2817 Schema::Unit => "@unit".to_string(),
2818 Schema::Any => "@any".to_string(),
2819 Schema::Type { name: Some(n) } => format!("@{}", n),
2820 Schema::Type { name: None } => "@type".to_string(),
2821 Schema::Object(_) => "@object{...}".to_string(),
2822 Schema::Seq(_) => "@seq(...)".to_string(),
2823 Schema::Map(_) => "@map(...)".to_string(),
2824 Schema::Enum(_) => "@enum{...}".to_string(),
2825 Schema::Union(_) => "@union(...)".to_string(),
2826 Schema::OneOf(_) => "@oneof(...)".to_string(),
2827 Schema::Flatten(_) => "@flatten(...)".to_string(),
2828 Schema::Literal(s) => format!("\"{}\"", s),
2829 Schema::Optional(opt) => format!("@optional({})", schema_to_type_str(&opt.0.0)),
2830 Schema::Default(def) => format!("@default(..., {})", schema_to_type_str(&def.0.1)),
2831 Schema::Deprecated(dep) => format!("@deprecated({})", schema_to_type_str(&dep.0.1)),
2832 Schema::Tuple(t) => {
2833 let elements: Vec<_> = t.0.iter().map(|d| schema_to_type_str(&d.value)).collect();
2834 format!("@tuple({})", elements.join(" "))
2835 }
2836 }
2837}
2838
2839fn get_schema_fields_from_source_at_path(
2841 schema_source: &str,
2842 path: &[String],
2843) -> Vec<(String, String)> {
2844 let mut fields = Vec::new();
2845
2846 let Ok(tree) = styx_tree::parse(schema_source) else {
2847 return fields;
2848 };
2849
2850 let Some(obj) = tree.as_object() else {
2851 return fields;
2852 };
2853
2854 let schema_entry = obj
2856 .entries
2857 .iter()
2858 .find(|e| e.key.as_str() == Some("schema"));
2859 let Some(schema_entry) = schema_entry else {
2860 return fields;
2861 };
2862
2863 let target_value = if path.is_empty() {
2865 &schema_entry.value
2867 } else {
2868 match navigate_schema_path(&schema_entry.value, path) {
2870 Some(v) => v,
2871 None => return fields,
2872 }
2873 };
2874
2875 collect_fields_from_object(target_value, schema_source, &mut fields);
2876 fields
2877}
2878
2879fn navigate_schema_path<'a>(value: &'a Value, path: &[String]) -> Option<&'a Value> {
2881 if path.is_empty() {
2882 return Some(value);
2883 }
2884
2885 let field_name = &path[0];
2886 let remaining = &path[1..];
2887
2888 let obj = extract_object_from_value(value)?;
2890
2891 for entry in &obj.entries {
2892 if entry.key.as_str() == Some(field_name.as_str()) {
2894 let inner = unwrap_type_wrappers(&entry.value);
2896 return navigate_schema_path(inner, remaining);
2897 }
2898
2899 if entry.key.is_unit()
2901 && let Some(result) = navigate_schema_path(&entry.value, path)
2902 {
2903 return Some(result);
2904 }
2905 }
2906
2907 None
2908}
2909
2910fn unwrap_type_wrappers(value: &Value) -> &Value {
2912 if let Some(tag) = value.tag_name()
2914 && matches!(tag, "optional" | "default" | "deprecated")
2915 {
2916 match &value.payload {
2918 Some(styx_tree::Payload::Sequence(seq)) => {
2920 if let Some(first) = seq.items.first() {
2921 return unwrap_type_wrappers(first);
2922 }
2923 }
2924 Some(styx_tree::Payload::Object(obj)) => {
2926 for entry in &obj.entries {
2928 if entry.key.is_unit() {
2929 return unwrap_type_wrappers(&entry.value);
2930 }
2931 }
2932 }
2933 _ => {}
2934 }
2935 }
2936 value
2937}
2938
2939fn collect_fields_from_object(value: &Value, source: &str, fields: &mut Vec<(String, String)>) {
2941 let obj = if let Some(obj) = value.as_object() {
2942 obj
2943 } else if value.tag.is_some() {
2944 match &value.payload {
2945 Some(styx_tree::Payload::Object(obj)) => obj,
2946 _ => return,
2947 }
2948 } else {
2949 return;
2950 };
2951
2952 for entry in &obj.entries {
2953 if let Some(name) = entry.key.as_str()
2955 && let Some(span) = entry.value.span
2956 {
2957 let type_str = source[span.start as usize..span.end as usize].trim();
2958 fields.push((name.to_string(), type_str.to_string()));
2959 }
2960
2961 if entry.key.is_unit() {
2963 collect_fields_from_object(&entry.value, source, fields);
2964 }
2965 }
2966}
2967
2968fn get_existing_fields(tree: &Value) -> Vec<String> {
2970 let mut fields = Vec::new();
2971
2972 if let Some(obj) = tree.as_object() {
2973 for entry in &obj.entries {
2974 if let Some(name) = entry.key.as_str() {
2975 fields.push(name.to_string());
2976 }
2977 }
2978 }
2979
2980 fields
2981}
2982
2983fn get_word_range_at_position(content: &str, position: Position) -> Option<(String, Range)> {
2986 let offset = position_to_offset(content, position);
2987 if offset == 0 {
2988 return None;
2989 }
2990
2991 let before = &content[..offset];
2993 let word_start = before
2994 .rfind(|c: char| c.is_whitespace() || c == '{' || c == '}')
2995 .map(|i| i + 1)
2996 .unwrap_or(0);
2997
2998 let word = &before[word_start..];
2999 if word.is_empty() {
3000 None
3001 } else {
3002 let start_position = offset_to_position(content, word_start);
3004 let range = Range {
3005 start: start_position,
3006 end: position,
3007 };
3008 Some((word.to_string(), range))
3009 }
3010}
3011
3012fn levenshtein(a: &str, b: &str) -> usize {
3014 let a_chars: Vec<char> = a.chars().collect();
3015 let b_chars: Vec<char> = b.chars().collect();
3016 let m = a_chars.len();
3017 let n = b_chars.len();
3018
3019 if m == 0 {
3020 return n;
3021 }
3022 if n == 0 {
3023 return m;
3024 }
3025
3026 let mut prev: Vec<usize> = (0..=n).collect();
3027 let mut curr = vec![0; n + 1];
3028
3029 for i in 1..=m {
3030 curr[0] = i;
3031 for j in 1..=n {
3032 let cost = if a_chars[i - 1] == b_chars[j - 1] {
3033 0
3034 } else {
3035 1
3036 };
3037 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
3038 }
3039 std::mem::swap(&mut prev, &mut curr);
3040 }
3041
3042 prev[n]
3043}
3044
3045fn convert_styx_value_to_json(value: &styx_tree::Value) -> serde_json::Value {
3047 if let Some(s) = value.as_str() {
3049 return serde_json::Value::String(s.to_string());
3050 }
3051
3052 if let Some(styx_tree::Payload::Object(obj)) = &value.payload {
3054 let mut map = serde_json::Map::new();
3055 for entry in &obj.entries {
3056 if let Some(key) = entry.key.as_str() {
3057 map.insert(key.to_string(), convert_styx_value_to_json(&entry.value));
3058 }
3059 }
3060 return serde_json::Value::Object(map);
3061 }
3062
3063 if let Some(styx_tree::Payload::Sequence(seq)) = &value.payload {
3065 let arr: Vec<_> = seq.items.iter().map(convert_styx_value_to_json).collect();
3066 return serde_json::Value::Array(arr);
3067 }
3068
3069 serde_json::Value::Null
3071}
3072
3073fn json_to_styx_value(json: &serde_json::Value) -> styx_tree::Value {
3075 use styx_tree::{Entry, Object, Payload, Scalar, ScalarKind, Sequence, Value};
3076
3077 match json {
3078 serde_json::Value::Null => Value::unit(),
3079
3080 serde_json::Value::Bool(b) => Value {
3081 tag: None,
3082 payload: Some(Payload::Scalar(Scalar {
3083 text: b.to_string(),
3084 kind: ScalarKind::Bare,
3085 span: None,
3086 })),
3087 span: None,
3088 },
3089
3090 serde_json::Value::Number(n) => Value {
3091 tag: None,
3092 payload: Some(Payload::Scalar(Scalar {
3093 text: n.to_string(),
3094 kind: ScalarKind::Bare,
3095 span: None,
3096 })),
3097 span: None,
3098 },
3099
3100 serde_json::Value::String(s) => Value {
3101 tag: None,
3102 payload: Some(Payload::Scalar(Scalar {
3103 text: s.clone(),
3104 kind: ScalarKind::Bare,
3105 span: None,
3106 })),
3107 span: None,
3108 },
3109
3110 serde_json::Value::Array(arr) => {
3111 let items = arr.iter().map(json_to_styx_value).collect();
3112 Value {
3113 tag: None,
3114 payload: Some(Payload::Sequence(Sequence { items, span: None })),
3115 span: None,
3116 }
3117 }
3118
3119 serde_json::Value::Object(obj) => {
3120 let entries = obj
3121 .iter()
3122 .map(|(k, v)| Entry {
3123 key: Value {
3124 tag: None,
3125 payload: Some(Payload::Scalar(Scalar {
3126 text: k.clone(),
3127 kind: ScalarKind::Bare,
3128 span: None,
3129 })),
3130 span: None,
3131 },
3132 value: json_to_styx_value(v),
3133 doc_comment: None,
3134 })
3135 .collect();
3136
3137 Value {
3138 tag: None,
3139 payload: Some(Payload::Object(Object {
3140 entries,
3141 span: None,
3142 })),
3143 span: None,
3144 }
3145 }
3146 }
3147}
3148
3149fn convert_ext_completion(item: ext::CompletionItem, edit_range: Range) -> CompletionItem {
3151 let text_edit = item.insert_text.as_ref().map(|text| {
3153 CompletionTextEdit::Edit(TextEdit {
3154 range: edit_range,
3155 new_text: text.clone(),
3156 })
3157 });
3158
3159 CompletionItem {
3160 label: item.label.clone(),
3161 kind: item.kind.map(|k| match k {
3162 ext::CompletionKind::Field => CompletionItemKind::FIELD,
3163 ext::CompletionKind::Value => CompletionItemKind::VALUE,
3164 ext::CompletionKind::Keyword => CompletionItemKind::KEYWORD,
3165 ext::CompletionKind::Type => CompletionItemKind::CLASS,
3166 }),
3167 detail: item.detail,
3168 documentation: item.documentation.map(|d| {
3169 Documentation::MarkupContent(MarkupContent {
3170 kind: MarkupKind::Markdown,
3171 value: d,
3172 })
3173 }),
3174 sort_text: item.sort_text,
3175 text_edit,
3176 filter_text: Some(item.label),
3177 preselect: Some(false),
3179 ..Default::default()
3180 }
3181}
3182
3183fn is_schema_file(tree: &Value) -> bool {
3185 let Some(obj) = tree.as_object() else {
3186 return false;
3187 };
3188
3189 let mut has_schema = false;
3190 let mut has_meta = false;
3191
3192 for entry in &obj.entries {
3193 match entry.key.as_str() {
3194 Some("schema") => has_schema = true,
3195 Some("meta") => has_meta = true,
3196 _ => {}
3197 }
3198 }
3199
3200 has_schema && has_meta
3201}
3202
3203fn find_field_in_doc(tree: &Value, field_name: &str, content: &str) -> Option<Range> {
3205 let obj = tree.as_object()?;
3206
3207 for entry in &obj.entries {
3208 if entry.key.as_str() == Some(field_name)
3209 && let Some(span) = entry.key.span
3210 {
3211 return Some(Range {
3212 start: offset_to_position(content, span.start as usize),
3213 end: offset_to_position(content, span.end as usize),
3214 });
3215 }
3216 }
3217
3218 None
3219}
3220
3221struct InsertPosition {
3223 position: Position,
3224 indent: String,
3225}
3226
3227fn find_field_insert_position(content: &str) -> InsertPosition {
3229 let lines: Vec<&str> = content.lines().collect();
3230
3231 for (i, line) in lines.iter().enumerate().rev() {
3233 let trimmed = line.trim();
3234 if !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with('#') {
3235 let indent_len = line.len() - line.trim_start().len();
3237 let indent = line[..indent_len].to_string();
3238
3239 return InsertPosition {
3240 position: Position {
3241 line: i as u32,
3242 character: line.len() as u32,
3243 },
3244 indent,
3245 };
3246 }
3247 }
3248
3249 InsertPosition {
3251 position: Position {
3252 line: lines.len().saturating_sub(1) as u32,
3253 character: lines.last().map(|l| l.len()).unwrap_or(0) as u32,
3254 },
3255 indent: String::new(),
3256 }
3257}
3258
3259fn generate_fields_text(fields: &[&crate::schema_validation::SchemaField], indent: &str) -> String {
3261 use crate::schema_validation::generate_placeholder;
3262
3263 let mut result = String::new();
3264
3265 for field in fields {
3266 result.push('\n');
3267 result.push_str(indent);
3268 result.push_str(&field.name);
3269 result.push(' ');
3270
3271 let value = field
3273 .default_value
3274 .clone()
3275 .unwrap_or_else(|| generate_placeholder(&field.schema));
3276 result.push_str(&value);
3277 }
3278
3279 result
3280}
3281
3282fn generate_reorder_edit(
3285 tree: &Value,
3286 schema_fields: &[crate::schema_validation::SchemaField],
3287 content: &str,
3288) -> Option<TextEdit> {
3289 let obj = tree.as_object()?;
3290
3291 let schema_order: std::collections::HashMap<&str, usize> = schema_fields
3293 .iter()
3294 .enumerate()
3295 .map(|(i, f)| (f.name.as_str(), i))
3296 .collect();
3297
3298 let mut doc_entries: Vec<(Option<usize>, &styx_tree::Entry)> = obj
3300 .entries
3301 .iter()
3302 .map(|e| {
3303 let order = e
3304 .key
3305 .as_str()
3306 .and_then(|name| schema_order.get(name).copied());
3307 (order, e)
3308 })
3309 .collect();
3310
3311 let current_order: Vec<Option<usize>> = doc_entries.iter().map(|(o, _)| *o).collect();
3313 let mut sorted_order = current_order.clone();
3314 sorted_order.sort_by(|a, b| match (a, b) {
3315 (Some(x), Some(y)) => x.cmp(y),
3316 (Some(_), None) => std::cmp::Ordering::Less,
3317 (None, Some(_)) => std::cmp::Ordering::Greater,
3318 (None, None) => std::cmp::Ordering::Equal,
3319 });
3320
3321 if current_order == sorted_order {
3322 return None; }
3324
3325 doc_entries.sort_by(|(a_order, a_entry), (b_order, b_entry)| {
3327 if a_entry.key.is_unit() {
3329 return std::cmp::Ordering::Less;
3330 }
3331 if b_entry.key.is_unit() {
3332 return std::cmp::Ordering::Greater;
3333 }
3334 match (a_order, b_order) {
3336 (Some(x), Some(y)) => x.cmp(y),
3337 (Some(_), None) => std::cmp::Ordering::Less,
3338 (None, Some(_)) => std::cmp::Ordering::Greater,
3339 (None, None) => std::cmp::Ordering::Equal,
3340 }
3341 });
3342
3343 let mut new_content = String::new();
3345
3346 for (i, (_, entry)) in doc_entries.iter().enumerate() {
3347 if i > 0 {
3348 new_content.push('\n');
3349 }
3350
3351 if let (Some(key_span), Some(val_span)) = (entry.key.span, entry.value.span) {
3354 let key_start = key_span.start as usize;
3356 let line_start = content[..key_start].rfind('\n').map(|i| i + 1).unwrap_or(0);
3357 let indent = &content[line_start..key_start];
3358
3359 new_content.push_str(indent);
3360 new_content.push_str(&content[key_start..val_span.end as usize]);
3361 }
3362 }
3363
3364 let start_offset = obj.span?.start as usize;
3366 let end_offset = obj.span?.end as usize;
3367
3368 Some(TextEdit {
3369 range: Range {
3370 start: offset_to_position(content, start_offset),
3371 end: offset_to_position(content, end_offset),
3372 },
3373 new_text: new_content,
3374 })
3375}
3376
3377pub async fn run() -> eyre::Result<()> {
3379 tracing_subscriber::fmt()
3381 .with_ansi(false)
3382 .with_env_filter(
3383 tracing_subscriber::EnvFilter::from_default_env()
3384 .add_directive(tracing::Level::INFO.into()),
3385 )
3386 .with_writer(std::io::stderr)
3387 .init();
3388
3389 eprintln!(
3390 "styx-lsp PID: {} - attach debugger now!",
3391 std::process::id()
3392 );
3393
3394 let stdin = tokio::io::stdin();
3395 let stdout = tokio::io::stdout();
3396
3397 let (service, socket) = LspService::new(StyxLanguageServer::new);
3398 Server::new(stdin, stdout, socket).serve(service).await;
3399
3400 Ok(())
3401}
3402
3403#[cfg(test)]
3404mod tests {
3405 use super::*;
3406
3407 #[test]
3408 fn test_find_field_in_schema_source() {
3409 let schema_source = r#"meta {
3410 name "test"
3411}
3412schema {
3413 @ @object{
3414 name @string
3415 port @int
3416 }
3417}"#;
3418
3419 let range = find_field_in_schema_source(schema_source, "name");
3421 assert!(range.is_some(), "should find 'name' field");
3422
3423 let range = find_field_in_schema_source(schema_source, "port");
3425 assert!(range.is_some(), "should find 'port' field");
3426
3427 let range = find_field_in_schema_source(schema_source, "unknown");
3429 assert!(range.is_none(), "should not find 'unknown' field");
3430 }
3431
3432 #[test]
3433 fn test_find_field_in_schema_no_space() {
3434 let schema_no_space = r#"schema {
3436 @ @object{
3437 name @string
3438 }
3439}"#;
3440
3441 let range = find_field_in_schema_source(schema_no_space, "name");
3442 assert!(
3443 range.is_some(),
3444 "should find 'name' without space before brace"
3445 );
3446 }
3447
3448 #[test]
3449 fn test_offset_to_position() {
3450 let content = "line1\nline2\nline3";
3451 assert_eq!(offset_to_position(content, 0), Position::new(0, 0));
3452 assert_eq!(offset_to_position(content, 5), Position::new(0, 5));
3453 assert_eq!(offset_to_position(content, 6), Position::new(1, 0));
3454 assert_eq!(offset_to_position(content, 12), Position::new(2, 0));
3455 }
3456
3457 #[test]
3458 fn test_levenshtein() {
3459 assert_eq!(levenshtein("", ""), 0);
3460 assert_eq!(levenshtein("abc", "abc"), 0);
3461 assert_eq!(levenshtein("abc", "ab"), 1);
3462 assert_eq!(levenshtein("port", "prot"), 2);
3463 assert_eq!(levenshtein("name", "nme"), 1);
3464 }
3465
3466 #[test]
3467 fn test_find_field_path_at_offset() {
3468 let content = "logging {\n format {\n timestamp true\n }\n}";
3470 let tree = styx_tree::parse(content).unwrap();
3471
3472 let path = find_field_path_at_offset(&tree, 3);
3474 assert_eq!(path, Some(vec!["logging".to_string()]));
3475
3476 let path = find_field_path_at_offset(&tree, 16);
3479 assert_eq!(
3480 path,
3481 Some(vec!["logging".to_string(), "format".to_string()])
3482 );
3483
3484 let timestamp_offset = content.find("timestamp").unwrap();
3487 let path = find_field_path_at_offset(&tree, timestamp_offset + 2);
3488 assert_eq!(
3489 path,
3490 Some(vec![
3491 "logging".to_string(),
3492 "format".to_string(),
3493 "timestamp".to_string()
3494 ])
3495 );
3496 }
3497
3498 #[test]
3499 fn test_format_breadcrumb() {
3500 assert_eq!(format_breadcrumb(&[], None), "@");
3502 assert_eq!(
3503 format_breadcrumb(&["logging".to_string()], None),
3504 "@ › logging"
3505 );
3506 assert_eq!(
3507 format_breadcrumb(&["logging".to_string(), "format".to_string()], None),
3508 "@ › logging › format"
3509 );
3510
3511 assert_eq!(
3513 format_breadcrumb(&[], Some("file:///schema.styx")),
3514 "[@](file:///schema.styx)"
3515 );
3516 assert_eq!(
3517 format_breadcrumb(&["logging".to_string()], Some("file:///schema.styx")),
3518 "[@](file:///schema.styx) › logging"
3519 );
3520 assert_eq!(
3521 format_breadcrumb(
3522 &[
3523 "logging".to_string(),
3524 "format".to_string(),
3525 "timestamp".to_string()
3526 ],
3527 Some("file:///schema.styx")
3528 ),
3529 "[@](file:///schema.styx) › logging › format › timestamp"
3530 );
3531 }
3532
3533 #[test]
3534 fn test_get_field_info_from_schema() {
3535 let schema_source = r#"meta {
3536 id test
3537}
3538schema {
3539 @ @object{
3540 name @string
3541 logging @optional(@object{
3542 level @string
3543 format @optional(@object{
3544 timestamp @optional(@bool)
3545 })
3546 })
3547 }
3548}"#;
3549
3550 let info = get_field_info_from_schema(schema_source, &["name"]);
3552 assert!(info.is_some(), "Should find 'name' field");
3553 let info = info.unwrap();
3554 assert_eq!(info.type_str, "@string");
3555
3556 let info = get_field_info_from_schema(schema_source, &["logging", "level"]);
3558 assert!(info.is_some(), "Should find 'logging.level' field");
3559 let info = info.unwrap();
3560 assert_eq!(info.type_str, "@string");
3561
3562 let info = get_field_info_from_schema(schema_source, &["logging", "format", "timestamp"]);
3564 assert!(
3565 info.is_some(),
3566 "Should find 'logging.format.timestamp' field"
3567 );
3568 let info = info.unwrap();
3569 assert_eq!(info.type_str, "@optional(@bool)");
3570
3571 let map_schema = r#"meta { id test }
3573schema {
3574 @ @object{
3575 hints @map(@string @Hint)
3576 }
3577 Hint @object{
3578 title @string
3579 patterns @seq(@string)
3580 }
3581}"#;
3582
3583 let info = get_field_info_from_schema(map_schema, &["hints", "captain"]);
3585 assert!(info.is_some(), "Should find map key 'hints.captain'");
3586 let info = info.unwrap();
3587 assert_eq!(info.type_str, "@Hint");
3588
3589 let info = get_field_info_from_schema(map_schema, &["hints", "captain", "title"]);
3591 assert!(info.is_some(), "Should find 'hints.captain.title'");
3592 let info = info.unwrap();
3593 assert_eq!(info.type_str, "@string");
3594
3595 let nested_map_schema = r#"meta { id test }
3597schema {
3598 @ @object{
3599 data @map(@string @map(@string @Item))
3600 }
3601 Item @object{
3602 value @int
3603 }
3604}"#;
3605
3606 let info = get_field_info_from_schema(nested_map_schema, &["data", "outer"]);
3608 assert!(info.is_some(), "Should find 'data.outer'");
3609 assert_eq!(info.unwrap().type_str, "@map(@string @Item)");
3610
3611 let info = get_field_info_from_schema(nested_map_schema, &["data", "outer", "inner"]);
3613 assert!(info.is_some(), "Should find 'data.outer.inner'");
3614 assert_eq!(info.unwrap().type_str, "@Item");
3615
3616 let info =
3618 get_field_info_from_schema(nested_map_schema, &["data", "outer", "inner", "value"]);
3619 assert!(info.is_some(), "Should find 'data.outer.inner.value'");
3620 assert_eq!(info.unwrap().type_str, "@int");
3621
3622 let type_ref_schema = r#"meta { id test }
3624schema {
3625 @ @object{
3626 config @Config
3627 }
3628 Config @object{
3629 port @int
3630 host @string
3631 }
3632}"#;
3633
3634 let info = get_field_info_from_schema(type_ref_schema, &["config", "port"]);
3635 assert!(info.is_some(), "Should find 'config.port'");
3636 assert_eq!(info.unwrap().type_str, "@int");
3637
3638 let optional_ref_schema = r#"meta { id test }
3640schema {
3641 @ @object{
3642 config @optional(@Config)
3643 }
3644 Config @object{
3645 port @int
3646 }
3647}"#;
3648
3649 let info = get_field_info_from_schema(optional_ref_schema, &["config", "port"]);
3650 assert!(
3651 info.is_some(),
3652 "Should find 'config.port' through @optional(@Config)"
3653 );
3654 assert_eq!(info.unwrap().type_str, "@int");
3655
3656 let info = get_field_info_from_schema(schema_source, &["nonexistent"]);
3658 assert!(info.is_none(), "Should not find 'nonexistent' field");
3659
3660 let info = get_field_info_from_schema(schema_source, &["logging", "nonexistent"]);
3662 assert!(
3663 info.is_none(),
3664 "Should not find 'logging.nonexistent' field"
3665 );
3666
3667 let seq_schema = r#"meta { id test }
3669schema {
3670 @ @object{
3671 /// List of specifications
3672 specs @seq(@object{
3673 /// Name of the spec
3674 name @string
3675 /// Prefix for annotations
3676 prefix @string
3677 })
3678 }
3679}"#;
3680
3681 let info = get_field_info_from_schema(seq_schema, &["specs", "0", "name"]);
3684 assert!(
3685 info.is_some(),
3686 "Should find 'specs[0].name' field inside @seq element"
3687 );
3688 let info = info.unwrap();
3689 assert_eq!(info.type_str, "@string");
3690 assert_eq!(info.doc_comment, Some("Name of the spec".to_string()));
3691
3692 let nested_seq_schema = r#"meta { id test }
3694schema {
3695 @ @object{
3696 specs @seq(@Spec)
3697 }
3698 Spec @object{
3699 name @string
3700 impls @seq(@Impl)
3701 }
3702 Impl @object{
3703 /// Implementation name
3704 name @string
3705 include @seq(@string)
3706 }
3707}"#;
3708
3709 let info = get_field_info_from_schema(nested_seq_schema, &["specs", "0", "name"]);
3711 assert!(info.is_some(), "Should find 'specs[0].name'");
3712 assert_eq!(info.unwrap().type_str, "@string");
3713
3714 let info =
3716 get_field_info_from_schema(nested_seq_schema, &["specs", "0", "impls", "1", "name"]);
3717 assert!(info.is_some(), "Should find 'specs[0].impls[1].name'");
3718 let info = info.unwrap();
3719 assert_eq!(info.type_str, "@string");
3720 assert_eq!(info.doc_comment, Some("Implementation name".to_string()));
3721
3722 let optional_seq_schema = r#"meta { id test }
3724schema {
3725 @ @object{
3726 items @optional(@seq(@object{
3727 value @int
3728 }))
3729 }
3730}"#;
3731
3732 let info = get_field_info_from_schema(optional_seq_schema, &["items", "0", "value"]);
3733 assert!(
3734 info.is_some(),
3735 "Should find 'items[0].value' through @optional(@seq(...))"
3736 );
3737 assert_eq!(info.unwrap().type_str, "@int");
3738
3739 let seq_in_map_schema = r#"meta { id test }
3741schema {
3742 @ @object{
3743 groups @map(@string @seq(@Item))
3744 }
3745 Item @object{
3746 id @int
3747 label @string
3748 }
3749}"#;
3750
3751 let info = get_field_info_from_schema(seq_in_map_schema, &["groups", "mygroup"]);
3753 assert!(info.is_some(), "Should find map key 'groups.mygroup'");
3754 assert_eq!(info.unwrap().type_str, "@seq(@Item)");
3755
3756 let info =
3758 get_field_info_from_schema(seq_in_map_schema, &["groups", "mygroup", "0", "label"]);
3759 assert!(
3760 info.is_some(),
3761 "Should find 'groups.mygroup[0].label' through @map -> @seq"
3762 );
3763 assert_eq!(info.unwrap().type_str, "@string");
3764
3765 let map_in_seq_schema = r#"meta { id test }
3767schema {
3768 @ @object{
3769 records @seq(@map(@string @Value))
3770 }
3771 Value @object{
3772 data @string
3773 }
3774}"#;
3775
3776 let info = get_field_info_from_schema(map_in_seq_schema, &["records", "0", "somekey"]);
3778 assert!(
3779 info.is_some(),
3780 "Should find 'records[0].somekey' through @seq -> @map"
3781 );
3782 assert_eq!(info.unwrap().type_str, "@Value");
3783
3784 let info =
3786 get_field_info_from_schema(map_in_seq_schema, &["records", "0", "somekey", "data"]);
3787 assert!(
3788 info.is_some(),
3789 "Should find 'records[0].somekey.data' through @seq -> @map -> @object"
3790 );
3791 assert_eq!(info.unwrap().type_str, "@string");
3792
3793 let default_seq_schema = r#"meta { id test }
3795schema {
3796 @ @object{
3797 tags @default([] @seq(@object{
3798 name @string
3799 }))
3800 }
3801}"#;
3802
3803 let info = get_field_info_from_schema(default_seq_schema, &["tags", "0", "name"]);
3804 assert!(
3805 info.is_some(),
3806 "Should find 'tags[0].name' through @default([] @seq(...))"
3807 );
3808 assert_eq!(info.unwrap().type_str, "@string");
3809
3810 let nested_seq_seq_schema = r#"meta { id test }
3812schema {
3813 @ @object{
3814 matrix @seq(@seq(@Cell))
3815 }
3816 Cell @object{
3817 value @int
3818 }
3819}"#;
3820
3821 let _info = get_field_info_from_schema(nested_seq_seq_schema, &["matrix", "0"]);
3823
3824 let info =
3826 get_field_info_from_schema(nested_seq_seq_schema, &["matrix", "0", "1", "value"]);
3827 assert!(
3828 info.is_some(),
3829 "Should find 'matrix[0][1].value' through nested @seq"
3830 );
3831 assert_eq!(info.unwrap().type_str, "@int");
3832
3833 let seq_optional_element_schema = r#"meta { id test }
3835schema {
3836 @ @object{
3837 maybe_items @seq(@optional(@Item))
3838 }
3839 Item @object{
3840 name @string
3841 }
3842}"#;
3843
3844 let info =
3845 get_field_info_from_schema(seq_optional_element_schema, &["maybe_items", "0", "name"]);
3846 assert!(
3847 info.is_some(),
3848 "Should find 'maybe_items[0].name' through @seq(@optional(@Item))"
3849 );
3850 assert_eq!(info.unwrap().type_str, "@string");
3851 }
3852
3853 #[test]
3854 fn test_doc_comments_in_schema() {
3855 let schema_source = r#"meta {
3856 id test
3857}
3858schema {
3859 @ @object{
3860 /// The server name
3861 name @string
3862 }
3863}"#;
3864
3865 let tree = styx_tree::parse(schema_source).unwrap();
3867 let obj = tree.as_object().unwrap();
3868
3869 let schema_entry = obj
3871 .entries
3872 .iter()
3873 .find(|e| e.key.as_str() == Some("schema"))
3874 .unwrap();
3875 let schema_obj = schema_entry.value.as_object().unwrap();
3876
3877 let unit_entry = schema_obj.entries.iter().find(|e| e.key.is_unit()).unwrap();
3879
3880 println!("unit_entry.value tag: {:?}", unit_entry.value.tag);
3882 println!(
3883 "unit_entry.value payload: {:?}",
3884 unit_entry.value.payload.is_some()
3885 );
3886
3887 if let Some(inner_obj) = unit_entry.value.as_object() {
3889 for entry in &inner_obj.entries {
3890 println!(
3891 "Inner entry: key={:?}, tag={:?}, doc={:?}",
3892 entry.key.as_str(),
3893 entry.key.tag_name(),
3894 entry.doc_comment
3895 );
3896 }
3897 }
3898
3899 let info = get_field_info_from_schema(schema_source, &["name"]);
3901 assert!(info.is_some(), "Should find 'name' field");
3902 let info = info.unwrap();
3903 assert_eq!(
3904 info.doc_comment,
3905 Some("The server name".to_string()),
3906 "Doc comment should be extracted"
3907 );
3908 }
3909
3910 #[test]
3911 fn test_get_completion_fields_at_path() {
3912 let schema_source = r#"meta { id test }
3914schema {
3915 @ @object{
3916 content @string
3917 output @string
3918 syntax_highlight @optional(@object{
3919 light_theme @string
3920 dark_theme @string
3921 })
3922 }
3923}"#;
3924
3925 let root_fields = get_schema_fields_from_source_at_path(schema_source, &[]);
3927 let root_names: Vec<_> = root_fields.iter().map(|(n, _)| n.as_str()).collect();
3928 assert!(
3929 root_names.contains(&"content"),
3930 "Root should have 'content', got: {:?}",
3931 root_names
3932 );
3933 assert!(root_names.contains(&"output"), "Root should have 'output'");
3934 assert!(
3935 root_names.contains(&"syntax_highlight"),
3936 "Root should have 'syntax_highlight'"
3937 );
3938 assert!(
3939 !root_names.contains(&"light_theme"),
3940 "Root should NOT have 'light_theme' (it's nested)"
3941 );
3942
3943 let tree = styx_tree::parse(schema_source).unwrap();
3946 let obj = tree.as_object().unwrap();
3947 let schema_entry = obj
3948 .entries
3949 .iter()
3950 .find(|e| e.key.as_str() == Some("schema"))
3951 .unwrap();
3952 tracing::debug!(
3953 "schema_entry.value.tag: {:?}",
3954 schema_entry.value.tag_name()
3955 );
3956
3957 if let Some(inner_obj) = extract_object_from_value(&schema_entry.value) {
3959 for entry in &inner_obj.entries {
3960 tracing::debug!(
3961 key = ?entry.key.as_str(),
3962 tag = ?entry.key.tag_name(),
3963 is_unit = entry.key.is_unit(),
3964 "L1 entry"
3965 );
3966
3967 if entry.key.is_unit() {
3969 tracing::debug!(tag = ?entry.value.tag_name(), "unit value");
3970 if let Some(l2_obj) = extract_object_from_value(&entry.value) {
3971 for l2_entry in &l2_obj.entries {
3972 tracing::debug!(
3973 key = ?l2_entry.key.as_str(),
3974 tag = ?l2_entry.key.tag_name(),
3975 "L2 entry"
3976 );
3977 if l2_entry.key.as_str() == Some("syntax_highlight") {
3978 tracing::debug!("found syntax_highlight!");
3979 tracing::debug!(tag = ?l2_entry.value.tag_name(), "value");
3980 let unwrapped = unwrap_type_wrappers(&l2_entry.value);
3981 tracing::debug!(tag = ?unwrapped.tag_name(), "unwrapped");
3982 }
3983 }
3984 }
3985 }
3986 }
3987 }
3988
3989 let nested_fields =
3990 get_schema_fields_from_source_at_path(schema_source, &["syntax_highlight".to_string()]);
3991 let nested_names: Vec<_> = nested_fields.iter().map(|(n, _)| n.as_str()).collect();
3992 assert!(
3993 nested_names.contains(&"light_theme"),
3994 "syntax_highlight should have 'light_theme', got: {:?}",
3995 nested_names
3996 );
3997 assert!(
3998 nested_names.contains(&"dark_theme"),
3999 "syntax_highlight should have 'dark_theme'"
4000 );
4001 assert!(
4002 !nested_names.contains(&"content"),
4003 "syntax_highlight should NOT have 'content' (it's at root)"
4004 );
4005 }
4006
4007 #[test]
4008 fn test_find_object_context_at_cursor() {
4009 let content = "syntax_highlight {\n light_theme foo\n \n}";
4011 let tree = styx_tree::parse(content).unwrap();
4013
4014 let cursor_offset = content.find(" \n}").unwrap() + 4; let ctx = find_object_at_offset(&tree, cursor_offset);
4018 assert!(ctx.is_some(), "Should find object context at cursor");
4019 let ctx = ctx.unwrap();
4020 assert_eq!(
4021 ctx.path,
4022 vec!["syntax_highlight".to_string()],
4023 "Path should be ['syntax_highlight'], got: {:?}",
4024 ctx.path
4025 );
4026 }
4027
4028 #[test]
4029 fn test_hover_with_catchall_key_schema() {
4030 let schema_source = r#"meta {id test}
4033schema {
4034 Decl @enum{
4035 /// A query declaration.
4036 query @Query
4037 }
4038 Query @object{
4039 /// Source table to query from.
4040 from @optional(@string)
4041 /// Filter conditions.
4042 where @optional(@object{@string @string})
4043 }
4044 @ @object{@string @Decl}
4045}"#;
4046
4047 let info = get_field_info_from_schema(schema_source, &["AllProducts"]);
4051 assert!(
4052 info.is_some(),
4053 "Should find field info for dynamic key 'AllProducts' in catch-all schema"
4054 );
4055 let info = info.unwrap();
4056 assert!(
4058 info.type_str.contains("Decl"),
4059 "Type should reference Decl, got: {}",
4060 info.type_str
4061 );
4062
4063 let info = get_field_info_from_schema(schema_source, &["AllProducts", "from"]);
4067 assert!(
4068 info.is_some(),
4069 "Should find field info for 'from' inside a query"
4070 );
4071 let info = info.unwrap();
4072 assert!(
4073 info.type_str.contains("string"),
4074 "Type of 'from' should be string, got: {}",
4075 info.type_str
4076 );
4077 }
4078
4079 fn parse_cursor(input: &str) -> (String, usize) {
4082 let cursor_pos = input
4083 .find('⏐')
4084 .expect("test input must contain ⏐ cursor marker");
4085 let content = input.replace('⏐', "");
4086 (content, cursor_pos)
4087 }
4088
4089 #[test]
4090 fn test_nesting_depth_at_root() {
4091 let (content, offset) = parse_cursor("⏐name value");
4093 let parse = styx_cst::parse(&content);
4094 assert_eq!(find_nesting_depth_cst(&parse, offset), 0);
4095 }
4096
4097 #[test]
4098 fn test_nesting_depth_after_root_entry() {
4099 let (content, offset) = parse_cursor("name value\n⏐");
4101 let parse = styx_cst::parse(&content);
4102 assert_eq!(find_nesting_depth_cst(&parse, offset), 0);
4103 }
4104
4105 #[test]
4106 fn test_nesting_depth_inside_object() {
4107 let (content, offset) = parse_cursor("server {\n ⏐\n}");
4109 let parse = styx_cst::parse(&content);
4110 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4111 }
4112
4113 #[test]
4114 fn test_nesting_depth_inside_object_with_content() {
4115 let (content, offset) = parse_cursor("server {\n host localhost\n ⏐\n}");
4117 let parse = styx_cst::parse(&content);
4118 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4119 }
4120
4121 #[test]
4122 fn test_nesting_depth_nested_object() {
4123 let (content, offset) = parse_cursor("server {\n tls {\n ⏐\n }\n}");
4125 let parse = styx_cst::parse(&content);
4126 assert_eq!(find_nesting_depth_cst(&parse, offset), 2);
4127 }
4128
4129 #[test]
4130 fn test_nesting_depth_deeply_nested() {
4131 let (content, offset) =
4133 parse_cursor("a {\n b {\n c {\n ⏐\n }\n }\n}");
4134 let parse = styx_cst::parse(&content);
4135 assert_eq!(find_nesting_depth_cst(&parse, offset), 3);
4136 }
4137
4138 #[test]
4139 fn test_nesting_depth_inside_sequence() {
4140 let (content, offset) = parse_cursor("items (\n ⏐\n)");
4142 let parse = styx_cst::parse(&content);
4143 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4144 }
4145
4146 #[test]
4147 fn test_nesting_depth_object_inside_sequence() {
4148 let (content, offset) = parse_cursor("items (\n {\n ⏐\n }\n)");
4150 let parse = styx_cst::parse(&content);
4151 assert_eq!(find_nesting_depth_cst(&parse, offset), 2);
4152 }
4153
4154 #[test]
4155 fn test_nesting_depth_sequence_inside_object() {
4156 let (content, offset) = parse_cursor("server {\n ports (\n ⏐\n )\n}");
4158 let parse = styx_cst::parse(&content);
4159 assert_eq!(find_nesting_depth_cst(&parse, offset), 2);
4160 }
4161
4162 #[test]
4163 fn test_nesting_depth_between_siblings() {
4164 let (content, offset) = parse_cursor("first {\n a 1\n}\n⏐\nsecond {\n b 2\n}");
4166 let parse = styx_cst::parse(&content);
4167 assert_eq!(find_nesting_depth_cst(&parse, offset), 0);
4168 }
4169
4170 #[test]
4171 fn test_nesting_depth_just_after_open_brace() {
4172 let (content, offset) = parse_cursor("server {⏐}");
4174 let parse = styx_cst::parse(&content);
4175 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4176 }
4177
4178 #[test]
4179 fn test_nesting_depth_just_before_close_brace() {
4180 let (content, offset) = parse_cursor("server {\n host localhost\n⏐}");
4182 let parse = styx_cst::parse(&content);
4183 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4184 }
4185
4186 #[test]
4187 fn test_nesting_depth_inline_object() {
4188 let (content, offset) = parse_cursor("server {host localhost, ⏐}");
4190 let parse = styx_cst::parse(&content);
4191 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4192 }
4193
4194 #[test]
4195 fn test_nesting_depth_tagged_object() {
4196 let (content, offset) = parse_cursor("AllProducts @query{\n ⏐\n}");
4198 let parse = styx_cst::parse(&content);
4199 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4200 }
4201
4202 #[test]
4203 fn test_nesting_depth_nested_tagged_objects() {
4204 let (content, offset) =
4206 parse_cursor("AllProducts @query{\n select {\n ⏐\n }\n}");
4207 let parse = styx_cst::parse(&content);
4208 assert_eq!(find_nesting_depth_cst(&parse, offset), 2);
4209 }
4210
4211 #[test]
4214 fn test_nesting_depth_empty_object() {
4215 let (content, offset) = parse_cursor("empty {⏐}");
4217 let parse = styx_cst::parse(&content);
4218 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4219 }
4220
4221 #[test]
4222 fn test_nesting_depth_empty_sequence() {
4223 let (content, offset) = parse_cursor("empty (⏐)");
4225 let parse = styx_cst::parse(&content);
4226 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4227 }
4228
4229 #[test]
4230 fn test_nesting_depth_after_closing_brace() {
4231 let (content, offset) = parse_cursor("server { host localhost }⏐");
4233 let parse = styx_cst::parse(&content);
4234 assert_eq!(find_nesting_depth_cst(&parse, offset), 0);
4235 }
4236
4237 #[test]
4238 fn test_nesting_depth_empty_document() {
4239 let (content, offset) = parse_cursor("⏐");
4241 let parse = styx_cst::parse(&content);
4242 assert_eq!(find_nesting_depth_cst(&parse, offset), 0);
4243 }
4244
4245 #[test]
4246 fn test_nesting_depth_whitespace_only() {
4247 let (content, offset) = parse_cursor(" ⏐ ");
4249 let parse = styx_cst::parse(&content);
4250 assert_eq!(find_nesting_depth_cst(&parse, offset), 0);
4251 }
4252
4253 #[test]
4254 fn test_nesting_depth_multiline_sequence_elements() {
4255 let (content, offset) = parse_cursor("items (\n a\n ⏐\n b\n)");
4257 let parse = styx_cst::parse(&content);
4258 assert_eq!(find_nesting_depth_cst(&parse, offset), 1);
4259 }
4260
4261 #[test]
4262 fn test_nesting_depth_complex_dibs_like() {
4263 let (content, offset) = parse_cursor(
4265 r#"AllProducts @query{
4266 from product
4267 where {deleted_at @null}
4268 select {
4269 id
4270 handle
4271 ⏐
4272 }
4273}"#,
4274 );
4275 let parse = styx_cst::parse(&content);
4276 assert_eq!(find_nesting_depth_cst(&parse, offset), 2);
4277 }
4278
4279 #[test]
4280 fn test_nesting_depth_inside_where_clause() {
4281 let (content, offset) = parse_cursor(
4283 r#"Query @query{
4284 where {
4285 status "published"
4286 ⏐
4287 }
4288}"#,
4289 );
4290 let parse = styx_cst::parse(&content);
4291 assert_eq!(find_nesting_depth_cst(&parse, offset), 2);
4292 }
4293
4294 #[test]
4295 fn test_parse_error_diagnostics() {
4296 let content = "foo {a b c}";
4298 let parsed = styx_cst::parse(content);
4299
4300 assert!(
4302 !parsed.errors().is_empty(),
4303 "CST should report a parse error for 'foo {{a b c}}'"
4304 );
4305
4306 let error = &parsed.errors()[0];
4308 assert!(
4309 error.message.contains("unexpected atom"),
4310 "Error message should mention 'unexpected atom': {}",
4311 error.message
4312 );
4313 }
4314
4315 #[test]
4316 fn test_compute_diagnostics_reports_parse_errors() {
4317 let content = "foo {a b c}";
4319 let parsed = styx_cst::parse(content);
4320
4321 let _uri = tower_lsp::lsp_types::Url::parse("file:///test.styx").unwrap();
4324
4325 let tree = styx_tree::parse(content).ok();
4327 assert!(tree.is_none(), "tree parse should fail for invalid input");
4328
4329 let mut diagnostics = Vec::new();
4331
4332 for error in parsed.errors() {
4334 let range = tower_lsp::lsp_types::Range {
4335 start: offset_to_position(content, error.offset as usize),
4336 end: offset_to_position(content, error.offset as usize + 1),
4337 };
4338
4339 diagnostics.push(tower_lsp::lsp_types::Diagnostic {
4340 range,
4341 severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR),
4342 code: None,
4343 code_description: None,
4344 source: Some("styx".to_string()),
4345 message: error.message.clone(),
4346 related_information: None,
4347 tags: None,
4348 data: None,
4349 });
4350 }
4351
4352 assert!(
4354 !diagnostics.is_empty(),
4355 "Should have at least one diagnostic for parse error"
4356 );
4357
4358 assert!(
4360 diagnostics[0].message.contains("unexpected atom"),
4361 "Diagnostic message should mention the error: {}",
4362 diagnostics[0].message
4363 );
4364 }
4365}