Skip to main content

lex_lsp/features/
formatting.rs

1use lex_babel::formats::lex::formatting_rules::FormattingRules;
2use lex_babel::transforms::{serialize_to_lex, serialize_to_lex_with_rules};
3use lex_core::lex::ast::Document;
4
5/// Text edit expressed as byte offsets over the original document.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct TextEditSpan {
8    pub start: usize,
9    pub end: usize,
10    pub new_text: String,
11}
12
13/// Inclusive/exclusive line range (kept for API compatibility).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct LineRange {
16    pub start: usize,
17    pub end: usize,
18}
19
20/// Produce formatting edits for the entire document.
21///
22/// Returns a single TextEditSpan that replaces the entire document content
23/// (full replacement strategy). This is simpler and more reliable than
24/// incremental edits while the formatter and parser are maturing.
25pub fn format_document(
26    document: &Document,
27    source: &str,
28    rules: Option<FormattingRules>,
29) -> Vec<TextEditSpan> {
30    let formatted = match rules {
31        Some(r) => serialize_to_lex_with_rules(document, r),
32        None => serialize_to_lex(document),
33    };
34    let formatted = match formatted {
35        Ok(text) => text,
36        Err(_) => return Vec::new(),
37    };
38
39    // No changes needed
40    if formatted == source {
41        return Vec::new();
42    }
43
44    // Full document replacement: single edit from start to end
45    vec![TextEditSpan {
46        start: 0,
47        end: source.len(),
48        new_text: formatted,
49    }]
50}
51
52/// Produce formatting edits for a range (currently formats entire document).
53///
54/// Note: Range formatting currently applies full document replacement.
55/// True range-limited formatting can be added once the formatter matures.
56pub fn format_range(
57    document: &Document,
58    source: &str,
59    _range: LineRange,
60    rules: Option<FormattingRules>,
61) -> Vec<TextEditSpan> {
62    format_document(document, source, rules)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use lex_core::lex::parsing;
69
70    const FULL_FIXTURE: &str = "Section:\n\n    - item one   \n\n\n\n\n  - item two\n\n";
71
72    fn parse(source: &str) -> Document {
73        parsing::parse_document(source).expect("parse fixture")
74    }
75
76    fn apply_span(source: &str, edit: &TextEditSpan) -> String {
77        let mut result = source.to_string();
78        result.replace_range(edit.start..edit.end, &edit.new_text);
79        result
80    }
81
82    #[test]
83    fn formats_entire_document() {
84        let source = FULL_FIXTURE;
85        let document = parse(source);
86        let formatted = serialize_to_lex(&document).unwrap();
87        assert_ne!(formatted, source);
88
89        let edits = format_document(&document, source, None);
90        assert_eq!(edits.len(), 1, "should return single full-replacement edit");
91
92        let edit = &edits[0];
93        assert_eq!(edit.start, 0);
94        assert_eq!(edit.end, source.len());
95
96        let applied = apply_span(source, edit);
97        assert_eq!(applied, formatted);
98    }
99
100    #[test]
101    fn range_formatting_does_full_replacement() {
102        // Range formatting currently does full document replacement
103        let source = FULL_FIXTURE;
104        let document = parse(source);
105        let range = LineRange { start: 2, end: 5 }; // Range is ignored
106        let edits = format_range(&document, source, range, None);
107
108        assert_eq!(edits.len(), 1);
109        assert_eq!(edits[0].start, 0);
110        assert_eq!(edits[0].end, source.len());
111    }
112
113    #[test]
114    fn no_edits_when_already_formatted() {
115        let source = "Section:\n    - item\n";
116        let document = parse(source);
117        let edits = format_document(&document, source, None);
118        assert!(edits.is_empty());
119    }
120
121    #[test]
122    fn format_with_custom_rules() {
123        let source = "Section:\n    - item\n";
124        let document = parse(source);
125        let rules = FormattingRules {
126            indent_string: "  ".to_string(), // 2-space indent
127            ..Default::default()
128        };
129
130        let edits = format_document(&document, source, Some(rules));
131        assert_eq!(edits.len(), 1);
132        let applied = apply_span(source, &edits[0]);
133        assert!(applied.contains("  - item")); // 2-space indent
134    }
135}