1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | //! |
5 | //! Tool to change the syntax or reformat a .slint file |
6 | |
7 | use clap::Parser; |
8 | use experiments::lookup_changes::LookupChangeState; |
9 | use i_slint_compiler::diagnostics::BuildDiagnostics; |
10 | use i_slint_compiler::object_tree::{self, Component, Document, ElementRc}; |
11 | use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode}; |
12 | use i_slint_compiler::typeloader::TypeLoader; |
13 | use std::cell::RefCell; |
14 | use std::io::{BufWriter, Write}; |
15 | use std::path::Path; |
16 | use std::rc::Rc; |
17 | |
18 | mod experiments { |
19 | pub(super) mod geometry_changes; |
20 | pub(super) mod input_output_properties; |
21 | pub(super) mod lookup_changes; |
22 | pub(super) mod new_component_declaration; |
23 | pub(super) mod purity; |
24 | pub(super) mod transitions; |
25 | } |
26 | |
27 | mod transforms { |
28 | pub(super) mod renames; |
29 | } |
30 | |
31 | #[derive (clap::Parser)] |
32 | #[command(author, version, about, long_about = None)] |
33 | pub struct Cli { |
34 | #[arg(name = "path to .slint file(s)" , action)] |
35 | paths: Vec<std::path::PathBuf>, |
36 | |
37 | /// Modify the file inline instead of printing to updated contents to stdout |
38 | #[arg(short, long, action)] |
39 | inline: bool, |
40 | |
41 | /// Move all properties declarations to root of each component |
42 | #[arg(long, action)] |
43 | move_declarations: bool, |
44 | } |
45 | |
46 | fn main() -> std::io::Result<()> { |
47 | let args: Cli = Cli::parse(); |
48 | |
49 | for path: &Path in &args.paths { |
50 | let source: String = std::fs::read_to_string(path)?; |
51 | |
52 | if args.inline { |
53 | let file: BufWriter = BufWriter::new(inner:std::fs::File::create(path)?); |
54 | process_file(source, path, file, &args)? |
55 | } else { |
56 | process_file(source, path, file:std::io::stdout(), &args)? |
57 | } |
58 | } |
59 | Ok(()) |
60 | } |
61 | |
62 | fn process_rust_file(source: String, mut file: impl Write, args: &Cli) -> std::io::Result<()> { |
63 | let mut last: usize = 0; |
64 | for range: Range in i_slint_compiler::lexer::locate_slint_macro(&source) { |
65 | file.write_all(buf:source[last..=range.start].as_bytes())?; |
66 | last = range.end; |
67 | let code: &str = &source[range]; |
68 | |
69 | let mut diag: BuildDiagnostics = BuildDiagnostics::default(); |
70 | let syntax_node: SyntaxNode = i_slint_compiler::parser::parse(source:code.to_owned(), path:None, version:None, &mut diag); |
71 | let len: usize = syntax_node.text_range().end().into(); |
72 | let mut state: State = init_state(&syntax_node, &mut diag); |
73 | visit_node(syntax_node, &mut file, &mut state, args)?; |
74 | if diag.has_error() { |
75 | file.write_all(&code.as_bytes()[len..])?; |
76 | diag.print(); |
77 | } |
78 | } |
79 | file.write_all(buf:source[last..].as_bytes()) |
80 | } |
81 | |
82 | fn process_markdown_file(source: String, mut file: impl Write, args: &Cli) -> std::io::Result<()> { |
83 | let mut source_slice = &source[..]; |
84 | const CODE_FENCE_START: &str = "```slint" ; |
85 | const CODE_FENCE_END: &str = "``` \n" ; |
86 | 'l: while let Some(code_start) = source_slice |
87 | .find(CODE_FENCE_START) |
88 | .map(|idx| idx + CODE_FENCE_START.len()) |
89 | .and_then(|x| source_slice[x..].find(' \n' ).map(|idx| idx + x)) |
90 | { |
91 | let code_end = if let Some(code_end) = source_slice[code_start..].find(CODE_FENCE_END) { |
92 | code_end |
93 | } else { |
94 | break 'l; |
95 | }; |
96 | file.write_all(source_slice[..=code_start - 1].as_bytes())?; |
97 | source_slice = &source_slice[code_start..]; |
98 | let code = &source_slice[..code_end]; |
99 | source_slice = &source_slice[code_end..]; |
100 | |
101 | let mut diag = BuildDiagnostics::default(); |
102 | let syntax_node = i_slint_compiler::parser::parse(code.to_owned(), None, None, &mut diag); |
103 | let len = syntax_node.text_range().end().into(); |
104 | let mut state = init_state(&syntax_node, &mut diag); |
105 | visit_node(syntax_node, &mut file, &mut state, args)?; |
106 | if diag.has_error() { |
107 | file.write_all(&code.as_bytes()[len..])?; |
108 | diag.print(); |
109 | } |
110 | } |
111 | return file.write_all(source_slice.as_bytes()); |
112 | } |
113 | |
114 | fn process_file( |
115 | source: String, |
116 | path: &Path, |
117 | mut file: impl Write, |
118 | args: &Cli, |
119 | ) -> std::io::Result<()> { |
120 | match path.extension() { |
121 | Some(ext: &OsStr) if ext == "rs" => return process_rust_file(source, file, args), |
122 | Some(ext: &OsStr) if ext == "md" => return process_markdown_file(source, file, args), |
123 | _ => {} |
124 | } |
125 | |
126 | let mut diag: BuildDiagnostics = BuildDiagnostics::default(); |
127 | let syntax_node: SyntaxNode = i_slint_compiler::parser::parse(source.clone(), path:Some(path), version:None, &mut diag); |
128 | let len: usize = syntax_node.node.text_range().end().into(); |
129 | let mut state: State = init_state(&syntax_node, &mut diag); |
130 | visit_node(syntax_node, &mut file, &mut state, args)?; |
131 | if diag.has_error() { |
132 | file.write_all(&source.as_bytes()[len..])?; |
133 | diag.print(); |
134 | } |
135 | Ok(()) |
136 | } |
137 | |
138 | fn init_state(syntax_node: &SyntaxNode, diag: &mut BuildDiagnostics) -> State { |
139 | let mut state = State::default(); |
140 | let doc = syntax_node.clone().into(); |
141 | let mut type_loader = TypeLoader::new( |
142 | i_slint_compiler::typeregister::TypeRegister::builtin(), |
143 | i_slint_compiler::CompilerConfiguration::new( |
144 | i_slint_compiler::generator::OutputFormat::Llr, |
145 | ), |
146 | &mut BuildDiagnostics::default(), |
147 | ); |
148 | let dependency_registry = Rc::new(RefCell::new( |
149 | i_slint_compiler::typeregister::TypeRegister::new(&type_loader.global_type_registry), |
150 | )); |
151 | let (foreign_imports, reexports) = spin_on::spin_on(type_loader.load_dependencies_recursively( |
152 | &doc, |
153 | diag, |
154 | &dependency_registry, |
155 | )); |
156 | let current_doc = crate::object_tree::Document::from_node( |
157 | doc, |
158 | foreign_imports, |
159 | reexports, |
160 | diag, |
161 | &dependency_registry, |
162 | ); |
163 | i_slint_compiler::passes::infer_aliases_types::resolve_aliases(¤t_doc, diag); |
164 | state.current_doc = Rc::new(current_doc).into(); |
165 | state |
166 | } |
167 | |
168 | #[derive (Default, Clone)] |
169 | struct State { |
170 | /// When visiting a binding, this is the name of the current property |
171 | property_name: Option<String>, |
172 | |
173 | /// The Document being visited |
174 | current_doc: Option<Rc<Document>>, |
175 | /// The component in scope, |
176 | current_component: Option<Rc<Component>>, |
177 | /// The element in scope |
178 | current_elem: Option<ElementRc>, |
179 | |
180 | lookup_change: LookupChangeState, |
181 | } |
182 | |
183 | fn visit_node( |
184 | node: SyntaxNode, |
185 | file: &mut impl Write, |
186 | state: &mut State, |
187 | args: &Cli, |
188 | ) -> std::io::Result<()> { |
189 | let mut state = state.clone(); |
190 | match node.kind() { |
191 | SyntaxKind::PropertyDeclaration => { |
192 | state.property_name = node.child_text(SyntaxKind::DeclaredIdentifier) |
193 | } |
194 | SyntaxKind::Binding => state.property_name = node.child_text(SyntaxKind::Identifier), |
195 | SyntaxKind::CallbackDeclaration => { |
196 | state.property_name = node.child_text(SyntaxKind::Identifier) |
197 | } |
198 | SyntaxKind::Component => { |
199 | if let Some(doc) = &state.current_doc { |
200 | let component_name = i_slint_compiler::parser::normalize_identifier( |
201 | &syntax_nodes::Component::from(node.clone()) |
202 | .DeclaredIdentifier() |
203 | .child_text(SyntaxKind::Identifier) |
204 | .unwrap_or(String::new()), |
205 | ); |
206 | |
207 | state.current_component = |
208 | doc.inner_components.iter().find(|c| c.id == component_name).cloned(); |
209 | if args.move_declarations { |
210 | experiments::lookup_changes::collect_movable_properties(&mut state); |
211 | } |
212 | } |
213 | } |
214 | SyntaxKind::RepeatedElement | SyntaxKind::ConditionalElement => { |
215 | if args.move_declarations { |
216 | experiments::lookup_changes::collect_movable_properties(&mut state); |
217 | } |
218 | } |
219 | SyntaxKind::Element => { |
220 | if let Some(parent_el) = state.current_elem.take() { |
221 | state.current_elem = parent_el |
222 | .borrow() |
223 | .children |
224 | .iter() |
225 | .find(|c| c.borrow().debug.first().map_or(false, |n| n.0.node == node.node)) |
226 | .cloned() |
227 | } else if let Some(parent_co) = &state.current_component { |
228 | if node.parent().map_or(false, |n| n.kind() == SyntaxKind::Component) { |
229 | state.current_elem = Some(parent_co.root_element.clone()) |
230 | } |
231 | } |
232 | |
233 | experiments::lookup_changes::enter_element(&mut state); |
234 | } |
235 | _ => (), |
236 | } |
237 | |
238 | if fold_node(&node, file, &mut state, args)? { |
239 | return Ok(()); |
240 | } |
241 | for n in node.children_with_tokens() { |
242 | visit_node_or_token(n, file, &mut state, args)?; |
243 | } |
244 | Ok(()) |
245 | } |
246 | |
247 | pub(crate) fn visit_node_or_token( |
248 | n: NodeOrToken, |
249 | file: &mut impl Write, |
250 | state: &mut State, |
251 | args: &Cli, |
252 | ) -> std::io::Result<()> { |
253 | match n { |
254 | NodeOrToken::Node(n: SyntaxNode) => visit_node(node:n, file, state, args)?, |
255 | NodeOrToken::Token(t: SyntaxToken) => fold_token(node:t, file, state)?, |
256 | }; |
257 | Ok(()) |
258 | } |
259 | |
260 | /// return false if one need to continue folding the children |
261 | fn fold_node( |
262 | node: &SyntaxNode, |
263 | file: &mut impl Write, |
264 | state: &mut State, |
265 | args: &Cli, |
266 | ) -> std::io::Result<bool> { |
267 | if experiments::input_output_properties::fold_node(node, file, state, args)? { |
268 | return Ok(true); |
269 | } |
270 | if experiments::new_component_declaration::fold_node(node, file, state, args)? { |
271 | return Ok(true); |
272 | } |
273 | if experiments::lookup_changes::fold_node(node, file, state, args)? { |
274 | return Ok(true); |
275 | } |
276 | if experiments::geometry_changes::fold_node(node, file, state, args)? { |
277 | return Ok(true); |
278 | } |
279 | if experiments::transitions::fold_node(node, file, state, args)? { |
280 | return Ok(true); |
281 | } |
282 | if experiments::purity::fold_node(node, file, state, args)? { |
283 | return Ok(true); |
284 | } |
285 | if transforms::renames::fold_node(node, file, state, args)? { |
286 | return Ok(true); |
287 | } |
288 | Ok(false) |
289 | } |
290 | |
291 | fn fold_token( |
292 | node: i_slint_compiler::parser::SyntaxToken, |
293 | file: &mut impl Write, |
294 | #[allow (unused)] state: &mut State, |
295 | ) -> std::io::Result<()> { |
296 | /* Example: this adds the "ms" prefix to the number within a "duration" binding |
297 | if state.property_name == Some("duration".into()) && node.kind() == SyntaxKind::NumberLiteral { |
298 | if !node.text().ends_with("s") { |
299 | return write!(file, "{}ms", node.text()); |
300 | } |
301 | }*/ |
302 | /* Example: replace _ by - in identifiers |
303 | if node.kind() == SyntaxKind::Identifier |
304 | && node.text().contains('_') |
305 | && !node.text().starts_with("_") |
306 | { |
307 | return file.write_all(node.text().replace('_', "-").as_bytes()) |
308 | }*/ |
309 | file.write_all(buf:node.text().as_bytes()) |
310 | } |
311 | |