From d708bdfe2d28cfd80a6658719c22e00c6ec5c0e0 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:12:20 +0800 Subject: [PATCH] feat: complete parameters on user functions (#148) * fix: skip tabs that have no uris for reopening pdf * dev: lift call analysis * feat: complete parameters on user functions --- crates/tinymist-query/src/analysis.rs | 2 + crates/tinymist-query/src/analysis/call.rs | 436 ++++++++++++++++++ crates/tinymist-query/src/analysis/global.rs | 2 +- crates/tinymist-query/src/completion.rs | 6 +- crates/tinymist-query/src/hover.rs | 3 +- crates/tinymist-query/src/inlay_hint.rs | 398 +--------------- .../tinymist-query/src/upstream/complete.rs | 230 +-------- .../src/upstream/complete/ext.rs | 139 +++++- 8 files changed, 605 insertions(+), 611 deletions(-) create mode 100644 crates/tinymist-query/src/analysis/call.rs diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs index 2405bf8b..9f7bcbe7 100644 --- a/crates/tinymist-query/src/analysis.rs +++ b/crates/tinymist-query/src/analysis.rs @@ -1,5 +1,7 @@ //! Semantic static and dynamic analysis of the source code. +pub mod call; +pub use call::*; pub mod def_use; pub use def_use::*; pub mod track_values; diff --git a/crates/tinymist-query/src/analysis/call.rs b/crates/tinymist-query/src/analysis/call.rs new file mode 100644 index 00000000..4277da45 --- /dev/null +++ b/crates/tinymist-query/src/analysis/call.rs @@ -0,0 +1,436 @@ +//! Hybrid analysis for function calls. +use core::fmt; +use std::borrow::Cow; + +use ecow::{eco_format, eco_vec}; +use typst::{ + foundations::{Args, CastInfo, Closure}, + syntax::SyntaxNode, + util::LazyHash, +}; + +use crate::prelude::*; + +/// Describes kind of a parameter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ParamKind { + /// A positional parameter. + Positional, + /// A named parameter. + Named, + /// A rest (spread) parameter. + Rest, +} + +/// Describes a function call parameter. +#[derive(Debug, Clone)] +pub struct CallParamInfo { + /// The parameter's kind. + pub kind: ParamKind, + /// Whether the parameter is a content block. + pub is_content_block: bool, + /// The parameter's specification. + pub param: Arc, + // types: EcoVec<()>, +} + +/// Describes a function call. +#[derive(Debug, Clone)] +pub struct CallInfo { + /// The called function's signature. + pub signature: Arc, + /// The mapping of arguments syntax nodes to their respective parameter + /// info. + pub arg_mapping: HashMap, +} + +/// Analyzes a function call. +#[comemo::memoize] +pub fn analyze_call(func: Func, args: ast::Args<'_>) -> Option> { + Some(Arc::new(analyze_call_no_cache(func, args)?)) +} + +/// Analyzes a function call without caching the result. +pub fn analyze_call_no_cache(func: Func, args: ast::Args<'_>) -> Option { + #[derive(Debug, Clone)] + enum ArgValue<'a> { + Instance(Args), + Instantiating(ast::Args<'a>), + } + + let mut with_args = eco_vec![ArgValue::Instantiating(args)]; + + use typst::foundations::func::Repr; + let mut func = func; + while let Repr::With(f) = func.inner() { + with_args.push(ArgValue::Instance(f.1.clone())); + func = f.0.clone(); + } + + let signature = analyze_signature(func); + trace!("got signature {signature:?}"); + + let mut info = CallInfo { + arg_mapping: HashMap::new(), + signature: signature.clone(), + }; + + enum PosState { + Init, + Pos(usize), + Variadic, + Final, + } + + struct PosBuilder { + state: PosState, + signature: Arc, + } + + impl PosBuilder { + fn advance(&mut self, info: &mut CallInfo, arg: Option) { + let (kind, param) = match self.state { + PosState::Init => { + if !self.signature.pos.is_empty() { + self.state = PosState::Pos(0); + } else if self.signature.rest.is_some() { + self.state = PosState::Variadic; + } else { + self.state = PosState::Final; + } + + return; + } + PosState::Pos(i) => { + if i + 1 < self.signature.pos.len() { + self.state = PosState::Pos(i + 1); + } else if self.signature.rest.is_some() { + self.state = PosState::Variadic; + } else { + self.state = PosState::Final; + } + + (ParamKind::Positional, &self.signature.pos[i]) + } + PosState::Variadic => (ParamKind::Rest, self.signature.rest.as_ref().unwrap()), + PosState::Final => return, + }; + + if let Some(arg) = arg { + // todo: process desugar + let is_content_block = arg.kind() == SyntaxKind::ContentBlock; + info.arg_mapping.insert( + arg, + CallParamInfo { + kind, + is_content_block, + param: param.clone(), + // types: eco_vec![], + }, + ); + } + } + + fn advance_rest(&mut self, info: &mut CallInfo, arg: Option) { + match self.state { + PosState::Init => unreachable!(), + // todo: not precise + PosState::Pos(..) => { + if self.signature.rest.is_some() { + self.state = PosState::Variadic; + } else { + self.state = PosState::Final; + } + } + PosState::Variadic => {} + PosState::Final => return, + }; + + let Some(rest) = self.signature.rest.as_ref() else { + return; + }; + + if let Some(arg) = arg { + // todo: process desugar + let is_content_block = arg.kind() == SyntaxKind::ContentBlock; + info.arg_mapping.insert( + arg, + CallParamInfo { + kind: ParamKind::Rest, + is_content_block, + param: rest.clone(), + // types: eco_vec![], + }, + ); + } + } + } + + let mut pos_builder = PosBuilder { + state: PosState::Init, + signature: signature.clone(), + }; + pos_builder.advance(&mut info, None); + + for arg in with_args.iter().rev() { + match arg { + ArgValue::Instance(args) => { + for _ in args.items.iter().filter(|arg| arg.name.is_none()) { + pos_builder.advance(&mut info, None); + } + } + ArgValue::Instantiating(args) => { + for arg in args.items() { + let arg_tag = arg.to_untyped().clone(); + match arg { + ast::Arg::Named(named) => { + let n = named.name().as_str(); + + if let Some(param) = signature.named.get(n) { + info.arg_mapping.insert( + arg_tag, + CallParamInfo { + kind: ParamKind::Named, + is_content_block: false, + param: param.clone(), + // types: eco_vec![], + }, + ); + } + } + ast::Arg::Pos(..) => { + pos_builder.advance(&mut info, Some(arg_tag)); + } + ast::Arg::Spread(..) => pos_builder.advance_rest(&mut info, Some(arg_tag)), + } + } + } + } + } + + Some(info) +} + +/// Describes a function parameter. +#[derive(Debug, Clone)] +pub struct ParamSpec { + /// The parameter's name. + pub name: Cow<'static, str>, + /// Documentation for the parameter. + pub docs: Cow<'static, str>, + /// Describe what values this parameter accepts. + pub input: CastInfo, + /// The parameter's default name. + pub expr: Option, + /// Creates an instance of the parameter's default value. + pub default: Option Value>, + /// Is the parameter positional? + pub positional: bool, + /// Is the parameter named? + /// + /// Can be true even if `positional` is true if the parameter can be given + /// in both variants. + pub named: bool, + /// Can the parameter be given any number of times? + pub variadic: bool, + /// Is the parameter settable with a set rule? + pub settable: bool, +} + +impl ParamSpec { + fn from_static(s: &ParamInfo) -> Arc { + Arc::new(Self { + name: Cow::Borrowed(s.name), + docs: Cow::Borrowed(s.docs), + input: s.input.clone(), + expr: Some(eco_format!("{}", TypeExpr(&s.input))), + default: s.default, + positional: s.positional, + named: s.named, + variadic: s.variadic, + settable: s.settable, + }) + } +} + +/// Describes a function signature. +#[derive(Debug, Clone)] +pub struct Signature { + /// The positional parameters. + pub pos: Vec>, + /// The named parameters. + pub named: HashMap, Arc>, + /// Whether the function has fill, stroke, or size parameters. + pub has_fill_or_size_or_stroke: bool, + /// The rest parameter. + pub rest: Option>, + _broken: bool, +} + +#[comemo::memoize] +pub(crate) fn analyze_signature(func: Func) -> Arc { + use typst::foundations::func::Repr; + let params = match func.inner() { + Repr::With(..) => unreachable!(), + Repr::Closure(c) => analyze_closure_signature(c.clone()), + Repr::Element(..) | Repr::Native(..) => { + let params = func.params().unwrap(); + params.iter().map(ParamSpec::from_static).collect() + } + }; + + let mut pos = vec![]; + let mut named = HashMap::new(); + let mut rest = None; + let mut broken = false; + let mut has_fill = false; + let mut has_stroke = false; + let mut has_size = false; + + for param in params.into_iter() { + if param.named { + match param.name.as_ref() { + "fill" => { + has_fill = true; + } + "stroke" => { + has_stroke = true; + } + "size" => { + has_size = true; + } + _ => {} + } + named.insert(param.name.clone(), param.clone()); + } + + if param.variadic { + if rest.is_some() { + broken = true; + } else { + rest = Some(param.clone()); + } + } + + if param.positional { + pos.push(param); + } + } + + Arc::new(Signature { + pos, + named, + rest, + has_fill_or_size_or_stroke: has_fill || has_stroke || has_size, + _broken: broken, + }) +} + +fn analyze_closure_signature(c: Arc>) -> Vec> { + let mut params = vec![]; + + trace!("closure signature for: {:?}", c.node.kind()); + + let closure = &c.node; + let closure_ast = match closure.kind() { + SyntaxKind::Closure => closure.cast::().unwrap(), + _ => return params, + }; + + for param in closure_ast.params().children() { + match param { + ast::Param::Pos(ast::Pattern::Placeholder(..)) => { + params.push(Arc::new(ParamSpec { + name: Cow::Borrowed("_"), + input: CastInfo::Any, + expr: None, + default: None, + positional: true, + named: false, + variadic: false, + settable: false, + docs: Cow::Borrowed(""), + })); + } + ast::Param::Pos(e) => { + // todo: destructing + let name = e.bindings(); + if name.len() != 1 { + continue; + } + let name = name[0].as_str(); + + params.push(Arc::new(ParamSpec { + name: Cow::Owned(name.to_owned()), + input: CastInfo::Any, + expr: None, + default: None, + positional: true, + named: false, + variadic: false, + settable: false, + docs: Cow::Borrowed(""), + })); + } + // todo: pattern + ast::Param::Named(n) => { + let expr = unwrap_expr(n.expr()).to_untyped().clone().into_text(); + params.push(Arc::new(ParamSpec { + name: Cow::Owned(n.name().as_str().to_owned()), + input: CastInfo::Any, + expr: Some(expr.clone()), + default: None, + positional: false, + named: true, + variadic: false, + settable: true, + docs: Cow::Owned("Default value: ".to_owned() + expr.as_str()), + })); + } + ast::Param::Spread(n) => { + let ident = n.sink_ident().map(|e| e.as_str()); + params.push(Arc::new(ParamSpec { + name: Cow::Owned(ident.unwrap_or_default().to_owned()), + input: CastInfo::Any, + expr: None, + default: None, + positional: false, + named: true, + variadic: false, + settable: false, + docs: Cow::Borrowed(""), + })); + } + } + } + + params +} + +fn unwrap_expr(mut e: ast::Expr) -> ast::Expr { + while let ast::Expr::Parenthesized(p) = e { + e = p.expr(); + } + + e +} + +struct TypeExpr<'a>(&'a CastInfo); + +impl<'a> fmt::Display for TypeExpr<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self.0 { + CastInfo::Any => "any", + CastInfo::Value(.., v) => v, + CastInfo::Type(v) => { + f.write_str(v.short_name())?; + return Ok(()); + } + CastInfo::Union(v) => { + let mut values = v.iter().map(|e| TypeExpr(e).to_string()); + f.write_str(&values.join(" | "))?; + return Ok(()); + } + }) + } +} diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index 6e55a77e..a6dbbf99 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -115,7 +115,7 @@ impl<'w> AnalysisContext<'w> { } /// Get the world surface for Typst compiler. - pub fn world(&self) -> &dyn World { + pub fn world(&self) -> &'w dyn World { self.resources.world() } diff --git a/crates/tinymist-query/src/completion.rs b/crates/tinymist-query/src/completion.rs index d3d0778b..79683b5e 100644 --- a/crates/tinymist-query/src/completion.rs +++ b/crates/tinymist-query/src/completion.rs @@ -6,7 +6,7 @@ use crate::{ prelude::*, syntax::{get_deref_target, DerefTarget}, typst_to_lsp::completion_kind, - upstream::{autocomplete_, Completion, CompletionContext, CompletionKind}, + upstream::{autocomplete, Completion, CompletionContext, CompletionKind}, LspCompletion, StatefulRequest, }; @@ -111,8 +111,8 @@ impl StatefulRequest for CompletionRequest { } let items = completion_result.or_else(|| { - let cc_ctx = CompletionContext::new(ctx.world(), doc, &source, cursor, explicit)?; - let (offset, mut completions) = autocomplete_(cc_ctx)?; + let cc_ctx = CompletionContext::new(ctx, doc, &source, cursor, explicit)?; + let (offset, mut completions) = autocomplete(cc_ctx)?; let replace_range; if match_ident.as_ref().is_some_and(|i| i.offset() == offset) { diff --git a/crates/tinymist-query/src/hover.rs b/crates/tinymist-query/src/hover.rs index 49fedb8b..48745a72 100644 --- a/crates/tinymist-query/src/hover.rs +++ b/crates/tinymist-query/src/hover.rs @@ -1,7 +1,8 @@ use core::fmt; use crate::{ - analyze_signature, find_definition, + analysis::analyze_signature, + find_definition, prelude::*, syntax::{find_document_before, get_deref_target, LexicalKind, LexicalVarKind}, upstream::{expr_tooltip, tooltip, Tooltip}, diff --git a/crates/tinymist-query/src/inlay_hint.rs b/crates/tinymist-query/src/inlay_hint.rs index 6a084835..616b60b8 100644 --- a/crates/tinymist-query/src/inlay_hint.rs +++ b/crates/tinymist-query/src/inlay_hint.rs @@ -1,16 +1,13 @@ -use core::fmt; -use std::{borrow::Cow, ops::Range}; +use std::ops::Range; -use ecow::{eco_format, eco_vec}; use log::debug; use lsp_types::{InlayHintKind, InlayHintLabel}; -use typst::{ - foundations::{Args, CastInfo, Closure}, - syntax::SyntaxNode, - util::LazyHash, -}; -use crate::{prelude::*, SemanticRequest}; +use crate::{ + analysis::{analyze_call, ParamKind}, + prelude::*, + SemanticRequest, +}; /// Configuration for inlay hints. pub struct InlayHintConfig { @@ -314,361 +311,6 @@ fn inlay_hint( Ok(worker.hints) } -#[derive(Debug, Clone, PartialEq, Eq)] -enum ParamKind { - Positional, - Named, - Rest, -} - -#[derive(Debug, Clone)] -struct CallParamInfo { - kind: ParamKind, - is_content_block: bool, - param: Arc, - // types: EcoVec<()>, -} - -#[derive(Debug, Clone)] -struct CallInfo { - signature: Arc, - arg_mapping: HashMap, -} - -#[comemo::memoize] -fn analyze_call(func: Func, args: ast::Args<'_>) -> Option> { - Some(Arc::new(analyze_call_no_cache(func, args)?)) -} - -fn analyze_call_no_cache(func: Func, args: ast::Args<'_>) -> Option { - #[derive(Debug, Clone)] - enum ArgValue<'a> { - Instance(Args), - Instantiating(ast::Args<'a>), - } - - let mut with_args = eco_vec![ArgValue::Instantiating(args)]; - - use typst::foundations::func::Repr; - let mut func = func; - while let Repr::With(f) = func.inner() { - with_args.push(ArgValue::Instance(f.1.clone())); - func = f.0.clone(); - } - - let signature = analyze_signature(func); - trace!("got signature {signature:?}"); - - let mut info = CallInfo { - arg_mapping: HashMap::new(), - signature: signature.clone(), - }; - - enum PosState { - Init, - Pos(usize), - Variadic, - Final, - } - - struct PosBuilder { - state: PosState, - signature: Arc, - } - - impl PosBuilder { - fn advance(&mut self, info: &mut CallInfo, arg: Option) { - let (kind, param) = match self.state { - PosState::Init => { - if !self.signature.pos.is_empty() { - self.state = PosState::Pos(0); - } else if self.signature.rest.is_some() { - self.state = PosState::Variadic; - } else { - self.state = PosState::Final; - } - - return; - } - PosState::Pos(i) => { - if i + 1 < self.signature.pos.len() { - self.state = PosState::Pos(i + 1); - } else if self.signature.rest.is_some() { - self.state = PosState::Variadic; - } else { - self.state = PosState::Final; - } - - (ParamKind::Positional, &self.signature.pos[i]) - } - PosState::Variadic => (ParamKind::Rest, self.signature.rest.as_ref().unwrap()), - PosState::Final => return, - }; - - if let Some(arg) = arg { - // todo: process desugar - let is_content_block = arg.kind() == SyntaxKind::ContentBlock; - info.arg_mapping.insert( - arg, - CallParamInfo { - kind, - is_content_block, - param: param.clone(), - // types: eco_vec![], - }, - ); - } - } - - fn advance_rest(&mut self, info: &mut CallInfo, arg: Option) { - match self.state { - PosState::Init => unreachable!(), - // todo: not precise - PosState::Pos(..) => { - if self.signature.rest.is_some() { - self.state = PosState::Variadic; - } else { - self.state = PosState::Final; - } - } - PosState::Variadic => {} - PosState::Final => return, - }; - - let Some(rest) = self.signature.rest.as_ref() else { - return; - }; - - if let Some(arg) = arg { - // todo: process desugar - let is_content_block = arg.kind() == SyntaxKind::ContentBlock; - info.arg_mapping.insert( - arg, - CallParamInfo { - kind: ParamKind::Rest, - is_content_block, - param: rest.clone(), - // types: eco_vec![], - }, - ); - } - } - } - - let mut pos_builder = PosBuilder { - state: PosState::Init, - signature: signature.clone(), - }; - pos_builder.advance(&mut info, None); - - for arg in with_args.iter().rev() { - match arg { - ArgValue::Instance(args) => { - for _ in args.items.iter().filter(|arg| arg.name.is_none()) { - pos_builder.advance(&mut info, None); - } - } - ArgValue::Instantiating(args) => { - for arg in args.items() { - let arg_tag = arg.to_untyped().clone(); - match arg { - ast::Arg::Named(named) => { - let n = named.name().as_str(); - - if let Some(param) = signature.named.get(n) { - info.arg_mapping.insert( - arg_tag, - CallParamInfo { - kind: ParamKind::Named, - is_content_block: false, - param: param.clone(), - // types: eco_vec![], - }, - ); - } - } - ast::Arg::Pos(..) => { - pos_builder.advance(&mut info, Some(arg_tag)); - } - ast::Arg::Spread(..) => pos_builder.advance_rest(&mut info, Some(arg_tag)), - } - } - } - } - } - - Some(info) -} - -/// Describes a function parameter. -#[derive(Debug, Clone)] -pub struct ParamSpec { - /// The parameter's name. - pub name: Cow<'static, str>, - /// The parameter's default name. - pub expr: Option, - /// Creates an instance of the parameter's default value. - pub default: Option Value>, - /// Is the parameter positional? - pub positional: bool, - /// Is the parameter named? - /// - /// Can be true even if `positional` is true if the parameter can be given - /// in both variants. - pub named: bool, - /// Can the parameter be given any number of times? - pub variadic: bool, -} - -impl ParamSpec { - fn from_static(s: &ParamInfo) -> Arc { - Arc::new(Self { - name: Cow::Borrowed(s.name), - expr: Some(eco_format!("{}", TypeExpr(&s.input))), - default: s.default, - positional: s.positional, - named: s.named, - variadic: s.variadic, - }) - } -} - -#[derive(Debug, Clone)] -pub(crate) struct Signature { - pub pos: Vec>, - pub named: HashMap, Arc>, - has_fill_or_size_or_stroke: bool, - pub rest: Option>, - _broken: bool, -} - -#[comemo::memoize] -pub(crate) fn analyze_signature(func: Func) -> Arc { - use typst::foundations::func::Repr; - let params = match func.inner() { - Repr::With(..) => unreachable!(), - Repr::Closure(c) => analyze_closure_signature(c.clone()), - Repr::Element(..) | Repr::Native(..) => { - let params = func.params().unwrap(); - params.iter().map(ParamSpec::from_static).collect() - } - }; - - let mut pos = vec![]; - let mut named = HashMap::new(); - let mut rest = None; - let mut broken = false; - let mut has_fill = false; - let mut has_stroke = false; - let mut has_size = false; - - for param in params.into_iter() { - if param.named { - match param.name.as_ref() { - "fill" => { - has_fill = true; - } - "stroke" => { - has_stroke = true; - } - "size" => { - has_size = true; - } - _ => {} - } - named.insert(param.name.clone(), param.clone()); - } - - if param.variadic { - if rest.is_some() { - broken = true; - } else { - rest = Some(param.clone()); - } - } - - if param.positional { - pos.push(param); - } - } - - Arc::new(Signature { - pos, - named, - rest, - has_fill_or_size_or_stroke: has_fill || has_stroke || has_size, - _broken: broken, - }) -} - -fn analyze_closure_signature(c: Arc>) -> Vec> { - let mut params = vec![]; - - trace!("closure signature for: {:?}", c.node.kind()); - - let closure = &c.node; - let closure_ast = match closure.kind() { - SyntaxKind::Closure => closure.cast::().unwrap(), - _ => return params, - }; - - for param in closure_ast.params().children() { - match param { - ast::Param::Pos(ast::Pattern::Placeholder(..)) => { - params.push(Arc::new(ParamSpec { - name: Cow::Borrowed("_"), - expr: None, - default: None, - positional: true, - named: false, - variadic: false, - })); - } - ast::Param::Pos(e) => { - // todo: destructing - let name = e.bindings(); - if name.len() != 1 { - continue; - } - let name = name[0].as_str(); - - params.push(Arc::new(ParamSpec { - name: Cow::Owned(name.to_owned()), - expr: None, - default: None, - positional: true, - named: false, - variadic: false, - })); - } - // todo: pattern - ast::Param::Named(n) => { - params.push(Arc::new(ParamSpec { - name: Cow::Owned(n.name().as_str().to_owned()), - expr: Some(unwrap_expr(n.expr()).to_untyped().clone().into_text()), - default: None, - positional: false, - named: true, - variadic: false, - })); - } - ast::Param::Spread(n) => { - let ident = n.sink_ident().map(|e| e.as_str()); - params.push(Arc::new(ParamSpec { - name: Cow::Owned(ident.unwrap_or_default().to_owned()), - expr: None, - default: None, - positional: false, - named: true, - variadic: false, - })); - } - } - } - - params -} - fn is_one_line(src: &Source, arg_node: &LinkedNode<'_>) -> bool { is_one_line_(src, arg_node).unwrap_or(true) } @@ -681,34 +323,6 @@ fn is_one_line_(src: &Source, arg_node: &LinkedNode<'_>) -> Option { Some(ll == rl) } -fn unwrap_expr(mut e: ast::Expr) -> ast::Expr { - while let ast::Expr::Parenthesized(p) = e { - e = p.expr(); - } - - e -} - -struct TypeExpr<'a>(&'a CastInfo); - -impl<'a> fmt::Display for TypeExpr<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self.0 { - CastInfo::Any => "any", - CastInfo::Value(.., v) => v, - CastInfo::Type(v) => { - f.write_str(v.short_name())?; - return Ok(()); - } - CastInfo::Union(v) => { - let mut values = v.iter().map(|e| TypeExpr(e).to_string()); - f.write_str(&values.join(" | "))?; - return Ok(()); - } - }) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/tinymist-query/src/upstream/complete.rs b/crates/tinymist-query/src/upstream/complete.rs index 5038c139..805e3b94 100644 --- a/crates/tinymist-query/src/upstream/complete.rs +++ b/crates/tinymist-query/src/upstream/complete.rs @@ -1,24 +1,25 @@ use std::cmp::Reverse; -use std::collections::{BTreeSet, HashSet}; +use std::collections::HashSet; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use serde::{Deserialize, Serialize}; use typst::foundations::{ fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, - Repr, Scope, StyleChain, Styles, Type, Value, + Repr, StyleChain, Styles, Type, Value, }; use typst::model::Document; use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind}; use typst::text::RawElem; use typst::visualize::Color; -use typst::World; use unscanny::Scanner; use super::{plain_docs_sentence, summarize_font_family}; use crate::analysis::{analyze_expr, analyze_import, analyze_labels}; +use crate::AnalysisContext; mod ext; +use ext::*; /// Autocomplete a cursor position in a source file. /// @@ -31,30 +32,7 @@ mod ext; /// Passing a `document` (from a previous compilation) is optional, but enhances /// the autocompletions. Label completions, for instance, are only generated /// when the document is available. -pub fn autocomplete( - world: &dyn World, - document: Option<&Document>, - source: &Source, - cursor: usize, - explicit: bool, -) -> Option<(usize, Vec)> { - let mut ctx = CompletionContext::new(world, document, source, cursor, explicit)?; - - let _ = complete_comments(&mut ctx) - || complete_field_accesses(&mut ctx) - || complete_open_labels(&mut ctx) - || complete_imports(&mut ctx) - || complete_rules(&mut ctx) - || complete_params(&mut ctx) - || complete_markup(&mut ctx) - || complete_math(&mut ctx) - || complete_code(&mut ctx); - - Some((ctx.from, ctx.completions)) -} - -pub fn autocomplete_(mut ctx: CompletionContext) -> Option<(usize, Vec)> { - let _ = autocomplete; +pub fn autocomplete(mut ctx: CompletionContext) -> Option<(usize, Vec)> { let _ = complete_comments(&mut ctx) || complete_field_accesses(&mut ctx) || complete_open_labels(&mut ctx) @@ -368,7 +346,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { if prev.is::(); if prev.parent_kind() != Some(SyntaxKind::Markup) || prev.prev_sibling_kind() == Some(SyntaxKind::Hash); - if let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next(); + if let Some((value, styles)) = analyze_expr(ctx.world(), &prev).into_iter().next(); then { ctx.from = ctx.cursor; field_access_completions(ctx, &value, &styles); @@ -383,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { if prev.kind() == SyntaxKind::Dot; if let Some(prev_prev) = prev.prev_sibling(); if prev_prev.is::(); - if let Some((value, styles)) = analyze_expr(ctx.world, &prev_prev).into_iter().next(); + if let Some((value, styles)) = analyze_expr(ctx.world(), &prev_prev).into_iter().next(); then { ctx.from = ctx.leaf.offset(); field_access_completions(ctx, &value, &styles); @@ -552,11 +530,11 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { /// Add completions for all exports of a module. fn import_item_completions<'a>( - ctx: &mut CompletionContext<'a>, + ctx: &mut CompletionContext<'a, '_>, existing: ast::ImportItems<'a>, source: &LinkedNode, ) { - let Some(value) = analyze_import(ctx.world, source) else { + let Some(value) = analyze_import(ctx.world(), source) else { return; }; let Some(scope) = value.scope() else { return }; @@ -742,58 +720,9 @@ fn complete_params(ctx: &mut CompletionContext) -> bool { false } -/// Add completions for the parameters of a function. -fn param_completions<'a>( - ctx: &mut CompletionContext<'a>, - callee: ast::Expr<'a>, - set: bool, - args: ast::Args<'a>, -) { - let Some(func) = resolve_global_callee(ctx, callee) else { - return; - }; - let Some(params) = func.params() else { return }; - - // Exclude named arguments which are already present. - let exclude: Vec<_> = args - .items() - .filter_map(|arg| match arg { - ast::Arg::Named(named) => Some(named.name()), - _ => None, - }) - .collect(); - - for param in params { - if exclude.iter().any(|ident| ident.as_str() == param.name) { - continue; - } - - if set && !param.settable { - continue; - } - - if param.named { - ctx.completions.push(Completion { - kind: CompletionKind::Param, - label: param.name.into(), - apply: Some(eco_format!("{}: ${{}}", param.name)), - detail: Some(plain_docs_sentence(param.docs)), - }); - } - - if param.positional { - ctx.cast_completions(¶m.input); - } - } - - if ctx.before.ends_with(',') { - ctx.enrich(" ", ""); - } -} - /// Add completions for the values of a named function parameter. fn named_param_value_completions<'a>( - ctx: &mut CompletionContext<'a>, + ctx: &mut CompletionContext<'a, '_>, callee: ast::Expr<'a>, name: &str, ) { @@ -817,30 +746,6 @@ fn named_param_value_completions<'a>( } } -/// Resolve a callee expression to a global function. -fn resolve_global_callee<'a>( - ctx: &CompletionContext<'a>, - callee: ast::Expr<'a>, -) -> Option<&'a Func> { - let value = match callee { - ast::Expr::Ident(ident) => ctx.global.get(&ident)?, - ast::Expr::FieldAccess(access) => match access.target() { - ast::Expr::Ident(target) => match ctx.global.get(&target)? { - Value::Module(module) => module.field(&access.field()).ok()?, - Value::Func(func) => func.field(&access.field()).ok()?, - _ => return None, - }, - _ => return None, - }, - _ => return None, - }; - - match value { - Value::Func(func) => Some(func), - _ => None, - } -} - /// Complete in code mode. fn complete_code(ctx: &mut CompletionContext) -> bool { if matches!( @@ -1040,14 +945,13 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) { } /// Context for autocompletion. -pub struct CompletionContext<'a> { - pub world: &'a (dyn World + 'a), +pub struct CompletionContext<'a, 'w> { + pub ctx: &'a mut AnalysisContext<'w>, pub document: Option<&'a Document>, - pub global: &'a Scope, - pub math: &'a Scope, pub text: &'a str, pub before: &'a str, pub after: &'a str, + pub root: LinkedNode<'a>, pub leaf: LinkedNode<'a>, pub cursor: usize, pub explicit: bool, @@ -1056,26 +960,25 @@ pub struct CompletionContext<'a> { pub seen_casts: HashSet, } -impl<'a> CompletionContext<'a> { +impl<'a, 'w> CompletionContext<'a, 'w> { /// Create a new autocompletion context. pub fn new( - world: &'a (dyn World + 'a), + ctx: &'a mut AnalysisContext<'w>, document: Option<&'a Document>, source: &'a Source, cursor: usize, explicit: bool, ) -> Option { let text = source.text(); - let library = world.library(); - let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; + let root = LinkedNode::new(source.root()); + let leaf = root.leaf_at(cursor)?; Some(Self { - world, + ctx, document, - global: library.global.scope(), - math: library.math.scope(), text, before: &text[..cursor], after: &text[cursor..], + root, leaf, cursor, explicit, @@ -1116,7 +1019,7 @@ impl<'a> CompletionContext<'a> { /// Add completions for all font families. fn font_completions(&mut self) { let equation = self.before_window(25).contains("equation"); - for (family, iter) in self.world.book().families() { + for (family, iter) in self.world().book().families() { let detail = summarize_font_family(iter); if !equation || family.contains("Math") { self.value_completion( @@ -1131,7 +1034,7 @@ impl<'a> CompletionContext<'a> { /// Add completions for all available packages. fn package_completions(&mut self, all_versions: bool) { - let mut packages: Vec<_> = self.world.packages().iter().collect(); + let mut packages: Vec<_> = self.world().packages().iter().collect(); packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version))); if !all_versions { packages.dedup_by_key(|(spec, _)| (&spec.namespace, &spec.name)); @@ -1261,7 +1164,7 @@ impl<'a> CompletionContext<'a> { } /// Add completions for a castable. - fn cast_completions(&mut self, cast: &'a CastInfo) { + fn cast_completions(&mut self, cast: &CastInfo) { // Prevent duplicate completions from appearing. if !self.seen_casts.insert(typst::util::hash128(cast)) { return; @@ -1343,93 +1246,4 @@ impl<'a> CompletionContext<'a> { } } } - - /// Add completions for definitions that are available at the cursor. - /// - /// Filters the global/math scope with the given filter. - fn _scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) { - let mut defined = BTreeSet::new(); - - let mut ancestor = Some(self.leaf.clone()); - while let Some(node) = &ancestor { - let mut sibling = Some(node.clone()); - while let Some(node) = &sibling { - if let Some(v) = node.cast::() { - for ident in v.kind().bindings() { - defined.insert(ident.get().clone()); - } - } - - if let Some(v) = node.cast::() { - let imports = v.imports(); - match imports { - None | Some(ast::Imports::Wildcard) => { - if let Some(value) = node - .children() - .find(|child| child.is::()) - .and_then(|source| analyze_import(self.world, &source)) - { - if imports.is_none() { - defined.extend(value.name().map(Into::into)); - } else if let Some(scope) = value.scope() { - for (name, _) in scope.iter() { - defined.insert(name.clone()); - } - } - } - } - Some(ast::Imports::Items(items)) => { - for item in items.iter() { - defined.insert(item.bound_name().get().clone()); - } - } - } - } - - sibling = node.prev_sibling(); - } - - if let Some(parent) = node.parent() { - if let Some(v) = parent.cast::() { - if node.prev_sibling_kind() != Some(SyntaxKind::In) { - let pattern = v.pattern(); - for ident in pattern.bindings() { - defined.insert(ident.get().clone()); - } - } - } - - ancestor = Some(parent.clone()); - continue; - } - - break; - } - - let in_math = matches!( - self.leaf.parent_kind(), - Some(SyntaxKind::Equation) - | Some(SyntaxKind::Math) - | Some(SyntaxKind::MathFrac) - | Some(SyntaxKind::MathAttach) - ); - - let scope = if in_math { self.math } else { self.global }; - for (name, value) in scope.iter() { - if filter(value) && !defined.contains(name) { - self.value_completion(Some(name.clone()), value, parens, None); - } - } - - for name in defined { - if !name.is_empty() { - self.completions.push(Completion { - kind: CompletionKind::Constant, - label: name, - apply: None, - detail: None, - }); - } - } - } } diff --git a/crates/tinymist-query/src/upstream/complete/ext.rs b/crates/tinymist-query/src/upstream/complete/ext.rs index 6827894a..81369563 100644 --- a/crates/tinymist-query/src/upstream/complete/ext.rs +++ b/crates/tinymist-query/src/upstream/complete/ext.rs @@ -1,13 +1,22 @@ use super::{Completion, CompletionContext, CompletionKind}; use std::collections::BTreeMap; -use ecow::EcoString; -use typst::foundations::Value; +use ecow::{eco_format, EcoString}; +use typst::foundations::{Func, Value}; +use typst::syntax::ast::AstNode; use typst::syntax::{ast, SyntaxKind}; -use crate::analysis::analyze_import; +use crate::analysis::{analyze_import, analyze_signature}; +use crate::find_definition; +use crate::prelude::analyze_expr; +use crate::syntax::{get_deref_target, LexicalKind, LexicalVarKind}; +use crate::upstream::plain_docs_sentence; + +impl<'a, 'w> CompletionContext<'a, 'w> { + pub fn world(&self) -> &'w dyn typst::World { + self.ctx.world() + } -impl<'a> CompletionContext<'a> { /// Add completions for definitions that are available at the cursor. /// /// Filters the global/math scope with the given filter. @@ -43,7 +52,7 @@ impl<'a> CompletionContext<'a> { let anaylyze = node.children().find(|child| child.is::()); let analyzed = anaylyze .as_ref() - .and_then(|source| analyze_import(self.world, source)); + .and_then(|source| analyze_import(self.world(), source)); if analyzed.is_none() { log::info!("failed to analyze import: {:?}", anaylyze); } @@ -113,7 +122,10 @@ impl<'a> CompletionContext<'a> { | Some(SyntaxKind::MathAttach) ); - let scope = if in_math { self.math } else { self.global }; + let lib = self.world().library(); + let scope = if in_math { &lib.math } else { &lib.global } + .scope() + .clone(); for (name, value) in scope.iter() { if filter(value) && !defined.contains_key(name) { self.value_completion(Some(name.clone()), value, parens, None); @@ -132,3 +144,118 @@ impl<'a> CompletionContext<'a> { } } } + +/// Add completions for the parameters of a function. +pub fn param_completions<'a>( + ctx: &mut CompletionContext<'a, '_>, + callee: ast::Expr<'a>, + set: bool, + args: ast::Args<'a>, +) { + let Some(func) = resolve_global_callee(ctx, callee) else { + return; + }; + + use typst::foundations::func::Repr; + let mut func = func; + while let Repr::With(f) = func.inner() { + // todo: complete with positional arguments + // with_args.push(ArgValue::Instance(f.1.clone())); + func = f.0.clone(); + } + + let signature = analyze_signature(func.clone()); + + // Exclude named arguments which are already present. + let exclude: Vec<_> = args + .items() + .filter_map(|arg| match arg { + ast::Arg::Named(named) => Some(named.name()), + _ => None, + }) + .collect(); + + for (name, param) in &signature.named { + if exclude.iter().any(|ident| ident.as_str() == name) { + continue; + } + + if set && !param.settable { + continue; + } + + if param.named { + ctx.completions.push(Completion { + kind: CompletionKind::Param, + label: param.name.clone().into(), + apply: Some(eco_format!("{}: ${{}}", param.name)), + detail: Some(plain_docs_sentence(¶m.docs)), + }); + } + + if param.positional { + ctx.cast_completions(¶m.input); + } + } + + if ctx.before.ends_with(',') { + ctx.enrich(" ", ""); + } +} + +/// Resolve a callee expression to a function. +// todo: fallback to static analysis if we can't resolve the callee +pub fn resolve_global_callee<'a>( + ctx: &mut CompletionContext<'a, '_>, + callee: ast::Expr<'a>, +) -> Option { + resolve_global_dyn_callee(ctx, callee) + .or_else(|| { + let source = ctx.ctx.source_by_id(callee.span().id()?).ok()?; + let node = source.find(callee.span())?; + let cursor = node.offset(); + let deref_target = get_deref_target(node, cursor)?; + let def = find_definition(ctx.ctx, source.clone(), deref_target)?; + match def.kind { + LexicalKind::Var(LexicalVarKind::Function) => match def.value { + Some(Value::Func(f)) => Some(f), + _ => None, + }, + _ => None, + } + }) + .or_else(|| { + let lib = ctx.world().library(); + let value = match callee { + ast::Expr::Ident(ident) => lib.global.scope().get(&ident)?, + ast::Expr::FieldAccess(access) => match access.target() { + ast::Expr::Ident(target) => match lib.global.scope().get(&target)? { + Value::Module(module) => module.field(&access.field()).ok()?, + Value::Func(func) => func.field(&access.field()).ok()?, + _ => return None, + }, + _ => return None, + }, + _ => return None, + }; + + match value { + Value::Func(func) => Some(func.clone()), + _ => None, + } + }) +} + +/// Resolve a callee expression to a dynamic function. +// todo: fallback to static analysis if we can't resolve the callee +fn resolve_global_dyn_callee<'a>( + ctx: &CompletionContext<'a, '_>, + callee: ast::Expr<'a>, +) -> Option { + let values = analyze_expr(ctx.world(), &ctx.root.find(callee.span())?); + + values.into_iter().find_map(|v| match v.0 { + Value::Func(f) => Some(f), + _ => None, + }) +}