slint_macros/
lib.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell:ignore punct
5
6#![doc = include_str!("README.md")]
7#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
8
9extern crate proc_macro;
10
11use i_slint_compiler::diagnostics::BuildDiagnostics;
12use i_slint_compiler::parser::SyntaxKind;
13use i_slint_compiler::*;
14use proc_macro::{Spacing, TokenStream, TokenTree};
15use quote::quote;
16
17/// Returns true if the two token are touching. For example the two token `foo`and `-` are touching if
18/// it was written like so in the source code: `foo-` but not when written like so `foo -`
19///
20/// Returns None if we couldn't detect whether they are touching  (eg, our heuristics don't work with rust-analyzer)
21fn are_token_touching(token1: proc_macro::Span, token2: proc_macro::Span) -> Option<bool> {
22    // There is no way with stable API to find out if the token are touching, so do it by
23    // extracting the range from the debug representation of the span
24    are_token_touching_impl(&format!("{token1:?}"), &format!("{token2:?}"))
25}
26
27fn are_token_touching_impl(token1_debug: &str, token2_debug: &str) -> Option<bool> {
28    // The debug representation of a span look like this: "#0 bytes(6662789..6662794)"
29    // we just have to find out if the first number of the range of second span
30    // is the same as the second number of the first span
31    let is_byte_char = |c: char| c.is_numeric() || c == ':';
32    let not_is_byte_char = |c: char| !is_byte_char(c);
33    let end_of_token1 = token1_debug
34        .trim_end_matches(not_is_byte_char)
35        .rsplit(not_is_byte_char)
36        .next()?
37        .trim_matches(':');
38    let begin_of_token2 = token2_debug
39        .trim_end_matches(not_is_byte_char)
40        .strip_suffix(is_byte_char)?
41        .trim_end_matches(is_byte_char)
42        .trim_end_matches(not_is_byte_char)
43        .rsplit(not_is_byte_char)
44        .next()?
45        .trim_matches(':');
46    (!begin_of_token2.is_empty()).then_some(end_of_token1 == begin_of_token2)
47}
48
49#[test]
50fn are_token_touching_impl_test() {
51    assert!(are_token_touching_impl("#0 bytes(6662788..6662789)", "#0 bytes(6662789..6662794)")
52        .unwrap());
53    assert!(!are_token_touching_impl("#0 bytes(6662788..6662789)", "#0 bytes(6662790..6662794)")
54        .unwrap());
55    assert!(!are_token_touching_impl("#0 bytes(6662789..6662794)", "#0 bytes(6662788..6662789)")
56        .unwrap());
57    assert!(
58        !are_token_touching_impl("#0 bytes(6662788..6662789)", "#0 bytes(662789..662794)").unwrap()
59    );
60    assert!(are_token_touching_impl("#0 bytes(123..456)", "#0 bytes(456..789)").unwrap());
61
62    // Alternative representation on nightly with a special flag
63    assert!(are_token_touching_impl("/foo/bar.rs:12:7: 12:18", "/foo/bar.rs:12:18: 12:19").unwrap());
64    assert!(are_token_touching_impl("/foo/bar.rs:2:7: 13:18", "/foo/bar.rs:13:18: 14:29").unwrap());
65    assert!(!are_token_touching_impl("/foo/bar.rs:2:7: 13:18", "/foo/bar.rs:14:18: 14:29").unwrap());
66    assert!(!are_token_touching_impl("/foo/bar.rs:2:7: 2:8", "/foo/bar.rs:2:18: 2:29").unwrap());
67
68    // What happens if the representation change
69    assert!(are_token_touching_impl("hello", "hello").is_none());
70    assert!(are_token_touching_impl("hello42", "hello42").is_none());
71
72    // rust-analyzer just has indices that means nothing
73    assert!(are_token_touching_impl("55", "56").is_none());
74}
75
76fn fill_token_vec(stream: impl Iterator<Item = TokenTree>, vec: &mut Vec<parser::Token>) {
77    let mut prev_spacing = Spacing::Alone;
78    let mut prev_span = proc_macro::Span::call_site();
79    for t in stream {
80        let span = t.span();
81        match t {
82            TokenTree::Ident(i) => {
83                if let Some(last) = vec.last_mut() {
84                    if (last.kind == SyntaxKind::ColorLiteral && last.text.len() == 1)
85                        || (last.kind == SyntaxKind::Identifier
86                            && are_token_touching(prev_span, span)
87                                .unwrap_or_else(|| last.text.ends_with('-')))
88                    {
89                        last.text = format!("{}{}", last.text, i).into();
90                        prev_span = span;
91                        continue;
92                    }
93                }
94                vec.push(parser::Token {
95                    kind: SyntaxKind::Identifier,
96                    text: i.to_string().into(),
97                    span: Some(i.span()),
98                    ..Default::default()
99                });
100            }
101            TokenTree::Punct(p) => {
102                let kind = match p.as_char() {
103                    ':' => SyntaxKind::Colon,
104                    '=' => {
105                        if let Some(last) = vec.last_mut() {
106                            let kt = match last.kind {
107                                SyntaxKind::Star => Some((SyntaxKind::StarEqual, "*=")),
108                                SyntaxKind::Colon => Some((SyntaxKind::ColonEqual, ":=")),
109                                SyntaxKind::Plus => Some((SyntaxKind::PlusEqual, "+=")),
110                                SyntaxKind::Minus => Some((SyntaxKind::MinusEqual, "-=")),
111                                SyntaxKind::Div => Some((SyntaxKind::DivEqual, "/=")),
112                                SyntaxKind::LAngle => Some((SyntaxKind::LessEqual, "<=")),
113                                SyntaxKind::RAngle => Some((SyntaxKind::GreaterEqual, ">=")),
114                                SyntaxKind::Equal => Some((SyntaxKind::EqualEqual, "==")),
115                                SyntaxKind::Bang => Some((SyntaxKind::NotEqual, "!=")),
116                                _ => None,
117                            };
118                            if let Some((k, t)) = kt {
119                                if prev_spacing == Spacing::Joint {
120                                    last.kind = k;
121                                    last.text = t.into();
122                                    continue;
123                                }
124                            }
125                        }
126                        SyntaxKind::Equal
127                    }
128                    ';' => SyntaxKind::Semicolon,
129                    '!' => SyntaxKind::Bang,
130                    '.' => {
131                        // `4..log` is lexed as `4 . . log` in rust, but should be `4. . log` in slint
132                        if let Some(last) = vec.last_mut() {
133                            if last.kind == SyntaxKind::NumberLiteral
134                                && are_token_touching(prev_span, p.span()).unwrap_or(false)
135                                && !last.text.contains('.')
136                                && !last.text.ends_with(char::is_alphabetic)
137                            {
138                                last.text = format!("{}.", last.text).into();
139                                prev_span = span;
140                                continue;
141                            }
142                        }
143                        SyntaxKind::Dot
144                    }
145                    '+' => SyntaxKind::Plus,
146                    '-' => {
147                        if let Some(last) = vec.last_mut() {
148                            if last.kind == SyntaxKind::Identifier
149                                && are_token_touching(prev_span, p.span()).unwrap_or(true)
150                            {
151                                last.text = format!("{}-", last.text).into();
152                                prev_span = span;
153                                continue;
154                            }
155                        }
156                        SyntaxKind::Minus
157                    }
158                    '*' => SyntaxKind::Star,
159                    '/' => SyntaxKind::Div,
160                    '<' => SyntaxKind::LAngle,
161                    '>' => {
162                        if let Some(last) = vec.last_mut() {
163                            if last.kind == SyntaxKind::LessEqual && prev_spacing == Spacing::Joint
164                            {
165                                last.kind = SyntaxKind::DoubleArrow;
166                                last.text = "<=>".into();
167                                continue;
168                            } else if last.kind == SyntaxKind::Equal
169                                && prev_spacing == Spacing::Joint
170                            {
171                                last.kind = SyntaxKind::FatArrow;
172                                last.text = "=>".into();
173                                continue;
174                            } else if last.kind == SyntaxKind::Minus
175                                && prev_spacing == Spacing::Joint
176                            {
177                                last.kind = SyntaxKind::Arrow;
178                                last.text = "->".into();
179                                continue;
180                            }
181                        }
182                        SyntaxKind::RAngle
183                    }
184                    '#' => SyntaxKind::ColorLiteral,
185                    '?' => SyntaxKind::Question,
186                    ',' => SyntaxKind::Comma,
187                    '&' => {
188                        // Since the '&' alone does not exist or cannot be part of any other token that &&
189                        // just consider it as '&&' and skip the joint ones.  FIXME. do that properly
190                        if let Some(last) = vec.last_mut() {
191                            if last.kind == SyntaxKind::AndAnd && prev_spacing == Spacing::Joint {
192                                continue;
193                            }
194                        }
195                        SyntaxKind::AndAnd
196                    }
197                    '|' => {
198                        // Since the '|' alone does not exist or cannot be part of any other token that ||
199                        // just consider it as '||' and skip the joint ones.
200                        if let Some(last) = vec.last_mut() {
201                            if last.kind == SyntaxKind::Pipe && prev_spacing == Spacing::Joint {
202                                last.kind = SyntaxKind::OrOr;
203                                continue;
204                            }
205                        }
206                        SyntaxKind::Pipe
207                    }
208                    '%' => {
209                        // handle % as a unit
210                        if let Some(last) = vec.last_mut() {
211                            if last.kind == SyntaxKind::NumberLiteral {
212                                last.text = format!("{}%", last.text).into();
213                                continue;
214                            }
215                        }
216                        SyntaxKind::Percent
217                    }
218                    '$' => SyntaxKind::Dollar,
219                    '@' => SyntaxKind::At,
220                    _ => SyntaxKind::Error,
221                };
222                prev_spacing = p.spacing();
223                vec.push(parser::Token {
224                    kind,
225                    text: p.to_string().into(),
226                    span: Some(p.span()),
227                    ..Default::default()
228                });
229            }
230            TokenTree::Literal(l) => {
231                let s = l.to_string();
232                // Why can't the rust API give me the type of the literal
233                let f = s.chars().next().unwrap();
234                let kind = if f == '"' {
235                    SyntaxKind::StringLiteral
236                } else if f.is_ascii_digit() {
237                    if let Some(last) = vec.last_mut() {
238                        if (last.kind == SyntaxKind::ColorLiteral && last.text.len() == 1)
239                            || (last.kind == SyntaxKind::Identifier
240                                && are_token_touching(prev_span, span)
241                                    .unwrap_or_else(|| last.text.ends_with('-')))
242                        {
243                            last.text = format!("{}{}", last.text, s).into();
244                            prev_span = span;
245                            continue;
246                        }
247                    }
248                    SyntaxKind::NumberLiteral
249                } else {
250                    SyntaxKind::Error
251                };
252                vec.push(parser::Token {
253                    kind,
254                    text: s.into(),
255                    span: Some(l.span()),
256                    ..Default::default()
257                });
258            }
259            TokenTree::Group(g) => {
260                use proc_macro::Delimiter::*;
261                use SyntaxKind::*;
262                let (l, r, sl, sr) = match g.delimiter() {
263                    Parenthesis => (LParent, RParent, "(", ")"),
264                    Brace => (LBrace, RBrace, "{", "}"),
265                    Bracket => (LBracket, RBracket, "[", "]"),
266                    None => todo!(),
267                };
268                vec.push(parser::Token {
269                    kind: l,
270                    text: sl.into(),
271                    span: Some(g.span()), // span_open is not stable
272                    ..Default::default()
273                });
274                fill_token_vec(g.stream().into_iter(), vec);
275                vec.push(parser::Token {
276                    kind: r,
277                    text: sr.into(),
278                    span: Some(g.span()), // span_clone is not stable
279                    ..Default::default()
280                });
281            }
282        }
283        prev_span = span;
284    }
285}
286
287fn extract_path(literal: proc_macro::Literal) -> std::path::PathBuf {
288    let path_with_quotes = literal.to_string();
289    let path_with_quotes_stripped = if let Some(p) = path_with_quotes.strip_prefix('r') {
290        let hash_removed = p.trim_matches('#');
291        hash_removed.strip_prefix('\"').unwrap().strip_suffix('\"').unwrap()
292    } else {
293        // FIXME: unescape
294        path_with_quotes.trim_matches('\"')
295    };
296    path_with_quotes_stripped.into()
297}
298
299fn extract_compiler_config(
300    mut stream: proc_macro::token_stream::IntoIter,
301    compiler_config: &mut CompilerConfiguration,
302) -> impl Iterator<Item = TokenTree> {
303    let mut remaining_stream;
304    loop {
305        remaining_stream = stream.clone();
306        match (stream.next(), stream.next()) {
307            (Some(TokenTree::Punct(p)), Some(TokenTree::Group(group)))
308                if p.as_char() == '#' && group.delimiter() == proc_macro::Delimiter::Bracket =>
309            {
310                let mut attr_stream = group.stream().into_iter();
311                match attr_stream.next() {
312                    Some(TokenTree::Ident(include_ident))
313                        if include_ident.to_string() == "include_path" =>
314                    {
315                        match (attr_stream.next(), attr_stream.next()) {
316                            (
317                                Some(TokenTree::Punct(equal_punct)),
318                                Some(TokenTree::Literal(path)),
319                            ) if equal_punct.as_char() == '=' => {
320                                compiler_config.include_paths.push(extract_path(path));
321                            }
322                            _ => break,
323                        }
324                    }
325                    Some(TokenTree::Ident(library_ident))
326                        if library_ident.to_string() == "library_path" =>
327                    {
328                        match (attr_stream.next(), attr_stream.next(), attr_stream.next()) {
329                            (
330                                Some(TokenTree::Group(group)),
331                                Some(TokenTree::Punct(equal_punct)),
332                                Some(TokenTree::Literal(path)),
333                            ) if group.delimiter() == proc_macro::Delimiter::Parenthesis
334                                && equal_punct.as_char() == '=' =>
335                            {
336                                let library_name = group.stream().into_iter().next().unwrap();
337                                compiler_config
338                                    .library_paths
339                                    .insert(library_name.to_string(), extract_path(path));
340                            }
341                            _ => break,
342                        }
343                    }
344                    Some(TokenTree::Ident(style_ident)) if style_ident.to_string() == "style" => {
345                        match (attr_stream.next(), attr_stream.next()) {
346                            (
347                                Some(TokenTree::Punct(equal_punct)),
348                                Some(TokenTree::Literal(requested_style)),
349                            ) if equal_punct.as_char() == '=' => {
350                                compiler_config.style = requested_style
351                                    .to_string()
352                                    .strip_prefix('\"')
353                                    .unwrap()
354                                    .strip_suffix('\"')
355                                    .unwrap()
356                                    .to_string()
357                                    .into();
358                            }
359                            _ => break,
360                        }
361                    }
362                    _ => break,
363                }
364            }
365            _ => break,
366        }
367    }
368    remaining_stream
369}
370
371/// This macro allows you to use the Slint design markup language inline in Rust code. Within the braces of the macro
372/// you can use place Slint code and the named exported components will be available for instantiation.
373///
374/// For the documentation about the syntax of the language, see
375#[doc = concat!("[The Slint Language Documentation](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint)")]
376///
377/// When `import`ing `.slint` files or loading images with `@image-url`, the specified paths are relative to the
378/// the directory that contains Cargo.toml.
379///
380/// ### Limitations
381///
382/// Within `.slint` files, you can interpolate string literals using `\{...}` syntax.
383/// This is not possible in this macro as this wouldn't parse as a Rust string.
384#[proc_macro]
385pub fn slint(stream: TokenStream) -> TokenStream {
386    let token_iter = stream.into_iter();
387
388    let mut compiler_config =
389        CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust);
390
391    let token_iter = extract_compiler_config(token_iter, &mut compiler_config);
392
393    let mut tokens = vec![];
394    fill_token_vec(token_iter, &mut tokens);
395
396    let source_file = if let Some(cargo_manifest) = std::env::var_os("CARGO_MANIFEST_DIR") {
397        let mut path: std::path::PathBuf = cargo_manifest.into();
398        path.push("Cargo.toml");
399        diagnostics::SourceFileInner::from_path_only(path)
400    } else {
401        diagnostics::SourceFileInner::from_path_only(Default::default())
402    };
403    let mut diag = BuildDiagnostics::default();
404    let syntax_node = parser::parse_tokens(tokens.clone(), source_file, &mut diag);
405    if diag.has_errors() {
406        return diag.report_macro_diagnostic(&tokens);
407    }
408
409    //println!("{syntax_node:#?}");
410    compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
411    let (root_component, diag, loader) =
412        spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));
413    //println!("{tree:#?}");
414    if diag.has_errors() {
415        return diag.report_macro_diagnostic(&tokens);
416    }
417
418    let mut result = generator::rust::generate(&root_component, &loader.compiler_config)
419        .unwrap_or_else(|e| {
420            let e_str = e.to_string();
421            quote!(compile_error!(#e_str))
422        });
423
424    // Make sure to recompile if any of the external files changes
425    let reload = diag
426        .all_loaded_files
427        .iter()
428        .filter(|path| path.is_absolute() && !path.ends_with("Cargo.toml"))
429        .filter_map(|p| p.to_str())
430        .map(|p| quote! {const _ : &'static [u8] = ::core::include_bytes!(#p);});
431
432    result.extend(reload);
433    result.extend(quote! {const _ : ::core::option::Option<&'static str> = ::core::option_env!("SLINT_STYLE");});
434
435    let mut result = TokenStream::from(result);
436    if !diag.is_empty() {
437        result.extend(diag.report_macro_diagnostic(&tokens));
438    }
439    result
440}