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