//! Diagnostics conversion from Storybook errors to LSP diagnostics use tower_lsp::lsp_types::{ Diagnostic, DiagnosticSeverity, Position, Range, }; use crate::syntax::lexer::{ Lexer, Token, }; /// Compute diagnostics for a document pub fn compute_diagnostics(text: &str) -> Vec { let mut diagnostics = Vec::new(); // Try to parse the document // For now, we'll do a simple check - a real implementation would use the full // parser match try_parse(text) { | Ok(_) => { // No syntax errors }, | Err(errors) => { for error in errors { diagnostics.push(error); } }, } diagnostics } /// Attempt to parse the document and return diagnostics fn try_parse(text: &str) -> Result<(), Vec> { // TODO: Integrate with actual parser // For now, return Ok for all documents // This will be implemented when we add Span tracking // Simple placeholder: check for common syntax errors using lexer tokens let mut errors = Vec::new(); // Track brace nesting level and position of each brace let mut nesting_stack: Vec = Vec::new(); // Stack of opening brace positions let lexer = Lexer::new(text); for (offset, token, _end) in lexer { match token { | Token::LBrace => { nesting_stack.push(offset); }, | Token::RBrace => { if nesting_stack.is_empty() { // Unexpected closing brace - no matching opening brace let pos = byte_offset_to_position(text, offset); errors.push(Diagnostic { range: Range { start: pos, end: Position { line: pos.line, character: pos.character + 1, }, }, severity: Some(DiagnosticSeverity::ERROR), code: None, source: Some("storybook".to_string()), message: "Unexpected closing brace".to_string(), related_information: None, tags: None, code_description: None, data: None, }); } else { nesting_stack.pop(); } }, | _ => {}, } } // Note: We don't report unclosed braces (nesting_stack not empty) // because those are common in incomplete/in-progress code if errors.is_empty() { Ok(()) } else { Err(errors) } } fn byte_offset_to_line(text: &str, offset: usize) -> usize { let mut line = 0; let mut current_offset = 0; for ch in text.chars() { if current_offset >= offset { break; } if ch == '\n' { line += 1; } current_offset += ch.len_utf8(); } line } /// Convert a byte offset to line/column position /// This is a placeholder - will be replaced when we have proper Span tracking pub fn byte_offset_to_position(text: &str, offset: usize) -> Position { let mut line = 0; let mut character = 0; let mut current_offset = 0; for ch in text.chars() { if current_offset >= offset { break; } if ch == '\n' { line += 1; character = 0; } else { character += 1; } current_offset += ch.len_utf8(); } Position { line: line as u32, character: character as u32, } } /// Create a diagnostic from a span and message pub fn create_diagnostic( text: &str, start: usize, end: usize, message: String, severity: DiagnosticSeverity, ) -> Diagnostic { Diagnostic { range: Range { start: byte_offset_to_position(text, start), end: byte_offset_to_position(text, end), }, severity: Some(severity), code: None, source: Some("storybook".to_string()), message, related_information: None, tags: None, code_description: None, data: None, } }