// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use crate::Cli; use by_address::ByAddress; use i_slint_compiler::{ expression_tree::Expression, langtype::Type, lookup::{LookupCtx, LookupObject, LookupResult}, namedreference::NamedReference, object_tree::ElementRc, parser::{SyntaxKind, SyntaxNode}, }; use std::{ cell::RefCell, collections::{HashMap, HashSet}, io::Write, rc::Rc, }; #[derive(Clone, Default)] pub struct LookupChangeState { /// the lookup change pass will map that property reference to another property_mappings: HashMap, /// What needs to be added before the closing brace of the component extra_component_stuff: Rc>>, /// Replace `self.` with that id replace_self: Option, /// Elements that should get an id elements_id: HashMap, String>, /// the full lookup scope pub scope: Vec, } pub(crate) fn fold_node( node: &SyntaxNode, file: &mut impl Write, state: &mut crate::State, args: &Cli, ) -> std::io::Result { let kind = node.kind(); if kind == SyntaxKind::QualifiedName && node.parent().map_or(false, |n| n.kind() == SyntaxKind::Expression) { return fully_qualify_property_access(node, file, state); } else if kind == SyntaxKind::Element && node.parent().map_or(false, |n| n.kind() == SyntaxKind::Component) { return move_properties_to_root(node, state, file, args); } else if kind == SyntaxKind::Element { if let Some(new_id) = state .current_elem .as_ref() .and_then(|e| state.lookup_change.elements_id.get(&ByAddress(e.clone()))) { write!(file, "{new_id} := ")?; } } else if matches!( kind, SyntaxKind::Binding | SyntaxKind::TwoWayBinding | SyntaxKind::CallbackConnection ) && !state.lookup_change.property_mappings.is_empty() && node.parent().map_or(false, |n| n.kind() == SyntaxKind::Element) { if let Some(el) = &state.current_elem { let prop_name = i_slint_compiler::parser::normalize_identifier( node.child_token(SyntaxKind::Identifier).unwrap().text(), ); let nr = NamedReference::new(el, &prop_name); if let Some(new_name) = state.lookup_change.property_mappings.get(&nr).cloned() { state.lookup_change.replace_self = Some( state .lookup_change .elements_id .get(&ByAddress(el.clone())) .map_or_else(|| el.borrow().id.clone(), |s| s.clone()), ); for n in node.children_with_tokens() { let extra = state.lookup_change.extra_component_stuff.clone(); if n.kind() == SyntaxKind::Identifier { write!(&mut *extra.borrow_mut(), "{new_name}")?; } else { crate::visit_node_or_token(n, &mut *extra.borrow_mut(), state, args)?; } } state.lookup_change.replace_self = None; return Ok(true); } } } else if matches!(kind, SyntaxKind::PropertyDeclaration | SyntaxKind::CallbackDeclaration) { if let Some(el) = &state.current_elem { let prop_name = i_slint_compiler::parser::normalize_identifier( &node.child_node(SyntaxKind::DeclaredIdentifier).unwrap().text().to_string(), ); let nr = NamedReference::new(el, &prop_name); if state.lookup_change.property_mappings.contains_key(&nr) { return Ok(true); } } } Ok(false) } /// Make sure that a qualified name is fully qualified with `self.`. /// Also rename the property in `state.lookup_change.property_mappings` fn fully_qualify_property_access( node: &SyntaxNode, file: &mut impl Write, state: &mut crate::State, ) -> std::io::Result { let mut it = node .children_with_tokens() .filter(|n| n.kind() == SyntaxKind::Identifier) .filter_map(|n| n.into_token()); let first = match it.next() { Some(first) => first, None => return Ok(false), }; let first_str = i_slint_compiler::parser::normalize_identifier(first.text()); with_lookup_ctx(state, |ctx| -> std::io::Result { ctx.current_token = Some(first.clone().into()); let global_lookup = i_slint_compiler::lookup::global_lookup(); match global_lookup.lookup(ctx, &first_str) { Some(LookupResult::Expression { expression: Expression::PropertyReference(nr) | Expression::CallbackReference(nr, _) | Expression::FunctionReference(nr, _), .. }) => { if let Some(new_name) = state.lookup_change.property_mappings.get(&nr) { write!(file, "root.{new_name} ")?; Ok(true) } else { let element = nr.element(); if state .current_component .as_ref() .map_or(false, |c| Rc::ptr_eq(&element, &c.root_element)) { write!(file, "root.")?; } else if state .lookup_change .scope .last() .map_or(false, |e| Rc::ptr_eq(&element, e)) { if let Some(replace_self) = &state.lookup_change.replace_self { write!(file, "{replace_self}.")?; } else { write!(file, "self.")?; } } Ok(false) } } Some(LookupResult::Expression { expression: Expression::ElementReference(el), .. }) => { let second = match it.next() { Some(second) => second, None => return Ok(false), }; let prop_name = i_slint_compiler::parser::normalize_identifier(second.text()); let nr = NamedReference::new(&el.upgrade().unwrap(), &prop_name); if let Some(new_name) = state.lookup_change.property_mappings.get(&nr) { write!(file, "root.{new_name} ")?; Ok(true) } else if let Some(replace_self) = &state.lookup_change.replace_self { if first_str == "self" || first_str == "parent" { if first_str == "self" { write!(file, "{replace_self}.{second} ")?; } else { let replace_parent = state .lookup_change .elements_id .get(&ByAddress(nr.element())) .map_or_else(|| nr.element().borrow().id.clone(), |s| s.clone()); write!(file, "{replace_parent}.{second} ")?; } for t in it { write!(file, ".{} ", t.text())?; } Ok(true) } else { Ok(false) } } else { Ok(false) } } _ => Ok(false), } }) .unwrap_or(Ok(false)) } /// Move the properties from state.lookup_change.property_mappings to the root fn move_properties_to_root( node: &SyntaxNode, state: &mut crate::State, file: &mut impl Write, args: &Cli, ) -> std::io::Result { if state.lookup_change.property_mappings.is_empty() { return Ok(false); } let mut seen_brace = false; for c in node.children_with_tokens() { let k = c.kind(); if k == SyntaxKind::LBrace { seen_brace = true; } else if seen_brace && k != SyntaxKind::Whitespace { let property_mappings = state.lookup_change.property_mappings.clone(); for (nr, prop) in property_mappings.iter() { let decl = nr.element().borrow().property_declarations.get(nr.name()).unwrap().clone(); let n2: SyntaxNode = decl.node.clone().unwrap(); let old_current_element = std::mem::replace(&mut state.current_elem, Some(nr.element())); state.lookup_change.replace_self = Some( state .lookup_change .elements_id .get(&ByAddress(nr.element())) .map_or_else(|| nr.element().borrow().id.clone(), |s| s.clone()), ); for c2 in n2.children_with_tokens() { if c2.kind() == SyntaxKind::DeclaredIdentifier { write!(file, " {prop} ")?; } else { crate::visit_node_or_token(c2, file, state, args)?; } } state.lookup_change.replace_self = None; write!(file, "\n ")?; state.current_elem = old_current_element; } seen_brace = false; } if k == SyntaxKind::RBrace { file.write_all(&*std::mem::take( &mut *state.lookup_change.extra_component_stuff.borrow_mut(), ))?; } crate::visit_node_or_token(c, file, state, args)?; } Ok(true) } pub(crate) fn with_lookup_ctx( state: &crate::State, f: impl FnOnce(&mut LookupCtx) -> R, ) -> Option { let mut build_diagnostics = Default::default(); let tr = &state.current_doc.as_ref()?.local_registry; let mut lookup_context = LookupCtx::empty_context(tr, &mut build_diagnostics); let ty = state .current_elem .as_ref() .zip(state.property_name.as_ref()) .map_or(Type::Invalid, |(e, n)| e.borrow().lookup_property(&n).property_type); lookup_context.property_name = state.property_name.as_ref().map(String::as_str); lookup_context.property_type = ty; lookup_context.component_scope = &state.lookup_change.scope; Some(f(&mut lookup_context)) } pub(crate) fn collect_movable_properties(state: &mut crate::State) { pub fn collect_movable_properties_recursive(vec: &mut Vec, elem: &ElementRc) { for c in &elem.borrow().children { if c.borrow().repeated.is_some() { continue; } vec.extend( c.borrow() .property_declarations .iter() .map(|(name, _)| NamedReference::new(c, &name)), ); collect_movable_properties_recursive(vec, &c); } } if let Some(c) = &state.current_component { let mut props_to_move = Vec::new(); collect_movable_properties_recursive(&mut props_to_move, &c.root_element); let mut seen = HashSet::new(); state.lookup_change.property_mappings = props_to_move .into_iter() .map(|nr| { let element = nr.element(); ensure_element_has_id(&element, &mut state.lookup_change.elements_id); if let Some(parent) = i_slint_compiler::object_tree::find_parent_element(&element) { ensure_element_has_id(&parent, &mut state.lookup_change.elements_id); } let mut name = format!("_{}_{}", element.borrow().id, nr.name()); while !seen.insert(name.clone()) || c.root_element.borrow().lookup_property(&name).is_valid() { name = format!("_{name}"); } (nr, name) }) .collect() } } fn ensure_element_has_id( element: &ElementRc, elements_id: &mut HashMap, String>, ) { static ID_GEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); if element.borrow().id.is_empty() { elements_id.entry(ByAddress(element.clone())).or_insert_with(|| { format!("_{}", ID_GEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) }); } } pub(crate) fn enter_element(state: &mut crate::State) { if state .lookup_change .scope .last() .map_or(false, |e| e.borrow().base_type.to_string() == "Path") { // Path's sub-elements have strange lookup rules: They are considering self as the Path state.lookup_change.replace_self = Some("parent".into()); } else { state.lookup_change.scope.extend(state.current_elem.iter().cloned()) } }