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 | use std::cell::RefCell; |
5 | use std::collections::{HashMap, HashSet}; |
6 | use std::path::{Path, PathBuf}; |
7 | use std::rc::Rc; |
8 | |
9 | use crate::diagnostics::{BuildDiagnostics, SourceFileVersion, Spanned}; |
10 | use crate::object_tree::{self, Document, ExportedName, Exports}; |
11 | use crate::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxToken}; |
12 | use crate::typeregister::TypeRegister; |
13 | use crate::CompilerConfiguration; |
14 | use crate::{fileaccess, parser}; |
15 | use core::future::Future; |
16 | use itertools::Itertools; |
17 | |
18 | /// Storage for a cache of all loaded documents |
19 | #[derive (Default)] |
20 | struct LoadedDocuments { |
21 | /// maps from the canonical file name to the object_tree::Document |
22 | docs: HashMap<PathBuf, Document>, |
23 | /// The .slint files that are currently being loaded, potentially asynchronously. |
24 | /// When a task start loading a file, it will add an empty vector to this map, and |
25 | /// the same task will remove the entry from the map when finished, and awake all |
26 | /// wakers. |
27 | currently_loading: HashMap<PathBuf, Vec<std::task::Waker>>, |
28 | } |
29 | |
30 | pub enum ImportKind { |
31 | ImportList(syntax_nodes::ImportSpecifier), |
32 | ModuleReexport(syntax_nodes::ExportModule), // re-export all types, as per export * from "foo". |
33 | } |
34 | |
35 | pub struct ImportedTypes { |
36 | pub import_uri_token: SyntaxToken, |
37 | pub import_kind: ImportKind, |
38 | pub file: String, |
39 | } |
40 | |
41 | #[derive (Debug)] |
42 | pub struct ImportedName { |
43 | // name of export to match in the other file |
44 | pub external_name: String, |
45 | // name to be used locally |
46 | pub internal_name: String, |
47 | } |
48 | |
49 | impl ImportedName { |
50 | pub fn extract_imported_names( |
51 | import: &syntax_nodes::ImportSpecifier, |
52 | ) -> impl Iterator<Item = ImportedName> + '_ { |
53 | import.ImportIdentifierList().into_iter().flat_map(|import_identifiers: ImportIdentifierList| { |
54 | import_identifiers.ImportIdentifier().map(Self::from_node) |
55 | }) |
56 | } |
57 | |
58 | pub fn from_node(importident: syntax_nodes::ImportIdentifier) -> Self { |
59 | let external_name: String = |
60 | parser::normalize_identifier(ident:importident.ExternalName().text().to_string().trim()); |
61 | |
62 | let internal_name: String = match importident.InternalName() { |
63 | Some(name_ident: InternalName) => parser::normalize_identifier(ident:name_ident.text().to_string().trim()), |
64 | None => external_name.clone(), |
65 | }; |
66 | |
67 | ImportedName { internal_name, external_name } |
68 | } |
69 | } |
70 | |
71 | pub struct TypeLoader { |
72 | pub global_type_registry: Rc<RefCell<TypeRegister>>, |
73 | pub compiler_config: CompilerConfiguration, |
74 | style: String, |
75 | all_documents: LoadedDocuments, |
76 | } |
77 | |
78 | struct BorrowedTypeLoader<'a> { |
79 | tl: &'a mut TypeLoader, |
80 | diag: &'a mut BuildDiagnostics, |
81 | } |
82 | |
83 | impl TypeLoader { |
84 | pub fn new( |
85 | global_type_registry: Rc<RefCell<TypeRegister>>, |
86 | compiler_config: CompilerConfiguration, |
87 | diag: &mut BuildDiagnostics, |
88 | ) -> Self { |
89 | let mut style = compiler_config |
90 | .style |
91 | .clone() |
92 | .or_else(|| std::env::var("SLINT_STYLE" ).ok()) |
93 | .unwrap_or_else(|| "native" .into()); |
94 | |
95 | if style == "native" { |
96 | style = get_native_style(&mut diag.all_loaded_files); |
97 | } |
98 | |
99 | let myself = Self { |
100 | global_type_registry, |
101 | compiler_config, |
102 | style: style.clone(), |
103 | all_documents: Default::default(), |
104 | }; |
105 | |
106 | let mut known_styles = fileaccess::styles(); |
107 | known_styles.push("native" ); |
108 | if !known_styles.contains(&style.as_ref()) |
109 | && myself |
110 | .find_file_in_include_path(None, &format!(" {}/std-widgets.slint" , style)) |
111 | .is_none() |
112 | { |
113 | diag.push_diagnostic_with_span( |
114 | format!( |
115 | "Style {} in not known. Use one of the builtin styles [ {}] or make sure your custom style is found in the include directories" , |
116 | &style, |
117 | known_styles.join(", " ) |
118 | ), |
119 | Default::default(), |
120 | crate::diagnostics::DiagnosticLevel::Error, |
121 | ); |
122 | } |
123 | |
124 | myself |
125 | } |
126 | |
127 | /// Imports of files that don't have the .slint extension are returned. |
128 | pub async fn load_dependencies_recursively<'a>( |
129 | &'a mut self, |
130 | doc: &'a syntax_nodes::Document, |
131 | diag: &'a mut BuildDiagnostics, |
132 | registry_to_populate: &'a Rc<RefCell<TypeRegister>>, |
133 | ) -> (Vec<ImportedTypes>, Exports) { |
134 | let state = RefCell::new(BorrowedTypeLoader { tl: self, diag }); |
135 | Self::load_dependencies_recursively_impl( |
136 | &state, |
137 | doc, |
138 | registry_to_populate, |
139 | &Default::default(), |
140 | ) |
141 | .await |
142 | } |
143 | |
144 | fn load_dependencies_recursively_impl<'a: 'b, 'b>( |
145 | state: &'a RefCell<BorrowedTypeLoader<'a>>, |
146 | doc: &'b syntax_nodes::Document, |
147 | registry_to_populate: &'b Rc<RefCell<TypeRegister>>, |
148 | import_stack: &'b HashSet<PathBuf>, |
149 | ) -> core::pin::Pin<Box<dyn std::future::Future<Output = (Vec<ImportedTypes>, Exports)> + 'b>> |
150 | { |
151 | let mut foreign_imports = vec![]; |
152 | let mut dependencies = Self::collect_dependencies(state, doc) |
153 | .filter_map(|mut import| { |
154 | let resolved_import = if let Some((path, _)) = state.borrow().tl.resolve_import_path(Some(&import.import_uri_token.clone().into()), &import.file) { |
155 | path.to_string_lossy().to_string() |
156 | } else { |
157 | import.file.clone() |
158 | }; |
159 | if resolved_import.ends_with(".slint" ) || resolved_import.ends_with(".60" ) || import.file.starts_with('@' ) { |
160 | Some(Box::pin(async move { |
161 | let file = import.file.as_str(); |
162 | let doc_path = Self::ensure_document_loaded( |
163 | state, |
164 | file, |
165 | Some(import.import_uri_token.clone().into()), |
166 | import_stack.clone() |
167 | ) |
168 | .await?; |
169 | let mut state = state.borrow_mut(); |
170 | let state = &mut *state; |
171 | let doc = state.tl.all_documents.docs.get(&doc_path).unwrap(); |
172 | match &import.import_kind { |
173 | ImportKind::ImportList(imported_types) => { |
174 | let mut imported_types = |
175 | ImportedName::extract_imported_names(imported_types).peekable(); |
176 | if imported_types.peek().is_some() { |
177 | Self::register_imported_types(doc, &import, imported_types, registry_to_populate, state.diag); |
178 | } else { |
179 | state.diag.push_error("Import names are missing. Please specify which types you would like to import" .into(), &import.import_uri_token.parent()); |
180 | } |
181 | None |
182 | } |
183 | ImportKind::ModuleReexport(export_module_syntax_node) => { |
184 | let mut exports = Exports::default(); |
185 | exports.add_reexports( |
186 | doc.exports.iter().map(|(exported_name, compo_or_type)| { |
187 | ( |
188 | ExportedName { |
189 | name: exported_name.name.clone(), |
190 | name_ident: (**export_module_syntax_node).clone(), |
191 | }, |
192 | compo_or_type.clone(), |
193 | ) |
194 | }), |
195 | state.diag, |
196 | ); |
197 | Some((exports, export_module_syntax_node.clone())) |
198 | } |
199 | } |
200 | })) |
201 | } else { |
202 | import.file = resolved_import; |
203 | foreign_imports.push(import); |
204 | None |
205 | } |
206 | }) |
207 | .collect::<Vec<_>>(); |
208 | |
209 | Box::pin(async move { |
210 | let mut reexports = None; |
211 | std::future::poll_fn(|cx| { |
212 | dependencies.retain_mut(|fut| { |
213 | let core::task::Poll::Ready(export) = fut.as_mut().poll(cx) else { |
214 | return true; |
215 | }; |
216 | let Some((exports, node)) = export else { return false }; |
217 | if reexports.is_none() { |
218 | reexports = Some(exports); |
219 | } else { |
220 | state.borrow_mut().diag.push_error( |
221 | "re-exporting modules is only allowed once per file" .into(), |
222 | &node, |
223 | ); |
224 | }; |
225 | false |
226 | }); |
227 | if dependencies.is_empty() { |
228 | core::task::Poll::Ready(()) |
229 | } else { |
230 | core::task::Poll::Pending |
231 | } |
232 | }) |
233 | .await; |
234 | (foreign_imports, reexports.unwrap_or_default()) |
235 | }) |
236 | } |
237 | |
238 | pub async fn import_component( |
239 | &mut self, |
240 | file_to_import: &str, |
241 | type_name: &str, |
242 | diag: &mut BuildDiagnostics, |
243 | ) -> Option<Rc<object_tree::Component>> { |
244 | let state = RefCell::new(BorrowedTypeLoader { tl: self, diag }); |
245 | let doc_path = |
246 | match Self::ensure_document_loaded(&state, file_to_import, None, Default::default()) |
247 | .await |
248 | { |
249 | Some(doc_path) => doc_path, |
250 | None => return None, |
251 | }; |
252 | |
253 | let doc = self.all_documents.docs.get(&doc_path).unwrap(); |
254 | |
255 | doc.exports.find(type_name).and_then(|compo_or_type| compo_or_type.left()) |
256 | } |
257 | |
258 | /// Append a possibly relative path to a base path. Returns the data if it resolves to a built-in (compiled-in) |
259 | /// file. |
260 | pub fn resolve_import_path( |
261 | &self, |
262 | import_token: Option<&NodeOrToken>, |
263 | maybe_relative_path_or_url: &str, |
264 | ) -> Option<(PathBuf, Option<&'static [u8]>)> { |
265 | if let Some(maybe_library_import) = maybe_relative_path_or_url.strip_prefix('@' ) { |
266 | self.find_file_in_library_path(maybe_library_import) |
267 | } else { |
268 | let referencing_file_or_url = |
269 | import_token.and_then(|tok| tok.source_file().map(|s| s.path())); |
270 | self.find_file_in_include_path(referencing_file_or_url, maybe_relative_path_or_url) |
271 | .or_else(|| { |
272 | referencing_file_or_url |
273 | .and_then(|base_path_or_url| { |
274 | crate::pathutils::join( |
275 | &crate::pathutils::dirname(base_path_or_url), |
276 | &PathBuf::from(maybe_relative_path_or_url), |
277 | ) |
278 | }) |
279 | .filter(|p| p.exists()) |
280 | .map(|p| (p, None)) |
281 | }) |
282 | } |
283 | } |
284 | |
285 | async fn ensure_document_loaded<'a: 'b, 'b>( |
286 | state: &'a RefCell<BorrowedTypeLoader<'a>>, |
287 | file_to_import: &'b str, |
288 | import_token: Option<NodeOrToken>, |
289 | mut import_stack: HashSet<PathBuf>, |
290 | ) -> Option<PathBuf> { |
291 | let mut borrowed_state = state.borrow_mut(); |
292 | |
293 | let (path_canon, builtin) = match borrowed_state |
294 | .tl |
295 | .resolve_import_path(import_token.as_ref(), file_to_import) |
296 | { |
297 | Some(x) => x, |
298 | None => { |
299 | let import_path = crate::pathutils::clean_path(Path::new(file_to_import)); |
300 | if import_path.exists() { |
301 | if import_token.as_ref().and_then(|x| x.source_file()).is_some() { |
302 | borrowed_state.diag.push_warning( |
303 | format!( |
304 | "Loading \"{file_to_import}\" relative to the work directory is deprecated. Files should be imported relative to their import location" , |
305 | ), |
306 | &import_token, |
307 | ); |
308 | } |
309 | (import_path, None) |
310 | } else { |
311 | // We will load using the `open_import_fallback` |
312 | // Simplify the path to remove the ".." |
313 | let base_path = import_token |
314 | .as_ref() |
315 | .and_then(|tok| tok.source_file().map(|s| s.path())) |
316 | .map_or(PathBuf::new(), |p| p.into()); |
317 | let path = crate::pathutils::join( |
318 | &crate::pathutils::dirname(&base_path), |
319 | Path::new(file_to_import), |
320 | )?; |
321 | (path, None) |
322 | } |
323 | } |
324 | }; |
325 | |
326 | if !import_stack.insert(path_canon.clone()) { |
327 | borrowed_state.diag.push_error( |
328 | format!("Recursive import of \"{}\"" , path_canon.display()), |
329 | &import_token, |
330 | ); |
331 | return None; |
332 | } |
333 | drop(borrowed_state); |
334 | |
335 | let is_loaded = core::future::poll_fn(|cx| { |
336 | let mut state = state.borrow_mut(); |
337 | let all_documents = &mut state.tl.all_documents; |
338 | match all_documents.currently_loading.entry(path_canon.clone()) { |
339 | std::collections::hash_map::Entry::Occupied(mut e) => { |
340 | let waker = cx.waker(); |
341 | if !e.get().iter().any(|w| w.will_wake(waker)) { |
342 | e.get_mut().push(cx.waker().clone()); |
343 | } |
344 | core::task::Poll::Pending |
345 | } |
346 | std::collections::hash_map::Entry::Vacant(v) => { |
347 | if all_documents.docs.contains_key(path_canon.as_path()) { |
348 | core::task::Poll::Ready(true) |
349 | } else { |
350 | v.insert(Default::default()); |
351 | core::task::Poll::Ready(false) |
352 | } |
353 | } |
354 | } |
355 | }) |
356 | .await; |
357 | if is_loaded { |
358 | return Some(path_canon); |
359 | } |
360 | |
361 | let source_code_result = if let Some(builtin) = builtin { |
362 | Ok(String::from( |
363 | core::str::from_utf8(builtin) |
364 | .expect("internal error: embedded file is not UTF-8 source code" ), |
365 | )) |
366 | } else if let Some(fallback) = { |
367 | let fallback = state.borrow().tl.compiler_config.open_import_fallback.clone(); |
368 | fallback |
369 | } { |
370 | let result = fallback(path_canon.to_string_lossy().into()).await; |
371 | result.unwrap_or_else(|| std::fs::read_to_string(&path_canon)) |
372 | } else { |
373 | std::fs::read_to_string(&path_canon) |
374 | }; |
375 | |
376 | let ok = match source_code_result { |
377 | Ok(source) => { |
378 | Self::load_file_impl( |
379 | state, |
380 | &path_canon, |
381 | None, |
382 | &path_canon, |
383 | source, |
384 | builtin.is_some(), |
385 | &import_stack, |
386 | ) |
387 | .await; |
388 | |
389 | true |
390 | } |
391 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => { |
392 | state.borrow_mut().diag.push_error( |
393 | if file_to_import.starts_with('@' ) { |
394 | format!( |
395 | "Cannot find requested import \"{file_to_import}\" in the library search path" , |
396 | ) |
397 | } else { |
398 | format!( |
399 | "Cannot find requested import \"{file_to_import}\" in the include search path" , |
400 | ) |
401 | }, |
402 | &import_token, |
403 | ); |
404 | false |
405 | } |
406 | Err(err) => { |
407 | state.borrow_mut().diag.push_error( |
408 | format!("Error reading requested import \"{}\": {}" , path_canon.display(), err), |
409 | &import_token, |
410 | ); |
411 | false |
412 | } |
413 | }; |
414 | |
415 | let wakers = state |
416 | .borrow_mut() |
417 | .tl |
418 | .all_documents |
419 | .currently_loading |
420 | .remove(path_canon.as_path()) |
421 | .unwrap(); |
422 | for x in wakers { |
423 | x.wake(); |
424 | } |
425 | |
426 | ok.then_some(path_canon) |
427 | } |
428 | |
429 | /// Load a file, and its dependency not run the passes. |
430 | /// |
431 | /// the path must be the canonical path |
432 | pub async fn load_file( |
433 | &mut self, |
434 | path: &Path, |
435 | version: SourceFileVersion, |
436 | source_path: &Path, |
437 | source_code: String, |
438 | is_builtin: bool, |
439 | diag: &mut BuildDiagnostics, |
440 | ) { |
441 | let state = RefCell::new(BorrowedTypeLoader { tl: self, diag }); |
442 | Self::load_file_impl( |
443 | &state, |
444 | path, |
445 | version, |
446 | source_path, |
447 | source_code, |
448 | is_builtin, |
449 | &Default::default(), |
450 | ) |
451 | .await; |
452 | } |
453 | |
454 | /// Load a file, and its dependency not run the passes. |
455 | /// |
456 | /// the path must be the canonical path |
457 | pub async fn load_root_file( |
458 | &mut self, |
459 | path: &Path, |
460 | version: SourceFileVersion, |
461 | source_path: &Path, |
462 | source_code: String, |
463 | diag: &mut BuildDiagnostics, |
464 | ) -> PathBuf { |
465 | let path = crate::pathutils::clean_path(path); |
466 | let state = RefCell::new(BorrowedTypeLoader { tl: self, diag }); |
467 | let (path, doc) = Self::load_file_no_pass( |
468 | &state, |
469 | &path, |
470 | version, |
471 | source_path, |
472 | source_code, |
473 | false, |
474 | &Default::default(), |
475 | ) |
476 | .await; |
477 | |
478 | let mut state = state.borrow_mut(); |
479 | let state = &mut *state; |
480 | if !state.diag.has_error() { |
481 | crate::passes::run_passes(&doc, state.tl, state.diag).await; |
482 | } |
483 | state.tl.all_documents.docs.insert(path.clone(), doc); |
484 | path |
485 | } |
486 | |
487 | async fn load_file_impl<'a>( |
488 | state: &'a RefCell<BorrowedTypeLoader<'a>>, |
489 | path: &Path, |
490 | version: SourceFileVersion, |
491 | source_path: &Path, |
492 | source_code: String, |
493 | is_builtin: bool, |
494 | import_stack: &HashSet<PathBuf>, |
495 | ) { |
496 | let (path, doc) = Self::load_file_no_pass( |
497 | state, |
498 | path, |
499 | version, |
500 | source_path, |
501 | source_code, |
502 | is_builtin, |
503 | import_stack, |
504 | ) |
505 | .await; |
506 | |
507 | let mut state = state.borrow_mut(); |
508 | let state = &mut *state; |
509 | if !state.diag.has_error() { |
510 | crate::passes::run_import_passes(&doc, state.tl, state.diag); |
511 | } |
512 | state.tl.all_documents.docs.insert(path, doc); |
513 | } |
514 | |
515 | async fn load_file_no_pass<'a>( |
516 | state: &'a RefCell<BorrowedTypeLoader<'a>>, |
517 | path: &Path, |
518 | version: SourceFileVersion, |
519 | source_path: &Path, |
520 | source_code: String, |
521 | is_builtin: bool, |
522 | import_stack: &HashSet<PathBuf>, |
523 | ) -> (PathBuf, Document) { |
524 | let dependency_doc: syntax_nodes::Document = |
525 | crate::parser::parse(source_code, Some(source_path), version, state.borrow_mut().diag) |
526 | .into(); |
527 | |
528 | let dependency_registry = |
529 | Rc::new(RefCell::new(TypeRegister::new(&state.borrow().tl.global_type_registry))); |
530 | dependency_registry.borrow_mut().expose_internal_types = is_builtin; |
531 | let (foreign_imports, reexports) = Self::load_dependencies_recursively_impl( |
532 | state, |
533 | &dependency_doc, |
534 | &dependency_registry, |
535 | import_stack, |
536 | ) |
537 | .await; |
538 | |
539 | if state.borrow().diag.has_error() { |
540 | // If there was error (esp parse error) we don't want to report further error in this document. |
541 | // because they might be nonsense (TODO: we should check that the parse error were really in this document). |
542 | // But we still want to create a document to give better error messages in the root document. |
543 | let mut ignore_diag = BuildDiagnostics::default(); |
544 | ignore_diag.push_error_with_span( |
545 | "Dummy error because some of the code asserts there was an error" .into(), |
546 | Default::default(), |
547 | ); |
548 | let doc = crate::object_tree::Document::from_node( |
549 | dependency_doc, |
550 | foreign_imports, |
551 | reexports, |
552 | &mut ignore_diag, |
553 | &dependency_registry, |
554 | ); |
555 | return (path.to_owned(), doc); |
556 | } |
557 | let mut state = state.borrow_mut(); |
558 | let state = &mut *state; |
559 | let doc = crate::object_tree::Document::from_node( |
560 | dependency_doc, |
561 | foreign_imports, |
562 | reexports, |
563 | state.diag, |
564 | &dependency_registry, |
565 | ); |
566 | (path.to_owned(), doc) |
567 | } |
568 | |
569 | fn register_imported_types( |
570 | doc: &Document, |
571 | import: &ImportedTypes, |
572 | imported_types: impl Iterator<Item = ImportedName>, |
573 | registry_to_populate: &Rc<RefCell<TypeRegister>>, |
574 | build_diagnostics: &mut BuildDiagnostics, |
575 | ) { |
576 | for import_name in imported_types { |
577 | let imported_type = doc.exports.find(&import_name.external_name); |
578 | |
579 | let imported_type = match imported_type { |
580 | Some(ty) => ty, |
581 | None => { |
582 | build_diagnostics.push_error( |
583 | format!( |
584 | "No exported type called ' {}' found in \"{}\"" , |
585 | import_name.external_name, import.file |
586 | ), |
587 | &import.import_uri_token, |
588 | ); |
589 | continue; |
590 | } |
591 | }; |
592 | |
593 | match imported_type { |
594 | itertools::Either::Left(c) => { |
595 | registry_to_populate.borrow_mut().add_with_name(import_name.internal_name, c) |
596 | } |
597 | itertools::Either::Right(ty) => registry_to_populate |
598 | .borrow_mut() |
599 | .insert_type_with_name(ty, import_name.internal_name), |
600 | } |
601 | } |
602 | } |
603 | |
604 | /// Lookup a library and filename and try to find the absolute filename based on the library path |
605 | fn find_file_in_library_path( |
606 | &self, |
607 | maybe_library_import: &str, |
608 | ) -> Option<(PathBuf, Option<&'static [u8]>)> { |
609 | let (library, file) = maybe_library_import |
610 | .splitn(2, '/' ) |
611 | .collect_tuple() |
612 | .map(|(library, path)| (library, Some(path))) |
613 | .unwrap_or((maybe_library_import, None)); |
614 | self.compiler_config.library_paths.get(library).and_then(|library_path| { |
615 | let path = match file { |
616 | // "@library/file.slint" -> "/path/to/library/" + "file.slint" |
617 | Some(file) => library_path.join(file), |
618 | // "@library" -> "/path/to/library/lib.slint" |
619 | None => library_path.clone(), |
620 | }; |
621 | crate::fileaccess::load_file(path.as_path()) |
622 | .map(|virtual_file| (virtual_file.canon_path, virtual_file.builtin_contents)) |
623 | }) |
624 | } |
625 | |
626 | /// Lookup a filename and try to find the absolute filename based on the include path or |
627 | /// the current file directory |
628 | pub fn find_file_in_include_path( |
629 | &self, |
630 | referencing_file: Option<&Path>, |
631 | file_to_import: &str, |
632 | ) -> Option<(PathBuf, Option<&'static [u8]>)> { |
633 | // The directory of the current file is the first in the list of include directories. |
634 | referencing_file |
635 | .map(base_directory) |
636 | .into_iter() |
637 | .chain(self.compiler_config.include_paths.iter().map(PathBuf::as_path).map( |
638 | |include_path| { |
639 | let base = referencing_file.map(Path::to_path_buf).unwrap_or_default(); |
640 | crate::pathutils::join(&crate::pathutils::dirname(&base), include_path) |
641 | .unwrap_or_else(|| include_path.to_path_buf()) |
642 | }, |
643 | )) |
644 | .chain( |
645 | (file_to_import == "std-widgets.slint" |
646 | || referencing_file.map_or(false, |x| x.starts_with("builtin:/" ))) |
647 | .then(|| format!("builtin:/ {}" , self.style).into()), |
648 | ) |
649 | .find_map(|include_dir| { |
650 | let candidate = crate::pathutils::join(&include_dir, Path::new(file_to_import))?; |
651 | crate::fileaccess::load_file(&candidate) |
652 | .map(|virtual_file| (virtual_file.canon_path, virtual_file.builtin_contents)) |
653 | }) |
654 | } |
655 | |
656 | fn collect_dependencies<'a: 'b, 'b>( |
657 | state: &'a RefCell<BorrowedTypeLoader<'a>>, |
658 | doc: &'b syntax_nodes::Document, |
659 | ) -> impl Iterator<Item = ImportedTypes> + 'a { |
660 | doc.ImportSpecifier() |
661 | .map(|import| { |
662 | let maybe_import_uri = import.child_token(SyntaxKind::StringLiteral); |
663 | (maybe_import_uri, ImportKind::ImportList(import)) |
664 | }) |
665 | .chain( |
666 | // process `export * from "foo"` |
667 | doc.ExportsList().flat_map(|exports| exports.ExportModule()).map(|reexport| { |
668 | let maybe_import_uri = reexport.child_token(SyntaxKind::StringLiteral); |
669 | (maybe_import_uri, ImportKind::ModuleReexport(reexport)) |
670 | }), |
671 | ) |
672 | .filter_map(|(maybe_import_uri, type_specifier)| { |
673 | let import_uri = match maybe_import_uri { |
674 | Some(import_uri) => import_uri, |
675 | None => { |
676 | debug_assert!(state.borrow().diag.has_error()); |
677 | return None; |
678 | } |
679 | }; |
680 | let path_to_import = import_uri.text().to_string(); |
681 | let path_to_import = path_to_import.trim_matches(' \"' ).to_string(); |
682 | |
683 | if path_to_import.is_empty() { |
684 | state |
685 | .borrow_mut() |
686 | .diag |
687 | .push_error("Unexpected empty import url" .to_owned(), &import_uri); |
688 | return None; |
689 | } |
690 | |
691 | Some(ImportedTypes { |
692 | import_uri_token: import_uri, |
693 | import_kind: type_specifier, |
694 | file: path_to_import, |
695 | }) |
696 | }) |
697 | } |
698 | |
699 | /// Return a document if it was already loaded |
700 | pub fn get_document<'b>(&'b self, path: &Path) -> Option<&'b object_tree::Document> { |
701 | let path = crate::pathutils::clean_path(path); |
702 | self.all_documents.docs.get(&path) |
703 | } |
704 | |
705 | /// Return an iterator over all the loaded file path |
706 | pub fn all_files(&self) -> impl Iterator<Item = &PathBuf> { |
707 | self.all_documents.docs.keys() |
708 | } |
709 | |
710 | /// Returns an iterator over all the loaded documents |
711 | pub fn all_documents(&self) -> impl Iterator<Item = &object_tree::Document> + '_ { |
712 | self.all_documents.docs.values() |
713 | } |
714 | |
715 | /// Returns an iterator over all the loaded documents |
716 | pub fn all_file_documents( |
717 | &self, |
718 | ) -> impl Iterator<Item = (&PathBuf, &object_tree::Document)> + '_ { |
719 | self.all_documents.docs.iter() |
720 | } |
721 | } |
722 | |
723 | fn get_native_style(all_loaded_files: &mut Vec<PathBuf>) -> String { |
724 | // Try to get the value written by the i-slint-backend-selector's build script |
725 | |
726 | // It is in the target/xxx/build directory |
727 | let target_path = std::env::var_os("OUT_DIR" ) |
728 | .and_then(|path| { |
729 | // Same logic as in i-slint-backend-selector's build script to get the path |
730 | crate::pathutils::join(Path::new(&path), Path::new("../../SLINT_DEFAULT_STYLE.txt" )) |
731 | }) |
732 | .or_else(|| { |
733 | // When we are called from a slint!, OUT_DIR is only defined when the crate having the macro has a build.rs script. |
734 | // As a fallback, try to parse the rustc arguments |
735 | // https://stackoverflow.com/questions/60264534/getting-the-target-folder-from-inside-a-rust-proc-macro |
736 | let mut args = std::env::args(); |
737 | let mut out_dir = None; |
738 | while let Some(arg) = args.next() { |
739 | if arg == "--out-dir" { |
740 | out_dir = args.next(); |
741 | break; |
742 | } |
743 | } |
744 | out_dir.and_then(|od| { |
745 | crate::pathutils::join( |
746 | Path::new(&od), |
747 | Path::new("../build/SLINT_DEFAULT_STYLE.txt" ), |
748 | ) |
749 | }) |
750 | }); |
751 | if let Some(style) = target_path.and_then(|target_path| { |
752 | all_loaded_files.push(target_path.clone()); |
753 | std::fs::read_to_string(target_path).map(|style| style.trim().into()).ok() |
754 | }) { |
755 | return style; |
756 | } |
757 | i_slint_common::get_native_style(false, &std::env::var("TARGET" ).unwrap_or_default()).into() |
758 | } |
759 | |
760 | /// return the base directory from which imports are loaded |
761 | /// |
762 | /// For a .slint file, this is the parent directory. |
763 | /// For a .rs file, this is relative to the CARGO_MANIFEST_DIR |
764 | /// |
765 | /// Note: this function is only called for .rs path as part of the LSP or viewer. |
766 | /// Because from a proc_macro, we don't actually know the path of the current file, and this |
767 | /// is why we must be relative to CARGO_MANIFEST_DIR. |
768 | pub fn base_directory(referencing_file: &Path) -> PathBuf { |
769 | if referencing_file.extension().map_or(false, |e| e == "rs" ) { |
770 | // For .rs file, this is a rust macro, and rust macro locates the file relative to the CARGO_MANIFEST_DIR which is the directory that has a Cargo.toml file. |
771 | let mut candidate = referencing_file; |
772 | loop { |
773 | candidate = |
774 | if let Some(c) = candidate.parent() { c } else { break referencing_file.parent() }; |
775 | |
776 | if candidate.join("Cargo.toml" ).exists() { |
777 | break Some(candidate); |
778 | } |
779 | } |
780 | } else { |
781 | referencing_file.parent() |
782 | } |
783 | .map_or_else(Default::default, |p: &Path| p.to_path_buf()) |
784 | } |
785 | |
786 | #[test ] |
787 | fn test_dependency_loading() { |
788 | let test_source_path: PathBuf = |
789 | [env!("CARGO_MANIFEST_DIR" ), "tests" , "typeloader" ].iter().collect(); |
790 | |
791 | let mut incdir = test_source_path.clone(); |
792 | incdir.push("incpath" ); |
793 | |
794 | let mut compiler_config = |
795 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
796 | compiler_config.include_paths = vec![incdir]; |
797 | compiler_config.library_paths = |
798 | HashMap::from([("library" .into(), test_source_path.join("library" ).join("lib.slint" ))]); |
799 | compiler_config.style = Some("fluent" .into()); |
800 | |
801 | let mut main_test_path = test_source_path; |
802 | main_test_path.push("dependency_test_main.slint" ); |
803 | |
804 | let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); |
805 | let doc_node = crate::parser::parse_file(main_test_path, &mut test_diags).unwrap(); |
806 | |
807 | let doc_node: syntax_nodes::Document = doc_node.into(); |
808 | |
809 | let global_registry = TypeRegister::builtin(); |
810 | |
811 | let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); |
812 | |
813 | let mut build_diagnostics = BuildDiagnostics::default(); |
814 | |
815 | let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); |
816 | |
817 | let (foreign_imports, _) = spin_on::spin_on(loader.load_dependencies_recursively( |
818 | &doc_node, |
819 | &mut build_diagnostics, |
820 | ®istry, |
821 | )); |
822 | |
823 | assert!(!test_diags.has_error()); |
824 | assert!(!build_diagnostics.has_error()); |
825 | assert!(foreign_imports.is_empty()); |
826 | } |
827 | |
828 | #[test ] |
829 | fn test_dependency_loading_from_rust() { |
830 | let test_source_path: PathBuf = |
831 | [env!("CARGO_MANIFEST_DIR" ), "tests" , "typeloader" ].iter().collect(); |
832 | |
833 | let mut incdir = test_source_path.clone(); |
834 | incdir.push("incpath" ); |
835 | |
836 | let mut compiler_config = |
837 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
838 | compiler_config.include_paths = vec![incdir]; |
839 | compiler_config.library_paths = |
840 | HashMap::from([("library" .into(), test_source_path.join("library" ).join("lib.slint" ))]); |
841 | compiler_config.style = Some("fluent" .into()); |
842 | |
843 | let mut main_test_path = test_source_path; |
844 | main_test_path.push("some_rust_file.rs" ); |
845 | |
846 | let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); |
847 | let doc_node = crate::parser::parse_file(main_test_path, &mut test_diags).unwrap(); |
848 | |
849 | let doc_node: syntax_nodes::Document = doc_node.into(); |
850 | |
851 | let global_registry = TypeRegister::builtin(); |
852 | |
853 | let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); |
854 | |
855 | let mut build_diagnostics = BuildDiagnostics::default(); |
856 | |
857 | let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); |
858 | |
859 | let (foreign_imports, _) = spin_on::spin_on(loader.load_dependencies_recursively( |
860 | &doc_node, |
861 | &mut build_diagnostics, |
862 | ®istry, |
863 | )); |
864 | |
865 | assert!(!test_diags.has_error()); |
866 | assert!(test_diags.is_empty()); // also no warnings |
867 | assert!(!build_diagnostics.has_error()); |
868 | assert!(build_diagnostics.is_empty()); // also no warnings |
869 | assert!(foreign_imports.is_empty()); |
870 | } |
871 | |
872 | #[test ] |
873 | fn test_load_from_callback_ok() { |
874 | let ok = Rc::new(core::cell::Cell::new(false)); |
875 | let ok_ = ok.clone(); |
876 | |
877 | let mut compiler_config = |
878 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
879 | compiler_config.style = Some("fluent" .into()); |
880 | compiler_config.open_import_fallback = Some(Rc::new(move |path| { |
881 | let ok_ = ok_.clone(); |
882 | Box::pin(async move { |
883 | assert_eq!(path.replace(' \\' , "/" ), "../FooBar.slint" ); |
884 | assert!(!ok_.get()); |
885 | ok_.set(true); |
886 | Some(Ok("export XX := Rectangle {} " .to_owned())) |
887 | }) |
888 | })); |
889 | |
890 | let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); |
891 | let doc_node = crate::parser::parse( |
892 | r#" |
893 | /* ... */ |
894 | import { XX } from "../Ab/.././FooBar.slint"; |
895 | X := XX {} |
896 | "# |
897 | .into(), |
898 | Some(std::path::Path::new("HELLO" )), |
899 | None, |
900 | &mut test_diags, |
901 | ); |
902 | |
903 | let doc_node: syntax_nodes::Document = doc_node.into(); |
904 | let global_registry = TypeRegister::builtin(); |
905 | let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); |
906 | let mut build_diagnostics = BuildDiagnostics::default(); |
907 | let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); |
908 | spin_on::spin_on(loader.load_dependencies_recursively( |
909 | &doc_node, |
910 | &mut build_diagnostics, |
911 | ®istry, |
912 | )); |
913 | assert!(ok.get()); |
914 | assert!(!test_diags.has_error()); |
915 | assert!(!build_diagnostics.has_error()); |
916 | } |
917 | |
918 | #[test ] |
919 | fn test_load_error_twice() { |
920 | let mut compiler_config = |
921 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
922 | compiler_config.style = Some("fluent" .into()); |
923 | let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); |
924 | |
925 | let doc_node = crate::parser::parse( |
926 | r#" |
927 | /* ... */ |
928 | import { XX } from "error.slint"; |
929 | component Foo { XX {} } |
930 | "# |
931 | .into(), |
932 | Some(std::path::Path::new("HELLO" )), |
933 | None, |
934 | &mut test_diags, |
935 | ); |
936 | |
937 | let doc_node: syntax_nodes::Document = doc_node.into(); |
938 | let global_registry = TypeRegister::builtin(); |
939 | let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); |
940 | let mut build_diagnostics = BuildDiagnostics::default(); |
941 | let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); |
942 | spin_on::spin_on(loader.load_dependencies_recursively( |
943 | &doc_node, |
944 | &mut build_diagnostics, |
945 | ®istry, |
946 | )); |
947 | assert!(!test_diags.has_error()); |
948 | assert!(build_diagnostics.has_error()); |
949 | let diags = build_diagnostics.to_string_vec(); |
950 | assert_eq!( |
951 | diags, |
952 | &["HELLO:3: Cannot find requested import \"error.slint \" in the include search path" ] |
953 | ); |
954 | // Try loading another time with the same registry |
955 | let mut build_diagnostics = BuildDiagnostics::default(); |
956 | spin_on::spin_on(loader.load_dependencies_recursively( |
957 | &doc_node, |
958 | &mut build_diagnostics, |
959 | ®istry, |
960 | )); |
961 | assert!(build_diagnostics.has_error()); |
962 | let diags = build_diagnostics.to_string_vec(); |
963 | assert_eq!( |
964 | diags, |
965 | &["HELLO:3: Cannot find requested import \"error.slint \" in the include search path" ] |
966 | ); |
967 | } |
968 | |
969 | #[test ] |
970 | fn test_manual_import() { |
971 | let mut compiler_config: CompilerConfiguration = |
972 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
973 | compiler_config.style = Some("fluent" .into()); |
974 | let global_registry: Rc> = TypeRegister::builtin(); |
975 | let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default(); |
976 | let mut loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics); |
977 | |
978 | let maybe_button_type: Option> = spin_on::spin_on(future:loader.import_component( |
979 | file_to_import:"std-widgets.slint" , |
980 | type_name:"Button" , |
981 | &mut build_diagnostics, |
982 | )); |
983 | |
984 | assert!(!build_diagnostics.has_error()); |
985 | assert!(maybe_button_type.is_some()); |
986 | } |
987 | |
988 | #[test ] |
989 | fn test_builtin_style() { |
990 | let test_source_path: PathBuf = |
991 | [env!("CARGO_MANIFEST_DIR" ), "tests" , "typeloader" ].iter().collect(); |
992 | |
993 | let incdir: PathBuf = test_source_path.join(path:"custom_style" ); |
994 | |
995 | let mut compiler_config: CompilerConfiguration = |
996 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
997 | compiler_config.include_paths = vec![incdir]; |
998 | compiler_config.style = Some("fluent" .into()); |
999 | |
1000 | let global_registry: Rc> = TypeRegister::builtin(); |
1001 | let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default(); |
1002 | let _loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics); |
1003 | |
1004 | assert!(!build_diagnostics.has_error()); |
1005 | } |
1006 | |
1007 | #[test ] |
1008 | fn test_user_style() { |
1009 | let test_source_path: PathBuf = |
1010 | [env!("CARGO_MANIFEST_DIR" ), "tests" , "typeloader" ].iter().collect(); |
1011 | |
1012 | let incdir: PathBuf = test_source_path.join(path:"custom_style" ); |
1013 | |
1014 | let mut compiler_config: CompilerConfiguration = |
1015 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
1016 | compiler_config.include_paths = vec![incdir]; |
1017 | compiler_config.style = Some("TestStyle" .into()); |
1018 | |
1019 | let global_registry: Rc> = TypeRegister::builtin(); |
1020 | let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default(); |
1021 | let _loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics); |
1022 | |
1023 | assert!(!build_diagnostics.has_error()); |
1024 | } |
1025 | |
1026 | #[test ] |
1027 | fn test_unknown_style() { |
1028 | let test_source_path: PathBuf = |
1029 | [env!("CARGO_MANIFEST_DIR" ), "tests" , "typeloader" ].iter().collect(); |
1030 | |
1031 | let incdir: PathBuf = test_source_path.join(path:"custom_style" ); |
1032 | |
1033 | let mut compiler_config: CompilerConfiguration = |
1034 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
1035 | compiler_config.include_paths = vec![incdir]; |
1036 | compiler_config.style = Some("FooBar" .into()); |
1037 | |
1038 | let global_registry: Rc> = TypeRegister::builtin(); |
1039 | let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default(); |
1040 | let _loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics); |
1041 | |
1042 | assert!(build_diagnostics.has_error()); |
1043 | let diags: Vec = build_diagnostics.to_string_vec(); |
1044 | assert_eq!(diags.len(), 1); |
1045 | assert!(diags[0].starts_with("Style FooBar in not known. Use one of the builtin styles [" )); |
1046 | } |
1047 | |
1048 | #[test ] |
1049 | fn test_library_import() { |
1050 | let test_source_path: PathBuf = |
1051 | [env!("CARGO_MANIFEST_DIR" ), "tests" , "typeloader" , "library" ].iter().collect(); |
1052 | |
1053 | let library_paths = HashMap::from([ |
1054 | ("libdir" .into(), test_source_path.clone()), |
1055 | ("libfile.slint" .into(), test_source_path.join("lib.slint" )), |
1056 | ]); |
1057 | |
1058 | let mut compiler_config = |
1059 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
1060 | compiler_config.library_paths = library_paths; |
1061 | compiler_config.style = Some("fluent" .into()); |
1062 | let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); |
1063 | |
1064 | let doc_node = crate::parser::parse( |
1065 | r#" |
1066 | /* ... */ |
1067 | import { LibraryType } from "@libfile.slint"; |
1068 | import { LibraryHelperType } from "@libdir/library_helper_type.slint"; |
1069 | "# |
1070 | .into(), |
1071 | Some(std::path::Path::new("HELLO" )), |
1072 | None, |
1073 | &mut test_diags, |
1074 | ); |
1075 | |
1076 | let doc_node: syntax_nodes::Document = doc_node.into(); |
1077 | let global_registry = TypeRegister::builtin(); |
1078 | let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); |
1079 | let mut build_diagnostics = BuildDiagnostics::default(); |
1080 | let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); |
1081 | spin_on::spin_on(loader.load_dependencies_recursively( |
1082 | &doc_node, |
1083 | &mut build_diagnostics, |
1084 | ®istry, |
1085 | )); |
1086 | assert!(!test_diags.has_error()); |
1087 | assert!(!build_diagnostics.has_error()); |
1088 | } |
1089 | |
1090 | #[test ] |
1091 | fn test_library_import_errors() { |
1092 | let test_source_path: PathBuf = |
1093 | [env!("CARGO_MANIFEST_DIR" ), "tests" , "typeloader" , "library" ].iter().collect(); |
1094 | |
1095 | let library_paths = HashMap::from([ |
1096 | ("libdir" .into(), test_source_path.clone()), |
1097 | ("libfile.slint" .into(), test_source_path.join("lib.slint" )), |
1098 | ]); |
1099 | |
1100 | let mut compiler_config = |
1101 | CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); |
1102 | compiler_config.library_paths = library_paths; |
1103 | compiler_config.style = Some("fluent" .into()); |
1104 | let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); |
1105 | |
1106 | let doc_node = crate::parser::parse( |
1107 | r#" |
1108 | /* ... */ |
1109 | import { A } from "@libdir"; |
1110 | import { B } from "@libdir/unknown.slint"; |
1111 | import { C } from "@libfile.slint/unknown.slint"; |
1112 | import { D } from "@unknown"; |
1113 | import { E } from "@unknown/lib.slint"; |
1114 | "# |
1115 | .into(), |
1116 | Some(std::path::Path::new("HELLO" )), |
1117 | None, |
1118 | &mut test_diags, |
1119 | ); |
1120 | |
1121 | let doc_node: syntax_nodes::Document = doc_node.into(); |
1122 | let global_registry = TypeRegister::builtin(); |
1123 | let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); |
1124 | let mut build_diagnostics = BuildDiagnostics::default(); |
1125 | let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); |
1126 | spin_on::spin_on(loader.load_dependencies_recursively( |
1127 | &doc_node, |
1128 | &mut build_diagnostics, |
1129 | ®istry, |
1130 | )); |
1131 | assert!(!test_diags.has_error()); |
1132 | assert!(build_diagnostics.has_error()); |
1133 | let diags = build_diagnostics.to_string_vec(); |
1134 | assert_eq!(diags.len(), 5); |
1135 | assert!(diags[0].starts_with(&format!( |
1136 | "HELLO:3: Error reading requested import \"{}\": " , |
1137 | test_source_path.to_string_lossy() |
1138 | ))); |
1139 | assert_eq!(&diags[1], "HELLO:4: Cannot find requested import \"@libdir/unknown.slint \" in the library search path" ); |
1140 | assert_eq!(&diags[2], "HELLO:5: Cannot find requested import \"@libfile.slint/unknown.slint \" in the library search path" ); |
1141 | assert_eq!( |
1142 | &diags[3], |
1143 | "HELLO:6: Cannot find requested import \"@unknown \" in the library search path" |
1144 | ); |
1145 | assert_eq!( |
1146 | &diags[4], |
1147 | "HELLO:7: Cannot find requested import \"@unknown/lib.slint \" in the library search path" |
1148 | ); |
1149 | } |
1150 | |