diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs index 010e3a86..82ab16de 100644 --- a/crates/tinymist-query/src/analysis.rs +++ b/crates/tinymist-query/src/analysis.rs @@ -12,8 +12,8 @@ pub mod linked_def; pub use linked_def::*; pub mod signature; pub use signature::*; -pub mod r#type; -pub(crate) use r#type::*; +mod ty; +pub(crate) use ty::*; pub mod track_values; pub use track_values::*; mod prelude; @@ -28,7 +28,7 @@ mod type_check_tests { use typst::syntax::Source; - use crate::analysis::type_check; + use crate::analysis::ty; use crate::tests::*; use super::TypeCheckInfo; @@ -38,7 +38,7 @@ mod type_check_tests { snapshot_testing("type_check", &|ctx, path| { let source = ctx.source_by_path(&path).unwrap(); - let result = type_check(ctx, source.clone()); + let result = ty::type_check(ctx, source.clone()); let result = result .as_deref() .map(|e| format!("{:#?}", TypeCheckSnapshot(&source, e))); diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index c8162406..dd3e353a 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -13,6 +13,7 @@ use reflexo::hash::hash128; use reflexo::{cow_mut::CowMut, debug_loc::DataSource, ImmutPath}; use typst::eval::Eval; use typst::foundations; +use typst::syntax::SyntaxNode; use typst::{ diag::{eco_format, FileError, FileResult, PackageError}, syntax::{package::PackageSpec, Source, Span, VirtualPath}, @@ -21,7 +22,9 @@ use typst::{ use typst::{foundations::Value, syntax::ast, text::Font}; use typst::{layout::Position, syntax::FileId as TypstFileId}; -use super::{DefUseInfo, ImportInfo, Signature, SignatureTarget, TypeCheckInfo}; +use super::{ + DefUseInfo, FlowType, ImportInfo, PathPreference, Signature, SignatureTarget, TypeCheckInfo, +}; use crate::{ lsp_to_typst, syntax::{ @@ -246,7 +249,7 @@ impl AnalysisGlobalCaches { pub fn signature(&self, source: Option, func: &SignatureTarget) -> Option { match func { SignatureTarget::Syntax(node) => { - // todo: performance + // todo: check performance on peeking signature source frequently let cache = self.modules.get(&node.span().id()?)?; if cache .signature_source @@ -276,7 +279,7 @@ impl AnalysisGlobalCaches { match func { SignatureTarget::Syntax(node) => { let cache = self.modules.entry(node.span().id().unwrap()).or_default(); - // todo: performance + // todo: check performance on peeking signature source frequently if cache .signature_source .as_ref() @@ -306,6 +309,7 @@ impl AnalysisGlobalCaches { #[derive(Default)] pub struct AnalysisCaches { modules: HashMap, + completion_files: OnceCell>, root_files: OnceCell>, module_deps: OnceCell>, } @@ -374,15 +378,42 @@ impl<'w> AnalysisContext<'w> { } #[cfg(test)] - pub fn test_files(&mut self, f: impl FnOnce() -> Vec) -> &Vec { - self.caches.root_files.get_or_init(f) + pub fn test_completion_files(&mut self, f: impl FnOnce() -> Vec) { + self.caches.completion_files.get_or_init(f); } - /// Get all the files in the workspace. - pub fn files(&mut self) -> &Vec { + #[cfg(test)] + pub fn test_files(&mut self, f: impl FnOnce() -> Vec) { + self.caches.root_files.get_or_init(f); + } + + /// Get all the source files in the workspace. + pub(crate) fn completion_files(&self, pref: &PathPreference) -> impl Iterator { + let r = pref.ext_matcher(); self.caches - .root_files - .get_or_init(|| scan_workspace_files(&self.analysis.root)) + .completion_files + .get_or_init(|| { + scan_workspace_files( + &self.analysis.root, + PathPreference::Special.ext_matcher(), + |relative_path| relative_path.to_owned(), + ) + }) + .iter() + .filter(move |p| { + p.extension() + .and_then(|p| p.to_str()) + .is_some_and(|e| r.is_match(e)) + }) + } + + /// Get all the source files in the workspace. + pub fn source_files(&self) -> &Vec { + self.caches.root_files.get_or_init(|| { + self.completion_files(&PathPreference::Source) + .map(|p| TypstFileId::new(None, VirtualPath::new(p.as_path()))) + .collect() + }) } /// Get the module dependencies of the workspace. @@ -492,7 +523,7 @@ impl<'w> AnalysisContext<'w> { let tl = cache.type_check.clone(); let res = tl .compute(source, |_before, after| { - let next = crate::analysis::type_check(self, after); + let next = crate::analysis::ty::type_check(self, after); next.or_else(|| tl.output.read().clone()) }) .ok() @@ -586,7 +617,7 @@ impl<'w> AnalysisContext<'w> { f(&mut vm) } - pub(crate) fn mini_eval(&self, rr: ast::Expr<'_>) -> Option { + pub(crate) fn const_eval(&self, rr: ast::Expr<'_>) -> Option { Some(match rr { ast::Expr::None(_) => Value::None, ast::Expr::Auto(_) => Value::Auto, @@ -595,9 +626,25 @@ impl<'w> AnalysisContext<'w> { ast::Expr::Float(v) => Value::Float(v.get()), ast::Expr::Numeric(v) => Value::numeric(v.get()), ast::Expr::Str(v) => Value::Str(v.get().into()), - e => return self.with_vm(|vm| e.eval(vm).ok()), + _ => return None, }) } + + pub(crate) fn mini_eval(&self, rr: ast::Expr<'_>) -> Option { + self.const_eval(rr) + .or_else(|| self.with_vm(|vm| rr.eval(vm).ok())) + } + + pub(crate) fn type_of(&mut self, rr: &SyntaxNode) -> Option { + self.type_of_span(rr.span()) + } + + pub(crate) fn type_of_span(&mut self, s: Span) -> Option { + let id = s.id()?; + let source = self.source_by_id(id).ok()?; + let ty_chk = self.type_check(source)?; + ty_chk.mapping.get(&s).cloned() + } } /// The context for searching in the workspace. diff --git a/crates/tinymist-query/src/analysis/linked_def.rs b/crates/tinymist-query/src/analysis/linked_def.rs index 5cb24094..8ea44772 100644 --- a/crates/tinymist-query/src/analysis/linked_def.rs +++ b/crates/tinymist-query/src/analysis/linked_def.rs @@ -68,6 +68,10 @@ pub fn find_definition( name_range: None, }); } + // todo: label, reference + DerefTarget::Label(..) | DerefTarget::Ref(..) | DerefTarget::Normal(..) => { + return None; + } }; // syntactic definition diff --git a/crates/tinymist-query/src/analysis/type.rs b/crates/tinymist-query/src/analysis/ty.rs similarity index 76% rename from crates/tinymist-query/src/analysis/type.rs rename to crates/tinymist-query/src/analysis/ty.rs index 4731a9bb..d8a513a7 100644 --- a/crates/tinymist-query/src/analysis/type.rs +++ b/crates/tinymist-query/src/analysis/ty.rs @@ -1,16 +1,16 @@ //! Top-level evaluation of a source file. -use core::fmt; use std::{ collections::{HashMap, HashSet}, sync::Arc, }; -use ecow::EcoString; +use ecow::{EcoString, EcoVec}; +use once_cell::sync::Lazy; use parking_lot::{Mutex, RwLock}; use reflexo::{hash::hash128, vector::ir::DefId}; use typst::{ - foundations::{CastInfo, Element, Func, ParamInfo, Value}, + foundations::{Func, Value}, syntax::{ ast::{self, AstNode}, LinkedNode, Source, Span, SyntaxKind, @@ -21,405 +21,10 @@ use crate::{analysis::analyze_dyn_signature, AnalysisContext}; use super::{resolve_global_value, DefUseInfo, IdentRef}; -struct RefDebug<'a>(&'a FlowType); - -impl<'a> fmt::Debug for RefDebug<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0 { - FlowType::Var(v) => write!(f, "@{}", v.1), - _ => write!(f, "{:?}", self.0), - } - } -} -#[derive(Debug, Clone, Hash)] -pub(crate) enum FlowUnaryType { - Pos(Box), - Neg(Box), - Not(Box), -} - -impl FlowUnaryType { - pub fn lhs(&self) -> &FlowType { - match self { - FlowUnaryType::Pos(e) => e, - FlowUnaryType::Neg(e) => e, - FlowUnaryType::Not(e) => e, - } - } -} - -#[derive(Debug, Clone, Hash)] -pub(crate) enum FlowBinaryType { - Add(FlowBinaryRepr), - Sub(FlowBinaryRepr), - Mul(FlowBinaryRepr), - Div(FlowBinaryRepr), - And(FlowBinaryRepr), - Or(FlowBinaryRepr), - Eq(FlowBinaryRepr), - Neq(FlowBinaryRepr), - Lt(FlowBinaryRepr), - Leq(FlowBinaryRepr), - Gt(FlowBinaryRepr), - Geq(FlowBinaryRepr), - Assign(FlowBinaryRepr), - In(FlowBinaryRepr), - NotIn(FlowBinaryRepr), - AddAssign(FlowBinaryRepr), - SubAssign(FlowBinaryRepr), - MulAssign(FlowBinaryRepr), - DivAssign(FlowBinaryRepr), -} - -impl FlowBinaryType { - pub fn repr(&self) -> &FlowBinaryRepr { - match self { - FlowBinaryType::Add(r) - | FlowBinaryType::Sub(r) - | FlowBinaryType::Mul(r) - | FlowBinaryType::Div(r) - | FlowBinaryType::And(r) - | FlowBinaryType::Or(r) - | FlowBinaryType::Eq(r) - | FlowBinaryType::Neq(r) - | FlowBinaryType::Lt(r) - | FlowBinaryType::Leq(r) - | FlowBinaryType::Gt(r) - | FlowBinaryType::Geq(r) - | FlowBinaryType::Assign(r) - | FlowBinaryType::In(r) - | FlowBinaryType::NotIn(r) - | FlowBinaryType::AddAssign(r) - | FlowBinaryType::SubAssign(r) - | FlowBinaryType::MulAssign(r) - | FlowBinaryType::DivAssign(r) => r, - } - } -} - -#[derive(Clone, Hash)] -pub(crate) struct FlowBinaryRepr(Box<(FlowType, FlowType)>); - -impl fmt::Debug for FlowBinaryRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // shorter - write!(f, "{:?}, {:?}", RefDebug(&self.0 .0), RefDebug(&self.0 .1)) - } -} - -#[derive(Clone, Hash)] -pub(crate) struct FlowVarStore { - pub lbs: Vec, - pub ubs: Vec, -} - -impl fmt::Debug for FlowVarStore { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // write!(f, "{}", self.name) - // also where - if !self.lbs.is_empty() { - write!(f, " ⪰ {:?}", self.lbs[0])?; - for lb in &self.lbs[1..] { - write!(f, " | {lb:?}")?; - } - } - if !self.ubs.is_empty() { - write!(f, " ⪯ {:?}", self.ubs[0])?; - for ub in &self.ubs[1..] { - write!(f, " & {ub:?}")?; - } - } - Ok(()) - } -} - -#[derive(Clone)] -pub(crate) enum FlowVarKind { - Weak(Arc>), -} - -#[derive(Clone)] -pub(crate) struct FlowVar { - pub name: EcoString, - pub id: DefId, - pub kind: FlowVarKind, -} - -impl std::hash::Hash for FlowVar { - fn hash(&self, state: &mut H) { - 0.hash(state); - self.id.hash(state); - } -} - -impl fmt::Debug for FlowVar { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "@{}", self.name)?; - match &self.kind { - // FlowVarKind::Strong(t) => write!(f, " = {:?}", t), - FlowVarKind::Weak(w) => write!(f, "{w:?}"), - } - } -} - -impl FlowVar { - pub fn name(&self) -> EcoString { - self.name.clone() - } - - pub fn id(&self) -> DefId { - self.id - } - - pub fn get_ref(&self) -> FlowType { - FlowType::Var(Box::new((self.id, self.name.clone()))) - } - - fn ever_be(&self, exp: FlowType) { - match &self.kind { - // FlowVarKind::Strong(_t) => {} - FlowVarKind::Weak(w) => { - let mut w = w.write(); - w.lbs.push(exp.clone()); - } - } - } - - fn as_strong(&mut self, exp: FlowType) { - // self.kind = FlowVarKind::Strong(value); - match &self.kind { - // FlowVarKind::Strong(_t) => {} - FlowVarKind::Weak(w) => { - let mut w = w.write(); - w.lbs.push(exp.clone()); - } - } - } -} - -#[derive(Hash, Clone)] -pub(crate) struct FlowAt(Box<(FlowType, EcoString)>); - -impl fmt::Debug for FlowAt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}.{}", RefDebug(&self.0 .0), self.0 .1) - } -} - -#[derive(Clone, Hash)] -pub(crate) struct FlowArgs { - pub args: Vec, - pub named: Vec<(EcoString, FlowType)>, -} -impl FlowArgs { - fn start_match(&self) -> &[FlowType] { - &self.args - } -} - -impl fmt::Debug for FlowArgs { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use std::fmt::Write; - - f.write_str("&(")?; - if let Some((first, args)) = self.args.split_first() { - write!(f, "{first:?}")?; - for arg in args { - write!(f, "{arg:?}, ")?; - } - } - f.write_char(')') - } -} - -#[derive(Clone, Hash)] -pub(crate) struct FlowSignature { - pub pos: Vec, - pub named: Vec<(EcoString, FlowType)>, - pub rest: Option, - pub ret: FlowType, -} - -impl fmt::Debug for FlowSignature { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("(")?; - if let Some((first, pos)) = self.pos.split_first() { - write!(f, "{first:?}")?; - for p in pos { - write!(f, ", {p:?}")?; - } - } - for (name, ty) in &self.named { - write!(f, ", {name}: {ty:?}")?; - } - if let Some(rest) = &self.rest { - write!(f, ", ...: {rest:?}")?; - } - f.write_str(") -> ")?; - write!(f, "{:?}", self.ret) - } -} - -#[derive(Debug, Clone, Hash)] -pub(crate) enum PathPreference { - None, - Image, - Json, - Yaml, - Xml, - Toml, -} - -#[derive(Debug, Clone, Hash)] -pub(crate) enum FlowBuiltinType { - Args, - Path(PathPreference), -} - -#[derive(Hash, Clone)] -#[allow(clippy::box_collection)] -pub(crate) enum FlowType { - Clause, - Undef, - Content, - Any, - Array, - Dict, - None, - Infer, - FlowNone, - Auto, - Builtin(FlowBuiltinType), - - Args(Box), - Func(Box), - With(Box<(FlowType, Vec)>), - At(FlowAt), - Union(Box>), - Let(Arc), - Var(Box<(DefId, EcoString)>), - Unary(FlowUnaryType), - Binary(FlowBinaryType), - Value(Box), - Element(Element), -} - -impl fmt::Debug for FlowType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - FlowType::Clause => f.write_str("Clause"), - FlowType::Undef => f.write_str("Undef"), - FlowType::Content => f.write_str("Content"), - FlowType::Any => f.write_str("Any"), - FlowType::Array => f.write_str("Array"), - FlowType::Dict => f.write_str("Dict"), - FlowType::None => f.write_str("None"), - FlowType::Infer => f.write_str("Infer"), - FlowType::FlowNone => f.write_str("FlowNone"), - FlowType::Auto => f.write_str("Auto"), - FlowType::Builtin(t) => write!(f, "{t:?}"), - FlowType::Args(a) => write!(f, "&({a:?})"), - FlowType::Func(s) => write!(f, "{s:?}"), - FlowType::With(w) => write!(f, "({:?}).with(..{:?})", w.0, w.1), - FlowType::At(a) => write!(f, "{a:?}"), - FlowType::Union(u) => { - f.write_str("(")?; - if let Some((first, u)) = u.split_first() { - write!(f, "{first:?}")?; - for u in u { - write!(f, " | {u:?}")?; - } - } - f.write_str(")") - } - FlowType::Let(v) => write!(f, "{v:?}"), - FlowType::Var(v) => write!(f, "@{}", v.1), - FlowType::Unary(u) => write!(f, "{u:?}"), - FlowType::Binary(b) => write!(f, "{b:?}"), - FlowType::Value(v) => write!(f, "{v:?}"), - FlowType::Element(e) => write!(f, "{e:?}"), - } - } -} - -impl FlowType { - pub fn from_return_site(f: &Func, c: &'_ CastInfo) -> Option { - use typst::foundations::func::Repr; - match f.inner() { - Repr::Element(e) => return Some(FlowType::Element(*e)), - Repr::Closure(_) => {} - Repr::With(w) => return FlowType::from_return_site(&w.0, c), - Repr::Native(_) => {} - }; - - let ty = match c { - CastInfo::Any => FlowType::Any, - CastInfo::Value(v, _) => FlowType::Value(Box::new(v.clone())), - CastInfo::Type(ty) => FlowType::Value(Box::new(Value::Type(*ty))), - CastInfo::Union(e) => FlowType::Union(Box::new( - e.iter() - .flat_map(|e| Self::from_return_site(f, e)) - .collect(), - )), - }; - - Some(ty) - } - - pub(crate) fn from_param_site(f: &Func, p: &ParamInfo, s: &CastInfo) -> Option { - use typst::foundations::func::Repr; - match f.inner() { - Repr::Element(..) | Repr::Native(..) => match (f.name().unwrap(), p.name) { - ("image", "path") => { - return Some(FlowType::Builtin(FlowBuiltinType::Path( - PathPreference::Image, - ))) - } - ("read", "path") => { - return Some(FlowType::Builtin(FlowBuiltinType::Path( - PathPreference::None, - ))) - } - ("json", "path") => { - return Some(FlowType::Builtin(FlowBuiltinType::Path( - PathPreference::Json, - ))) - } - ("yaml", "path") => { - return Some(FlowType::Builtin(FlowBuiltinType::Path( - PathPreference::Yaml, - ))) - } - ("xml", "path") => { - return Some(FlowType::Builtin(FlowBuiltinType::Path( - PathPreference::Xml, - ))) - } - ("toml", "path") => { - return Some(FlowType::Builtin(FlowBuiltinType::Path( - PathPreference::Toml, - ))) - } - _ => {} - }, - Repr::Closure(_) => {} - Repr::With(w) => return FlowType::from_param_site(&w.0, p, s), - }; - - let ty = match &s { - CastInfo::Any => FlowType::Any, - CastInfo::Value(v, _) => FlowType::Value(Box::new(v.clone())), - CastInfo::Type(ty) => FlowType::Value(Box::new(Value::Type(*ty))), - CastInfo::Union(e) => FlowType::Union(Box::new( - e.iter() - .flat_map(|e| Self::from_param_site(f, p, e)) - .collect(), - )), - }; - - Some(ty) - } -} +mod def; +pub(crate) use def::*; +mod builtin; +pub(crate) use builtin::*; pub(crate) struct TypeCheckInfo { pub vars: HashMap, @@ -472,7 +77,6 @@ pub(crate) fn type_check(ctx: &mut AnalysisContext, source: Source) -> Option TypeChecker<'a, 'w> { return self .ctx .mini_eval(root.cast()?) - .map(Box::new) - .map(FlowType::Value) + .map(|v| (FlowType::Value(Box::new((v, root.span()))))) } SyntaxKind::Parenthesized => return self.check_children(root), SyntaxKind::Array => return self.check_array(root), @@ -676,8 +279,9 @@ impl<'a, 'w> TypeChecker<'a, 'w> { }; let Some(def_id) = self.def_use_info.get_ref(&ident_ref) else { + let s = root.span(); let v = resolve_global_value(self.ctx, root, mode == InterpretMode::Math)?; - return Some(FlowType::Value(Box::new(v))); + return Some(FlowType::Value(Box::new((v, s)))); }; let var = self.info.vars.get(&def_id)?.clone(); @@ -691,16 +295,37 @@ impl<'a, 'w> TypeChecker<'a, 'w> { } fn check_dict(&mut self, root: LinkedNode<'_>) -> Option { - let _dict: ast::Dict = root.cast()?; + let dict: ast::Dict = root.cast()?; - Some(FlowType::Dict) + let mut fields = EcoVec::new(); + + for field in dict.items() { + match field { + ast::DictItem::Named(n) => { + let name = n.name().get().clone(); + let value = self.check_expr_in(n.expr().span(), root.clone()); + fields.push((name, value, n.span())); + } + ast::DictItem::Keyed(k) => { + let key = self.ctx.const_eval(k.key()); + if let Some(Value::Str(key)) = key { + let value = self.check_expr_in(k.expr().span(), root.clone()); + fields.push((key.into(), value, k.span())); + } + } + // todo: var dict union + ast::DictItem::Spread(_s) => {} + } + } + + Some(FlowType::Dict(FlowRecord { fields })) } fn check_unary(&mut self, root: LinkedNode<'_>) -> Option { let unary: ast::Unary = root.cast()?; if let Some(constant) = self.ctx.mini_eval(ast::Expr::Unary(unary)) { - return Some(FlowType::Value(Box::new(constant))); + return Some(FlowType::Value(Box::new((constant, root.span())))); } let op = unary.op(); @@ -719,7 +344,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> { let binary: ast::Binary = root.cast()?; if let Some(constant) = self.ctx.mini_eval(ast::Expr::Binary(binary)) { - return Some(FlowType::Value(Box::new(constant))); + return Some(FlowType::Value(Box::new((constant, root.span())))); } let op = binary.op(); @@ -1036,7 +661,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> { syntax_args: &ast::Args, candidates: &mut Vec, ) -> Option<()> { - // println!("check func callee {callee:?}"); + // log::debug!("check func callee {callee:?}"); match &callee { FlowType::Var(v) => { @@ -1071,23 +696,28 @@ impl<'a, 'w> TypeChecker<'a, 'w> { } } - // println!("check applied {v:?}"); + // log::debug!("check applied {v:?}"); candidates.push(f.ret.clone()); } + FlowType::Dict(_v) => {} // todo: with FlowType::With(_e) => {} FlowType::Args(_e) => {} FlowType::Union(_e) => {} FlowType::Let(_) => {} FlowType::Value(f) => { - if let Value::Func(f) = f.as_ref() { + if let Value::Func(f) = &f.0 { + self.check_apply_runtime(f, args, syntax_args, candidates); + } + } + FlowType::ValueDoc(f) => { + if let Value::Func(f) = &f.0 { self.check_apply_runtime(f, args, syntax_args, candidates); } } FlowType::Array => {} - FlowType::Dict => {} FlowType::Clause => {} FlowType::Undef => {} FlowType::Content => {} @@ -1110,6 +740,17 @@ impl<'a, 'w> TypeChecker<'a, 'w> { } fn constrain(&mut self, lhs: &FlowType, rhs: &FlowType) { + static FLOW_STROKE_DICT_TYPE: Lazy = + Lazy::new(|| FlowType::Dict(FLOW_STROKE_DICT.clone())); + static FLOW_MARGIN_DICT_TYPE: Lazy = + Lazy::new(|| FlowType::Dict(FLOW_MARGIN_DICT.clone())); + static FLOW_INSET_DICT_TYPE: Lazy = + Lazy::new(|| FlowType::Dict(FLOW_INSET_DICT.clone())); + static FLOW_OUTSET_DICT_TYPE: Lazy = + Lazy::new(|| FlowType::Dict(FLOW_OUTSET_DICT.clone())); + static FLOW_RADIUS_DICT_TYPE: Lazy = + Lazy::new(|| FlowType::Dict(FLOW_RADIUS_DICT.clone())); + match (lhs, rhs) { (FlowType::Var(v), FlowType::Var(w)) => { if v.0 .0 == w.0 .0 { @@ -1130,7 +771,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> { } } } - (_, FlowType::Var(v)) => { + (lhs, FlowType::Var(v)) => { let v = self.info.vars.get(&v.0).unwrap(); match &v.kind { FlowVarKind::Weak(v) => { @@ -1139,7 +780,99 @@ impl<'a, 'w> TypeChecker<'a, 'w> { } } } - _ => {} + (FlowType::Union(v), rhs) => { + for e in v.iter() { + self.constrain(e, rhs); + } + } + (lhs, FlowType::Union(v)) => { + for e in v.iter() { + self.constrain(lhs, e); + } + } + (lhs, FlowType::Builtin(FlowBuiltinType::Stroke)) => { + // empty array is also a constructing dict but we can safely ignore it during + // type checking, since no fields are added yet. + if lhs.is_dict() { + self.constrain(lhs, &FLOW_STROKE_DICT_TYPE); + } + } + (FlowType::Builtin(FlowBuiltinType::Stroke), rhs) => { + if rhs.is_dict() { + self.constrain(&FLOW_STROKE_DICT_TYPE, rhs); + } + } + (lhs, FlowType::Builtin(FlowBuiltinType::Margin)) => { + if lhs.is_dict() { + self.constrain(lhs, &FLOW_MARGIN_DICT_TYPE); + } + } + (FlowType::Builtin(FlowBuiltinType::Margin), rhs) => { + if rhs.is_dict() { + self.constrain(&FLOW_MARGIN_DICT_TYPE, rhs); + } + } + (lhs, FlowType::Builtin(FlowBuiltinType::Inset)) => { + if lhs.is_dict() { + self.constrain(lhs, &FLOW_INSET_DICT_TYPE); + } + } + (FlowType::Builtin(FlowBuiltinType::Inset), rhs) => { + if rhs.is_dict() { + self.constrain(&FLOW_INSET_DICT_TYPE, rhs); + } + } + (lhs, FlowType::Builtin(FlowBuiltinType::Outset)) => { + if lhs.is_dict() { + self.constrain(lhs, &FLOW_OUTSET_DICT_TYPE); + } + } + (FlowType::Builtin(FlowBuiltinType::Outset), rhs) => { + if rhs.is_dict() { + self.constrain(&FLOW_OUTSET_DICT_TYPE, rhs); + } + } + (lhs, FlowType::Builtin(FlowBuiltinType::Radius)) => { + if lhs.is_dict() { + self.constrain(lhs, &FLOW_RADIUS_DICT_TYPE); + } + } + (FlowType::Builtin(FlowBuiltinType::Radius), rhs) => { + if rhs.is_dict() { + self.constrain(&FLOW_RADIUS_DICT_TYPE, rhs); + } + } + (FlowType::Dict(lhs), FlowType::Dict(rhs)) => { + for ((key, lhs, sl), (_, rhs, sr)) in lhs.intersect_keys(rhs) { + log::debug!("constrain record item {key} {lhs:?} ⪯ {rhs:?}"); + self.constrain(lhs, rhs); + if !sl.is_detached() { + // todo: intersect/union + self.info.mapping.entry(*sl).or_insert(rhs.clone()); + } + if !sr.is_detached() { + // todo: intersect/union + self.info.mapping.entry(*sr).or_insert(lhs.clone()); + } + } + } + (FlowType::Value(lhs), rhs) => { + log::debug!("constrain value {lhs:?} ⪯ {rhs:?}"); + if !lhs.1.is_detached() { + // todo: intersect/union + self.info.mapping.entry(lhs.1).or_insert(rhs.clone()); + } + } + (lhs, FlowType::Value(rhs)) => { + log::debug!("constrain value {lhs:?} ⪯ {rhs:?}"); + if !rhs.1.is_detached() { + // todo: intersect/union + self.info.mapping.entry(rhs.1).or_insert(lhs.clone()); + } + } + _ => { + log::debug!("constrain {lhs:?} ⪯ {rhs:?}"); + } } } @@ -1161,14 +894,15 @@ impl<'a, 'w> TypeChecker<'a, 'w> { } } FlowType::Func(..) => e, + FlowType::Dict(..) => e, FlowType::With(..) => e, FlowType::Args(..) => e, FlowType::Union(..) => e, FlowType::Let(_) => e, FlowType::Value(..) => e, + FlowType::ValueDoc(..) => e, FlowType::Array => e, - FlowType::Dict => e, FlowType::Clause => e, FlowType::Undef => e, FlowType::Content => e, @@ -1196,7 +930,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> { match primary_type { FlowType::Func(v) => match method_name.as_str() { "with" => { - // println!("check method at args: {v:?}.with({args:?})"); + // log::debug!("check method at args: {v:?}.with({args:?})"); let f = v.as_ref(); let mut pos = f.pos.iter(); @@ -1218,7 +952,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> { _candidates.push(self.partial_apply(f, args)); } "where" => { - // println!("where method at args: {args:?}"); + // log::debug!("where method at args: {args:?}"); } _ => {} }, @@ -1273,11 +1007,14 @@ impl<'a, 'w> TypeChecker<'a, 'w> { .find(|n| n.name().get() == name.as_ref()); if let Some(named_ty) = named_ty { self.constrain(named_in, named_ty); - } - if let Some(syntax_named) = syntax_named { - self.info - .mapping - .insert(syntax_named.span(), named_in.clone()); + if let Some(syntax_named) = syntax_named { + self.info + .mapping + .insert(syntax_named.span(), named_ty.clone()); + self.info + .mapping + .insert(syntax_named.expr().span(), named_ty.clone()); + } } } @@ -1361,6 +1098,11 @@ impl<'a, 'b> TypeSimplifier<'a, 'b> { } self.analyze(&f.ret, pol); } + FlowType::Dict(r) => { + for (_, p, _) in &r.fields { + self.analyze(p, pol); + } + } FlowType::With(w) => { self.analyze(&w.0, pol); for m in &w.1 { @@ -1397,6 +1139,7 @@ impl<'a, 'b> TypeSimplifier<'a, 'b> { } } FlowType::Value(_v) => {} + FlowType::ValueDoc(_v) => {} FlowType::Clause => {} FlowType::Undef => {} FlowType::Content => {} @@ -1408,7 +1151,6 @@ impl<'a, 'b> TypeSimplifier<'a, 'b> { FlowType::Builtin(_) => {} // todo FlowType::Array => {} - FlowType::Dict => {} FlowType::Element(_) => {} } } @@ -1424,7 +1166,7 @@ impl<'a, 'b> TypeSimplifier<'a, 'b> { FlowVarKind::Weak(w) => { let w = w.read(); - // println!("transform var {:?} {pol}", v.0); + // log::debug!("transform var {:?} {pol}", v.0); let mut lbs = Vec::with_capacity(w.lbs.len()); let mut ubs = Vec::with_capacity(w.ubs.len()); @@ -1473,6 +1215,15 @@ impl<'a, 'b> TypeSimplifier<'a, 'b> { ret, })) } + FlowType::Dict(f) => { + let fields = f + .fields + .iter() + .map(|p| (p.0.clone(), self.transform(&p.1, !pol), p.2)) + .collect(); + + FlowType::Dict(FlowRecord { fields }) + } FlowType::With(w) => { let primary = self.transform(&w.0, pol); FlowType::With(Box::new((primary, w.1.clone()))) @@ -1513,8 +1264,8 @@ impl<'a, 'b> TypeSimplifier<'a, 'b> { // todo FlowType::Let(_) => FlowType::Any, FlowType::Array => FlowType::Array, - FlowType::Dict => FlowType::Dict, FlowType::Value(v) => FlowType::Value(v.clone()), + FlowType::ValueDoc(v) => FlowType::ValueDoc(v.clone()), FlowType::Element(v) => FlowType::Element(*v), FlowType::Clause => FlowType::Clause, FlowType::Undef => FlowType::Undef, diff --git a/crates/tinymist-query/src/analysis/ty/builtin.rs b/crates/tinymist-query/src/analysis/ty/builtin.rs new file mode 100644 index 00000000..ab761ec3 --- /dev/null +++ b/crates/tinymist-query/src/analysis/ty/builtin.rs @@ -0,0 +1,321 @@ +use ecow::EcoVec; +use once_cell::sync::Lazy; +use regex::RegexSet; +use typst::{ + foundations::{Func, ParamInfo, Value}, + syntax::Span, +}; + +use super::{FlowRecord, FlowType}; + +#[derive(Debug, Clone, Hash)] +pub(crate) enum PathPreference { + None, + Special, + Source, + Csv, + Image, + Json, + Yaml, + Xml, + Toml, + Bibliography, + RawTheme, + RawSyntax, +} + +impl PathPreference { + pub fn ext_matcher(&self) -> &'static RegexSet { + static SOURCE_REGSET: Lazy = + Lazy::new(|| RegexSet::new([r"^typ$", r"^typc$"]).unwrap()); + static IMAGE_REGSET: Lazy = Lazy::new(|| { + RegexSet::new([ + r"^png$", r"^webp$", r"^jpg$", r"^jpeg$", r"^svg$", r"^svgz$", + ]) + .unwrap() + }); + static JSON_REGSET: Lazy = + Lazy::new(|| RegexSet::new([r"^json$", r"^jsonc$", r"^json5$"]).unwrap()); + static YAML_REGSET: Lazy = + Lazy::new(|| RegexSet::new([r"^yaml$", r"^yml$"]).unwrap()); + static XML_REGSET: Lazy = Lazy::new(|| RegexSet::new([r"^xml$"]).unwrap()); + static TOML_REGSET: Lazy = Lazy::new(|| RegexSet::new([r"^toml$"]).unwrap()); + static CSV_REGSET: Lazy = Lazy::new(|| RegexSet::new([r"^csv$"]).unwrap()); + static BIB_REGSET: Lazy = + Lazy::new(|| RegexSet::new([r"^yaml$", r"^yml$", r"^bib$"]).unwrap()); + static RAW_THEME_REGSET: Lazy = + Lazy::new(|| RegexSet::new([r"^tmTheme$", r"^xml$"]).unwrap()); + static RAW_SYNTAX_REGSET: Lazy = + Lazy::new(|| RegexSet::new([r"^tmLanguage$", r"^sublime-syntax$"]).unwrap()); + static ALL_REGSET: Lazy = Lazy::new(|| RegexSet::new([r".*"]).unwrap()); + static ALL_SPECIAL_REGSET: Lazy = Lazy::new(|| { + RegexSet::new({ + let patterns = SOURCE_REGSET.patterns(); + let patterns = patterns.iter().chain(IMAGE_REGSET.patterns()); + let patterns = patterns.chain(JSON_REGSET.patterns()); + let patterns = patterns.chain(YAML_REGSET.patterns()); + let patterns = patterns.chain(XML_REGSET.patterns()); + let patterns = patterns.chain(TOML_REGSET.patterns()); + let patterns = patterns.chain(CSV_REGSET.patterns()); + let patterns = patterns.chain(BIB_REGSET.patterns()); + let patterns = patterns.chain(RAW_THEME_REGSET.patterns()); + patterns.chain(RAW_SYNTAX_REGSET.patterns()) + }) + .unwrap() + }); + + match self { + PathPreference::None => &ALL_REGSET, + PathPreference::Special => &ALL_SPECIAL_REGSET, + PathPreference::Source => &SOURCE_REGSET, + PathPreference::Csv => &CSV_REGSET, + PathPreference::Image => &IMAGE_REGSET, + PathPreference::Json => &JSON_REGSET, + PathPreference::Yaml => &YAML_REGSET, + PathPreference::Xml => &XML_REGSET, + PathPreference::Toml => &TOML_REGSET, + PathPreference::Bibliography => &BIB_REGSET, + PathPreference::RawTheme => &RAW_THEME_REGSET, + PathPreference::RawSyntax => &RAW_SYNTAX_REGSET, + } + } +} + +pub(in crate::analysis::ty) fn param_mapping(f: &Func, p: &ParamInfo) -> Option { + match (f.name().unwrap(), p.name) { + ("cbor", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::None, + ))), + ("csv", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::Csv, + ))), + ("image", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::Image, + ))), + ("read", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::None, + ))), + ("json", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::Json, + ))), + ("yaml", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::Yaml, + ))), + ("xml", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::Xml, + ))), + ("toml", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::Toml, + ))), + ("raw", "theme") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::RawTheme, + ))), + ("raw", "syntaxes") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::RawSyntax, + ))), + ("bibliography", "path") => Some(FlowType::Builtin(FlowBuiltinType::Path( + PathPreference::Bibliography, + ))), + ("text", "size") => Some(FlowType::Builtin(FlowBuiltinType::TextSize)), + ("text" | "stack", "dir") => Some(FlowType::Builtin(FlowBuiltinType::Dir)), + ("text", "font") => Some(FlowType::Builtin(FlowBuiltinType::TextFont)), + ( + // todo: polygon.regular + "page" | "highlight" | "text" | "path" | "rect" | "ellipse" | "circle" | "polygon" + | "box" | "block" | "table" | "regular", + "fill", + ) => Some(FlowType::Builtin(FlowBuiltinType::Color)), + ( + // todo: table.cell + "table" | "cell" | "block" | "box" | "circle" | "ellipse" | "rect" | "square", + "inset", + ) => Some(FlowType::Builtin(FlowBuiltinType::Inset)), + ("block" | "box" | "circle" | "ellipse" | "rect" | "square", "outset") => { + Some(FlowType::Builtin(FlowBuiltinType::Outset)) + } + ("block" | "box" | "rect" | "square", "radius") => { + Some(FlowType::Builtin(FlowBuiltinType::Radius)) + } + ( + //todo: table.cell, table.hline, table.vline, math.cancel, grid.cell, polygon.regular + "cancel" | "highlight" | "overline" | "strike" | "underline" | "text" | "path" | "rect" + | "ellipse" | "circle" | "polygon" | "box" | "block" | "table" | "line" | "cell" + | "hline" | "vline" | "regular", + "stroke", + ) => Some(FlowType::Builtin(FlowBuiltinType::Stroke)), + ("page", "margin") => Some(FlowType::Builtin(FlowBuiltinType::Margin)), + _ => None, + } +} + +#[derive(Debug, Clone, Hash)] +pub(crate) enum FlowBuiltinType { + Args, + Color, + TextSize, + TextFont, + + Dir, + Length, + Float, + + Stroke, + Margin, + Inset, + Outset, + Radius, + + Path(PathPreference), +} + +fn literally(s: impl FlowBuiltinLiterally) -> FlowType { + s.literally() +} + +trait FlowBuiltinLiterally { + fn literally(&self) -> FlowType; +} + +impl FlowBuiltinLiterally for &str { + fn literally(&self) -> FlowType { + FlowType::Value(Box::new((Value::Str((*self).into()), Span::detached()))) + } +} + +impl FlowBuiltinLiterally for FlowBuiltinType { + fn literally(&self) -> FlowType { + FlowType::Builtin(self.clone()) + } +} + +// separate by middle +macro_rules! flow_builtin_union_inner { + ($literal_kind:expr) => { + literally($literal_kind) + }; + ($($x:expr),+ $(,)?) => { + Vec::from_iter([ + $(flow_builtin_union_inner!($x)),* + ]) + }; +} + +macro_rules! flow_union { + // the first one is string + ($($b:tt)*) => { + FlowType::Union(Box::new(flow_builtin_union_inner!( $($b)* ))) + }; + +} + +macro_rules! flow_record { + ($($name:expr => $ty:expr),* $(,)?) => { + FlowRecord { + fields: EcoVec::from_iter([ + $( + ( + $name.into(), + $ty, + Span::detached(), + ), + )* + ]) + } + }; +} + +use FlowBuiltinType::*; + +pub static FLOW_STROKE_DICT: Lazy = Lazy::new(|| { + flow_record!( + "paint" => literally(Color), + "thickness" => literally(Length), + "cap" => flow_union!("butt", "round", "square"), + "join" => flow_union!("miter", "round", "bevel"), + "dash" => flow_union!( + "solid", + "dotted", + "densely-dotted", + "loosely-dotted", + "dashed", + "densely-dashed", + "loosely-dashed", + "dash-dotted", + "densely-dash-dotted", + "loosely-dash-dotted", + ), + "miter-limit" => literally(Float), + ) +}); + +pub static FLOW_MARGIN_DICT: Lazy = Lazy::new(|| { + flow_record!( + "top" => literally(Length), + "right" => literally(Length), + "bottom" => literally(Length), + "left" => literally(Length), + "inside" => literally(Length), + "outside" => literally(Length), + "x" => literally(Length), + "y" => literally(Length), + "rest" => literally(Length), + ) +}); + +pub static FLOW_INSET_DICT: Lazy = Lazy::new(|| { + flow_record!( + "top" => literally(Length), + "right" => literally(Length), + "bottom" => literally(Length), + "left" => literally(Length), + "x" => literally(Length), + "y" => literally(Length), + "rest" => literally(Length), + ) +}); + +pub static FLOW_OUTSET_DICT: Lazy = Lazy::new(|| { + flow_record!( + "top" => literally(Length), + "right" => literally(Length), + "bottom" => literally(Length), + "left" => literally(Length), + "x" => literally(Length), + "y" => literally(Length), + "rest" => literally(Length), + ) +}); + +pub static FLOW_RADIUS_DICT: Lazy = Lazy::new(|| { + flow_record!( + "top" => literally(Length), + "right" => literally(Length), + "bottom" => literally(Length), + "left" => literally(Length), + "top-left" => literally(Length), + "top-right" => literally(Length), + "bottom-left" => literally(Length), + "bottom-right" => literally(Length), + "rest" => literally(Length), + ) +}); + +// todo bad case: function.with +// todo bad case: function.where +// todo bad case: array.fold +// todo bad case: datetime +// todo bad case: selector +// todo: function signatures, for example: `locate(loc => ...)` + +// todo: numbering/supplement +// todo: grid/table.columns/rows/gutter/column-gutter/row-gutter array of length +// todo: pattern.size array of length +// todo: grid/table.fill/align/stroke/inset can be a function +// todo: math.cancel.angle can be a function +// todo: text.features array/dictionary +// todo: math.mat.augment +// todo: text.lang +// todo: text.region +// todo: text.font array +// todo: stroke.dash can be an array +// todo: csv.row-type can be an array or a dictionary diff --git a/crates/tinymist-query/src/analysis/ty/def.rs b/crates/tinymist-query/src/analysis/ty/def.rs new file mode 100644 index 00000000..04fdf50c --- /dev/null +++ b/crates/tinymist-query/src/analysis/ty/def.rs @@ -0,0 +1,435 @@ +use core::fmt; +use std::sync::Arc; + +use ecow::{EcoString, EcoVec}; +use parking_lot::RwLock; +use reflexo::vector::ir::DefId; +use typst::{ + foundations::{CastInfo, Element, Func, ParamInfo, Value}, + syntax::Span, +}; + +use crate::analysis::ty::param_mapping; + +use super::FlowBuiltinType; + +struct RefDebug<'a>(&'a FlowType); + +impl<'a> fmt::Debug for RefDebug<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + FlowType::Var(v) => write!(f, "@{}", v.1), + _ => write!(f, "{:?}", self.0), + } + } +} + +#[derive(Hash, Clone)] +#[allow(clippy::box_collection)] +pub(crate) enum FlowType { + Clause, + Undef, + Content, + Any, + Array, + None, + Infer, + FlowNone, + Auto, + Builtin(FlowBuiltinType), + Value(Box<(Value, Span)>), + ValueDoc(Box<(Value, &'static str)>), + Element(Element), + + Var(Box<(DefId, EcoString)>), + Func(Box), + Dict(FlowRecord), + With(Box<(FlowType, Vec)>), + Args(Box), + At(FlowAt), + Unary(FlowUnaryType), + Binary(FlowBinaryType), + Union(Box>), + Let(Arc), +} + +impl fmt::Debug for FlowType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FlowType::Clause => f.write_str("Clause"), + FlowType::Undef => f.write_str("Undef"), + FlowType::Content => f.write_str("Content"), + FlowType::Any => f.write_str("Any"), + FlowType::Array => f.write_str("Array"), + FlowType::None => f.write_str("None"), + FlowType::Infer => f.write_str("Infer"), + FlowType::FlowNone => f.write_str("FlowNone"), + FlowType::Auto => f.write_str("Auto"), + FlowType::Builtin(t) => write!(f, "{t:?}"), + FlowType::Args(a) => write!(f, "&({a:?})"), + FlowType::Func(s) => write!(f, "{s:?}"), + FlowType::Dict(r) => write!(f, "{r:?}"), + FlowType::With(w) => write!(f, "({:?}).with(..{:?})", w.0, w.1), + FlowType::At(a) => write!(f, "{a:?}"), + FlowType::Union(u) => { + f.write_str("(")?; + if let Some((first, u)) = u.split_first() { + write!(f, "{first:?}")?; + for u in u { + write!(f, " | {u:?}")?; + } + } + f.write_str(")") + } + FlowType::Let(v) => write!(f, "{v:?}"), + FlowType::Var(v) => write!(f, "@{}", v.1), + FlowType::Unary(u) => write!(f, "{u:?}"), + FlowType::Binary(b) => write!(f, "{b:?}"), + FlowType::Value(v) => write!(f, "{v:?}", v = v.0), + FlowType::ValueDoc(v) => write!(f, "{v:?}"), + FlowType::Element(e) => write!(f, "{e:?}"), + } + } +} + +impl FlowType { + pub fn from_return_site(f: &Func, c: &'_ CastInfo) -> Option { + use typst::foundations::func::Repr; + match f.inner() { + Repr::Element(e) => return Some(FlowType::Element(*e)), + Repr::Closure(_) => {} + Repr::With(w) => return FlowType::from_return_site(&w.0, c), + Repr::Native(_) => {} + }; + + let ty = match c { + CastInfo::Any => FlowType::Any, + CastInfo::Value(v, doc) => FlowType::ValueDoc(Box::new((v.clone(), *doc))), + CastInfo::Type(ty) => FlowType::Value(Box::new((Value::Type(*ty), Span::detached()))), + CastInfo::Union(e) => FlowType::Union(Box::new( + e.iter() + .flat_map(|e| Self::from_return_site(f, e)) + .collect(), + )), + }; + + Some(ty) + } + + pub(crate) fn from_param_site(f: &Func, p: &ParamInfo, s: &CastInfo) -> Option { + use typst::foundations::func::Repr; + match f.inner() { + Repr::Element(..) | Repr::Native(..) => { + if let Some(ty) = param_mapping(f, p) { + return Some(ty); + } + } + Repr::Closure(_) => {} + Repr::With(w) => return FlowType::from_param_site(&w.0, p, s), + }; + + let ty = match &s { + CastInfo::Any => FlowType::Any, + CastInfo::Value(v, doc) => FlowType::ValueDoc(Box::new((v.clone(), *doc))), + CastInfo::Type(ty) => FlowType::Value(Box::new((Value::Type(*ty), Span::detached()))), + CastInfo::Union(e) => FlowType::Union(Box::new( + e.iter() + .flat_map(|e| Self::from_param_site(f, p, e)) + .collect(), + )), + }; + + Some(ty) + } + + pub(crate) fn is_dict(&self) -> bool { + matches!(self, FlowType::Dict(..)) + } +} + +#[derive(Debug, Clone, Hash)] +pub(crate) enum FlowUnaryType { + Pos(Box), + Neg(Box), + Not(Box), +} + +impl FlowUnaryType { + pub fn lhs(&self) -> &FlowType { + match self { + FlowUnaryType::Pos(e) => e, + FlowUnaryType::Neg(e) => e, + FlowUnaryType::Not(e) => e, + } + } +} + +#[derive(Debug, Clone, Hash)] +pub(crate) enum FlowBinaryType { + Add(FlowBinaryRepr), + Sub(FlowBinaryRepr), + Mul(FlowBinaryRepr), + Div(FlowBinaryRepr), + And(FlowBinaryRepr), + Or(FlowBinaryRepr), + Eq(FlowBinaryRepr), + Neq(FlowBinaryRepr), + Lt(FlowBinaryRepr), + Leq(FlowBinaryRepr), + Gt(FlowBinaryRepr), + Geq(FlowBinaryRepr), + Assign(FlowBinaryRepr), + In(FlowBinaryRepr), + NotIn(FlowBinaryRepr), + AddAssign(FlowBinaryRepr), + SubAssign(FlowBinaryRepr), + MulAssign(FlowBinaryRepr), + DivAssign(FlowBinaryRepr), +} + +impl FlowBinaryType { + pub fn repr(&self) -> &FlowBinaryRepr { + match self { + FlowBinaryType::Add(r) + | FlowBinaryType::Sub(r) + | FlowBinaryType::Mul(r) + | FlowBinaryType::Div(r) + | FlowBinaryType::And(r) + | FlowBinaryType::Or(r) + | FlowBinaryType::Eq(r) + | FlowBinaryType::Neq(r) + | FlowBinaryType::Lt(r) + | FlowBinaryType::Leq(r) + | FlowBinaryType::Gt(r) + | FlowBinaryType::Geq(r) + | FlowBinaryType::Assign(r) + | FlowBinaryType::In(r) + | FlowBinaryType::NotIn(r) + | FlowBinaryType::AddAssign(r) + | FlowBinaryType::SubAssign(r) + | FlowBinaryType::MulAssign(r) + | FlowBinaryType::DivAssign(r) => r, + } + } +} + +#[derive(Clone, Hash)] +pub(crate) struct FlowBinaryRepr(pub Box<(FlowType, FlowType)>); + +impl fmt::Debug for FlowBinaryRepr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // shorter + write!(f, "{:?}, {:?}", RefDebug(&self.0 .0), RefDebug(&self.0 .1)) + } +} + +#[derive(Clone, Hash)] +pub(crate) struct FlowVarStore { + pub lbs: Vec, + pub ubs: Vec, +} + +impl fmt::Debug for FlowVarStore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // write!(f, "{}", self.name) + // also where + if !self.lbs.is_empty() { + write!(f, " ⪰ {:?}", self.lbs[0])?; + for lb in &self.lbs[1..] { + write!(f, " | {lb:?}")?; + } + } + if !self.ubs.is_empty() { + write!(f, " ⪯ {:?}", self.ubs[0])?; + for ub in &self.ubs[1..] { + write!(f, " & {ub:?}")?; + } + } + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) enum FlowVarKind { + Weak(Arc>), +} + +#[derive(Clone)] +pub(crate) struct FlowVar { + pub name: EcoString, + pub id: DefId, + pub kind: FlowVarKind, +} + +impl std::hash::Hash for FlowVar { + fn hash(&self, state: &mut H) { + 0.hash(state); + self.id.hash(state); + } +} + +impl fmt::Debug for FlowVar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "@{}", self.name)?; + match &self.kind { + // FlowVarKind::Strong(t) => write!(f, " = {:?}", t), + FlowVarKind::Weak(w) => write!(f, "{w:?}"), + } + } +} + +impl FlowVar { + pub fn name(&self) -> EcoString { + self.name.clone() + } + + pub fn id(&self) -> DefId { + self.id + } + + pub fn get_ref(&self) -> FlowType { + FlowType::Var(Box::new((self.id, self.name.clone()))) + } + + pub fn ever_be(&self, exp: FlowType) { + match &self.kind { + // FlowVarKind::Strong(_t) => {} + FlowVarKind::Weak(w) => { + let mut w = w.write(); + w.lbs.push(exp.clone()); + } + } + } + + pub fn as_strong(&mut self, exp: FlowType) { + // self.kind = FlowVarKind::Strong(value); + match &self.kind { + // FlowVarKind::Strong(_t) => {} + FlowVarKind::Weak(w) => { + let mut w = w.write(); + w.lbs.push(exp.clone()); + } + } + } +} + +#[derive(Hash, Clone)] +pub(crate) struct FlowAt(pub Box<(FlowType, EcoString)>); + +impl fmt::Debug for FlowAt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}.{}", RefDebug(&self.0 .0), self.0 .1) + } +} + +#[derive(Clone, Hash)] +pub(crate) struct FlowArgs { + pub args: Vec, + pub named: Vec<(EcoString, FlowType)>, +} +impl FlowArgs { + pub fn start_match(&self) -> &[FlowType] { + &self.args + } +} + +impl fmt::Debug for FlowArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use std::fmt::Write; + + f.write_str("&(")?; + if let Some((first, args)) = self.args.split_first() { + write!(f, "{first:?}")?; + for arg in args { + write!(f, "{arg:?}, ")?; + } + } + f.write_char(')') + } +} + +#[derive(Clone, Hash)] +pub(crate) struct FlowSignature { + pub pos: Vec, + pub named: Vec<(EcoString, FlowType)>, + pub rest: Option, + pub ret: FlowType, +} + +impl fmt::Debug for FlowSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("(")?; + if let Some((first, pos)) = self.pos.split_first() { + write!(f, "{first:?}")?; + for p in pos { + write!(f, ", {p:?}")?; + } + } + for (name, ty) in &self.named { + write!(f, ", {name}: {ty:?}")?; + } + if let Some(rest) = &self.rest { + write!(f, ", ...: {rest:?}")?; + } + f.write_str(") -> ")?; + write!(f, "{:?}", self.ret) + } +} + +#[derive(Clone, Hash)] +pub(crate) struct FlowRecord { + pub fields: EcoVec<(EcoString, FlowType, Span)>, +} +impl FlowRecord { + pub(crate) fn intersect_keys_enumerate<'a>( + &'a self, + rhs: &'a FlowRecord, + ) -> impl Iterator + 'a { + let mut lhs = self; + let mut rhs = rhs; + + // size optimization + let mut swapped = false; + if lhs.fields.len() < rhs.fields.len() { + swapped = true; + std::mem::swap(&mut lhs, &mut rhs); + } + + lhs.fields + .iter() + .enumerate() + .filter_map(move |(i, (name, _, _))| { + rhs.fields + .iter() + .position(|(name2, _, _)| name == name2) + .map(|j| (i, j)) + }) + .map(move |(i, j)| if swapped { (j, i) } else { (i, j) }) + } + + pub(crate) fn intersect_keys<'a>( + &'a self, + rhs: &'a FlowRecord, + ) -> impl Iterator + 'a + { + self.intersect_keys_enumerate(rhs) + .filter_map(move |(i, j)| { + self.fields + .get(i) + .and_then(|lhs| rhs.fields.get(j).map(|rhs| (lhs, rhs))) + }) + } +} + +impl fmt::Debug for FlowRecord { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("{")?; + if let Some((first, fields)) = self.fields.split_first() { + write!(f, "{name:?}: {ty:?}", name = first.0, ty = first.1)?; + for (name, ty, _) in fields { + write!(f, ", {name:?}: {ty:?}")?; + } + } + f.write_str("}") + } +} diff --git a/crates/tinymist-query/src/completion.rs b/crates/tinymist-query/src/completion.rs index 6f5b2396..574cdd81 100644 --- a/crates/tinymist-query/src/completion.rs +++ b/crates/tinymist-query/src/completion.rs @@ -1,13 +1,11 @@ -use ecow::eco_format; -use lsp_types::{CompletionItem, CompletionList, CompletionTextEdit, InsertTextFormat, TextEdit}; -use reflexo::path::{unix_slash, PathClean}; +use lsp_types::CompletionList; use crate::{ + analysis::{FlowBuiltinType, FlowType}, prelude::*, syntax::{get_deref_target, DerefTarget}, - typst_to_lsp::completion_kind, - upstream::{autocomplete, Completion, CompletionContext, CompletionKind}, - LspCompletion, StatefulRequest, + upstream::{autocomplete, complete_path, CompletionContext}, + StatefulRequest, }; use self::typst_to_lsp::completion; @@ -104,15 +102,47 @@ impl StatefulRequest for CompletionRequest { } Some(DerefTarget::ImportPath(v) | DerefTarget::IncludePath(v)) => { if !v.text().starts_with(r#""@"#) { - completion_result = complete_path(ctx, v, &source, cursor); + completion_result = complete_path( + ctx, + Some(v), + &source, + cursor, + &crate::analysis::PathPreference::Source, + ); } } + Some(DerefTarget::Normal(SyntaxKind::Str, cano_expr)) => { + let parent = cano_expr.parent()?; + if matches!(parent.kind(), SyntaxKind::Named | SyntaxKind::Args) { + let ty_chk = ctx.type_check(source.clone()); + if let Some(ty_chk) = ty_chk { + let ty = ty_chk.mapping.get(&cano_expr.span()); + log::info!("check string ty: {:?}", ty); + if let Some(FlowType::Builtin(FlowBuiltinType::Path(path_filter))) = ty { + completion_result = + complete_path(ctx, Some(cano_expr), &source, cursor, path_filter); + } + } + } + } + // todo: label, reference + Some(DerefTarget::Label(..) | DerefTarget::Ref(..) | DerefTarget::Normal(..)) => {} None => {} } - let items = completion_result.or_else(|| { + let mut completion_items_rest = None; + let is_incomplete = false; + + let mut items = completion_result.or_else(|| { let cc_ctx = CompletionContext::new(ctx, doc, &source, cursor, explicit)?; - let (offset, mut completions) = autocomplete(cc_ctx)?; + let (offset, ic, mut completions, completions_items2) = autocomplete(cc_ctx)?; + if !completions_items2.is_empty() { + completion_items_rest = Some(completions_items2); + } + // todo: define it well, we were needing it because we wanted to do interactive + // path completion, but now we've scanned all the paths at the same time. + // is_incomplete = ic; + let _ = ic; let replace_range; if match_ident.as_ref().is_some_and(|i| i.offset() == offset) { @@ -149,167 +179,24 @@ impl StatefulRequest for CompletionRequest { ) })?; + if let Some(items_rest) = completion_items_rest.as_mut() { + items.append(items_rest); + } + // To response completions in fine-grained manner, we need to mark result as // incomplete. This follows what rust-analyzer does. // https://github.com/rust-lang/rust-analyzer/blob/f5a9250147f6569d8d89334dc9cca79c0322729f/crates/rust-analyzer/src/handlers/request.rs#L940C55-L940C75 Some(CompletionResponse::List(CompletionList { - is_incomplete: true, + is_incomplete, items, })) } } -fn complete_path( - ctx: &AnalysisContext, - v: LinkedNode, - source: &Source, - cursor: usize, -) -> Option> { - let id = source.id(); - if id.package().is_some() { - return None; - } - - let vp = v.cast::()?; - // todo: path escape - let real_content = vp.get(); - let text = v.text(); - let unquoted = &text[1..text.len() - 1]; - if unquoted != real_content { - return None; - } - - let text = source.text(); - let vr = v.range(); - let offset = vr.start + 1; - if cursor < offset || vr.end <= cursor || vr.len() < 2 { - return None; - } - let path = Path::new(&text[offset..cursor]); - let is_abs = path.is_absolute(); - - let src_path = id.vpath(); - let base = src_path.resolve(&ctx.analysis.root)?; - let dst_path = src_path.join(path); - let mut compl_path = dst_path.as_rootless_path(); - if !compl_path.is_dir() { - compl_path = compl_path.parent().unwrap_or(Path::new("")); - } - log::debug!("compl_path: {src_path:?} + {path:?} -> {compl_path:?}"); - - if compl_path.is_absolute() { - log::warn!("absolute path completion is not supported for security consideration {path:?}"); - return None; - } - - let dirs = ctx.analysis.root.join(compl_path); - log::debug!("compl_dirs: {dirs:?}"); - // find directory or files in the path - let mut folder_completions = vec![]; - let mut module_completions = vec![]; - // todo: test it correctly - for entry in dirs.read_dir().ok()? { - let Ok(entry) = entry else { - continue; - }; - let path = entry.path(); - log::trace!("compl_check_path: {path:?}"); - if !path.is_dir() && !path.extension().is_some_and(|ext| ext == "typ") { - continue; - } - if path.is_dir() - && path - .file_name() - .is_some_and(|name| name.to_string_lossy().starts_with('.')) - { - continue; - } - - // diff with root - let path = dirs.join(path); - - // Skip self smartly - if path.clean() == base.clean() { - continue; - } - - let label = if is_abs { - // diff with root - let w = path.strip_prefix(&ctx.analysis.root).ok()?; - eco_format!("/{}", unix_slash(w)) - } else { - let base = base.parent()?; - let w = pathdiff::diff_paths(&path, base)?; - unix_slash(&w).into() - }; - log::debug!("compl_label: {label:?}"); - - if path.is_dir() { - folder_completions.push(Completion { - label, - kind: CompletionKind::Folder, - apply: None, - detail: None, - command: None, - }); - } else { - module_completions.push(Completion { - label, - kind: CompletionKind::Module, - apply: None, - detail: None, - command: None, - }); - } - } - - let rng = offset..vr.end - 1; - let replace_range = ctx.to_lsp_range(rng, source); - - module_completions.sort_by(|a, b| a.label.cmp(&b.label)); - folder_completions.sort_by(|a, b| a.label.cmp(&b.label)); - - let mut sorter = 0; - let digits = (module_completions.len() + folder_completions.len()) - .to_string() - .len(); - let completions = module_completions.into_iter().chain(folder_completions); - Some( - completions - .map(|typst_completion| { - let lsp_snippet = typst_completion - .apply - .as_ref() - .unwrap_or(&typst_completion.label); - let text_edit = - CompletionTextEdit::Edit(TextEdit::new(replace_range, lsp_snippet.to_string())); - - let sort_text = format!("{sorter:0>digits$}"); - sorter += 1; - - let res = LspCompletion { - label: typst_completion.label.to_string(), - kind: Some(completion_kind(typst_completion.kind.clone())), - detail: typst_completion.detail.as_ref().map(String::from), - text_edit: Some(text_edit), - // don't sort me - sort_text: Some(sort_text), - filter_text: Some("".to_owned()), - insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), - ..Default::default() - }; - - log::debug!("compl_res: {res:?}"); - - res - }) - .collect_vec(), - ) -} - #[cfg(test)] mod tests { use insta::with_settings; + use lsp_types::CompletionItem; use super::*; use crate::tests::*; diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@base.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@base.typ.snap index 8d8c54a0..99bcb3b7 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@base.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@base.typ.snap @@ -6,7 +6,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ --- [ { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 7, @@ -249,7 +249,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ ] }, { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 3, diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_params.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_params.typ.snap index aa5fd93c..da209957 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_params.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_params.typ.snap @@ -6,7 +6,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ --- [ { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 7, @@ -249,7 +249,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ ] }, { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 6, diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@item_shadow.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@item_shadow.typ.snap index 1bff28f6..28805d30 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@item_shadow.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@item_shadow.typ.snap @@ -6,7 +6,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/item_shadow.typ --- [ { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 7, @@ -215,7 +215,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/item_shadow.typ ] }, { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 3, diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@let.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@let.typ.snap index dc701b2c..0a0ea467 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@let.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@let.typ.snap @@ -6,7 +6,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ --- [ { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 7, @@ -249,7 +249,7 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ ] }, { - "isIncomplete": true, + "isIncomplete": false, "items": [ { "kind": 6, diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap index cbb62eb1..e422b5b2 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap @@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" input_file: crates/tinymist-query/src/fixtures/hover/builtin_var.typ --- { - "contents": "```typst\nrgb(\"#ff4136\")\n```", + "contents": "```typc\nrgb(\"#ff4136\")\n```", "range": "0:20:0:23" } diff --git a/crates/tinymist-query/src/fixtures/type_check/infer2.typ b/crates/tinymist-query/src/fixtures/type_check/infer2.typ new file mode 100644 index 00000000..87ca0cab --- /dev/null +++ b/crates/tinymist-query/src/fixtures/type_check/infer2.typ @@ -0,0 +1,16 @@ +#text(size: 1pt, font: (), stroke: 1pt, fill: red)[] +#path(fill: red, stroke: red) +#line(angle: 1deg, length: 1pt, stroke: red) +#rect(width: 1pt, height: 1pt, fill: red, stroke: red, radius: 1pt, inset: 1pt, outset: 1pt) +#ellipse(fill: red, stroke: red) +#circle(fill: red, stroke: red) +#box(fill: red, stroke: red) +#block(fill: red, stroke: red) +#table( + fill: red, + stroke: red, + table.hline(stroke: red), + table.vline(stroke: red), +) +#text(stroke: ()) + diff --git a/crates/tinymist-query/src/fixtures/type_check/infer_stroke_dict.typ b/crates/tinymist-query/src/fixtures/type_check/infer_stroke_dict.typ new file mode 100644 index 00000000..c80eca33 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/type_check/infer_stroke_dict.typ @@ -0,0 +1,5 @@ +#text(stroke: ( + paint: black, + thickness: 1pt, +))[] + diff --git a/crates/tinymist-query/src/fixtures/type_check/snaps/test@infer2.typ.snap b/crates/tinymist-query/src/fixtures/type_check/snaps/test@infer2.typ.snap new file mode 100644 index 00000000..2b567dc3 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/type_check/snaps/test@infer2.typ.snap @@ -0,0 +1,73 @@ +--- +source: crates/tinymist-query/src/analysis.rs +expression: result +input_file: crates/tinymist-query/src/fixtures/type_check/infer2.typ +--- +--- +1..52 -> Element(text) +6..15 -> TextSize +12..15 -> TextSize +17..25 -> TextFont +23..25 -> TextFont +27..38 -> Stroke +35..38 -> Stroke +40..49 -> Color +46..49 -> Color +50..52 -> Type(content) +54..82 -> Element(path) +59..68 -> Color +65..68 -> Color +70..81 -> Stroke +78..81 -> Stroke +84..127 -> Element(line) +89..100 -> Type(angle) +96..100 -> Type(angle) +102..113 -> Type(relative length) +110..113 -> Type(relative length) +115..126 -> Stroke +123..126 -> Stroke +129..220 -> Element(rect) +134..144 -> (Type(relative length) | Type(auto)) +141..144 -> (Type(relative length) | Type(auto)) +146..157 -> (Type(relative length) | Type(auto)) +154..157 -> (Type(relative length) | Type(auto)) +159..168 -> Color +165..168 -> Color +170..181 -> Stroke +178..181 -> Stroke +183..194 -> Radius +191..194 -> Radius +196..206 -> Inset +203..206 -> Inset +208..219 -> Outset +216..219 -> Outset +222..253 -> Element(ellipse) +230..239 -> Color +236..239 -> Color +241..252 -> Stroke +249..252 -> Stroke +255..285 -> Element(circle) +262..271 -> Color +268..271 -> Color +273..284 -> Stroke +281..284 -> Stroke +287..314 -> Element(box) +291..300 -> Color +297..300 -> Color +302..313 -> Stroke +310..313 -> Stroke +316..345 -> Element(block) +322..331 -> Color +328..331 -> Color +333..344 -> Stroke +341..344 -> Stroke +347..439 -> Element(table) +356..365 -> Color +362..365 -> Color +369..380 -> Stroke +377..380 -> Stroke +384..408 -> Type(content) +412..436 -> Any +441..457 -> Element(text) +446..456 -> Stroke +454..456 -> Stroke diff --git a/crates/tinymist-query/src/fixtures/type_check/snaps/test@infer_stroke_dict.typ.snap b/crates/tinymist-query/src/fixtures/type_check/snaps/test@infer_stroke_dict.typ.snap new file mode 100644 index 00000000..ede69ba6 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/type_check/snaps/test@infer_stroke_dict.typ.snap @@ -0,0 +1,14 @@ +--- +source: crates/tinymist-query/src/analysis.rs +expression: result +input_file: crates/tinymist-query/src/fixtures/type_check/infer_stroke_dict.typ +--- +--- +1..54 -> Element(text) +6..51 -> Stroke +14..51 -> Stroke +18..30 -> Color +25..30 -> Color +34..48 -> Length +45..48 -> Length +52..54 -> Type(content) diff --git a/crates/tinymist-query/src/lsp_typst_boundary.rs b/crates/tinymist-query/src/lsp_typst_boundary.rs index 88b17479..754677bd 100644 --- a/crates/tinymist-query/src/lsp_typst_boundary.rs +++ b/crates/tinymist-query/src/lsp_typst_boundary.rs @@ -272,11 +272,13 @@ pub mod typst_to_lsp { TypstCompletionKind::Syntax => LspCompletionKind::SNIPPET, TypstCompletionKind::Func => LspCompletionKind::FUNCTION, TypstCompletionKind::Param => LspCompletionKind::VARIABLE, + TypstCompletionKind::Field => LspCompletionKind::FIELD, TypstCompletionKind::Variable => LspCompletionKind::VARIABLE, TypstCompletionKind::Constant => LspCompletionKind::CONSTANT, TypstCompletionKind::Symbol(_) => LspCompletionKind::FIELD, TypstCompletionKind::Type => LspCompletionKind::CLASS, TypstCompletionKind::Module => LspCompletionKind::MODULE, + TypstCompletionKind::File => LspCompletionKind::FILE, TypstCompletionKind::Folder => LspCompletionKind::FOLDER, } } @@ -324,7 +326,7 @@ pub mod typst_to_lsp { let lsp_marked_string = match typst_tooltip { TypstTooltip::Text(text) => MarkedString::String(text.to_string()), TypstTooltip::Code(code) => MarkedString::LanguageString(LanguageString { - language: "typst".to_owned(), + language: "typc".to_owned(), value: code.to_string(), }), }; diff --git a/crates/tinymist-query/src/prelude.rs b/crates/tinymist-query/src/prelude.rs index a022ffc7..351277f4 100644 --- a/crates/tinymist-query/src/prelude.rs +++ b/crates/tinymist-query/src/prelude.rs @@ -20,7 +20,7 @@ pub use lsp_types::{ pub use reflexo::vector::ir::DefId; pub use serde_json::Value as JsonValue; pub use typst::diag::{EcoString, FileError, FileResult, Tracepoint}; -pub use typst::foundations::{Func, ParamInfo, Value}; +pub use typst::foundations::{Func, Value}; pub use typst::syntax::FileId as TypstFileId; pub use typst::syntax::{ ast::{self, AstNode}, diff --git a/crates/tinymist-query/src/references.rs b/crates/tinymist-query/src/references.rs index 60b4ea87..c244dcb8 100644 --- a/crates/tinymist-query/src/references.rs +++ b/crates/tinymist-query/src/references.rs @@ -51,6 +51,10 @@ pub(crate) fn find_references( DerefTarget::ImportPath(..) | DerefTarget::IncludePath(..) => { return None; } + // todo: label, reference + DerefTarget::Label(..) | DerefTarget::Ref(..) | DerefTarget::Normal(..) => { + return None; + } }; let mut may_ident = node.cast::()?; diff --git a/crates/tinymist-query/src/rename.rs b/crates/tinymist-query/src/rename.rs index 8c34d9aa..d443bda6 100644 --- a/crates/tinymist-query/src/rename.rs +++ b/crates/tinymist-query/src/rename.rs @@ -73,7 +73,7 @@ impl SemanticRequest for RenameRequest { }); } - // todo: conflict analysis + // todo: name conflict analysis Some(WorkspaceEdit { changes: Some(editions), ..Default::default() diff --git a/crates/tinymist-query/src/signature_help.rs b/crates/tinymist-query/src/signature_help.rs index f46ea14b..b36062f5 100644 --- a/crates/tinymist-query/src/signature_help.rs +++ b/crates/tinymist-query/src/signature_help.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, SemanticRequest}; +use crate::{prelude::*, syntax::param_index_at_leaf, SemanticRequest}; /// The [`textDocument/signatureHelp`] request is sent from the client to the /// server to request signature information at a given cursor position. @@ -95,65 +95,6 @@ fn surrounding_function_syntax<'b>( Some((callee, grand.find(callee.span())?, args)) } -fn param_index_at_leaf(leaf: &LinkedNode, function: &Func, args: ast::Args) -> Option { - let deciding = deciding_syntax(leaf); - let params = function.params()?; - let param_index = find_param_index(&deciding, params, args)?; - trace!("got param index {param_index}"); - Some(param_index) -} - -/// Find the piece of syntax that decides what we're completing. -fn deciding_syntax<'b>(leaf: &'b LinkedNode) -> LinkedNode<'b> { - let mut deciding = leaf.clone(); - while !matches!( - deciding.kind(), - SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon - ) { - let Some(prev) = deciding.prev_leaf() else { - break; - }; - deciding = prev; - } - deciding -} - -fn find_param_index(deciding: &LinkedNode, params: &[ParamInfo], args: ast::Args) -> Option { - match deciding.kind() { - // After colon: "func(param:|)", "func(param: |)". - SyntaxKind::Colon => { - let prev = deciding.prev_leaf()?; - let param_ident = prev.cast::()?; - params - .iter() - .position(|param| param.name == param_ident.as_str()) - } - // Before: "func(|)", "func(hi|)", "func(12,|)". - SyntaxKind::Comma | SyntaxKind::LeftParen => { - let next = deciding.next_leaf(); - let following_param = next.as_ref().and_then(|next| next.cast::()); - match following_param { - Some(next) => params - .iter() - .position(|param| param.named && param.name.starts_with(next.as_str())), - None => { - let positional_args_so_far = args - .items() - .filter(|arg| matches!(arg, ast::Arg::Pos(_))) - .count(); - params - .iter() - .enumerate() - .filter(|(_, param)| param.positional) - .map(|(i, _)| i) - .nth(positional_args_so_far) - } - } - } - _ => None, - } -} - fn markdown_docs(docs: &str) -> Documentation { Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, diff --git a/crates/tinymist-query/src/symbol.rs b/crates/tinymist-query/src/symbol.rs index 29564c7d..0b93d8e5 100644 --- a/crates/tinymist-query/src/symbol.rs +++ b/crates/tinymist-query/src/symbol.rs @@ -32,7 +32,7 @@ impl SemanticRequest for SymbolRequest { type Response = Vec; fn request(self, ctx: &mut AnalysisContext) -> Option { - // todo: expose source + // todo: let typst.ts expose source let mut symbols = vec![]; diff --git a/crates/tinymist-query/src/syntax/matcher.rs b/crates/tinymist-query/src/syntax/matcher.rs index 3582eccb..3754f2ad 100644 --- a/crates/tinymist-query/src/syntax/matcher.rs +++ b/crates/tinymist-query/src/syntax/matcher.rs @@ -1,7 +1,10 @@ use log::debug; -use typst::syntax::{ - ast::{self, AstNode}, - LinkedNode, SyntaxKind, +use typst::{ + foundations::{Func, ParamInfo}, + syntax::{ + ast::{self, AstNode}, + LinkedNode, SyntaxKind, + }, }; pub fn deref_lvalue(mut node: LinkedNode) -> Option { @@ -11,25 +14,6 @@ pub fn deref_lvalue(mut node: LinkedNode) -> Option { Some(node) } -#[derive(Debug, Clone)] -pub enum DerefTarget<'a> { - VarAccess(LinkedNode<'a>), - Callee(LinkedNode<'a>), - ImportPath(LinkedNode<'a>), - IncludePath(LinkedNode<'a>), -} - -impl<'a> DerefTarget<'a> { - pub fn node(&self) -> &LinkedNode { - match self { - DerefTarget::VarAccess(node) => node, - DerefTarget::Callee(node) => node, - DerefTarget::ImportPath(node) => node, - DerefTarget::IncludePath(node) => node, - } - } -} - fn is_mark(sk: SyntaxKind) -> bool { use SyntaxKind::*; matches!( @@ -69,68 +53,95 @@ fn is_mark(sk: SyntaxKind) -> bool { ) } +#[derive(Debug, Clone)] +pub enum DerefTarget<'a> { + Label(LinkedNode<'a>), + Ref(LinkedNode<'a>), + VarAccess(LinkedNode<'a>), + Callee(LinkedNode<'a>), + ImportPath(LinkedNode<'a>), + IncludePath(LinkedNode<'a>), + Normal(SyntaxKind, LinkedNode<'a>), +} + +impl<'a> DerefTarget<'a> { + pub fn node(&self) -> &LinkedNode { + match self { + DerefTarget::Label(node) => node, + DerefTarget::Ref(node) => node, + DerefTarget::VarAccess(node) => node, + DerefTarget::Callee(node) => node, + DerefTarget::ImportPath(node) => node, + DerefTarget::IncludePath(node) => node, + DerefTarget::Normal(_, node) => node, + } + } +} + pub fn get_deref_target(node: LinkedNode, cursor: usize) -> Option { - fn same_line_skip(node: &LinkedNode, cursor: usize) -> bool { - // (ancestor.kind().is_trivia() && ancestor.text()) + /// Skips trivia nodes that are on the same line as the cursor. + fn skippable_trivia(node: &LinkedNode, cursor: usize) -> bool { + // A non-trivia node is our target so we stop at it. if !node.kind().is_trivia() { return false; } + + // Get the trivia text before the cursor. let pref = node.text(); - // slice - let pref = if cursor < pref.len() { - &pref[..cursor] + let pref = if node.range().contains(&cursor) { + &pref[..cursor - node.offset()] } else { pref }; - // no newlines + + // The deref target should be on the same line as the cursor. // todo: if we are in markup mode, we should check if we are at start of node !pref.contains('\n') } - let mut ancestor = node; - if same_line_skip(&ancestor, cursor) || is_mark(ancestor.kind()) { - ancestor = ancestor.prev_sibling()?; + // Move to the first non-trivia node before the cursor. + let mut node = node; + if skippable_trivia(&node, cursor) || is_mark(node.kind()) { + node = node.prev_sibling()?; } + // Move to the first ancestor that is an expression. + let mut ancestor = node; while !ancestor.is::() { ancestor = ancestor.parent()?.clone(); } debug!("deref expr: {ancestor:?}"); - let ancestor = deref_lvalue(ancestor)?; - debug!("deref lvalue: {ancestor:?}"); - let may_ident = ancestor.cast::()?; - if !may_ident.hash() && !matches!(may_ident, ast::Expr::MathIdent(_)) { - return None; - } + // Unwrap all parentheses to get the actual expression. + let cano_expr = deref_lvalue(ancestor)?; + debug!("deref lvalue: {cano_expr:?}"); - Some(match may_ident { - // todo: label, reference - // todo: import - // todo: include - ast::Expr::FuncCall(call) => DerefTarget::Callee(ancestor.find(call.callee().span())?), - ast::Expr::Set(set) => DerefTarget::Callee(ancestor.find(set.target().span())?), + // Identify convenient expression kinds. + let expr = cano_expr.cast::()?; + Some(match expr { + ast::Expr::Label(..) => DerefTarget::Label(cano_expr), + ast::Expr::Ref(..) => DerefTarget::Ref(cano_expr), + ast::Expr::FuncCall(call) => DerefTarget::Callee(cano_expr.find(call.callee().span())?), + ast::Expr::Set(set) => DerefTarget::Callee(cano_expr.find(set.target().span())?), ast::Expr::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => { - DerefTarget::VarAccess(ancestor.find(may_ident.span())?) + DerefTarget::VarAccess(cano_expr) } ast::Expr::Str(..) => { - let parent = ancestor.parent()?; + let parent = cano_expr.parent()?; if parent.kind() == SyntaxKind::ModuleImport { - return Some(DerefTarget::ImportPath(ancestor.find(may_ident.span())?)); + DerefTarget::ImportPath(cano_expr) + } else if parent.kind() == SyntaxKind::ModuleInclude { + DerefTarget::IncludePath(cano_expr) + } else { + DerefTarget::Normal(cano_expr.kind(), cano_expr) } - if parent.kind() == SyntaxKind::ModuleInclude { - return Some(DerefTarget::IncludePath(ancestor.find(may_ident.span())?)); - } - - return None; } - ast::Expr::Import(..) => { - return None; - } - _ => { - debug!("unsupported kind {kind:?}", kind = ancestor.kind()); - return None; + _ if expr.hash() + || matches!(cano_expr.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) => + { + DerefTarget::Normal(cano_expr.kind(), cano_expr) } + _ => return None, }) } @@ -201,3 +212,62 @@ pub fn get_def_target(node: LinkedNode) -> Option> { } }) } + +pub fn param_index_at_leaf(leaf: &LinkedNode, function: &Func, args: ast::Args) -> Option { + let deciding = deciding_syntax(leaf); + let params = function.params()?; + let param_index = find_param_index(&deciding, params, args)?; + log::trace!("got param index {param_index}"); + Some(param_index) +} + +/// Find the piece of syntax that decides what we're completing. +fn deciding_syntax<'b>(leaf: &'b LinkedNode) -> LinkedNode<'b> { + let mut deciding = leaf.clone(); + while !matches!( + deciding.kind(), + SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon + ) { + let Some(prev) = deciding.prev_leaf() else { + break; + }; + deciding = prev; + } + deciding +} + +fn find_param_index(deciding: &LinkedNode, params: &[ParamInfo], args: ast::Args) -> Option { + match deciding.kind() { + // After colon: "func(param:|)", "func(param: |)". + SyntaxKind::Colon => { + let prev = deciding.prev_leaf()?; + let param_ident = prev.cast::()?; + params + .iter() + .position(|param| param.name == param_ident.as_str()) + } + // Before: "func(|)", "func(hi|)", "func(12,|)". + SyntaxKind::Comma | SyntaxKind::LeftParen => { + let next = deciding.next_leaf(); + let following_param = next.as_ref().and_then(|next| next.cast::()); + match following_param { + Some(next) => params + .iter() + .position(|param| param.named && param.name.starts_with(next.as_str())), + None => { + let positional_args_so_far = args + .items() + .filter(|arg| matches!(arg, ast::Arg::Pos(_))) + .count(); + params + .iter() + .enumerate() + .filter(|(_, param)| param.positional) + .map(|(i, _)| i) + .nth(positional_args_so_far) + } + } + } + _ => None, + } +} diff --git a/crates/tinymist-query/src/syntax/module.rs b/crates/tinymist-query/src/syntax/module.rs index 87b3adaa..09b72e9f 100644 --- a/crates/tinymist-query/src/syntax/module.rs +++ b/crates/tinymist-query/src/syntax/module.rs @@ -1,5 +1,8 @@ use std::sync::Once; +use once_cell::sync::Lazy; +use regex::RegexSet; + use super::find_imports; use crate::prelude::*; @@ -21,7 +24,7 @@ pub fn construct_module_dependencies( let mut dependencies = HashMap::new(); let mut dependents = HashMap::new(); - for file_id in ctx.files().clone() { + for file_id in ctx.source_files().clone() { let source = match ctx.source_by_id(file_id) { Ok(source) => source, Err(err) => { @@ -58,22 +61,68 @@ pub fn construct_module_dependencies( dependencies } +fn is_hidden(entry: &walkdir::DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.starts_with('.')) + .unwrap_or(false) +} + /// Scan the files in the workspace and return the file ids. /// /// Note: this function will touch the physical file system. -pub fn scan_workspace_files(root: &Path) -> Vec { +pub(crate) fn scan_workspace_files( + root: &Path, + ext: &RegexSet, + f: impl Fn(&Path) -> T, +) -> Vec { let mut res = vec![]; - for path in walkdir::WalkDir::new(root).follow_links(false).into_iter() { - let Ok(de) = path else { - continue; + let mut it = walkdir::WalkDir::new(root).follow_links(false).into_iter(); + loop { + let de = match it.next() { + None => break, + Some(Err(_err)) => continue, + Some(Ok(entry)) => entry, }; + if is_hidden(&de) { + if de.file_type().is_dir() { + it.skip_current_dir(); + } + continue; + } + + /// this is a temporary solution to ignore some common build directories + static IGNORE_REGEX: Lazy = Lazy::new(|| { + RegexSet::new([ + r#"^build$"#, + r#"^target$"#, + r#"^node_modules$"#, + r#"^out$"#, + r#"^dist$"#, + ]) + .unwrap() + }); + if de + .path() + .file_name() + .and_then(|s| s.to_str()) + .is_some_and(|s| IGNORE_REGEX.is_match(s)) + { + if de.file_type().is_dir() { + it.skip_current_dir(); + } + continue; + } + if !de.file_type().is_file() { continue; } if !de .path() .extension() - .is_some_and(|e| e == "typ" || e == "typc") + .and_then(|e| e.to_str()) + .is_some_and(|e| ext.is_match(e)) { continue; } @@ -87,7 +136,12 @@ pub fn scan_workspace_files(root: &Path) -> Vec { } }; - res.push(TypstFileId::new(None, VirtualPath::new(relative_path))); + res.push(f(relative_path)); + + // two times of max number of typst file ids + if res.len() >= (u16::MAX as usize) { + break; + } } res diff --git a/crates/tinymist-query/src/tests.rs b/crates/tinymist-query/src/tests.rs index f78a4f50..752cfdad 100644 --- a/crates/tinymist-query/src/tests.rs +++ b/crates/tinymist-query/src/tests.rs @@ -78,6 +78,7 @@ pub fn snapshot_testing(name: &str, f: &impl Fn(&mut AnalysisContext, PathBuf)) caches: Default::default(), }, ); + ctx.test_completion_files(Vec::new); ctx.test_files(|| paths); f(&mut ctx, p); }); diff --git a/crates/tinymist-query/src/upstream/complete.rs b/crates/tinymist-query/src/upstream/complete.rs index 6888da9a..91b5c500 100644 --- a/crates/tinymist-query/src/upstream/complete.rs +++ b/crates/tinymist-query/src/upstream/complete.rs @@ -9,6 +9,7 @@ use typst::foundations::{ Repr, StyleChain, Styles, Type, Value, }; use typst::model::Document; +use typst::syntax::ast::AstNode; use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind}; use typst::text::RawElem; use typst::visualize::Color; @@ -19,6 +20,7 @@ use crate::analysis::{analyze_expr, analyze_import, analyze_labels}; use crate::AnalysisContext; mod ext; +pub use ext::complete_path; use ext::*; /// Autocomplete a cursor position in a source file. @@ -32,18 +34,23 @@ use 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(mut ctx: CompletionContext) -> Option<(usize, Vec)> { +pub fn autocomplete( + mut ctx: CompletionContext, +) -> Option<(usize, bool, Vec, Vec)> { 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); + || complete_literal(&mut ctx).is_none() && { + log::info!("continue after completing literal"); + 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)) + Some((ctx.from, ctx.incomplete, ctx.completions, ctx.completions2)) } /// An autocompletion option. @@ -76,6 +83,8 @@ pub enum CompletionKind { Type, /// A function parameter. Param, + /// A field. + Field, /// A constant. Constant, /// A symbol. @@ -84,6 +93,8 @@ pub enum CompletionKind { Variable, /// A module. Module, + /// A file. + File, /// A folder. Folder, } @@ -315,7 +326,7 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { /// Add completions for math snippets. #[rustfmt::skip] fn math_completions(ctx: &mut CompletionContext) { - ctx.scope_completions_(true, |_| true); + ctx.scope_completions(true, |_| true); ctx.snippet_completion( "subscript", @@ -600,7 +611,7 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool { /// Add completions for all functions from the global scope. fn set_rule_completions(ctx: &mut CompletionContext) { - ctx.scope_completions_(true, |value| { + ctx.scope_completions(true, |value| { matches!( value, Value::Func(func) if func.params() @@ -613,7 +624,7 @@ fn set_rule_completions(ctx: &mut CompletionContext) { /// Add completions for selectors. fn show_rule_selector_completions(ctx: &mut CompletionContext) { - ctx.scope_completions_( + ctx.scope_completions( false, |value| matches!(value, Value::Func(func) if func.element().is_some()), ); @@ -653,7 +664,7 @@ fn show_rule_recipe_completions(ctx: &mut CompletionContext) { "Transform the element with a function.", ); - ctx.scope_completions_(false, |value| matches!(value, Value::Func(_))); + ctx.scope_completions(false, |value| matches!(value, Value::Func(_))); } /// Complete call and set rule parameters. @@ -702,8 +713,13 @@ fn complete_params(ctx: &mut CompletionContext) -> bool { if let Some(next) = deciding.next_leaf() { ctx.from = ctx.cursor.min(next.offset()); } + let parent = deciding.parent().unwrap(); + log::info!("named param parent: {:?}", parent); + // get type of this param + let ty = ctx.ctx.type_of(param.to_untyped()); + log::info!("named param type: {:?}", ty); - named_param_value_completions(ctx, callee, ¶m); + named_param_value_completions(ctx, callee, ¶m, ty.as_ref()); return true; } } @@ -772,7 +788,7 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { /// Add completions for expression snippets. #[rustfmt::skip] fn code_completions(ctx: &mut CompletionContext, hash: bool) { - ctx.scope_completions_(true, |value| !hash || { + ctx.scope_completions(true, |value| !hash || { matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)) }); @@ -942,6 +958,8 @@ pub struct CompletionContext<'a, 'w> { pub explicit: bool, pub from: usize, pub completions: Vec, + pub completions2: Vec, + pub incomplete: bool, pub seen_casts: HashSet, } @@ -968,13 +986,16 @@ impl<'a, 'w> CompletionContext<'a, 'w> { cursor, explicit, from: cursor, + incomplete: true, completions: vec![], + completions2: vec![], seen_casts: HashSet::new(), }) } /// A small window of context before the cursor. fn before_window(&self, size: usize) -> &str { + // todo: bad slicing &self.before[self.cursor.saturating_sub(size)..] } @@ -1216,7 +1237,7 @@ impl<'a, 'w> CompletionContext<'a, 'w> { "color.hsl(${h}, ${s}, ${l}, ${a})", "A custom HSLA color.", ); - self.scope_completions_(false, |value| value.ty() == *ty); + self.scope_completions(false, |value| value.ty() == *ty); } else if *ty == Type::of::