Files
storybook/src/lsp/formatting.rs

147 lines
3.8 KiB
Rust
Raw Normal View History

//! 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<Vec<TextEdit>> {
// 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"));
}
}