summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-01-27 12:05:00 +0100
committerLaurenz <laurmaedje@gmail.com>2023-01-27 12:05:12 +0100
commit43ef60c09cc48f6b7c6dd752ab7af7c0d6071bc5 (patch)
tree525320e583fc53e9474fd4b4d4944cdf8117d406 /src
parentc56299c6bde121807c3febbef0766ff2fe2b32f2 (diff)
Tracing-based expression tooltips
Diffstat (limited to 'src')
-rw-r--r--src/ide/analyze.rs38
-rw-r--r--src/ide/mod.rs2
-rw-r--r--src/ide/tooltip.rs74
-rw-r--r--src/lib.rs4
-rw-r--r--src/model/array.rs14
-rw-r--r--src/model/dict.rs2
-rw-r--r--src/model/eval.rs80
-rw-r--r--src/model/func.rs22
-rw-r--r--src/model/methods.rs2
-rw-r--r--src/model/value.rs8
10 files changed, 199 insertions, 47 deletions
diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs
new file mode 100644
index 00000000..d8925cfc
--- /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<Value> {
+ match node.cast::<ast::Expr>() {
+ 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 f52fa076..ac69b38a 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 076e2b45..202efd8e 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<String> {
+pub fn tooltip(
+ world: &(dyn World + 'static),
+ source: &Source,
+ cursor: usize,
+) -> Option<String> {
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<String> {
- if_chain! {
- if let Some(ident) = leaf.cast::<ast::Ident>();
- 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<String> {
+ if !leaf.is::<ast::Expr>() {
+ 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<String> {
+fn named_param_tooltip(
+ world: &(dyn World + 'static),
+ leaf: &LinkedNode,
+) -> Option<String> {
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<String> {
+fn font_family_tooltip(
+ world: &(dyn World + 'static),
+ leaf: &LinkedNode,
+) -> Option<String> {
if_chain! {
// Ensure that we are on top of a string.
if let Some(string) = leaf.cast::<ast::Str>();
diff --git a/src/lib.rs b/src/lib.rs
index e6410d84..4d4cbb8b 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<Document> {
// 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 0071d4f4..be35c651 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<Option<Value>> {
+ pub fn find(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<Value>> {
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<Option<i64>> {
+ pub fn position(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<i64>> {
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<Self> {
+ pub fn filter(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> {
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<Self> {
+ pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> {
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<Value> {
+ pub fn fold(&self, vm: &mut Vm, init: Value, func: Func) -> SourceResult<Value> {
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<bool> {
+ pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> {
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<bool> {
+ pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> {
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 e94db923..76d194a8 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<Array> {
+ pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult<Array> {
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 6a1884eb..50d0ffc3 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<dyn World>,
route: Tracked<Route>,
+ tracer: TrackedMut<Tracer>,
source: &Source,
) -> SourceResult<Module> {
// 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<Span>,
}
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<Value>) {
+ 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<PathBuf> {
@@ -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<Span>,
+ values: Vec<Value>,
+}
+
+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<Span>) -> Self {
+ Self { span, values: vec![] }
+ }
+
+ /// Get the traced values.
+ pub fn finish(self) -> Vec<Value> {
+ self.values
+ }
+}
+
+#[comemo::track]
+impl Tracer {
+ /// The traced span if it is part of the given source file.
+ fn span(&self, id: SourceId) -> Option<Span> {
+ 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 00e59bbd..8cf3ea99 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<Value> {
+ pub fn call(&self, vm: &mut Vm, mut args: Args) -> SourceResult<Value> {
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<Value> {
+ fn call(&self, vm: &mut Vm, this: &Func, args: &mut Args) -> SourceResult<Value> {
// 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 f80839f8..173b95fe 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 15656c42..ba3a550f 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 {