gpui-rsx 0.7.0

A JSX-like macro for GPUI - simplify UI development with HTML-like syntax
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
//! 元素代码生成
//!
//! 将 RSX 元素转换为 GPUI 方法链代码:
//! - 基础标签构造
//! - 自动 ID 管理(支持 `key` 属性组合 ID)
//! - 子节点聚合优化
//! - Fragment 和 For 循环支持(for 循环中的 stateful 元素须提供 `id` 或 `key`)
//!
//! 优化:
//! - 缓存 `Ident::to_string()` 避免重复堆分配
//! - 使用 match-based `is_stateful_attr()` 替代双重线性扫描
//! - 使用 `lookup_tag_default()` 替代 `.iter().find()` 线性查找
//! - `generate_attr_methods` 直接 push 到调用方 Vec
//! - 每个子节点独立生成 `.child()` 调用,避免数组类型统一约束
//! - 自动 ID 基于源码 span 位置(行号 + 列号),增量编译下保持稳定

use super::attribute::{AttrHints, generate_attr_methods_with_mode, static_class_expr_needs_id};
use super::class::{ClassMode, parse_class_string_with_mode};
use super::tables::{
    is_stateful_attr, is_stateful_class, lookup_attr_flag_method, lookup_tag_default,
};
use crate::diagnostics::{for_loop_missing_key_error, missing_required_attribute_error};

#[derive(Default)]
struct AttrAnalysis {
    name: Option<String>,
    static_class: Option<String>,
    needs_id: bool,
}

impl AttrAnalysis {
    fn hints(&self) -> AttrHints<'_> {
        AttrHints {
            name: self.name.as_deref(),
            static_class: self.static_class.as_deref(),
        }
    }
}

fn analyze_attr(attr: &RsxAttribute) -> AttrAnalysis {
    match attr {
        RsxAttribute::Value { name, value: _ } if name == "id" || name == "key" => {
            AttrAnalysis::default()
        }
        RsxAttribute::Value { name, value } if name == "class" => {
            let static_class = if let syn::Expr::Lit(syn::ExprLit {
                lit: syn::Lit::Str(lit_str),
                ..
            }) = value
            {
                Some(lit_str.value())
            } else {
                None
            };
            let needs_id = if let Some(class) = static_class.as_deref() {
                class.split_ascii_whitespace().any(is_stateful_class)
            } else {
                static_class_expr_needs_id(value)
            };

            AttrAnalysis {
                static_class,
                needs_id,
                ..AttrAnalysis::default()
            }
        }
        RsxAttribute::Value { name, .. } => {
            let name = name.to_string();
            let needs_id = is_stateful_attr(&name);
            AttrAnalysis {
                name: Some(name),
                needs_id,
                ..AttrAnalysis::default()
            }
        }
        RsxAttribute::Flag(name) if name == "styled" => AttrAnalysis::default(),
        RsxAttribute::Flag(name) => {
            let name = name.to_string();
            let needs_id = is_stateful_attr(&name)
                || lookup_attr_flag_method(&name).is_some_and(is_stateful_attr);
            AttrAnalysis {
                name: Some(name),
                needs_id,
                ..AttrAnalysis::default()
            }
        }
        RsxAttribute::StateClass { method, .. } => {
            let name = method.to_string();
            let needs_id = is_stateful_attr(&name);
            AttrAnalysis {
                name: Some(name),
                needs_id,
                ..AttrAnalysis::default()
            }
        }
        _ => AttrAnalysis::default(),
    }
}

use crate::parser::{RsxAttribute, RsxBody, RsxElement, RsxElementName, RsxNode};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};

type CodegenResult = Result<TokenStream, TokenStream>;

/// 生成 GPUI 代码(入口)
///
/// 将解析后的 RSX AST 转换为 GPUI 的类型安全代码。
///
/// # 返回值
/// - 单个元素:返回实现 `IntoElement` 的表达式
/// - Fragment:返回 `Vec<impl IntoElement>`
pub fn generate_body_with_mode(body: &RsxBody, mode: ClassMode) -> TokenStream {
    generate_body_checked(body, mode).unwrap_or_else(|err| err)
}

pub fn generate_body_expansion_preview(body: &RsxBody, mode: ClassMode) -> String {
    generate_body_with_mode(body, mode).to_string()
}

fn generate_body_checked(body: &RsxBody, mode: ClassMode) -> CodegenResult {
    match body {
        RsxBody::Single(element) => generate_element_checked(element, false, mode),
        RsxBody::Fragment(children) => {
            let child_exprs: Vec<TokenStream> = children
                .iter()
                .map(|node| generate_node_checked(node, false, mode))
                .collect::<Result<_, _>>()?;
            // Fragment 保持 vec![] —— 返回类型是用户可见 API
            Ok(quote! { vec![#(#child_exprs),*] })
        }
    }
}

/// 生成单个子节点的代码
///
/// 确保生成的代码具有正确的类型推断,支持 IntoElement trait
fn generate_node_checked(node: &RsxNode, require_loop_key: bool, mode: ClassMode) -> CodegenResult {
    match node {
        RsxNode::Element(elem) => generate_element_checked(elem, require_loop_key, mode),
        // 表达式会被自动推断类型,GPUI 的 .child() 接受 impl IntoElement
        RsxNode::Expr(expr) => Ok(expr.to_token_stream()),
        RsxNode::Spread(expr) => Ok(expr.to_token_stream()),
        RsxNode::For {
            binding,
            iter,
            body,
        } => generate_for_loop_checked(binding, iter, body, mode),
    }
}

/// 生成 for 循环的迭代器代码
///
/// 单个子节点 → `.map()`,多个子节点 → `.flat_map()` + `AnyElement` 数组
///
/// 多子节点使用 `AnyElement` 做类型擦除后放入数组,避免每轮循环分配 `Vec`,
/// 同时允许循环体内混合不同具体元素类型(如 `div()` 和自定义组件)。
///
/// 安全检查:循环体内所有 stateful 元素(含深层嵌套)都必须提供 `id` 或 `key`,
/// 否则每次迭代会生成相同的自动 ID,导致 GPUI 状态冲突,因此在此阶段给出编译错误。
fn generate_for_loop_checked(
    binding: &syn::Pat,
    iter: &syn::Expr,
    body: &[RsxNode],
    mode: ClassMode,
) -> CodegenResult {
    let body_exprs: Vec<TokenStream> = body
        .iter()
        .map(|node| generate_node_checked(node, true, mode))
        .collect::<Result<_, _>>()?;
    if body_exprs.len() == 1 {
        let single = &body_exprs[0];
        Ok(quote! { (#iter).into_iter().map(|#binding| #single) })
    } else {
        Ok(quote! {
            (#iter).into_iter().flat_map(|#binding| [#((#body_exprs).into_any_element()),*])
        })
    }
}

/// 生成单个元素的代码
///
/// 生成形如 `div().id("x").flex().child(...)` 的方法链,
/// 而非 `let mut element = div(); element = element.flex();` 的赋值模式。
///
/// 方法链模式的优势:
/// - 与 GPUI 惯用写法一致
/// - 正确处理 `Div` → `Stateful<Div>` 的类型变换(`.id()` 后类型改变)
fn generate_element_checked(
    element: &RsxElement,
    require_loop_key: bool,
    mode: ClassMode,
) -> CodegenResult {
    // 缓存标签名字符串,避免多次 to_string() 堆分配
    let tag_str = element.name.to_string();

    // 快速路径:无属性且无子节点时,跳过所有扫描直接返回基础标签
    if element.attributes.is_empty() && element.children.is_empty() {
        return generate_tag(&tag_str, &element.name, None, None, None);
    }

    // 单次遍历提取所有需要的信息,同时生成用户属性方法。
    let mut user_id = None;
    let mut user_key = None;
    let mut base_expr = None;
    let mut img_source = None;
    let mut canvas_prepaint = None;
    let mut canvas_paint = None;
    let mut has_styled = false;
    let mut needs_id = false;

    // 预分配方法链容量:
    // - 每个属性乘以 2(class 属性平均展开 3-4 个方法,其余属性 1 个)
    // - 加上子节点数
    let mut methods: Vec<TokenStream> =
        Vec::with_capacity(element.attributes.len() * 2 + element.children.len());

    for attr in &element.attributes {
        match attr {
            RsxAttribute::Value { name, value } if name == "id" => {
                user_id = Some(value);
            }
            RsxAttribute::Value { name, value } if name == "key" => {
                user_key = Some(value);
            }
            RsxAttribute::Value { name, value } if name == "base" => {
                base_expr = Some(value);
            }
            RsxAttribute::Value { name, value }
                if tag_str == "img" && (name == "src" || name == "source") =>
            {
                img_source = Some(value);
            }
            RsxAttribute::Value { name, value } if tag_str == "canvas" && name == "prepaint" => {
                canvas_prepaint = Some(value);
            }
            RsxAttribute::Value { name, value } if tag_str == "canvas" && name == "paint" => {
                canvas_paint = Some(value);
            }
            RsxAttribute::Value { name, value } if tag_str == "svg" && name == "src" => {
                methods.push(quote! { .path(#value) });
            }
            RsxAttribute::Flag(name) if name == "styled" => {
                has_styled = true;
            }
            _ => {
                let analysis = analyze_attr(attr);
                if !needs_id && analysis.needs_id {
                    needs_id = true;
                }
                generate_attr_methods_with_mode(attr, analysis.hints(), &mut methods, mode);
            }
        }
    }

    if require_loop_key && needs_id && user_id.is_none() && user_key.is_none() {
        return Err(for_loop_missing_key_error(&element.name.path, &tag_str).to_compile_error());
    }

    // 生成基础元素和 id:
    //  1. 显式 id              → 直接使用,优先级最高
    //  2. 需要 id + key 存在   → 自动 ID 前缀 + key(运行时拼接,保证循环内唯一)
    //  3. 需要 id,无 key       → 纯源码位置的自动 ID
    //  4. 不需要 id            → 不注入(key 在此情况下静默忽略)
    let tag = if let Some(base) = base_expr {
        quote! { #base }
    } else {
        generate_tag(
            &tag_str,
            &element.name,
            img_source,
            canvas_prepaint,
            canvas_paint,
        )?
    };
    let base = if let Some(id_value) = user_id {
        quote! { #tag.id(#id_value) }
    } else if needs_id {
        if let Some(key_expr) = user_key {
            let keyed_id = make_keyed_auto_id(&element.name, key_expr);
            quote! { #tag.id(#keyed_id) }
        } else {
            let auto_id = make_auto_id(&element.name);
            quote! { #tag.id(#auto_id) }
        }
    } else {
        tag
    };

    // styled 标志 → 注入标签默认样式(在用户属性之前)
    let default_methods: Vec<TokenStream> =
        if has_styled && let Some(class_str) = lookup_tag_default(&tag_str) {
            parse_class_string_with_mode(class_str, mode).collect()
        } else {
            Vec::new()
        };

    // 子节点 → .child() / .children() 调用(含聚合优化)
    generate_children_methods(&element.children, require_loop_key, &mut methods, mode)?;

    Ok(quote! { #base #(#default_methods)* #(#methods)* })
}

/// 生成子节点的方法链片段
fn generate_children_methods(
    children: &[RsxNode],
    require_loop_key: bool,
    methods: &mut Vec<TokenStream>,
    mode: ClassMode,
) -> Result<(), TokenStream> {
    for node in children {
        match node {
            RsxNode::Expr(expr) => {
                methods.push(quote! { .child(#expr) });
            }
            RsxNode::Element(elem) => {
                let child_expr = generate_element_checked(elem, require_loop_key, mode)?;
                methods.push(quote! { .child(#child_expr) });
            }
            RsxNode::Spread(expr) => {
                methods.push(quote! { .children(#expr) });
            }
            RsxNode::For {
                binding,
                iter,
                body,
            } => {
                let for_expr = generate_for_loop_checked(binding, iter, body, mode)?;
                methods.push(quote! { .children(#for_expr) });
            }
        }
    }
    Ok(())
}

/// HTML 标签 → `div()`,特殊标签 → 同名函数,自定义组件 → 同名函数调用
///
/// 接受预缓存的 `tag_str` 避免重复 `to_string()`
fn generate_tag(
    tag_str: &str,
    name: &RsxElementName,
    img_source: Option<&syn::Expr>,
    canvas_prepaint: Option<&syn::Expr>,
    canvas_paint: Option<&syn::Expr>,
) -> CodegenResult {
    if name.as_single_ident().is_none() {
        let path = &name.path;
        return Ok(quote! { #path() });
    }

    let path = &name.path;
    Ok(match tag_str {
        // 特殊标签:保留为同名函数调用
        "svg" => quote! { svg() },
        "img" => {
            let Some(source) = img_source else {
                return Err(missing_required_attribute_error(
                    &name.path,
                    "img",
                    "src",
                    r#"<img src={"path/to/image.png"} />"#,
                )
                .to_compile_error());
            };
            quote! { img(#source) }
        }
        "canvas" => {
            let Some(prepaint) = canvas_prepaint else {
                return Err(missing_required_attribute_error(
                    &name.path,
                    "canvas",
                    "prepaint",
                    r#"<canvas prepaint={|bounds, window, cx| state} paint={|bounds, state, window, cx| { ... }} />"#,
                )
                .to_compile_error());
            };
            let Some(paint) = canvas_paint else {
                return Err(missing_required_attribute_error(
                    &name.path,
                    "canvas",
                    "paint",
                    r#"<canvas prepaint={|bounds, window, cx| state} paint={|bounds, state, window, cx| { ... }} />"#,
                )
                .to_compile_error());
            };
            quote! { canvas(#prepaint, #paint) }
        }
        // HTML 标签:统一映射为 div()
        "div" | "span" | "section" | "article" | "header" | "footer" | "main" | "nav" | "aside"
        | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "label" | "a" | "button" | "input"
        | "textarea" | "select" | "form" | "ul" | "ol" | "li" => {
            quote! { div() }
        }
        _ => quote! { #path() },
    })
}

/// 生成基于源码位置的稳定自动 ID(无 key)
///
/// 格式:`concat!(file!(), "::", "__rsx_{tag}_L{line}C{col}")`
///
/// **稳定性**:只要元素源码位置不变,ID 不变(增量编译安全)。
/// **唯一性**:`file!()` 在用户侧展开,包含完整路径,跨文件全局唯一。
///
/// 若需要跨重构完全稳定的 ID,请使用 `id` 属性;
/// 若在循环内使用,请改用 `key` 属性。
fn make_auto_id(tag_name: &RsxElementName) -> TokenStream {
    let span = tag_name.span();
    let loc = span.start(); // 需要 proc-macro2 的 span-locations 特性
    let id_suffix = format!("__rsx_{}_L{}C{}", tag_name, loc.line, loc.column);
    quote! { concat!(file!(), "::", #id_suffix) }
}

/// 生成带 `key` 的复合自动 ID(用于循环场景)
///
/// 动态 key 格式:`format!("{file}::{prefix}_{key}", file!(), key_expr)`
/// 字面量 key 格式:`concat!(file!(), "::{prefix}_", "literal")`
///
/// `concat!(file!(), ...)` 在编译期求值(零开销)。动态 `key_expr` 在运行时拼接,
/// 使同一循环迭代内的每个元素获得唯一 ID。
/// `key_expr` 需实现 `std::fmt::Display`(数字、字符串、自定义类型均可)。
fn make_keyed_auto_id(tag_name: &RsxElementName, key_expr: &syn::Expr) -> TokenStream {
    let span = tag_name.span();
    let loc = span.start();
    // 编译期常量前缀,包含文件路径 + 源码位置,格式如:
    //   "src/views/list.rs::__rsx_li_L42C8_"
    let prefix_suffix = format!("::__rsx_{}_L{}C{}_", tag_name, loc.line, loc.column);
    if let Some(static_suffix) = static_key_suffix(key_expr) {
        return quote! { concat!(file!(), #prefix_suffix, #static_suffix) };
    }
    // 运行时将 key 追加到前缀后,生成如:
    //   "src/views/list.rs::__rsx_li_L42C8_item_42"
    quote! { format!(concat!(file!(), #prefix_suffix, "{}"), #key_expr) }
}

fn static_key_suffix(expr: &syn::Expr) -> Option<String> {
    match expr {
        syn::Expr::Lit(syn::ExprLit {
            lit: syn::Lit::Str(lit),
            ..
        }) => Some(lit.value()),
        syn::Expr::Lit(syn::ExprLit {
            lit: syn::Lit::Int(lit),
            ..
        }) => lit
            .base10_parse::<u128>()
            .ok()
            .map(|value| value.to_string()),
        syn::Expr::Lit(syn::ExprLit {
            lit: syn::Lit::Bool(lit),
            ..
        }) => Some(lit.value.to_string()),
        syn::Expr::Lit(syn::ExprLit {
            lit: syn::Lit::Char(lit),
            ..
        }) => Some(lit.value().to_string()),
        syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => match &*unary.expr {
            syn::Expr::Lit(syn::ExprLit {
                lit: syn::Lit::Int(lit),
                ..
            }) => lit
                .base10_parse::<u128>()
                .ok()
                .map(|value| format!("-{value}")),
            _ => None,
        },
        syn::Expr::Paren(expr) => static_key_suffix(&expr.expr),
        syn::Expr::Group(expr) => static_key_suffix(&expr.expr),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn element_name(name: &str) -> RsxElementName {
        RsxElementName {
            path: syn::parse_str(name).expect("valid element name"),
        }
    }

    #[test]
    fn img_requires_source() {
        let error = generate_tag("img", &element_name("img"), None, None, None)
            .expect_err("img without src must fail")
            .to_string();

        assert!(error.contains("Element `<img>` requires `src`"));
    }

    #[test]
    fn canvas_requires_prepaint_and_paint() {
        let callback: syn::Expr = syn::parse_quote!(callback);
        let name = element_name("canvas");

        let missing_prepaint = generate_tag("canvas", &name, None, None, Some(&callback))
            .expect_err("canvas without prepaint must fail")
            .to_string();
        assert!(missing_prepaint.contains("Element `<canvas>` requires `prepaint`"));

        let missing_paint = generate_tag("canvas", &name, None, Some(&callback), None)
            .expect_err("canvas without paint must fail")
            .to_string();
        assert!(missing_paint.contains("Element `<canvas>` requires `paint`"));
    }

    #[test]
    fn static_key_suffix_supports_literal_display_types() {
        let cases: [(syn::Expr, &str); 6] = [
            (syn::parse_quote!("item"), "item"),
            (syn::parse_quote!(42), "42"),
            (syn::parse_quote!(true), "true"),
            (syn::parse_quote!('x'), "x"),
            (syn::parse_quote!(-7), "-7"),
            (syn::parse_quote!((9)), "9"),
        ];

        for (expr, expected) in cases {
            assert_eq!(static_key_suffix(&expr).as_deref(), Some(expected));
        }
    }

    #[test]
    fn static_key_suffix_keeps_dynamic_values_at_runtime() {
        let dynamic: syn::Expr = syn::parse_quote!(item.id);
        let non_integer_negative: syn::Expr = syn::parse_quote!(-1.5);

        assert_eq!(static_key_suffix(&dynamic), None);
        assert_eq!(static_key_suffix(&non_integer_negative), None);
    }
}