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 Work in progress for a formatter.
6 Use like this to format a file:
7 ```sh
8 cargo run --bin slint-lsp -- format -i some_file.slint
9 ```
10
11 Some code in this main.rs file is duplicated with the slint-updater, i guess it could
12 be refactored in a separate utility crate or module or something.
13
14 The [`writer::TokenWriter`] trait is meant to be able to support the LSP later as the
15 LSP wants just the edits, not the full file
16*/
17
18use i_slint_compiler::diagnostics::BuildDiagnostics;
19use i_slint_compiler::parser::{syntax_nodes, SyntaxNode};
20use std::io::{BufWriter, Write};
21use std::path::Path;
22
23use super::{fmt, writer};
24
25pub fn run(files: Vec<std::path::PathBuf>, inplace: bool) -> std::io::Result<()> {
26 for path: PathBuf in files {
27 let source: String = std::fs::read_to_string(&path)?;
28
29 if inplace {
30 let file: BufWriter = BufWriter::new(inner:std::fs::File::create(&path)?);
31 process_file(source, path, file)?
32 } else {
33 process_file(source, path, file:std::io::stdout())?
34 }
35 }
36 Ok(())
37}
38
39/// FIXME! this is duplicated with the updater
40fn process_rust_file(source: String, mut file: impl Write) -> std::io::Result<()> {
41 let mut last: usize = 0;
42 for range: Range in i_slint_compiler::lexer::locate_slint_macro(&source) {
43 file.write_all(buf:source[last..=range.start].as_bytes())?;
44 last = range.end;
45 let code: &str = &source[range];
46
47 let mut diag: BuildDiagnostics = BuildDiagnostics::default();
48 let syntax_node: SyntaxNode = i_slint_compiler::parser::parse(source:code.to_owned(), path:None, version:None, &mut diag);
49 let len: usize = syntax_node.text_range().end().into();
50 visit_node(syntax_node, &mut file)?;
51 if diag.has_error() {
52 file.write_all(&code.as_bytes()[len..])?;
53 diag.print();
54 }
55 }
56 file.write_all(buf:source[last..].as_bytes())
57}
58
59/// FIXME! this is duplicated with the updater
60fn process_markdown_file(source: String, mut file: impl Write) -> std::io::Result<()> {
61 let mut source_slice = &source[..];
62 const CODE_FENCE_START: &str = "```slint\n";
63 const CODE_FENCE_END: &str = "```\n";
64 'l: while let Some(code_start) =
65 source_slice.find(CODE_FENCE_START).map(|idx| idx + CODE_FENCE_START.len())
66 {
67 let code_end = if let Some(code_end) = source_slice[code_start..].find(CODE_FENCE_END) {
68 code_end
69 } else {
70 break 'l;
71 };
72 file.write_all(source_slice[..=code_start - 1].as_bytes())?;
73 source_slice = &source_slice[code_start..];
74 let code = &source_slice[..code_end];
75 source_slice = &source_slice[code_end..];
76
77 let mut diag = BuildDiagnostics::default();
78 let syntax_node = i_slint_compiler::parser::parse(code.to_owned(), None, None, &mut diag);
79 let len = syntax_node.text_range().end().into();
80 visit_node(syntax_node, &mut file)?;
81 if diag.has_error() {
82 file.write_all(&code.as_bytes()[len..])?;
83 diag.print();
84 }
85 }
86 return file.write_all(source_slice.as_bytes());
87}
88
89fn process_slint_file(
90 source: String,
91 path: std::path::PathBuf,
92 mut file: impl Write,
93) -> std::io::Result<()> {
94 let mut diag: BuildDiagnostics = BuildDiagnostics::default();
95 let syntax_node: SyntaxNode = i_slint_compiler::parser::parse(source.clone(), path:Some(&path), version:None, &mut diag);
96 let len: usize = syntax_node.node.text_range().end().into();
97 visit_node(syntax_node, &mut file)?;
98 if diag.has_error() {
99 file.write_all(&source.as_bytes()[len..])?;
100 diag.print();
101 }
102 Ok(())
103}
104
105fn process_file(
106 source: String,
107 path: std::path::PathBuf,
108 mut file: impl Write,
109) -> std::io::Result<()> {
110 match path.extension() {
111 Some(ext: &OsStr) if ext == "rs" => process_rust_file(source, file),
112 Some(ext: &OsStr) if ext == "md" => process_markdown_file(source, file),
113 // Formatting .60 files because of backwards compatibility (project was recently renamed)
114 Some(ext: &OsStr) if ext == "slint" || ext == ".60" => process_slint_file(source, path, file),
115 _ => {
116 // This allows usage like `cat x.slint | slint-lsp format /dev/stdin`
117 if path.as_path() == Path::new("/dev/stdin") {
118 return process_slint_file(source, path, file);
119 }
120 // With other file types, we just output them in their original form.
121 return file.write_all(buf:source.as_bytes());
122 }
123 }
124}
125
126fn visit_node(node: SyntaxNode, file: &mut impl Write) -> std::io::Result<()> {
127 if let Some(doc: Document) = syntax_nodes::Document::new(node) {
128 let mut writer: FileWriter<'_, impl Write> = writer::FileWriter { file };
129 fmt::format_document(doc, &mut writer)
130 } else {
131 Err(std::io::Error::new(kind:std::io::ErrorKind::Other, error:"Not a Document"))
132 }
133}
134