nixpkgs_fmt/
lib.rs

1#[macro_use]
2mod dsl;
3mod engine;
4mod rules;
5mod tree_utils;
6mod pattern;
7
8use std::{borrow::Cow, fmt, fmt::Formatter};
9
10use rnix::{SyntaxNode, TextRange, TextSize};
11use smol_str::SmolStr;
12
13use crate::dsl::RuleName;
14
15/// The result of formatting.
16///
17/// From this Diff, you can get either the resulting `String`, or the
18/// reformatted syntax node.
19#[derive(Debug)]
20pub(crate) struct FmtDiff {
21    original_node: SyntaxNode,
22    edits: Vec<(AtomEdit, Option<RuleName>)>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub(crate) struct AtomEdit {
27    pub delete: TextRange,
28    pub insert: SmolStr,
29}
30
31impl fmt::Display for FmtDiff {
32    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
33        // TODO: don't copy strings all over the place
34        let old_text = self.original_node.to_string();
35
36        let mut total_len = old_text.len();
37        let mut edits = self.text_diff();
38        edits.sort_by_key(|edit| edit.delete.start());
39
40        for atom in edits.iter() {
41            total_len += atom.insert.len();
42            total_len -= u32::from(atom.delete.end() - atom.delete.start()) as usize;
43        }
44
45        let mut buf = String::with_capacity(total_len);
46        let mut prev = 0;
47        for atom in edits.iter() {
48            let start = u32::from(atom.delete.start()) as usize;
49            let end = u32::from(atom.delete.end()) as usize;
50            if start > prev {
51                buf.push_str(&old_text[prev..start]);
52            }
53            buf.push_str(&atom.insert);
54            prev = end;
55        }
56        buf.push_str(&old_text[prev..]);
57        assert_eq!(buf.len(), total_len);
58        write!(f, "{}", buf)
59    }
60}
61
62impl FmtDiff {
63    /// Get the diff of deletes and inserts
64    pub(crate) fn text_diff(&self) -> Vec<AtomEdit> {
65        self.edits.iter().map(|(edit, _reason)| edit.clone()).collect()
66    }
67
68    /// Whether or not formatting did caused any changes
69    pub(crate) fn has_changes(&self) -> bool {
70        !self.edits.is_empty()
71    }
72
73    /// Apply the formatting suggestions and return the new node
74    pub(crate) fn to_node(&self) -> SyntaxNode {
75        if self.has_changes() {
76            rnix::parse(&self.to_string()).node()
77        } else {
78            self.original_node.clone()
79        }
80    }
81}
82
83pub fn reformat_node(node: &SyntaxNode) -> SyntaxNode {
84    let spacing = rules::spacing();
85    let indentation = rules::indentation();
86    engine::reformat(&spacing, &indentation, node, None)
87}
88
89pub fn reformat_string(text: &str) -> String {
90    let (mut text, line_endings) = convert_to_unix_line_endings(text);
91
92    // Forcibly convert tabs to spaces as a pre-pass
93    if text.contains('\t') {
94        text = Cow::Owned(text.replace('\t', "  "))
95    }
96
97    let ast = rnix::parse(&*text);
98    let root_node = ast.node();
99    let res = reformat_node(&root_node).to_string();
100    match line_endings {
101        LineEndings::Unix => res,
102        LineEndings::Dos => convert_to_dos_line_endings(res),
103    }
104}
105
106pub fn explain(text: &str) -> String {
107    let (text, _line_endings) = convert_to_unix_line_endings(text);
108    let ast = rnix::parse(&*text);
109    let spacing = rules::spacing();
110    let indentation = rules::indentation();
111    let mut explanation = Vec::new();
112    engine::reformat(&spacing, &indentation, &ast.node(), Some(&mut explanation));
113
114    let mut buf = String::new();
115    let mut line_start: TextSize = 0.into();
116    for line in text.to_string().lines() {
117        let line_len = TextSize::of(line) + TextSize::of("\n");
118        let line_range = TextRange::at(line_start, line_len);
119
120        buf.push_str(line);
121        let mut first = true;
122        for (edit, reason) in explanation.iter() {
123            if line_range.contains(edit.delete.end()) {
124                if first {
125                    first = false;
126                    buf.push_str("  # ")
127                } else {
128                    buf.push_str(", ")
129                }
130                buf.push_str(&format!(
131                    "[{}; {}): ",
132                    usize::from(edit.delete.start()),
133                    usize::from(edit.delete.end())
134                ));
135                match reason {
136                    Some(reason) => buf.push_str(&reason.to_string()),
137                    None => buf.push_str("unnamed rule"),
138                }
139            }
140        }
141        buf.push('\n');
142
143        line_start += line_len;
144    }
145    buf
146}
147
148enum LineEndings {
149    Unix,
150    Dos,
151}
152
153fn convert_to_unix_line_endings(text: &str) -> (Cow<str>, LineEndings) {
154    if !text.contains("\r\n") {
155        return (Cow::Borrowed(text), LineEndings::Unix);
156    }
157    (Cow::Owned(text.replace("\r\n", "\n")), LineEndings::Dos)
158}
159
160fn convert_to_dos_line_endings(text: String) -> String {
161    text.replace('\n', "\r\n")
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn preserves_dos_line_endings() {
170        assert_eq!(&reformat_string("{foo = 92;\n}"), "{\n  foo = 92;\n}\n");
171        assert_eq!(&reformat_string("{foo = 92;\r\n}"), "{\r\n  foo = 92;\r\n}\r\n")
172    }
173
174    #[test]
175    fn converts_tabs_to_spaces() {
176        assert_eq!(&reformat_string("{\n\tfoo = 92;\t}\n"), "{\n  foo = 92;\n}\n");
177    }
178
179    #[test]
180    fn explain_smoke_test() {
181        let input = "{\nfoo =1;\n}\n";
182        let explanation = explain(input);
183        assert_eq!(
184            explanation,
185            "{
186foo =1;  # [7; 7): Space after =
187}
188"
189        )
190    }
191}