Skip to main content

typst_ide/
analyze.rs

1use comemo::Track;
2use ecow::{EcoString, EcoVec, eco_vec};
3use rustc_hash::FxHashSet;
4use typst::foundations::{AsOutput, Label, Styles, Value};
5use typst::model::{BibliographyElem, FigureElem};
6use typst::syntax::{LinkedNode, SyntaxKind, ast};
7use typst_layout::PagedDocument;
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                && matches!(
37                    parent.kind(),
38                    SyntaxKind::FieldAccess | SyntaxKind::MathFieldAccess
39                )
40                && node.index() > 0
41            {
42                return analyze_expr(world, parent);
43            }
44
45            return typst::trace::<PagedDocument>(world.upcast(), node.span());
46        }
47    };
48
49    eco_vec![(val, None)]
50}
51
52/// Tries to determine a single value for an expression with tracing, falling
53/// back to a standard library definition, if applicable.
54///
55/// This gives us best-effort results in dead code.
56pub fn analyze_expr_with_fallback(
57    world: &dyn IdeWorld,
58    node: &LinkedNode,
59) -> Option<Value> {
60    if let Some((value, _)) = analyze_expr(world, node).into_iter().next() {
61        return Some(value);
62    }
63
64    let globals = crate::utils::globals(world, node);
65    let value = match node.cast::<ast::Expr>()? {
66        ast::Expr::Ident(ident) => globals.get(&ident)?.read(),
67        ast::Expr::FieldAccess(access) => match access.target() {
68            ast::Expr::Ident(target) => {
69                globals.get(&target)?.read().scope()?.get(&access.field())?.read()
70            }
71            _ => return None,
72        },
73        _ => return None,
74    };
75
76    Some(value.clone())
77}
78
79/// Tries to load a module from the given `source` node.
80pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option<Value> {
81    // Use span in the node for resolving imports with relative paths.
82    let source_span = source.span();
83    let (source, _) = analyze_expr(world, source).into_iter().next()?;
84    if source.scope().is_some() {
85        return Some(source);
86    }
87
88    let Value::Str(path) = source else { return None };
89
90    crate::utils::with_engine(world, |engine| {
91        typst_eval::import(engine, &path, source_span).ok().map(Value::Module)
92    })
93}
94
95/// Find all labels and details for them.
96///
97/// Returns:
98/// - All labels and descriptions for them, if available
99/// - A split offset: All labels before this offset belong to nodes, all after
100///   belong to a bibliography.
101///
102/// Note: When multiple labels in the document have the same identifier,
103/// this only returns the first one.
104pub fn analyze_labels(output: impl AsOutput) -> (Vec<(Label, Option<EcoString>)>, usize) {
105    let introspector = output.as_output().introspector();
106
107    let mut output = vec![];
108    let mut seen_labels = FxHashSet::default();
109
110    // Labels in the document.
111    for elem in introspector.query_labelled() {
112        let Some(label) = elem.label() else { continue };
113        if !seen_labels.insert(label) {
114            continue;
115        }
116
117        let details = elem
118            .to_packed::<FigureElem>()
119            .and_then(|figure| match figure.caption.as_option() {
120                Some(Some(caption)) => Some(caption.pack_ref()),
121                _ => None,
122            })
123            .unwrap_or(&elem)
124            .get_by_name("body")
125            .ok()
126            .and_then(|field| match field {
127                Value::Content(content) => Some(content),
128                _ => None,
129            })
130            .as_ref()
131            .unwrap_or(&elem)
132            .plain_text();
133        output.push((label, Some(details)));
134    }
135
136    let split = output.len();
137
138    // Bibliography keys.
139    output.extend(BibliographyElem::keys(introspector.track()));
140
141    (output, split)
142}