typst_ide/
analyze.rs

1use comemo::Track;
2use ecow::{EcoString, EcoVec, eco_vec};
3use rustc_hash::FxHashSet;
4use typst::foundations::{Label, Styles, Value};
5use typst::layout::PagedDocument;
6use typst::model::{BibliographyElem, FigureElem};
7use typst::syntax::{LinkedNode, SyntaxKind, ast};
8
9use crate::IdeWorld;
10
11/// Try to determine a set of possible values for an expression.
12pub fn analyze_expr(
13    world: &dyn IdeWorld,
14    node: &LinkedNode,
15) -> EcoVec<(Value, Option<Styles>)> {
16    let Some(expr) = node.cast::<ast::Expr>() else {
17        return eco_vec![];
18    };
19
20    let val = match expr {
21        ast::Expr::None(_) => Value::None,
22        ast::Expr::Auto(_) => Value::Auto,
23        ast::Expr::Bool(v) => Value::Bool(v.get()),
24        ast::Expr::Int(v) => Value::Int(v.get()),
25        ast::Expr::Float(v) => Value::Float(v.get()),
26        ast::Expr::Numeric(v) => Value::numeric(v.get()),
27        ast::Expr::Str(v) => Value::Str(v.get().into()),
28        _ => {
29            if node.kind() == SyntaxKind::Contextual
30                && let Some(child) = node.children().next_back()
31            {
32                return analyze_expr(world, &child);
33            }
34
35            if let Some(parent) = node.parent()
36                && parent.kind() == SyntaxKind::FieldAccess
37                && node.index() > 0
38            {
39                return analyze_expr(world, parent);
40            }
41
42            return typst::trace::<PagedDocument>(world.upcast(), node.span());
43        }
44    };
45
46    eco_vec![(val, None)]
47}
48
49/// Tries to load a module from the given `source` node.
50pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option<Value> {
51    // Use span in the node for resolving imports with relative paths.
52    let source_span = source.span();
53    let (source, _) = analyze_expr(world, source).into_iter().next()?;
54    if source.scope().is_some() {
55        return Some(source);
56    }
57
58    let Value::Str(path) = source else { return None };
59
60    crate::utils::with_engine(world, |engine| {
61        typst_eval::import(engine, &path, source_span).ok().map(Value::Module)
62    })
63}
64
65/// Find all labels and details for them.
66///
67/// Returns:
68/// - All labels and descriptions for them, if available
69/// - A split offset: All labels before this offset belong to nodes, all after
70///   belong to a bibliography.
71///
72/// Note: When multiple labels in the document have the same identifier,
73/// this only returns the first one.
74pub fn analyze_labels(
75    document: &PagedDocument,
76) -> (Vec<(Label, Option<EcoString>)>, usize) {
77    let mut output = vec![];
78    let mut seen_labels = FxHashSet::default();
79
80    // Labels in the document.
81    for elem in document.introspector.all() {
82        let Some(label) = elem.label() else { continue };
83        if !seen_labels.insert(label) {
84            continue;
85        }
86
87        let details = elem
88            .to_packed::<FigureElem>()
89            .and_then(|figure| match figure.caption.as_option() {
90                Some(Some(caption)) => Some(caption.pack_ref()),
91                _ => None,
92            })
93            .unwrap_or(elem)
94            .get_by_name("body")
95            .ok()
96            .and_then(|field| match field {
97                Value::Content(content) => Some(content),
98                _ => None,
99            })
100            .as_ref()
101            .unwrap_or(elem)
102            .plain_text();
103        output.push((label, Some(details)));
104    }
105
106    let split = output.len();
107
108    // Bibliography keys.
109    output.extend(BibliographyElem::keys(document.introspector.track()));
110
111    (output, split)
112}