From 3955b25a10d56c83552763c0ca42fb137fcefb87 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Mon, 18 Sep 2023 20:51:55 +0800 Subject: [PATCH] Add tooltips to a closure (#2164) --- crates/typst/src/eval/func.rs | 42 ++++++++++++++++++--------------- crates/typst/src/eval/mod.rs | 8 ++++--- crates/typst/src/ide/tooltip.rs | 34 +++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/crates/typst/src/eval/func.rs b/crates/typst/src/eval/func.rs index 3e4ea3704..872373525 100644 --- a/crates/typst/src/eval/func.rs +++ b/crates/typst/src/eval/func.rs @@ -597,15 +597,15 @@ cast! { } /// A visitor that determines which variables to capture for a closure. -pub(super) struct CapturesVisitor<'a> { - external: &'a Scopes<'a>, +pub struct CapturesVisitor<'a> { + external: Option<&'a Scopes<'a>>, internal: Scopes<'a>, captures: Scope, } impl<'a> CapturesVisitor<'a> { /// Create a new visitor for the given external scopes. - pub fn new(external: &'a Scopes) -> Self { + pub fn new(external: Option<&'a Scopes<'a>>) -> Self { Self { external, internal: Scopes::new(None), @@ -626,8 +626,10 @@ impl<'a> CapturesVisitor<'a> { // Identifiers that shouldn't count as captures because they // actually bind a new name are handled below (individually through // the expressions that contain them). - Some(ast::Expr::Ident(ident)) => self.capture(ident), - Some(ast::Expr::MathIdent(ident)) => self.capture_in_math(ident), + Some(ast::Expr::Ident(ident)) => self.capture(&ident, Scopes::get), + Some(ast::Expr::MathIdent(ident)) => { + self.capture(&ident, Scopes::get_in_math) + } // Code and content blocks create a scope. Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { @@ -736,20 +738,22 @@ impl<'a> CapturesVisitor<'a> { } /// Capture a variable if it isn't internal. - fn capture(&mut self, ident: ast::Ident) { - if self.internal.get(&ident).is_err() { - if let Ok(value) = self.external.get(&ident) { - self.captures.define_captured(ident.get().clone(), value.clone()); - } - } - } + #[inline] + fn capture( + &mut self, + ident: &str, + getter: impl FnOnce(&'a Scopes<'a>, &str) -> StrResult<&'a Value>, + ) { + if self.internal.get(ident).is_err() { + let Some(value) = self + .external + .map(|external| getter(external, ident).ok()) + .unwrap_or(Some(&Value::None)) + else { + return; + }; - /// Capture a variable in math mode if it isn't internal. - fn capture_in_math(&mut self, ident: ast::MathIdent) { - if self.internal.get(&ident).is_err() { - if let Ok(value) = self.external.get_in_math(&ident) { - self.captures.define_captured(ident.get().clone(), value.clone()); - } + self.captures.define_captured(ident, value.clone()); } } } @@ -767,7 +771,7 @@ mod tests { scopes.top.define("y", 0); scopes.top.define("z", 0); - let mut visitor = CapturesVisitor::new(&scopes); + let mut visitor = CapturesVisitor::new(Some(&scopes)); let root = parse(text); visitor.visit(&root); diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index cbd00bc12..da0aadd07 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -50,7 +50,9 @@ pub use self::cast::{ pub use self::datetime::Datetime; pub use self::dict::{dict, Dict}; pub use self::duration::Duration; -pub use self::func::{func, Func, NativeFunc, NativeFuncData, ParamInfo}; +pub use self::func::{ + func, CapturesVisitor, Func, NativeFunc, NativeFuncData, ParamInfo, +}; pub use self::library::{set_lang_items, LangItems, Library}; pub use self::module::Module; pub use self::none::NoneValue; @@ -74,7 +76,7 @@ use if_chain::if_chain; use serde::{Deserialize, Serialize}; use unicode_segmentation::UnicodeSegmentation; -use self::func::{CapturesVisitor, Closure}; +use self::func::Closure; use crate::diag::{ bail, error, warning, At, FileError, Hint, SourceDiagnostic, SourceResult, StrResult, Trace, Tracepoint, @@ -1340,7 +1342,7 @@ impl Eval for ast::Closure<'_> { // Collect captured variables. let captured = { - let mut visitor = CapturesVisitor::new(&vm.scopes); + let mut visitor = CapturesVisitor::new(Some(&vm.scopes)); visitor.visit(self.to_untyped()); visitor.finish() }; diff --git a/crates/typst/src/ide/tooltip.rs b/crates/typst/src/ide/tooltip.rs index f310cad0e..6f8c89277 100644 --- a/crates/typst/src/ide/tooltip.rs +++ b/crates/typst/src/ide/tooltip.rs @@ -7,10 +7,11 @@ use if_chain::if_chain; use super::analyze::analyze_labels; use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; use crate::doc::Frame; -use crate::eval::{CastInfo, Tracer, Value}; +use crate::eval::{CapturesVisitor, CastInfo, Tracer, Value}; use crate::geom::{round_2, Length, Numeric}; -use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; -use crate::util::pretty_comma_list; +use crate::syntax::ast::{self, AstNode}; +use crate::syntax::{LinkedNode, Source, SyntaxKind}; +use crate::util::{pretty_comma_list, separated_list}; use crate::World; /// Describe the item under the cursor. @@ -29,6 +30,7 @@ pub fn tooltip( .or_else(|| font_tooltip(world, &leaf)) .or_else(|| ref_tooltip(world, frames, &leaf)) .or_else(|| expr_tooltip(world, &leaf)) + .or_else(|| closure_tooltip(&leaf)) } /// A hover tooltip. @@ -100,6 +102,32 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option Option { + // Find the closure to analyze. + let mut ancestor = leaf; + while !ancestor.is::() { + ancestor = ancestor.parent()?; + } + let closure = ancestor.cast::()?.to_untyped(); + + // Analyze the closure's captures. + let mut visitor = CapturesVisitor::new(None); + visitor.visit(closure); + + let captures = visitor.finish(); + let mut names: Vec<_> = + captures.iter().map(|(name, _)| eco_format!("`{name}`")).collect(); + if names.is_empty() { + return None; + } + + names.sort(); + + let tooltip = separated_list(&names, "and"); + Some(Tooltip::Text(eco_format!("This closure captures {tooltip}."))) +} + /// Tooltip text for a hovered length. fn length_tooltip(length: Length) -> Option { length.em.is_zero().then(|| {