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
7use clap::Parser;
8use experiments::lookup_changes::LookupChangeState;
9use i_slint_compiler::diagnostics::BuildDiagnostics;
10use i_slint_compiler::object_tree::{self, Component, Document, ElementRc};
11use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode};
12use i_slint_compiler::typeloader::TypeLoader;
13use std::cell::RefCell;
14use std::io::{BufWriter, Write};
15use std::path::Path;
16use std::rc::Rc;
17
18mod 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
27mod transforms {
28 pub(super) mod renames;
29}
30
31#[derive(clap::Parser)]
32#[command(author, version, about, long_about = None)]
33pub 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
46fn 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
62fn 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
82fn 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
114fn 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
138fn 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(&current_doc, diag);
164 state.current_doc = Rc::new(current_doc).into();
165 state
166}
167
168#[derive(Default, Clone)]
169struct 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
183fn 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
247pub(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
261fn 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
291fn 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