diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs index 162ba8c49..b181dff72 100644 --- a/library/src/compute/foundations.rs +++ b/library/src/compute/foundations.rs @@ -112,6 +112,7 @@ pub fn eval(vm: &Vm, args: &mut Args) -> SourceResult { let Spanned { v: text, span } = args.expect::>("source")?; let source = Source::synthesized(text, span); let route = model::Route::default(); - let module = model::eval(vm.world(), route.track(), &source)?; + let mut tracer = model::Tracer::default(); + let module = model::eval(vm.world(), route.track(), tracer.track_mut(), &source)?; Ok(Value::Content(module.content())) } diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs new file mode 100644 index 000000000..d8925cfca --- /dev/null +++ b/src/ide/analyze.rs @@ -0,0 +1,38 @@ +use comemo::Track; + +use crate::model::{eval, Route, Tracer, Value}; +use crate::syntax::{ast, LinkedNode, SyntaxKind}; +use crate::World; + +/// Try to determine a set of possible values for an expression. +pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { + match node.cast::() { + Some(ast::Expr::Ident(_) | ast::Expr::MathIdent(_)) => { + if let Some(parent) = node.parent() { + if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { + return analyze(world, parent); + } + } + + let span = node.span(); + let source = world.source(span.source()); + let route = Route::default(); + let mut tracer = Tracer::new(Some(span)); + eval(world.track(), route.track(), tracer.track_mut(), source).ok(); + return tracer.finish(); + } + + Some(ast::Expr::FieldAccess(access)) => { + if let Some(child) = node.children().next() { + return analyze(world, &child) + .into_iter() + .filter_map(|target| target.field(&access.field()).ok()) + .collect(); + } + } + + _ => {} + } + + vec![] +} diff --git a/src/ide/mod.rs b/src/ide/mod.rs index f52fa076c..ac69b38a8 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -1,9 +1,11 @@ //! Capabilities for IDE support. +mod analyze; mod complete; mod highlight; mod tooltip; +pub use analyze::*; pub use complete::*; pub use highlight::*; pub use tooltip::*; diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index 076e2b456..202efd8e1 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -1,40 +1,71 @@ use if_chain::if_chain; +use unicode_segmentation::UnicodeSegmentation; -use super::{plain_docs_sentence, summarize_font_family}; -use crate::model::{CastInfo, Value}; +use super::{analyze, plain_docs_sentence, summarize_font_family}; +use crate::model::{CastInfo, Tracer, Value}; use crate::syntax::ast; use crate::syntax::{LinkedNode, Source, SyntaxKind}; use crate::World; /// Describe the item under the cursor. -pub fn tooltip(world: &dyn World, source: &Source, cursor: usize) -> Option { +pub fn tooltip( + world: &(dyn World + 'static), + source: &Source, + cursor: usize, +) -> Option { let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; - function_tooltip(world, &leaf) - .or_else(|| named_param_tooltip(world, &leaf)) + named_param_tooltip(world, &leaf) .or_else(|| font_family_tooltip(world, &leaf)) + .or_else(|| expr_tooltip(world, &leaf)) } -/// Tooltip for a function or set rule name. -fn function_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { - if_chain! { - if let Some(ident) = leaf.cast::(); - if matches!( - leaf.parent_kind(), - Some(SyntaxKind::FuncCall | SyntaxKind::SetRule), - ); - if let Some(Value::Func(func)) = world.library().global.scope().get(&ident); - if let Some(info) = func.info(); - then { - return Some(plain_docs_sentence(info.docs)); +/// Tooltip for a hovered expression. +fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option { + if !leaf.is::() { + return None; + } + + let values = analyze(world, leaf); + if let [value] = values.as_slice() { + if let Some(docs) = value.docs() { + return Some(plain_docs_sentence(docs)); } } - None + let mut tooltip = String::new(); + let mut iter = values.into_iter().enumerate(); + for (i, value) in (&mut iter).take(Tracer::MAX - 1) { + if i > 0 && !tooltip.is_empty() { + tooltip.push_str(", "); + } + let repr = value.repr(); + let repr = repr.as_str(); + let len = repr.len(); + if len <= 40 { + tooltip.push_str(repr); + } else { + let mut graphemes = repr.graphemes(true); + let r = graphemes.next_back().map_or(0, str::len); + let l = graphemes.take(40).map(str::len).sum(); + tooltip.push_str(&repr[..l]); + tooltip.push_str("..."); + tooltip.push_str(&repr[len - r..]); + } + } + + if iter.next().is_some() { + tooltip.push_str(", ..."); + } + + (!tooltip.is_empty()).then(|| tooltip) } /// Tooltips for components of a named parameter. -fn named_param_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { +fn named_param_tooltip( + world: &(dyn World + 'static), + leaf: &LinkedNode, +) -> Option { let (info, named) = if_chain! { // Ensure that we are in a named pair in the arguments to a function // call or set rule. @@ -92,7 +123,10 @@ fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> { } /// Tooltip for font family. -fn font_family_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { +fn font_family_tooltip( + world: &(dyn World + 'static), + leaf: &LinkedNode, +) -> Option { if_chain! { // Ensure that we are on top of a string. if let Some(string) = leaf.cast::(); diff --git a/src/lib.rs b/src/lib.rs index e6410d84d..4d4cbb8b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ pub mod syntax; use std::path::Path; use comemo::{Prehashed, Track}; +use model::Tracer; use crate::diag::{FileResult, SourceResult}; use crate::doc::Document; @@ -63,7 +64,8 @@ use crate::util::Buffer; pub fn compile(world: &(dyn World + 'static), source: &Source) -> SourceResult { // Evaluate the source file into a module. let route = Route::default(); - let module = model::eval(world.track(), route.track(), source)?; + let mut tracer = Tracer::default(); + let module = model::eval(world.track(), route.track(), tracer.track_mut(), source)?; // Typeset the module's contents. model::typeset(world.track(), &module.content()) diff --git a/src/model/array.rs b/src/model/array.rs index 0071d4f41..be35c651c 100644 --- a/src/model/array.rs +++ b/src/model/array.rs @@ -136,7 +136,7 @@ impl Array { } /// Return the first matching element. - pub fn find(&self, vm: &Vm, func: Func) -> SourceResult> { + pub fn find(&self, vm: &mut Vm, func: Func) -> SourceResult> { if func.argc().map_or(false, |count| count != 1) { bail!(func.span(), "function must have exactly one parameter"); } @@ -151,7 +151,7 @@ impl Array { } /// Return the index of the first matching element. - pub fn position(&self, vm: &Vm, func: Func) -> SourceResult> { + pub fn position(&self, vm: &mut Vm, func: Func) -> SourceResult> { if func.argc().map_or(false, |count| count != 1) { bail!(func.span(), "function must have exactly one parameter"); } @@ -167,7 +167,7 @@ impl Array { /// Return a new array with only those elements for which the function /// returns true. - pub fn filter(&self, vm: &Vm, func: Func) -> SourceResult { + pub fn filter(&self, vm: &mut Vm, func: Func) -> SourceResult { if func.argc().map_or(false, |count| count != 1) { bail!(func.span(), "function must have exactly one parameter"); } @@ -182,7 +182,7 @@ impl Array { } /// Transform each item in the array with a function. - pub fn map(&self, vm: &Vm, func: Func) -> SourceResult { + pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult { if func.argc().map_or(false, |count| !(1..=2).contains(&count)) { bail!(func.span(), "function must have one or two parameters"); } @@ -201,7 +201,7 @@ impl Array { } /// Fold all of the array's elements into one with a function. - pub fn fold(&self, vm: &Vm, init: Value, func: Func) -> SourceResult { + pub fn fold(&self, vm: &mut Vm, init: Value, func: Func) -> SourceResult { if func.argc().map_or(false, |count| count != 2) { bail!(func.span(), "function must have exactly two parameters"); } @@ -214,7 +214,7 @@ impl Array { } /// Whether any element matches. - pub fn any(&self, vm: &Vm, func: Func) -> SourceResult { + pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult { if func.argc().map_or(false, |count| count != 1) { bail!(func.span(), "function must have exactly one parameter"); } @@ -229,7 +229,7 @@ impl Array { } /// Whether all elements match. - pub fn all(&self, vm: &Vm, func: Func) -> SourceResult { + pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult { if func.argc().map_or(false, |count| count != 1) { bail!(func.span(), "function must have exactly one parameter"); } diff --git a/src/model/dict.rs b/src/model/dict.rs index e94db9232..76d194a89 100644 --- a/src/model/dict.rs +++ b/src/model/dict.rs @@ -105,7 +105,7 @@ impl Dict { } /// Transform each pair in the dictionary with a function. - pub fn map(&self, vm: &Vm, func: Func) -> SourceResult { + pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult { if func.argc().map_or(false, |count| count != 2) { bail!(func.span(), "function must have exactly two parameters"); } diff --git a/src/model/eval.rs b/src/model/eval.rs index 6a1884eb4..50d0ffc3e 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use std::mem; use std::path::{Path, PathBuf}; -use comemo::{Track, Tracked}; +use comemo::{Track, Tracked, TrackedMut}; use unicode_segmentation::UnicodeSegmentation; use super::{ @@ -17,7 +17,7 @@ use crate::diag::{ use crate::geom::{Abs, Angle, Em, Fr, Ratio}; use crate::syntax::ast::AstNode; use crate::syntax::{ast, Source, SourceId, Span, Spanned, SyntaxKind, SyntaxNode}; -use crate::util::{EcoString, PathExt}; +use crate::util::PathExt; use crate::World; const MAX_ITERATIONS: usize = 10_000; @@ -28,6 +28,7 @@ const MAX_CALL_DEPTH: usize = 256; pub fn eval( world: Tracked, route: Tracked, + tracer: TrackedMut, source: &Source, ) -> SourceResult { // Prevent cyclic evaluation. @@ -44,7 +45,7 @@ pub fn eval( // Evaluate the module. let route = unsafe { Route::insert(route, id) }; let scopes = Scopes::new(Some(library)); - let mut vm = Vm::new(world, route.track(), id, scopes, 0); + let mut vm = Vm::new(world, route.track(), tracer, id, scopes, 0); let result = source.ast()?.eval(&mut vm); // Handle control flow. @@ -68,6 +69,8 @@ pub struct Vm<'a> { pub(super) items: LangItems, /// The route of source ids the VM took to reach its current location. pub(super) route: Tracked<'a, Route>, + /// The tracer for inspection of the values an expression produces. + pub(super) tracer: TrackedMut<'a, Tracer>, /// The current location. pub(super) location: SourceId, /// A control flow event that is currently happening. @@ -76,6 +79,8 @@ pub struct Vm<'a> { pub(super) scopes: Scopes<'a>, /// The current call depth. pub(super) depth: usize, + /// A span that is currently traced. + pub(super) traced: Option, } impl<'a> Vm<'a> { @@ -83,18 +88,22 @@ impl<'a> Vm<'a> { pub(super) fn new( world: Tracked<'a, dyn World>, route: Tracked<'a, Route>, + tracer: TrackedMut<'a, Tracer>, location: SourceId, scopes: Scopes<'a>, depth: usize, ) -> Self { + let traced = tracer.span(location); Self { world, items: world.library().items.clone(), route, + tracer, location, flow: None, scopes, depth, + traced, } } @@ -103,6 +112,15 @@ impl<'a> Vm<'a> { self.world } + /// Define a variable in the current scope. + pub fn define(&mut self, var: ast::Ident, value: impl Into) { + let value = value.into(); + if self.traced == Some(var.span()) { + self.tracer.trace(value.clone()); + } + self.scopes.top.define(var.take(), value); + } + /// Resolve a user-entered path to be relative to the compilation /// environment's root. pub fn locate(&self, path: &str) -> StrResult { @@ -182,6 +200,47 @@ impl Route { } } +/// Traces which values existed for the expression with the given span. +#[derive(Default, Clone)] +pub struct Tracer { + span: Option, + values: Vec, +} + +impl Tracer { + /// The maximum number of traced items. + pub const MAX: usize = 10; + + /// Create a new tracer, possibly with a span under inspection. + pub fn new(span: Option) -> Self { + Self { span, values: vec![] } + } + + /// Get the traced values. + pub fn finish(self) -> Vec { + self.values + } +} + +#[comemo::track] +impl Tracer { + /// The traced span if it is part of the given source file. + fn span(&self, id: SourceId) -> Option { + if self.span.map(Span::source) == Some(id) { + self.span + } else { + None + } + } + + /// Trace a value for the span. + fn trace(&mut self, v: Value) { + if self.values.len() < Self::MAX { + self.values.push(v); + } + } +} + /// Evaluate an expression. pub(super) trait Eval { /// The output of evaluating the expression. @@ -259,12 +318,11 @@ impl Eval for ast::Expr { error!(span, "{} is only allowed directly in code and content blocks", name) }; - match self { + let v = match self { Self::Text(v) => v.eval(vm).map(Value::Content), Self::Space(v) => v.eval(vm).map(Value::Content), Self::Linebreak(v) => v.eval(vm).map(Value::Content), Self::Parbreak(v) => v.eval(vm).map(Value::Content), - Self::Symbol(v) => v.eval(vm).map(Value::Content), Self::Escape(v) => v.eval(vm), Self::Shorthand(v) => v.eval(vm), Self::SmartQuote(v) => v.eval(vm).map(Value::Content), @@ -319,6 +377,8 @@ impl Eval for ast::Expr { }? .spanned(span); + if vm.traced == Some(span) { + vm.tracer.trace(v.clone()); } Ok(v) @@ -1049,7 +1109,7 @@ impl Eval for ast::LetBinding { Some(expr) => expr.eval(vm)?, None => Value::None, }; - vm.scopes.top.define(self.binding().take(), value); + vm.define(self.binding(), value); Ok(Value::None) } } @@ -1183,7 +1243,7 @@ impl Eval for ast::ForLoop { #[allow(unused_parens)] for ($($value),*) in $iter { - $(vm.scopes.top.define($binding.clone(), $value);)* + $(vm.define($binding.clone(), $value);)* let body = self.body(); let value = body.eval(vm)?; @@ -1206,8 +1266,8 @@ impl Eval for ast::ForLoop { let iter = self.iter().eval(vm)?; let pattern = self.pattern(); - let key = pattern.key().map(ast::Ident::take); - let value = pattern.value().take(); + let key = pattern.key(); + let value = pattern.value(); match (key, value, iter) { (None, v, Value::Str(string)) => { @@ -1271,7 +1331,7 @@ impl Eval for ast::ModuleImport { let mut errors = vec![]; for ident in idents { if let Some(value) = module.scope().get(&ident) { - vm.scopes.top.define(ident.take(), value.clone()); + vm.define(ident, value.clone()); } else { errors.push(error!(ident.span(), "unresolved import")); } diff --git a/src/model/func.rs b/src/model/func.rs index 00e59bbd7..8cf3ea991 100644 --- a/src/model/func.rs +++ b/src/model/func.rs @@ -2,11 +2,11 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use comemo::{Track, Tracked}; +use comemo::{Track, Tracked, TrackedMut}; use super::{ Args, CastInfo, Dict, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector, - StyleMap, Value, Vm, + StyleMap, Tracer, Value, Vm, }; use crate::diag::{bail, SourceResult, StrResult}; use crate::syntax::ast::{self, AstNode, Expr}; @@ -110,7 +110,7 @@ impl Func { } /// Call the function with the given arguments. - pub fn call(&self, vm: &Vm, mut args: Args) -> SourceResult { + pub fn call(&self, vm: &mut Vm, mut args: Args) -> SourceResult { let value = match self.0.as_ref() { Repr::Native(native) => (native.func)(vm, &mut args)?, Repr::Closure(closure) => closure.call(vm, self, &mut args)?, @@ -132,8 +132,9 @@ impl Func { let route = Route::default(); let id = SourceId::detached(); let scopes = Scopes::new(None); - let vm = Vm::new(world, route.track(), id, scopes, 0); - self.call(&vm, args) + let mut tracer = Tracer::default(); + let mut vm = Vm::new(world, route.track(), tracer.track_mut(), id, scopes, 0); + self.call(&mut vm, args) } /// Apply the given arguments to the function. @@ -292,7 +293,7 @@ pub(super) struct Closure { impl Closure { /// Call the function in the context with the arguments. - fn call(&self, vm: &Vm, this: &Func, args: &mut Args) -> SourceResult { + fn call(&self, vm: &mut Vm, this: &Func, args: &mut Args) -> SourceResult { // Don't leak the scopes from the call site. Instead, we use the scope // of captured variables we collected earlier. let mut scopes = Scopes::new(None); @@ -327,7 +328,14 @@ impl Closure { let route = if detached { fresh.track() } else { vm.route }; // Evaluate the body. - let mut sub = Vm::new(vm.world, route, self.location, scopes, vm.depth + 1); + let mut sub = Vm::new( + vm.world, + route, + TrackedMut::reborrow_mut(&mut vm.tracer), + self.location, + scopes, + vm.depth + 1, + ); let result = self.body.eval(&mut sub); // Handle control flow. diff --git a/src/model/methods.rs b/src/model/methods.rs index f80839f8e..173b95fea 100644 --- a/src/model/methods.rs +++ b/src/model/methods.rs @@ -7,7 +7,7 @@ use crate::util::EcoString; /// Call a method on a value. pub fn call( - vm: &Vm, + vm: &mut Vm, value: Value, method: &str, mut args: Args, diff --git a/src/model/value.rs b/src/model/value.rs index 15656c424..ba3a550f1 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -155,6 +155,14 @@ impl Value { _ => self.display(), } } + + /// Try to extract documentation for the value. + pub fn docs(&self) -> Option<&'static str> { + match self { + Self::Func(func) => func.info().map(|info| info.docs), + _ => None, + } + } } impl Default for Value { diff --git a/tests/src/benches.rs b/tests/src/benches.rs index e76f3c767..997d3b40c 100644 --- a/tests/src/benches.rs +++ b/tests/src/benches.rs @@ -60,13 +60,29 @@ fn bench_edit(iai: &mut Iai) { fn bench_eval(iai: &mut Iai) { let world = BenchWorld::new(); let route = typst::model::Route::default(); - iai.run(|| typst::model::eval(world.track(), route.track(), &world.source).unwrap()); + let mut tracer = typst::model::Tracer::default(); + iai.run(|| { + typst::model::eval( + world.track(), + route.track(), + tracer.track_mut(), + &world.source, + ) + .unwrap() + }); } fn bench_typeset(iai: &mut Iai) { let world = BenchWorld::new(); let route = typst::model::Route::default(); - let module = typst::model::eval(world.track(), route.track(), &world.source).unwrap(); + let mut tracer = typst::model::Tracer::default(); + let module = typst::model::eval( + world.track(), + route.track(), + tracer.track_mut(), + &world.source, + ) + .unwrap(); let content = module.content(); iai.run(|| typst::model::typeset(world.track(), &content)); } diff --git a/tests/src/tests.rs b/tests/src/tests.rs index ac3e4d85d..7cb1041f2 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -435,7 +435,9 @@ fn test_part( if world.print.model { let world = (world as &dyn World).track(); let route = typst::model::Route::default(); - let module = typst::model::eval(world, route.track(), source).unwrap(); + let mut tracer = typst::model::Tracer::default(); + let module = + typst::model::eval(world, route.track(), tracer.track_mut(), source).unwrap(); println!("Model:\n{:#?}\n", module.content()); }