// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial use crate::writer::TokenWriter; use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode}; pub(crate) fn format_document( doc: syntax_nodes::Document, writer: &mut impl TokenWriter, ) -> Result<(), std::io::Error> { let mut state = FormatState::default(); format_node(&doc, writer, &mut state) } #[derive(Default)] struct FormatState { /// The whitespace have been written, all further whitespace can be skipped skip_all_whitespace: bool, /// The whitespace to add before the next token whitespace_to_add: Option, /// The level of indentation indentation_level: u32, /// A counter that is incremented when something is inserted insertion_count: usize, /// a comment has been written followed maybe by some spacing after_comment: bool, } impl FormatState { fn new_line(&mut self) { if self.after_comment { return; } self.skip_all_whitespace = true; if let Some(x) = &mut self.whitespace_to_add { x.insert(0, '\n'); return; } let mut new_line = String::from("\n"); for _ in 0..self.indentation_level { new_line += " "; } self.whitespace_to_add = Some(new_line); } fn insert_whitespace(&mut self, arg: &str) { if self.after_comment { return; } self.skip_all_whitespace = true; if !arg.is_empty() { if let Some(ws) = &mut self.whitespace_to_add { *ws += arg; } else { self.whitespace_to_add = Some(arg.into()); } } } } fn format_node( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { match node.kind() { SyntaxKind::Component => { return format_component(node, writer, state); } SyntaxKind::Element => { return format_element(node, writer, state); } SyntaxKind::SubElement => { return format_sub_element(node, writer, state); } SyntaxKind::PropertyDeclaration => { return format_property_declaration(node, writer, state); } SyntaxKind::Binding => { return format_binding(node, writer, state); } SyntaxKind::CallbackConnection => { return format_callback_connection(node, writer, state); } SyntaxKind::CallbackDeclaration => { return format_callback_declaration(node, writer, state); } SyntaxKind::QualifiedName => { return format_qualified_name(node, writer, state); } SyntaxKind::BinaryExpression => { return format_binary_expression(node, writer, state); } SyntaxKind::Expression => { return format_expression(node, writer, state); } SyntaxKind::ChildrenPlaceholder => { return format_children_placeholder(node, writer, state); } _ => (), } for n in node.children_with_tokens() { fold(n, writer, state)?; } Ok(()) } fn fold( n: NodeOrToken, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> std::io::Result<()> { match n { NodeOrToken::Node(n) => format_node(&n, writer, state), NodeOrToken::Token(t) => { if t.kind() == SyntaxKind::Whitespace { if state.skip_all_whitespace { writer.with_new_content(t, "")?; return Ok(()); } } else { state.after_comment = t.kind() == SyntaxKind::Comment; state.skip_all_whitespace = false; if let Some(x) = state.whitespace_to_add.take() { state.insertion_count += 1; writer.insert_before(t, x.as_ref())?; return Ok(()); } } state.insertion_count += 1; writer.no_change(t) } } } fn whitespace_to( sub: &mut impl Iterator, element: SyntaxKind, writer: &mut impl TokenWriter, state: &mut FormatState, prefix_whitespace: &str, ) -> Result { whitespace_to_one_of(sub, &[element], writer, state, prefix_whitespace) } fn whitespace_to_one_of( sub: &mut impl Iterator, elements: &[SyntaxKind], writer: &mut impl TokenWriter, state: &mut FormatState, prefix_whitespace: &str, ) -> Result { state.insert_whitespace(prefix_whitespace); for n in sub { match n.kind() { SyntaxKind::Whitespace | SyntaxKind::Comment => (), x if elements.contains(&x) => { fold(n, writer, state)?; return Ok(true); } _ => { eprintln!("Inconsistency: expected {:?}, found {:?}", elements, n); fold(n, writer, state)?; return Ok(false); } } fold(n, writer, state)?; } eprintln!("Inconsistency: expected {:?}, not found", elements); Ok(false) } fn finish_node( sub: impl Iterator, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result { // FIXME: We should check that there are only comments or whitespace in sub for n in sub { fold(n, writer, state)?; } Ok(true) } fn format_component( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens(); let _ok = whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::ColonEqual, writer, state, " ")? && whitespace_to(&mut sub, SyntaxKind::Element, writer, state, " ")?; finish_node(sub, writer, state)?; state.new_line(); Ok(()) } fn format_element( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens(); if !(whitespace_to(&mut sub, SyntaxKind::QualifiedName, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::LBrace, writer, state, " ")?) { // There is an error finding the QualifiedName and LBrace when we do this branch. finish_node(sub, writer, state)?; return Ok(()); } state.indentation_level += 1; state.new_line(); let ins_ctn = state.insertion_count; for n in sub { if n.kind() == SyntaxKind::RBrace { state.indentation_level -= 1; state.whitespace_to_add = None; if ins_ctn == state.insertion_count { state.insert_whitespace(" "); } else { state.new_line(); } fold(n, writer, state)?; state.new_line(); } else { fold(n, writer, state)?; } } Ok(()) } fn format_sub_element( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens().peekable(); // Let's decide based on the first child match sub.peek() { Some(first_node_or_token) => { if first_node_or_token.kind() == SyntaxKind::Identifier { // In this branch the sub element starts with an identifier, eg. // Something := Text {...} if !(whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::ColonEqual, writer, state, " ")?) { // There is an error finding the Identifier and := when we do this branch. finish_node(sub, writer, state)?; return Ok(()); } state.insert_whitespace(" "); } // If the first child was not an identifier, we just fold it // (it might be an element, eg. Text {...}) for s in sub { fold(s, writer, state)?; } state.new_line(); Ok(()) } // No children found -> we ignore this node None => Ok(()), } } fn format_property_declaration( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens(); let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::LAngle, writer, state, " ")? && whitespace_to(&mut sub, SyntaxKind::Type, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::RAngle, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?; state.skip_all_whitespace = true; // FIXME: more formatting for s in sub { fold(s, writer, state)?; } state.new_line(); Ok(()) } fn format_binding( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens(); let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::Colon, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::BindingExpression, writer, state, " ")?; // FIXME: more formatting for s in sub { fold(s, writer, state)?; } state.new_line(); Ok(()) } fn format_callback_declaration( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens(); let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")? && whitespace_to(&mut sub, SyntaxKind::DeclaredIdentifier, writer, state, " ")?; while let Some(n) = sub.next() { state.skip_all_whitespace = true; match n.kind() { SyntaxKind::Comma => { fold(n, writer, state)?; state.insert_whitespace(" "); } SyntaxKind::Arrow => { state.insert_whitespace(" "); fold(n, writer, state)?; whitespace_to(&mut sub, SyntaxKind::ReturnType, writer, state, " ")?; } _ => { fold(n, writer, state)?; } } } state.new_line(); Ok(()) } fn format_callback_connection( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens(); let _ok = whitespace_to(&mut sub, SyntaxKind::Identifier, writer, state, "")?; for s in sub { state.skip_all_whitespace = true; match s.kind() { SyntaxKind::FatArrow => { state.insert_whitespace(" "); fold(s, writer, state)?; state.insert_whitespace(" "); } SyntaxKind::Comma => { fold(s, writer, state)?; state.insert_whitespace(" "); } _ => fold(s, writer, state)?, } } state.new_line(); Ok(()) } fn format_qualified_name( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { for n in node.children_with_tokens() { state.skip_all_whitespace = true; fold(n, writer, state)?; } /*if !node .last_token() .and_then(|x| x.next_token()) .map(|x| { matches!( x.kind(), SyntaxKind::LParent | SyntaxKind::RParent | SyntaxKind::Semicolon | SyntaxKind::Comma ) }) .unwrap_or(false) { state.insert_whitespace(" "); } else { state.skip_all_whitespace = true; }*/ Ok(()) } fn format_binary_expression( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { let mut sub = node.children_with_tokens(); let _ok = whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, "")? && whitespace_to_one_of( &mut sub, &[ SyntaxKind::Plus, SyntaxKind::Minus, SyntaxKind::Star, SyntaxKind::Div, SyntaxKind::AndAnd, SyntaxKind::OrOr, SyntaxKind::EqualEqual, SyntaxKind::NotEqual, SyntaxKind::LAngle, SyntaxKind::LessEqual, SyntaxKind::RAngle, SyntaxKind::GreaterEqual, ], writer, state, " ", )? && whitespace_to(&mut sub, SyntaxKind::Expression, writer, state, " ")?; Ok(()) } fn format_expression( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { // For expressions, we skip whitespace. // Since state.skip_all_whitespace is reset every time we find something that is not a whitespace, // we need to set it all the time. for n in node.children_with_tokens() { state.skip_all_whitespace = true; fold(n, writer, state)?; } Ok(()) } fn format_children_placeholder( node: &SyntaxNode, writer: &mut impl TokenWriter, state: &mut FormatState, ) -> Result<(), std::io::Error> { // Skips whitespace after a `@children` node. for n in node.children_with_tokens() { fold(n, writer, state)?; } state.skip_all_whitespace = true; Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::writer::FileWriter; use i_slint_compiler::diagnostics::BuildDiagnostics; use i_slint_compiler::parser::syntax_nodes; // FIXME more descriptive errors when an assertion fails fn assert_formatting(unformatted: &str, formatted: &str) { // Parse the unformatted string let syntax_node = i_slint_compiler::parser::parse( String::from(unformatted), None, &mut BuildDiagnostics::default(), ); // Turn the syntax node into a document let doc = syntax_nodes::Document::new(syntax_node).unwrap(); let mut file = Vec::new(); format_document(doc, &mut FileWriter { file: &mut file }).unwrap(); assert_eq!(String::from_utf8(file).unwrap(), formatted); } #[test] fn basic_formatting() { assert_formatting("A:=Text{}", "A := Text { }"); } #[test] fn complex_formatting() { assert_formatting( r#" Main :=Window{callback some-fn(string,string)->bool;some-fn(a, b)=>{a<=b} propertyprop-x; VerticalBox { combo:=ComboBox{} }} "#, r#" Main := Window { callback some-fn(string, string) -> bool; some-fn(a, b) => {a <= b} property prop-x; VerticalBox { combo := ComboBox { } } }"#, ); } #[test] fn parent_access() { assert_formatting( r#" Main := Parent{ Child{ some-prop: parent.foo - 60px; }}"#, r#" Main := Parent { Child { some-prop: parent.foo - 60px; } }"#, ); } #[test] fn space_with_braces() { assert_formatting(r#"Main := Window{}"#, r#"Main := Window { }"#); // Also in a child assert_formatting( r#" Main := Window{Child{}}"#, r#" Main := Window { Child { } }"#, ); assert_formatting( r#" Main := VerticalLayout{HorizontalLayout{prop:3;}}"#, r#" Main := VerticalLayout { HorizontalLayout { prop: 3; } }"#, ); } #[test] fn binary_expressions() { assert_formatting( r#" Main := Some{ a:3+2; b:4-7; c:3*7; d:3/9; e:3==4; f:3!=4; g:3<4; h:3<=4; i:3>4; j:3>=4; k:3&&4; l:3||4; }"#, r#" Main := Some { a: 3 + 2; b: 4 - 7; c: 3 * 7; d: 3 / 9; e: 3 == 4; f: 3 != 4; g: 3 < 4; h: 3 <= 4; i: 3 > 4; j: 3 >= 4; k: 3 && 4; l: 3 || 4; }"#, ); assert_formatting( r#" Main := Some{ m: 3 + 8; m:3 + 8; m: 3+ 8; m:3+ 8; m: 3 +8; m:3 +8; m: 3+8; m:3+8; }"#, r#" Main := Some { m: 3 + 8; m: 3 + 8; m: 3 + 8; m: 3 + 8; m: 3 + 8; m: 3 + 8; m: 3 + 8; m: 3 + 8; }"#, ); } #[test] fn file_with_an_import() { assert_formatting( r#" import { Some } from "./here.slint"; A := Some{ padding-left: 10px; Text{ x: 3px; }}"#, r#" import { Some } from "./here.slint"; A := Some { padding-left: 10px; Text { x: 3px; } }"#, ); } #[test] fn children() { // Regression test - children was causing additional newlines assert_formatting( r#" A := B { C { @children } }"#, r#" A := B { C { @children } }"#, ); } }