tinymist_analysis/
track_values.rs

1//! Dynamic analysis of an expression or import statement.
2
3use comemo::Track;
4use ecow::*;
5use tinymist_std::typst::{TypstDocument, TypstPagedDocument};
6use typst::World;
7use typst::engine::{Engine, Route, Sink, Traced};
8use typst::foundations::{Context, Label, Scopes, Styles, Value};
9use typst::introspection::Introspector;
10use typst::model::BibliographyElem;
11use typst::syntax::{LinkedNode, Span, SyntaxKind, SyntaxNode, ast};
12use typst_shim::eval::Vm;
13
14use crate::stats::GLOBAL_STATS;
15
16/// Try to determine a set of possible values for an expression.
17pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<(Value, Option<Styles>)> {
18    if let Some(parent) = node.parent()
19        && parent.kind() == SyntaxKind::FieldAccess
20        && node.index() > 0
21    {
22        return analyze_expr(world, parent);
23    }
24
25    analyze_expr_(world, node.get())
26}
27
28/// Try to determine a set of possible values for an expression.
29#[typst_macros::time(span = node.span())]
30pub fn analyze_expr_(world: &dyn World, node: &SyntaxNode) -> EcoVec<(Value, Option<Styles>)> {
31    let Some(expr) = node.cast::<ast::Expr>() else {
32        return eco_vec![];
33    };
34
35    let val = match expr {
36        ast::Expr::None(_) => Value::None,
37        ast::Expr::Auto(_) => Value::Auto,
38        ast::Expr::Bool(v) => Value::Bool(v.get()),
39        ast::Expr::Int(v) => Value::Int(v.get()),
40        ast::Expr::Float(v) => Value::Float(v.get()),
41        ast::Expr::Numeric(v) => Value::numeric(v.get()),
42        ast::Expr::Str(v) => Value::Str(v.get().into()),
43        _ => {
44            if node.kind() == SyntaxKind::Contextual
45                && let Some(child) = node.children().last()
46            {
47                return analyze_expr_(world, child);
48            }
49
50            let _guard = GLOBAL_STATS.stat(node.span().id(), "analyze_expr");
51            return typst::trace::<TypstPagedDocument>(world, node.span());
52        }
53    };
54
55    eco_vec![(val, None)]
56}
57
58/// Try to load a module from the current source file.
59#[typst_macros::time(span = source.span())]
60pub fn analyze_import_(world: &dyn World, source: &SyntaxNode) -> (Option<Value>, Option<Value>) {
61    let source_span = source.span();
62    let Some((source, _)) = analyze_expr_(world, source).into_iter().next() else {
63        return (None, None);
64    };
65    if source.scope().is_some() {
66        return (Some(source.clone()), Some(source));
67    }
68
69    let _guard = GLOBAL_STATS.stat(source_span.id(), "analyze_import");
70
71    let introspector = Introspector::default();
72    let traced = Traced::default();
73    let mut sink = Sink::new();
74    let engine = Engine {
75        routines: &typst::ROUTINES,
76        world: world.track(),
77        route: Route::default(),
78        introspector: introspector.track(),
79        traced: traced.track(),
80        sink: sink.track_mut(),
81    };
82
83    let context = Context::none();
84    let mut vm = Vm::new(
85        engine,
86        context.track(),
87        Scopes::new(Some(world.library())),
88        Span::detached(),
89    );
90    let module = match source.clone() {
91        Value::Str(path) => typst_shim::eval::import(&mut vm.engine, &path, source_span)
92            .ok()
93            .map(Value::Module),
94        Value::Module(module) => Some(Value::Module(module)),
95        _ => None,
96    };
97
98    (Some(source), module)
99}
100
101/// A label with a description and details.
102pub struct DynLabel {
103    /// The label itself.
104    pub label: Label,
105    /// A description of the label.
106    pub label_desc: Option<EcoString>,
107    /// Additional details about the label.
108    pub detail: Option<EcoString>,
109    /// The title of the bibliography entry. Not present for non-bibliography
110    /// labels.
111    pub bib_title: Option<EcoString>,
112}
113
114/// Find all labels and details for them.
115///
116/// Returns:
117/// - All labels and descriptions for them, if available
118/// - A split offset: All labels before this offset belong to nodes, all after
119///   belong to a bibliography.
120#[typst_macros::time]
121pub fn analyze_labels(document: &TypstDocument) -> (Vec<DynLabel>, usize) {
122    let mut output = vec![];
123
124    let _guard = GLOBAL_STATS.stat(None, "analyze_labels");
125
126    // Labels in the document.
127    for elem in document.introspector().all() {
128        let Some(label) = elem.label() else { continue };
129        let (is_derived, details) = {
130            let derived = elem
131                .get_by_name("caption")
132                .or_else(|_| elem.get_by_name("body"));
133
134            match derived {
135                Ok(Value::Content(content)) => (true, content.plain_text()),
136                Ok(Value::Str(s)) => (true, s.into()),
137                Ok(_) => (false, elem.plain_text()),
138                Err(_) => (false, elem.plain_text()),
139            }
140        };
141        output.push(DynLabel {
142            label,
143            label_desc: Some(if is_derived {
144                details.clone()
145            } else {
146                eco_format!("{}(..)", elem.func().name())
147            }),
148            detail: Some(details),
149            bib_title: None,
150        });
151    }
152
153    let split = output.len();
154
155    // Bibliography keys.
156    for (label, detail) in BibliographyElem::keys(document.introspector().track()) {
157        output.push(DynLabel {
158            label,
159            label_desc: detail.clone(),
160            detail: detail.clone(),
161            bib_title: detail,
162        });
163    }
164
165    (output, split)
166}