Allow specifying paths for @library imports

This commit is contained in:
J-P Nurmi 2023-10-18 14:51:22 +02:00 committed by Simon Hausmann
parent df544fe1c9
commit c5248c005e
32 changed files with 485 additions and 56 deletions

View file

@ -26,6 +26,7 @@ i-slint-core = { workspace = true, features = ["default"] }
slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] }
spin_on = "0.1"
css-color-parser2 = { workspace = true }
itertools = { workspace = true }
[build-dependencies]
napi-build = "2.0.1"

View file

@ -5,6 +5,7 @@ use std::path::PathBuf;
use super::JsComponentDefinition;
use super::JsDiagnostic;
use itertools::Itertools;
use slint_interpreter::ComponentCompiler;
/// ComponentCompiler is the entry point to the Slint interpreter that can be used
@ -26,8 +27,22 @@ impl JsComponentCompiler {
}
None => vec![],
};
let library_paths = match std::env::var_os("SLINT_LIBRARY_PATH") {
Some(paths) => std::env::split_paths(&paths)
.filter_map(|entry| {
entry
.to_str()
.unwrap_or_default()
.split('=')
.collect_tuple()
.map(|(k, v)| (k.into(), v.into()))
})
.collect(),
None => std::collections::HashMap::new(),
};
compiler.set_include_paths(include_paths);
compiler.set_library_paths(library_paths);
Self { internal: compiler }
}

View file

@ -28,6 +28,7 @@ vtable = { version = "0.1.6", path="../../../helper_crates/vtable" }
css-color-parser2 = { workspace = true }
generativity = "1"
itertools = { workspace = true }
neon = "0.8.0"
once_cell = "1.5"
rand = "0.8"

View file

@ -6,6 +6,7 @@ use i_slint_compiler::langtype::Type;
use i_slint_core::model::{Model, ModelRc};
use i_slint_core::window::WindowInner;
use i_slint_core::{ImageInner, SharedVector};
use itertools::Itertools;
use neon::prelude::*;
use rand::RngCore;
use slint_interpreter::ComponentHandle;
@ -59,8 +60,22 @@ fn load(mut cx: FunctionContext) -> JsResult<JsValue> {
}
None => vec![],
};
let library_paths = match std::env::var_os("SLINT_LIBRARY_PATH") {
Some(paths) => std::env::split_paths(&paths)
.filter_map(|entry| {
entry
.to_str()
.unwrap_or_default()
.split('=')
.collect_tuple()
.map(|(k, v)| (k.into(), v.into()))
})
.collect(),
None => std::collections::HashMap::new(),
};
let mut compiler = slint_interpreter::ComponentCompiler::default();
compiler.set_include_paths(include_paths);
compiler.set_library_paths(library_paths);
let c = spin_on::spin_on(compiler.build_from_path(path));
slint_interpreter::print_diagnostics(compiler.diagnostics());

View file

@ -50,6 +50,7 @@ compile_error!(
forward compatibility with future version of this crate"
);
use std::collections::HashMap;
use std::env;
use std::io::Write;
use std::path::Path;
@ -101,6 +102,39 @@ impl CompilerConfiguration {
Self { config }
}
/// Create a new configuration that sets the library paths used for looking up
/// `@library` imports to the specified map of paths.
///
/// Each library path can either be a path to a `.slint` file or a directory.
/// If it's a file, the library is imported by its name prefixed by `@` (e.g.
/// `@example`). The specified file is the only entry-point for the library
/// and other files from the library won't be accessible from the outside.
/// If it's a directory, a specific file in that directory must be specified
/// when importing the library (e.g. `@example/widgets.slint`). This allows
/// exposing multiple entry-points for a single library.
///
/// Compile `ui/main.slint` and specify an "example" library path:
/// ```rust,no_run
/// let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
/// let library_paths = std::collections::HashMap::from([(
/// "example".to_string(),
/// manifest_dir.join("third_party/example/ui/lib.slint"),
/// )]);
/// let config = slint_build::CompilerConfiguration::new().with_library_paths(library_paths);
/// slint_build::compile_with_config("ui/main.slint", config).unwrap();
/// ```
///
/// Import the "example" library in `ui/main.slint`:
/// ```slint,ignore
/// import { Example } from "@example";
/// ```
#[must_use]
pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
let mut config = self.config;
config.library_paths = library_paths;
Self { config }
}
/// Create a new configuration that selects the style to be used for widgets.
#[must_use]
pub fn with_style(self, style: String) -> Self {

View file

@ -8,6 +8,8 @@
extern crate proc_macro;
use std::collections::HashMap;
use i_slint_compiler::diagnostics::BuildDiagnostics;
use i_slint_compiler::parser::SyntaxKind;
use i_slint_compiler::*;
@ -257,10 +259,24 @@ fn fill_token_vec(stream: impl Iterator<Item = TokenTree>, vec: &mut Vec<parser:
}
}
fn extract_include_paths(
fn extract_path(literal: proc_macro::Literal) -> std::path::PathBuf {
let path_with_quotes = literal.to_string();
let path_with_quotes_stripped = if let Some(p) = path_with_quotes.strip_prefix('r') {
let hash_removed = p.trim_matches('#');
hash_removed.strip_prefix('\"').unwrap().strip_suffix('\"').unwrap()
} else {
// FIXME: unescape
path_with_quotes.trim_matches('\"')
};
path_with_quotes_stripped.into()
}
fn extract_include_and_library_paths(
mut stream: proc_macro::token_stream::IntoIter,
) -> (impl Iterator<Item = TokenTree>, Vec<std::path::PathBuf>) {
) -> (impl Iterator<Item = TokenTree>, Vec<std::path::PathBuf>, HashMap<String, std::path::PathBuf>)
{
let mut include_paths = Vec::new();
let mut library_paths = HashMap::new();
let mut remaining_stream;
loop {
@ -270,24 +286,36 @@ fn extract_include_paths(
if p.as_char() == '#' && group.delimiter() == proc_macro::Delimiter::Bracket =>
{
let mut attr_stream = group.stream().into_iter();
match (attr_stream.next(), attr_stream.next(), attr_stream.next()) {
(
Some(TokenTree::Ident(include_ident)),
Some(TokenTree::Punct(equal_punct)),
Some(TokenTree::Literal(path)),
) if include_ident.to_string() == "include_path"
&& equal_punct.as_char() == '=' =>
match attr_stream.next() {
Some(TokenTree::Ident(include_ident))
if include_ident.to_string() == "include_path" =>
{
let path_with_quotes = path.to_string();
let path_with_quotes_stripped =
if let Some(p) = path_with_quotes.strip_prefix('r') {
let hash_removed = p.trim_matches('#');
hash_removed.strip_prefix('\"').unwrap().strip_suffix('\"').unwrap()
} else {
// FIXME: unescape
path_with_quotes.trim_matches('\"')
};
include_paths.push(path_with_quotes_stripped.into());
match (attr_stream.next(), attr_stream.next()) {
(
Some(TokenTree::Punct(equal_punct)),
Some(TokenTree::Literal(path)),
) if equal_punct.as_char() == '=' => {
include_paths.push(extract_path(path));
}
_ => break,
}
}
Some(TokenTree::Ident(library_ident))
if library_ident.to_string() == "library_path" =>
{
match (attr_stream.next(), attr_stream.next(), attr_stream.next()) {
(
Some(TokenTree::Group(group)),
Some(TokenTree::Punct(equal_punct)),
Some(TokenTree::Literal(path)),
) if group.delimiter() == proc_macro::Delimiter::Parenthesis
&& equal_punct.as_char() == '=' =>
{
let library_name = group.stream().into_iter().next().unwrap();
library_paths.insert(library_name.to_string(), extract_path(path));
}
_ => break,
}
}
_ => break,
}
@ -295,7 +323,7 @@ fn extract_include_paths(
_ => break,
}
}
(remaining_stream, include_paths)
(remaining_stream, include_paths, library_paths)
}
/// This macro allows you to use the Slint design markup language inline in Rust code. Within the braces of the macro
@ -315,7 +343,7 @@ fn extract_include_paths(
pub fn slint(stream: TokenStream) -> TokenStream {
let token_iter = stream.into_iter();
let (token_iter, include_paths) = extract_include_paths(token_iter);
let (token_iter, include_paths, library_paths) = extract_include_and_library_paths(token_iter);
let mut tokens = vec![];
fill_token_vec(token_iter, &mut tokens);
@ -338,6 +366,7 @@ pub fn slint(stream: TokenStream) -> TokenStream {
CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust);
compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
compiler_config.include_paths = include_paths;
compiler_config.library_paths = library_paths;
let (root_component, diag) =
spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));
//println!("{:#?}", tree);

View file

@ -113,6 +113,15 @@
"type": "string"
},
"description": "List of paths in which the `import` statement and `@image-url` are looked up"
},
"slint.libraryPaths": {
"type": "object",
"patternProperties": {
"^[a-zA-Z][a-zA-Z0-9-_]*$": {
"type": "string"
}
},
"description": "Map of paths in which the `import` statement for `@library` imports are looked up"
}
}
},

View file

@ -12,6 +12,7 @@ extern crate proc_macro;
use core::future::Future;
use core::pin::Pin;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
pub mod builtin_macros;
@ -56,6 +57,8 @@ pub struct CompilerConfiguration {
pub embed_resources: EmbedResourcesKind,
/// The compiler will look in these paths for components used in the file to compile.
pub include_paths: Vec<std::path::PathBuf>,
/// The compiler will look in these paths for library imports.
pub library_paths: HashMap<String, std::path::PathBuf>,
/// the name of the style. (eg: "native")
pub style: Option<String>,
@ -140,6 +143,7 @@ impl CompilerConfiguration {
Self {
embed_resources,
include_paths: Default::default(),
library_paths: Default::default(),
style: Default::default(),
open_import_fallback: None,
resource_url_mapper: None,

View file

@ -3,8 +3,10 @@
import { SubType } from "./dependency_local.slint";
import { AnotherType } from "dependency_from_incpath.slint";
import { LibraryType } from "@library";
export Main := Rectangle {
SubType {}
AnotherType {}
LibraryType {}
}

View file

@ -0,0 +1,4 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
export component LibraryType inherits Rectangle {}

View file

@ -0,0 +1,5 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import { LibraryType } from "./dependency_from_library.slint";
export { LibraryType }

View file

@ -0,0 +1,4 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
export component LibraryHelperType inherits Rectangle {}

View file

@ -4,9 +4,11 @@
slint!(
import { SubType } from "./tests/typeloader/dependency_local.slint";
import { AnotherType } from "dependency_from_incpath.slint";
import { LibraryType } from "@library";
export component Main {
SubType {}
AnotherType {}
LibraryType {}
}
);

View file

@ -13,6 +13,7 @@ use crate::typeregister::TypeRegister;
use crate::CompilerConfiguration;
use crate::{fileaccess, parser};
use core::future::Future;
use itertools::Itertools;
/// Storage for a cache of all loaded documents
#[derive(Default)]
@ -149,7 +150,12 @@ impl TypeLoader {
let mut foreign_imports = vec![];
let mut dependencies = Self::collect_dependencies(state, doc)
.filter_map(|mut import| {
if import.file.ends_with(".60") || import.file.ends_with(".slint") {
let resolved_import = if let Some((path, _)) = state.borrow().tl.resolve_import_path(Some(&import.import_uri_token.clone().into()), &import.file) {
path.to_string_lossy().to_string()
} else {
import.file.clone()
};
if resolved_import.ends_with(".slint") || resolved_import.ends_with(".60") || import.file.starts_with('@') {
Some(Box::pin(async move {
let file = import.file.as_str();
let doc_path = Self::ensure_document_loaded(
@ -192,10 +198,7 @@ impl TypeLoader {
}
}))
} else {
if let Some((path, _)) = state.borrow().tl.resolve_import_path(Some(&import.import_uri_token.clone().into()), &import.file) {
import.file = path.to_string_lossy().to_string();
};
import.file = resolved_import;
foreign_imports.push(import);
None
}
@ -258,22 +261,24 @@ impl TypeLoader {
import_token: Option<&NodeOrToken>,
maybe_relative_path_or_url: &str,
) -> Option<(PathBuf, Option<&'static [u8]>)> {
let referencing_file_or_url =
import_token.and_then(|tok| tok.source_file().map(|s| s.path()));
self.find_file_in_include_path(referencing_file_or_url, maybe_relative_path_or_url).or_else(
|| {
referencing_file_or_url
.and_then(|base_path_or_url| {
crate::pathutils::join(
&crate::pathutils::dirname(base_path_or_url),
&PathBuf::from(maybe_relative_path_or_url),
)
})
.filter(|p| p.exists())
.map(|p| (p, None))
},
)
if let Some(maybe_library_import) = maybe_relative_path_or_url.strip_prefix('@') {
self.find_file_in_library_path(maybe_library_import)
} else {
let referencing_file_or_url =
import_token.and_then(|tok| tok.source_file().map(|s| s.path()));
self.find_file_in_include_path(referencing_file_or_url, maybe_relative_path_or_url)
.or_else(|| {
referencing_file_or_url
.and_then(|base_path_or_url| {
crate::pathutils::join(
&crate::pathutils::dirname(base_path_or_url),
&PathBuf::from(maybe_relative_path_or_url),
)
})
.filter(|p| p.exists())
.map(|p| (p, None))
})
}
}
async fn ensure_document_loaded<'a: 'b, 'b>(
@ -384,9 +389,15 @@ impl TypeLoader {
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
state.borrow_mut().diag.push_error(
format!(
"Cannot find requested import \"{file_to_import}\" in the include search path",
),
if file_to_import.starts_with('@') {
format!(
"Cannot find requested import \"{file_to_import}\" in the library search path",
)
} else {
format!(
"Cannot find requested import \"{file_to_import}\" in the include search path",
)
},
&import_token,
);
false
@ -526,6 +537,28 @@ impl TypeLoader {
}
}
/// Lookup a library and filename and try to find the absolute filename based on the library path
fn find_file_in_library_path(
&self,
maybe_library_import: &str,
) -> Option<(PathBuf, Option<&'static [u8]>)> {
let (library, file) = maybe_library_import
.splitn(2, '/')
.collect_tuple()
.map(|(library, path)| (library, Some(path)))
.unwrap_or((maybe_library_import, None));
self.compiler_config.library_paths.get(library).and_then(|library_path| {
let path = match file {
// "@library/file.slint" -> "/path/to/library/" + "file.slint"
Some(file) => library_path.join(file),
// "@library" -> "/path/to/library/lib.slint"
None => library_path.clone(),
};
crate::fileaccess::load_file(path.as_path())
.map(|virtual_file| (virtual_file.canon_path, virtual_file.builtin_contents))
})
}
/// Lookup a filename and try to find the absolute filename based on the include path or
/// the current file directory
pub fn find_file_in_include_path(
@ -693,6 +726,8 @@ fn test_dependency_loading() {
let mut compiler_config =
CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
compiler_config.include_paths = vec![incdir];
compiler_config.library_paths =
HashMap::from([("library".into(), test_source_path.join("library").join("lib.slint"))]);
compiler_config.style = Some("fluent".into());
let mut main_test_path = test_source_path;
@ -711,7 +746,7 @@ fn test_dependency_loading() {
let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
spin_on::spin_on(loader.load_dependencies_recursively(
let (foreign_imports, _) = spin_on::spin_on(loader.load_dependencies_recursively(
&doc_node,
&mut build_diagnostics,
&registry,
@ -719,6 +754,7 @@ fn test_dependency_loading() {
assert!(!test_diags.has_error());
assert!(!build_diagnostics.has_error());
assert!(foreign_imports.is_empty());
}
#[test]
@ -732,6 +768,8 @@ fn test_dependency_loading_from_rust() {
let mut compiler_config =
CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
compiler_config.include_paths = vec![incdir];
compiler_config.library_paths =
HashMap::from([("library".into(), test_source_path.join("library").join("lib.slint"))]);
compiler_config.style = Some("fluent".into());
let mut main_test_path = test_source_path;
@ -750,7 +788,7 @@ fn test_dependency_loading_from_rust() {
let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
spin_on::spin_on(loader.load_dependencies_recursively(
let (foreign_imports, _) = spin_on::spin_on(loader.load_dependencies_recursively(
&doc_node,
&mut build_diagnostics,
&registry,
@ -758,6 +796,7 @@ fn test_dependency_loading_from_rust() {
assert!(!test_diags.has_error());
assert!(!build_diagnostics.has_error());
assert!(foreign_imports.is_empty());
}
#[test]
@ -933,3 +972,104 @@ fn test_unknown_style() {
assert_eq!(diags.len(), 1);
assert!(diags[0].starts_with("Style FooBar in not known. Use one of the builtin styles ["));
}
#[test]
fn test_library_import() {
let test_source_path: PathBuf =
[env!("CARGO_MANIFEST_DIR"), "tests", "typeloader", "library"].iter().collect();
let library_paths = HashMap::from([
("libdir".into(), test_source_path.clone()),
("libfile.slint".into(), test_source_path.join("lib.slint")),
]);
let mut compiler_config =
CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
compiler_config.library_paths = library_paths;
compiler_config.style = Some("fluent".into());
let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
let doc_node = crate::parser::parse(
r#"
/* ... */
import { LibraryType } from "@libfile.slint";
import { LibraryHelperType } from "@libdir/library_helper_type.slint";
"#
.into(),
Some(std::path::Path::new("HELLO")),
&mut test_diags,
);
let doc_node: syntax_nodes::Document = doc_node.into();
let global_registry = TypeRegister::builtin();
let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
let mut build_diagnostics = BuildDiagnostics::default();
let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
spin_on::spin_on(loader.load_dependencies_recursively(
&doc_node,
&mut build_diagnostics,
&registry,
));
assert!(!test_diags.has_error());
assert!(!build_diagnostics.has_error());
}
#[test]
fn test_library_import_errors() {
let test_source_path: PathBuf =
[env!("CARGO_MANIFEST_DIR"), "tests", "typeloader", "library"].iter().collect();
let library_paths = HashMap::from([
("libdir".into(), test_source_path.clone()),
("libfile.slint".into(), test_source_path.join("lib.slint")),
]);
let mut compiler_config =
CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
compiler_config.library_paths = library_paths;
compiler_config.style = Some("fluent".into());
let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
let doc_node = crate::parser::parse(
r#"
/* ... */
import { A } from "@libdir";
import { B } from "@libdir/unknown.slint";
import { C } from "@libfile.slint/unknown.slint";
import { D } from "@unknown";
import { E } from "@unknown/lib.slint";
"#
.into(),
Some(std::path::Path::new("HELLO")),
&mut test_diags,
);
let doc_node: syntax_nodes::Document = doc_node.into();
let global_registry = TypeRegister::builtin();
let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
let mut build_diagnostics = BuildDiagnostics::default();
let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
spin_on::spin_on(loader.load_dependencies_recursively(
&doc_node,
&mut build_diagnostics,
&registry,
));
assert!(!test_diags.has_error());
assert!(build_diagnostics.has_error());
let diags = build_diagnostics.to_string_vec();
assert_eq!(diags.len(), 5);
assert!(diags[0].starts_with(&format!(
"HELLO:3: Error reading requested import \"{}\": ",
test_source_path.to_string_lossy()
)));
assert_eq!(&diags[1], "HELLO:4: Cannot find requested import \"@libdir/unknown.slint\" in the library search path");
assert_eq!(&diags[2], "HELLO:5: Cannot find requested import \"@libfile.slint/unknown.slint\" in the library search path");
assert_eq!(
&diags[3],
"HELLO:6: Cannot find requested import \"@unknown\" in the library search path"
);
assert_eq!(
&diags[4],
"HELLO:7: Cannot find requested import \"@unknown/lib.slint\" in the library search path"
);
}

View file

@ -537,6 +537,16 @@ impl ComponentCompiler {
&self.config.include_paths
}
/// Sets the library paths used for looking up `@library` imports to the specified map of library names to paths.
pub fn set_library_paths(&mut self, library_paths: HashMap<String, PathBuf>) {
self.config.library_paths = library_paths;
}
/// Returns the library paths the component compiler is currently configured with.
pub fn library_paths(&self) -> &HashMap<String, PathBuf> {
&self.config.library_paths
}
/// Sets the style to be used for widgets.
///
/// Use the "material" style as widget style when compiling:

View file

@ -0,0 +1,12 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
//library_path(helper_components): ../../helper_components/
//library_path(helper_buttons): ../../helper_components/test_button.slint
import { TestButton } from "@helper_components/test_button.slint";
import { ColorButton } from "@helper_buttons";
export component TestCase {
TestButton {}
ColorButton {}
}

View file

@ -12,11 +12,15 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
let include_paths = test_driver_lib::extract_include_paths(&source)
.map(std::path::PathBuf::from)
.collect::<Vec<_>>();
let library_paths = test_driver_lib::extract_library_paths(&source)
.map(|(k, v)| (k.to_string(), std::path::PathBuf::from(v)))
.collect::<std::collections::HashMap<_, _>>();
let mut diag = BuildDiagnostics::default();
let syntax_node = parser::parse(source.clone(), Some(&testcase.absolute_path), &mut diag);
let mut compiler_config = CompilerConfiguration::new(generator::OutputFormat::Cpp);
compiler_config.include_paths = include_paths;
compiler_config.library_paths = library_paths;
let (root_component, diag) =
spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));

View file

@ -146,6 +146,34 @@ fn test_extract_include_paths() {
assert_eq!(r, ["../first", "../second"]);
}
/// Extract extra library paths from a comment in the source if present.
pub fn extract_library_paths(source: &str) -> impl Iterator<Item = (&'_ str, &'_ str)> {
lazy_static::lazy_static! {
static ref RX: Regex = Regex::new(r"//library_path\((.+)\):\s*(.+)\s*\n").unwrap();
}
RX.captures_iter(source)
.map(|mat| (mat.get(1).unwrap().as_str().trim(), mat.get(2).unwrap().as_str().trim()))
}
#[test]
fn test_extract_library_paths() {
use std::collections::HashMap;
assert!(extract_library_paths("something").next().is_none());
let source = r"
//library_path(first): ../first/lib.slint
//library_path(second): ../second/lib.slint
Blah {}
";
let r = extract_library_paths(source).collect::<HashMap<_, _>>();
assert_eq!(
r,
HashMap::from([("first", "../first/lib.slint"), ("second", "../second/lib.slint")])
);
}
/// Extract `//ignore` comments from the source.
fn extract_ignores(source: &str) -> impl Iterator<Item = &'_ str> {
lazy_static::lazy_static! {

View file

@ -3,7 +3,7 @@
use itertools::Itertools;
use slint_interpreter::{DiagnosticLevel, Value, ValueType};
use std::error::Error;
use std::{collections::HashMap, error::Error};
pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>> {
i_slint_backend_testing::init();
@ -12,8 +12,12 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
let include_paths = test_driver_lib::extract_include_paths(&source)
.map(std::path::PathBuf::from)
.collect::<Vec<_>>();
let library_paths = test_driver_lib::extract_library_paths(&source)
.map(|(k, v)| (k.to_string(), std::path::PathBuf::from(v)))
.collect::<HashMap<_, _>>();
let mut compiler = slint_interpreter::ComponentCompiler::default();
compiler.set_include_paths(include_paths);
compiler.set_library_paths(library_paths);
compiler.set_style(String::from("fluent")); // force to fluent style as Qt does not like multi-threaded test execution
let component =

View file

@ -44,6 +44,14 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
)?;
let source = std::fs::read_to_string(&testcase.absolute_path)?;
let include_paths = test_driver_lib::extract_include_paths(&source);
let library_paths = test_driver_lib::extract_library_paths(&source)
.map(|(k, v)| {
let mut abs_path = testcase.absolute_path.clone();
abs_path.pop();
abs_path.push(v);
format!("{}={}", k, abs_path.to_string_lossy())
})
.collect::<Vec<_>>();
for x in test_driver_lib::extract_test_functions(&source).filter(|x| x.language_id == "js") {
write!(main_js, "{{\n {}\n}}\n", x.source.replace("\n", "\n "))?;
}
@ -52,6 +60,7 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
.arg(dir.path().join("main.js"))
.current_dir(dir.path())
.env("SLINT_INCLUDE_PATH", std::env::join_paths(include_paths).unwrap())
.env("SLINT_LIBRARY_PATH", std::env::join_paths(library_paths).unwrap())
.env("SLINT_SCALE_FACTOR", "1") // We don't have a testing backend, but we can try to force a SF1 as the tests expect.
.env("SLINT_ENABLE_EXPERIMENTAL_FEATURES", "1")
.stdout(std::process::Stdio::piped())

View file

@ -55,6 +55,14 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
)?;
let source = std::fs::read_to_string(&testcase.absolute_path)?;
let include_paths = test_driver_lib::extract_include_paths(&source);
let library_paths = test_driver_lib::extract_library_paths(&source)
.map(|(k, v)| {
let mut abs_path = testcase.absolute_path.clone();
abs_path.pop();
abs_path.push(v);
format!("{}={}", k, abs_path.to_string_lossy())
})
.collect::<Vec<_>>();
for x in test_driver_lib::extract_test_functions(&source).filter(|x| x.language_id == "js") {
write!(main_js, "{{\n {}\n}}\n", x.source.replace("\n", "\n "))?;
}
@ -64,6 +72,7 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
.current_dir(dir.path())
.env("SLINT_NODE_NATIVE_LIB", std::env::var_os("SLINT_NODE_NATIVE_LIB").unwrap())
.env("SLINT_INCLUDE_PATH", std::env::join_paths(include_paths).unwrap())
.env("SLINT_LIBRARY_PATH", std::env::join_paths(library_paths).unwrap())
.env("SLINT_SCALE_FACTOR", "1") // We don't have a testing backend, but we can try to force a SF1 as the tests expect.
.env("SLINT_ENABLE_EXPERIMENTAL_FEATURES", "1")
.stdout(std::process::Stdio::piped())

View file

@ -74,6 +74,7 @@ fn generate_macro(
// to silence all the warnings in .slint files that would be turned into errors
output.write_all(b"#![allow(deprecated)]")?;
let include_paths = test_driver_lib::extract_include_paths(source);
let library_paths = test_driver_lib::extract_library_paths(source);
output.write_all(b"slint::slint!{")?;
for path in include_paths {
let mut abs_path = testcase.absolute_path.clone();
@ -86,6 +87,19 @@ fn generate_macro(
println!("cargo:rerun-if-changed={}", abs_path.to_string_lossy());
}
for (lib, path) in library_paths {
let mut abs_path = testcase.absolute_path.clone();
abs_path.pop();
abs_path.push(path);
output.write_all(b"#[library_path(")?;
output.write_all(lib.as_bytes())?;
output.write_all(b")=r#\"")?;
output.write_all(abs_path.to_string_lossy().as_bytes())?;
output.write_all(b"\"#]\n")?;
println!("cargo:rerun-if-changed={}", abs_path.to_string_lossy());
}
let mut abs_path = testcase.absolute_path;
abs_path.pop();
output.write_all(b"#[include_path=r#\"")?;
@ -107,12 +121,16 @@ fn generate_source(
let include_paths = test_driver_lib::extract_include_paths(source)
.map(std::path::PathBuf::from)
.collect::<Vec<_>>();
let library_paths = test_driver_lib::extract_library_paths(source)
.map(|(k, v)| (k.to_string(), std::path::PathBuf::from(v)))
.collect::<std::collections::HashMap<_, _>>();
let mut diag = BuildDiagnostics::default();
let syntax_node = parser::parse(source.to_owned(), Some(&testcase.absolute_path), &mut diag);
let mut compiler_config = CompilerConfiguration::new(generator::OutputFormat::Rust);
compiler_config.enable_component_containers = true;
compiler_config.include_paths = include_paths;
compiler_config.library_paths = library_paths;
compiler_config.style = Some("fluent".to_string());
let (root_component, diag) =
spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));

View file

@ -28,3 +28,4 @@ i-slint-compiler = { workspace = true, features = ["default", "display-diagnosti
clap = { version = "4.0", features = ["derive", "wrap_help"] }
proc-macro2 = "1.0.11"
spin_on = "0.1"
itertools = { workspace = true }

View file

@ -4,6 +4,7 @@
use clap::{Parser, ValueEnum};
use i_slint_compiler::diagnostics::BuildDiagnostics;
use i_slint_compiler::*;
use itertools::Itertools;
use std::io::Write;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
@ -32,6 +33,12 @@ struct Cli {
#[arg(short = 'I', name = "include path", number_of_values = 1, action)]
include_paths: Vec<std::path::PathBuf>,
/// The argument should be in the format `<library>=<path>` specifying the
/// name of the library and the path to the library directory or a .slint
/// entry-point file.
#[arg(short = 'L', name = "library path", number_of_values = 1, action)]
library_paths: Vec<String>,
/// Path to .slint file ('-' for stdin)
#[arg(name = "file", action)]
path: std::path::PathBuf,
@ -81,6 +88,11 @@ fn main() -> std::io::Result<()> {
}
compiler_config.include_paths = args.include_paths;
compiler_config.library_paths = args
.library_paths
.iter()
.filter_map(|entry| entry.split('=').collect_tuple().map(|(k, v)| (k.into(), v.into())))
.collect();
if let Some(style) = args.style {
compiler_config.style = Some(style);
}

View file

@ -3,7 +3,10 @@
//! Data structures common between LSP and previewer
use std::path::{Path, PathBuf};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, Error>;
@ -15,7 +18,12 @@ pub trait PreviewApi {
fn design_mode(&self) -> bool;
fn set_contents(&self, path: &Path, contents: &str);
fn load_preview(&self, component: PreviewComponent, behavior: PostLoadBehavior);
fn config_changed(&self, style: &str, include_paths: &[PathBuf]);
fn config_changed(
&self,
style: &str,
include_paths: &[PathBuf],
library_paths: &HashMap<String, PathBuf>,
);
fn highlight(&self, path: Option<PathBuf>, offset: u32) -> Result<()>;
}
@ -32,6 +40,9 @@ pub struct PreviewComponent {
/// The list of include paths
pub include_paths: Vec<PathBuf>,
/// The map of library paths
pub library_paths: HashMap<String, PathBuf>,
/// The style name for the preview
pub style: String,
}

View file

@ -415,6 +415,7 @@ pub fn show_preview_command(params: &[serde_json::Value], ctx: &Rc<Context>) ->
path,
component,
include_paths: config.include_paths.clone(),
library_paths: config.library_paths.clone(),
style: config.style.clone().unwrap_or_default(),
},
crate::common::PostLoadBehavior::ShowAfterLoad,
@ -1206,6 +1207,14 @@ pub async fn load_configuration(ctx: &Context) -> Result<()> {
ip.iter().filter_map(|x| x.as_str()).map(PathBuf::from).collect();
}
}
if let Some(lp) = o.get("libraryPaths").and_then(|v| v.as_object()) {
if !lp.is_empty() {
document_cache.documents.compiler_config.library_paths = lp
.iter()
.filter_map(|(k, v)| v.as_str().map(|v| (k.to_string(), PathBuf::from(v))))
.collect();
}
}
if let Some(style) =
o.get("preview").and_then(|v| v.as_object()?.get("style")?.as_str())
{
@ -1222,7 +1231,11 @@ pub async fn load_configuration(ctx: &Context) -> Result<()> {
let cc = &document_cache.documents.compiler_config;
let empty_string = String::new();
ctx.preview.config_changed(cc.style.as_ref().unwrap_or(&empty_string), &cc.include_paths);
ctx.preview.config_changed(
cc.style.as_ref().unwrap_or(&empty_string),
&cc.include_paths,
&cc.library_paths,
);
Ok(())
}

View file

@ -62,9 +62,14 @@ impl PreviewApi for Previewer {
preview::load_preview(self.server_notifier.clone(), _component, _behavior);
}
fn config_changed(&self, _style: &str, _include_paths: &[PathBuf]) {
fn config_changed(
&self,
_style: &str,
_include_paths: &[PathBuf],
_library_paths: &HashMap<String, PathBuf>,
) {
#[cfg(feature = "preview")]
preview::config_changed(_style, _include_paths);
preview::config_changed(_style, _include_paths, _library_paths);
}
fn highlight(&self, _path: Option<std::path::PathBuf>, _offset: u32) -> Result<()> {

View file

@ -147,13 +147,21 @@ pub fn set_contents(path: &Path, content: String) {
}
}
pub fn config_changed(style: &str, include_paths: &[PathBuf]) {
pub fn config_changed(
style: &str,
include_paths: &[PathBuf],
library_paths: &HashMap<String, PathBuf>,
) {
if let Some(cache) = CONTENT_CACHE.get() {
let mut cache = cache.lock().unwrap();
let style = style.to_string();
if cache.current.style != style || cache.current.include_paths != include_paths {
if cache.current.style != style
|| cache.current.include_paths != include_paths
|| cache.current.library_paths != *library_paths
{
cache.current.style = style;
cache.current.include_paths = include_paths.to_vec();
cache.current.library_paths = library_paths.clone();
let current = cache.current.clone();
let sender = cache.sender.clone();
drop(cache);
@ -276,6 +284,7 @@ async fn reload_preview(
builder.set_style(preview_component.style);
}
builder.set_include_paths(preview_component.include_paths);
builder.set_library_paths(preview_component.library_paths);
builder.set_file_loader(|path| {
let path = path.to_owned();

View file

@ -17,6 +17,7 @@ use js_sys::Function;
pub use language::{Context, DocumentCache, RequestHandler};
use serde::Serialize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
use std::io::ErrorKind;
use std::path::PathBuf;
@ -69,7 +70,12 @@ impl PreviewApi for Previewer {
// do nothing!
}
fn config_changed(&self, _style: &str, _include_paths: &[PathBuf]) {
fn config_changed(
&self,
_style: &str,
_include_paths: &[PathBuf],
_library_paths: &HashMap<String, PathBuf>,
) {
// do nothing!
}

View file

@ -67,6 +67,7 @@ serde_json = "1"
shlex = "1"
spin_on = "0.1"
env_logger = "0.10.0"
itertools = { workspace = true }
# Enable image-rs' default features to make all image formats available for preview
image = { version = "0.24.0" }

View file

@ -35,6 +35,7 @@ slint-viewer path/to/myfile.slint
This option is incompatible with `--auto-reload`
- `--load-data <file>`: Load the values of public properties from a json file.
- `-I <path>`: Add an include path to look for imported .slint files or images.
- `-L <library:path>`: Add a library path to look for `@library` imports.
- `--style <style>`: Set the style. Defaults to `native` if the Qt backend is compiled, otherwise `fluent`
- `--backend <backend>`: Override the Slint rendering backend
- `--on <callback> <handler>`: Set a callback handler, see [callback handler](#callback-handlers)

View file

@ -13,6 +13,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use clap::Parser;
use itertools::Itertools;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
@ -22,6 +23,10 @@ struct Cli {
#[arg(short = 'I', name = "include path for other .slint files", number_of_values = 1, action)]
include_paths: Vec<std::path::PathBuf>,
/// The first argument is the library name, and the second argument is the path to the library.
#[arg(short = 'L', name = "library path for @library imports", number_of_values = 1, action)]
library_paths: Vec<String>,
/// The .slint file to load ('-' for stdin)
#[arg(name = "path to .slint file", action)]
path: std::path::PathBuf,
@ -165,6 +170,12 @@ fn init_compiler(
compiler.set_translation_domain(domain);
}
compiler.set_include_paths(args.include_paths.clone());
compiler.set_library_paths(
args.library_paths
.iter()
.filter_map(|entry| entry.split('=').collect_tuple().map(|(k, v)| (k.into(), v.into())))
.collect(),
);
if let Some(style) = &args.style {
compiler.set_style(style.clone());
}