Skip to main content

eure_fmt/
lib.rs

1//! Eure formatter.
2//!
3//! This crate provides formatting functionality for Eure files, using an
4//! IR-based architecture inspired by Wadler's "A Prettier Printer" algorithm.
5//!
6//! # Architecture
7//!
8//! The formatter uses a three-stage pipeline:
9//!
10//! 1. **Parse** - Source text → CST (done externally via `eure-parol`)
11//! 2. **Build** - CST → Doc IR (intermediate representation)
12//! 3. **Print** - Doc IR → Formatted string
13//!
14//! # Example
15//!
16//! ```ignore
17//! use eure_fmt::{format, FormatConfig};
18//!
19//! let input = "a={b=1,c=2}";
20//! let config = FormatConfig::default();
21//! let formatted = format(input, &config).unwrap();
22//! ```
23
24mod builder;
25pub mod config;
26pub mod doc;
27pub mod printer;
28pub mod source;
29
30#[cfg(any(feature = "unformat", test))]
31pub mod unformat;
32
33pub use config::FormatConfig;
34pub use doc::Doc;
35pub use source::{build_source_doc, format_source_document};
36
37use eure_tree::Cst;
38
39use builder::FormatBuilder;
40use printer::Printer;
41
42/// Error that can occur during formatting.
43#[derive(Debug, Clone)]
44pub enum FormatError {
45    /// Failed to parse the input.
46    ParseError(String),
47}
48
49impl std::fmt::Display for FormatError {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            FormatError::ParseError(msg) => write!(f, "Parse error: {}", msg),
53        }
54    }
55}
56
57impl std::error::Error for FormatError {}
58
59/// Result of checking if a file is formatted.
60#[derive(Debug, Clone)]
61pub enum FormatCheckResult {
62    /// The input is already well-formatted.
63    WellFormatted,
64    /// The input needs formatting.
65    NeedsFormatting {
66        /// The formatted output.
67        formatted: String,
68    },
69    /// Failed to parse the input.
70    ParseError(String),
71}
72
73impl FormatCheckResult {
74    /// Returns true if the input is well-formatted.
75    pub fn is_well_formatted(&self) -> bool {
76        matches!(self, FormatCheckResult::WellFormatted)
77    }
78
79    /// Returns true if the input needs formatting.
80    pub fn needs_formatting(&self) -> bool {
81        matches!(self, FormatCheckResult::NeedsFormatting { .. })
82    }
83
84    /// Returns true if there was a parse error.
85    pub fn is_parse_error(&self) -> bool {
86        matches!(self, FormatCheckResult::ParseError(_))
87    }
88}
89
90/// Format Eure source code using an already-parsed CST.
91///
92/// This is the lower-level API that works directly with the CST.
93pub fn format_cst(input: &str, cst: &Cst, config: &FormatConfig) -> String {
94    let builder = FormatBuilder::new(input, cst, config);
95    let doc = builder.build(cst);
96    Printer::new(config.clone()).print(&doc)
97}
98
99/// Build a Doc IR from a CST.
100///
101/// This is useful for debugging or custom printing.
102pub fn build_doc(input: &str, cst: &Cst, config: &FormatConfig) -> Doc {
103    let builder = FormatBuilder::new(input, cst, config);
104    builder.build(cst)
105}
106
107/// A text edit representing a change to apply.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct TextEdit {
110    /// Start offset in bytes.
111    pub start: usize,
112    /// End offset in bytes.
113    pub end: usize,
114    /// New text to insert.
115    pub new_text: String,
116}
117
118/// Compute text edits to transform input into formatted output.
119///
120/// This is useful for LSP integration where you need incremental edits
121/// rather than full file replacement.
122pub fn compute_edits(input: &str, formatted: &str) -> Vec<TextEdit> {
123    // For now, use a simple full-file replacement
124    // A more sophisticated implementation would use a diff algorithm
125    if input == formatted {
126        Vec::new()
127    } else {
128        vec![TextEdit {
129            start: 0,
130            end: input.len(),
131            new_text: formatted.to_string(),
132        }]
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    fn parse_and_format(input: &str) -> String {
141        let cst = eure_parol::parse(input).expect("parse failed");
142        format_cst(input, &cst, &FormatConfig::default())
143    }
144
145    #[test]
146    fn test_format_simple() {
147        let formatted = parse_and_format("a = 1");
148        assert_eq!(formatted, "a = 1\n");
149    }
150
151    #[test]
152    fn test_format_preserves_content() {
153        let input = "name = \"hello\"\nage = 42";
154        let formatted = parse_and_format(input);
155        assert!(formatted.contains("name"));
156        assert!(formatted.contains("\"hello\""));
157        assert!(formatted.contains("age"));
158        assert!(formatted.contains("42"));
159    }
160
161    #[test]
162    fn test_format_array() {
163        let formatted = parse_and_format("= [1, 2, 3]");
164        assert_eq!(formatted, "= [1, 2, 3]\n");
165    }
166
167    #[test]
168    fn test_format_empty_array() {
169        let formatted = parse_and_format("= []");
170        assert_eq!(formatted, "= []\n");
171    }
172
173    #[test]
174    fn test_format_empty_object() {
175        let formatted = parse_and_format("= {}");
176        assert_eq!(formatted, "= {}\n");
177    }
178
179    #[test]
180    fn test_compute_edits_no_change() {
181        let edits = compute_edits("a = 1\n", "a = 1\n");
182        assert!(edits.is_empty());
183    }
184
185    #[test]
186    fn test_compute_edits_with_change() {
187        let edits = compute_edits("a=1", "a = 1\n");
188        assert_eq!(edits.len(), 1);
189        assert_eq!(edits[0].start, 0);
190        assert_eq!(edits[0].end, 3);
191        assert_eq!(edits[0].new_text, "a = 1\n");
192    }
193}