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