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#[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 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 pub(crate) fn text_diff(&self) -> Vec<AtomEdit> {
65 self.edits.iter().map(|(edit, _reason)| edit.clone()).collect()
66 }
67
68 pub(crate) fn has_changes(&self) -> bool {
70 !self.edits.is_empty()
71 }
72
73 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 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}