//! Document formatting provider //! //! Provides auto-formatting for Storybook files use tower_lsp::lsp_types::{ FormattingOptions, Position, Range, TextEdit, }; use super::document::Document; /// Format the entire document pub fn format_document(doc: &Document, _options: &FormattingOptions) -> Option> { // Don't format if there are parse errors - the AST would be invalid // and formatting could produce garbage output doc.ast.as_ref()?; // For now, implement basic formatting rules: // 1. 4-space indentation // 2. Consistent spacing around colons // 3. Blank lines between top-level declarations let formatted = format_text(&doc.text); if formatted == doc.text { return None; // No changes needed } // Return a single edit that replaces the entire document Some(vec![TextEdit { range: Range { start: Position { line: 0, character: 0, }, end: Position { line: doc.positions.line_count() as u32, character: 0, }, }, new_text: formatted, }]) } /// Format the text according to Storybook style rules pub(crate) fn format_text(text: &str) -> String { let mut result = String::new(); let mut indent_level = 0; let mut prev_was_blank = false; let mut in_prose_block = false; for line in text.lines() { let trimmed = line.trim(); // Handle prose blocks - don't format their content if trimmed.starts_with("---") { in_prose_block = !in_prose_block; result.push_str(&" ".repeat(indent_level)); result.push_str(trimmed); result.push('\n'); continue; } if in_prose_block { // Preserve prose content exactly as-is result.push_str(line); result.push('\n'); continue; } // Skip blank lines if trimmed.is_empty() { if !prev_was_blank { result.push('\n'); prev_was_blank = true; } continue; } prev_was_blank = false; // Adjust indentation based on braces if trimmed.starts_with('}') { indent_level = indent_level.saturating_sub(1); } // Add indentation result.push_str(&" ".repeat(indent_level)); // Format the line content let formatted_line = format_line(trimmed); result.push_str(&formatted_line); result.push('\n'); // Increase indentation for opening braces if trimmed.ends_with('{') { indent_level += 1; } } result } /// Format a single line fn format_line(line: &str) -> String { // Ensure consistent spacing around colons in field assignments if let Some(colon_pos) = line.find(':') { if !line.starts_with("//") && !line.contains("::") { let before = line[..colon_pos].trim_end(); let after = line[colon_pos + 1..].trim_start(); return format!("{}: {}", before, after); } } line.to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn test_basic_formatting() { let input = "character Alice{age:25}"; let formatted = format_text(input); // Check key formatting features assert!( formatted.contains("age: 25"), "Should have space after colon" ); assert!( formatted.contains("character Alice"), "Should have character declaration" ); } #[test] fn test_preserve_prose() { let input = "---backstory\nSome irregular spacing\n---"; let formatted = format_text(input); assert!(formatted.contains("Some irregular spacing")); } }