1mod bib;
4pub(crate) use bib::*;
5pub mod call;
6pub use call::*;
7pub mod completion;
8pub use completion::*;
9pub mod code_action;
10pub use code_action::*;
11pub mod color_expr;
12pub use color_expr::*;
13pub mod doc_highlight;
14pub use doc_highlight::*;
15pub mod link_expr;
16pub use link_expr::*;
17pub mod definition;
18pub use definition::*;
19pub mod signature;
20pub use signature::*;
21pub mod semantic_tokens;
22pub use semantic_tokens::*;
23
24mod global;
25mod post_tyck;
26mod prelude;
27mod tyck;
28
29pub(crate) use crate::ty::*;
30pub use global::*;
31pub(crate) use post_tyck::*;
32pub(crate) use tinymist_analysis::stats::{AnalysisStats, QueryStatGuard};
33pub(crate) use tyck::*;
34
35use std::sync::Arc;
36
37use ecow::eco_format;
38use lsp_types::Url;
39use tinymist_project::LspComputeGraph;
40use tinymist_std::error::WithContextUntyped;
41use tinymist_std::{Result, bail};
42use tinymist_world::{EntryReader, EntryState, TaskInputs};
43use typst::diag::{FileError, FileResult, StrResult};
44use typst::foundations::{Func, Value};
45use typst::syntax::FileId;
46
47use crate::{CompilerQueryResponse, SemanticRequest, path_res_to_url};
48
49pub(crate) trait ToFunc {
50 fn to_func(&self) -> Option<Func>;
51}
52
53impl ToFunc for Value {
54 fn to_func(&self) -> Option<Func> {
55 match self {
56 Value::Func(func) => Some(func.clone()),
57 Value::Type(ty) => ty.constructor().ok(),
58 _ => None,
59 }
60 }
61}
62
63pub trait LspWorldExt {
65 fn uri_for_id(&self, fid: FileId) -> FileResult<Url>;
67}
68
69impl LspWorldExt for tinymist_project::LspWorld {
70 fn uri_for_id(&self, fid: FileId) -> Result<Url, FileError> {
71 let res = path_res_to_url(self.path_for_id(fid)?);
72
73 crate::log_debug_ct!("uri_for_id: {fid:?} -> {res:?}");
74 res.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}"))))
75 }
76}
77
78pub struct LspQuerySnapshot {
80 pub snap: LspComputeGraph,
82 analysis: Arc<Analysis>,
84 rev_lock: AnalysisRevLock,
86}
87
88impl std::ops::Deref for LspQuerySnapshot {
89 type Target = LspComputeGraph;
90
91 fn deref(&self) -> &Self::Target {
92 &self.snap
93 }
94}
95
96impl LspQuerySnapshot {
97 pub fn task(mut self, inputs: TaskInputs) -> Self {
99 self.snap = self.snap.task(inputs);
100 self
101 }
102
103 pub fn run_semantic<T: SemanticRequest>(
105 self,
106 query: T,
107 wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
108 ) -> Result<CompilerQueryResponse> {
109 self.run_analysis(|ctx| query.request(ctx)).map(wrapper)
110 }
111
112 pub fn run_analysis<T>(self, f: impl FnOnce(&mut LocalContextGuard) -> T) -> Result<T> {
114 let graph = self.snap.clone();
115 let Some(..) = graph.world().main_id() else {
116 log::error!("Project: main file is not set");
117 bail!("main file is not set");
118 };
119
120 let mut ctx = self.analysis.enter_(graph, self.rev_lock);
121 Ok(f(&mut ctx))
122 }
123
124 pub fn run_within_package<T>(
126 self,
127 info: &crate::package::PackageInfo,
128 f: impl FnOnce(&mut LocalContextGuard) -> Result<T> + Send + Sync,
129 ) -> Result<T> {
130 let world = self.world();
131
132 let entry: StrResult<EntryState> = Ok(()).and_then(|_| {
133 let toml_id = crate::package::get_manifest_id(info)?;
134 let toml_path = world.path_for_id(toml_id)?.as_path().to_owned();
135 let pkg_root = toml_path
136 .parent()
137 .ok_or_else(|| eco_format!("cannot get package root (parent of {toml_path:?})"))?;
138
139 let manifest = crate::package::get_manifest(world, toml_id)?;
140 let entry_point = toml_id.join(&manifest.package.entrypoint);
141
142 Ok(EntryState::new_rooted_by_id(pkg_root.into(), entry_point))
143 });
144 let entry = entry.context_ut("resolve package entry")?;
145
146 let snap = self.task(TaskInputs {
147 entry: Some(entry),
148 inputs: None,
149 });
150
151 snap.run_analysis(f)?
152 }
153}
154
155#[cfg(test)]
156mod matcher_tests {
157
158 use typst::syntax::LinkedNode;
159 use typst_shim::syntax::LinkedNodeExt;
160
161 use crate::{syntax::classify_def, tests::*};
162
163 #[test]
164 fn test() {
165 snapshot_testing("match_def", &|ctx, path| {
166 let source = ctx.source_by_path(&path).unwrap();
167
168 let pos = ctx
169 .to_typst_pos(find_test_position(&source), &source)
170 .unwrap();
171
172 let root = LinkedNode::new(source.root());
173 let node = root.leaf_at_compat(pos).unwrap();
174
175 let snap = classify_def(node).map(|def| format!("{:?}", def.node().range()));
176 let snap = snap.as_deref().unwrap_or("<nil>");
177
178 assert_snapshot!(snap);
179 });
180 }
181}
182
183#[cfg(test)]
184mod expr_tests {
185
186 use tinymist_std::path::unix_slash;
187 use tinymist_world::vfs::WorkspaceResolver;
188 use typst::syntax::Source;
189
190 use crate::syntax::{Expr, RefExpr};
191 use crate::tests::*;
192
193 trait ShowExpr {
194 fn show_expr(&self, expr: &Expr) -> String;
195 }
196
197 impl ShowExpr for Source {
198 fn show_expr(&self, node: &Expr) -> String {
199 match node {
200 Expr::Decl(decl) => {
201 let range = self.range(decl.span()).unwrap_or_default();
202 let fid = if let Some(fid) = decl.file_id() {
203 let vpath = fid.vpath().as_rooted_path();
204 match fid.package() {
205 Some(package) if WorkspaceResolver::is_package_file(fid) => {
206 format!(" in {package:?}{}", unix_slash(vpath))
207 }
208 Some(_) | None => format!(" in {}", unix_slash(vpath)),
209 }
210 } else {
211 "".to_string()
212 };
213 format!("{decl:?}@{range:?}{fid}")
214 }
215 _ => format!("{node}"),
216 }
217 }
218 }
219
220 #[test]
221 fn docs() {
222 snapshot_testing("docs", &|ctx, path| {
223 let source = ctx.source_by_path(&path).unwrap();
224
225 let result = ctx.shared_().expr_stage(&source);
226 let mut docstrings = result.docstrings.iter().collect::<Vec<_>>();
227 docstrings.sort_by(|x, y| x.0.cmp(y.0));
228 let mut docstrings = docstrings
229 .into_iter()
230 .map(|(ident, expr)| {
231 format!(
232 "{} -> {expr:?}",
233 source.show_expr(&Expr::Decl(ident.clone())),
234 )
235 })
236 .collect::<Vec<_>>();
237 let mut snap = vec![];
238 snap.push("= docstings".to_owned());
239 snap.append(&mut docstrings);
240
241 assert_snapshot!(snap.join("\n"));
242 });
243 }
244
245 #[test]
246 fn scope() {
247 snapshot_testing("expr_of", &|ctx, path| {
248 let source = ctx.source_by_path(&path).unwrap();
249
250 let result = ctx.shared_().expr_stage(&source);
251 let mut resolves = result.resolves.iter().collect::<Vec<_>>();
252 resolves.sort_by(|x, y| x.1.decl.cmp(&y.1.decl));
253
254 let mut resolves = resolves
255 .into_iter()
256 .map(|(_, expr)| {
257 let RefExpr {
258 decl: ident,
259 step,
260 root,
261 term,
262 } = expr.as_ref();
263
264 format!(
265 "{} -> {}, root {}, val: {term:?}",
266 source.show_expr(&Expr::Decl(ident.clone())),
267 step.as_ref()
268 .map(|expr| source.show_expr(expr))
269 .unwrap_or_default(),
270 root.as_ref()
271 .map(|expr| source.show_expr(expr))
272 .unwrap_or_default()
273 )
274 })
275 .collect::<Vec<_>>();
276 let mut exports = result.exports.iter().collect::<Vec<_>>();
277 exports.sort_by(|x, y| x.0.cmp(y.0));
278 let mut exports = exports
279 .into_iter()
280 .map(|(ident, node)| {
281 let node = source.show_expr(node);
282 format!("{ident} -> {node}",)
283 })
284 .collect::<Vec<_>>();
285
286 let mut snap = vec![];
287 snap.push("= resolves".to_owned());
288 snap.append(&mut resolves);
289 snap.push("= exports".to_owned());
290 snap.append(&mut exports);
291
292 assert_snapshot!(snap.join("\n"));
293 });
294 }
295}
296
297#[cfg(test)]
298mod module_tests {
299 use serde_json::json;
300 use tinymist_std::path::unix_slash;
301 use typst::syntax::FileId;
302
303 use crate::prelude::*;
304 use crate::syntax::module::*;
305 use crate::tests::*;
306
307 #[test]
308 fn test() {
309 snapshot_testing("modules", &|ctx, _| {
310 fn ids(ids: EcoVec<FileId>) -> Vec<String> {
311 let mut ids: Vec<String> = ids
312 .into_iter()
313 .map(|id| unix_slash(id.vpath().as_rooted_path()))
314 .collect();
315 ids.sort();
316 ids
317 }
318
319 let dependencies = construct_module_dependencies(ctx);
320
321 let mut dependencies = dependencies
322 .into_iter()
323 .map(|(id, v)| {
324 (
325 unix_slash(id.vpath().as_rooted_path()),
326 ids(v.dependencies),
327 ids(v.dependents),
328 )
329 })
330 .collect::<Vec<_>>();
331
332 dependencies.sort();
333 dependencies.retain(|(path, _, _)| path != "/main.typ");
335
336 let dependencies = dependencies
337 .into_iter()
338 .map(|(id, deps, dependents)| {
339 let mut mp = serde_json::Map::new();
340 mp.insert("id".to_string(), json!(id));
341 mp.insert("dependencies".to_string(), json!(deps));
342 mp.insert("dependents".to_string(), json!(dependents));
343 json!(mp)
344 })
345 .collect::<Vec<_>>();
346
347 assert_snapshot!(JsonRepr::new_pure(dependencies));
348 });
349 }
350}
351
352#[cfg(test)]
353mod type_check_tests {
354
355 use core::fmt;
356
357 use typst::syntax::Source;
358
359 use crate::tests::*;
360
361 use super::{Ty, TypeInfo};
362
363 #[test]
364 fn test() {
365 snapshot_testing("type_check", &|ctx, path| {
366 let source = ctx.source_by_path(&path).unwrap();
367
368 let result = ctx.type_check(&source);
369 let result = format!("{:#?}", TypeCheckSnapshot(&source, &result));
370
371 assert_snapshot!(result);
372 });
373 }
374
375 struct TypeCheckSnapshot<'a>(&'a Source, &'a TypeInfo);
376
377 impl fmt::Debug for TypeCheckSnapshot<'_> {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 let source = self.0;
380 let info = self.1;
381 let mut vars = info
382 .vars
383 .values()
384 .map(|bounds| (bounds.name(), bounds))
385 .collect::<Vec<_>>();
386
387 vars.sort_by(|x, y| x.1.var.strict_cmp(&y.1.var));
388
389 for (name, bounds) in vars {
390 writeln!(f, "{name:?} = {:?}", info.simplify(bounds.as_type(), true))?;
391 }
392
393 writeln!(f, "=====")?;
394 let mut mapping = info
395 .mapping
396 .iter()
397 .map(|pair| (source.range(*pair.0).unwrap_or_default(), pair.1))
398 .collect::<Vec<_>>();
399
400 mapping.sort_by(|x, y| {
401 x.0.start
402 .cmp(&y.0.start)
403 .then_with(|| x.0.end.cmp(&y.0.end))
404 });
405
406 for (range, value) in mapping {
407 let ty = Ty::from_types(value.clone().into_iter());
408 writeln!(f, "{range:?} -> {ty:?}")?;
409 }
410
411 Ok(())
412 }
413 }
414}
415
416#[cfg(test)]
417mod post_type_check_tests {
418
419 use typst::syntax::LinkedNode;
420 use typst_shim::syntax::LinkedNodeExt;
421
422 use crate::analysis::*;
423 use crate::tests::*;
424
425 #[test]
426 fn test() {
427 snapshot_testing("post_type_check", &|ctx, path| {
428 let source = ctx.source_by_path(&path).unwrap();
429
430 let pos = ctx
431 .to_typst_pos(find_test_position(&source), &source)
432 .unwrap();
433 let root = LinkedNode::new(source.root());
434 let node = root.leaf_at_compat(pos + 1).unwrap();
435 let text = node.get().clone().into_text();
436
437 let result = ctx.type_check(&source);
438 let post_ty = post_type_check(ctx.shared_(), &result, node);
439
440 with_settings!({
441 description => format!("Check on {text:?} ({pos:?})"),
442 }, {
443 let post_ty = post_ty.map(|ty| format!("{ty:#?}"))
444 .unwrap_or_else(|| "<nil>".to_string());
445 assert_snapshot!(post_ty);
446 })
447 });
448 }
449}
450
451#[cfg(test)]
452mod type_describe_tests {
453
454 use typst::syntax::LinkedNode;
455 use typst_shim::syntax::LinkedNodeExt;
456
457 use crate::analysis::*;
458 use crate::tests::*;
459
460 #[test]
461 fn test() {
462 snapshot_testing("type_describe", &|ctx, path| {
463 let source = ctx.source_by_path(&path).unwrap();
464
465 let pos = ctx
466 .to_typst_pos(find_test_position(&source), &source)
467 .unwrap();
468 let root = LinkedNode::new(source.root());
469 let node = root.leaf_at_compat(pos + 1).unwrap();
470 let text = node.get().clone().into_text();
471
472 let ti = ctx.type_check(&source);
473 let post_ty = post_type_check(ctx.shared_(), &ti, node);
474
475 with_settings!({
476 description => format!("Check on {text:?} ({pos:?})"),
477 }, {
478 let post_ty = post_ty.and_then(|ty| ty.describe())
479 .unwrap_or_else(|| "<nil>".into());
480 assert_snapshot!(post_ty);
481 })
482 });
483 }
484}
485
486#[cfg(test)]
487mod signature_tests {
488
489 use core::fmt;
490
491 use typst::syntax::LinkedNode;
492 use typst_shim::syntax::LinkedNodeExt;
493
494 use crate::analysis::{Signature, SignatureTarget, analyze_signature};
495 use crate::syntax::classify_syntax;
496 use crate::tests::*;
497
498 #[test]
499 fn test() {
500 snapshot_testing("signature", &|ctx, path| {
501 let source = ctx.source_by_path(&path).unwrap();
502
503 let pos = ctx
504 .to_typst_pos(find_test_position(&source), &source)
505 .unwrap();
506
507 let root = LinkedNode::new(source.root());
508 let callee_node = root.leaf_at_compat(pos).unwrap();
509 let callee_node = classify_syntax(callee_node, pos).unwrap();
510 let callee_node = callee_node.node();
511
512 let result = analyze_signature(
513 ctx.shared(),
514 SignatureTarget::Syntax(source.clone(), callee_node.span()),
515 );
516
517 assert_snapshot!(SignatureSnapshot(result.as_ref()));
518 });
519 }
520
521 struct SignatureSnapshot<'a>(pub Option<&'a Signature>);
522
523 impl fmt::Display for SignatureSnapshot<'_> {
524 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525 let Some(sig) = self.0 else {
526 return write!(f, "<nil>");
527 };
528
529 let primary_sig = match sig {
530 Signature::Primary(sig) => sig,
531 Signature::Partial(sig) => {
532 for w in &sig.with_stack {
533 write!(f, "with ")?;
534 for arg in &w.items {
535 if let Some(name) = &arg.name {
536 write!(f, "{name}: ")?;
537 }
538 let term = arg.term.as_ref();
539 let term = term.and_then(|v| v.describe()).unwrap_or_default();
540 write!(f, "{term}, ")?;
541 }
542 f.write_str("\n")?;
543 }
544
545 &sig.signature
546 }
547 };
548
549 writeln!(f, "fn(")?;
550 for param in primary_sig.pos() {
551 writeln!(f, " {},", param.name)?;
552 }
553 for param in primary_sig.named() {
554 if let Some(expr) = ¶m.default {
555 writeln!(f, " {}: {},", param.name, expr)?;
556 } else {
557 writeln!(f, " {},", param.name)?;
558 }
559 }
560 if let Some(param) = primary_sig.rest() {
561 writeln!(f, " ...{}, ", param.name)?;
562 }
563 write!(f, ")")?;
564
565 Ok(())
566 }
567 }
568}
569
570#[cfg(test)]
571mod call_info_tests {
572
573 use core::fmt;
574
575 use typst::syntax::{LinkedNode, SyntaxKind};
576 use typst_shim::syntax::LinkedNodeExt;
577
578 use crate::analysis::analyze_call;
579 use crate::tests::*;
580
581 use super::CallInfo;
582
583 #[test]
584 fn test() {
585 snapshot_testing("call_info", &|ctx, path| {
586 let source = ctx.source_by_path(&path).unwrap();
587
588 let pos = ctx
589 .to_typst_pos(find_test_position(&source), &source)
590 .unwrap();
591
592 let root = LinkedNode::new(source.root());
593 let mut call_node = root.leaf_at_compat(pos + 1).unwrap();
594
595 while let Some(parent) = call_node.parent() {
596 if call_node.kind() == SyntaxKind::FuncCall {
597 break;
598 }
599 call_node = parent.clone();
600 }
601
602 let result = analyze_call(ctx, source.clone(), call_node);
603
604 assert_snapshot!(CallSnapshot(result.as_deref()));
605 });
606 }
607
608 struct CallSnapshot<'a>(pub Option<&'a CallInfo>);
609
610 impl fmt::Display for CallSnapshot<'_> {
611 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612 let Some(ci) = self.0 else {
613 return write!(f, "<nil>");
614 };
615
616 let mut w = ci.arg_mapping.iter().collect::<Vec<_>>();
617 w.sort_by(|x, y| x.0.span().into_raw().cmp(&y.0.span().into_raw()));
618
619 for (arg, arg_call_info) in w {
620 writeln!(f, "{} -> {:?}", arg.clone().into_text(), arg_call_info)?;
621 }
622
623 Ok(())
624 }
625 }
626}
627
628#[cfg(test)]
629mod lint_tests {
630 use std::collections::BTreeMap;
631
632 use tinymist_lint::KnownIssues;
633
634 use crate::tests::*;
635
636 #[test]
637 fn test() {
638 snapshot_testing("lint", &|ctx, path| {
639 let source = ctx.source_by_path(&path).unwrap();
640
641 let result = ctx.lint(&source, &KnownIssues::default());
642 let result = crate::diagnostics::DiagWorker::new(ctx).convert_all(result.iter());
643 let result = result
644 .into_iter()
645 .map(|(k, v)| (file_uri_(&k), v))
646 .collect::<BTreeMap<_, _>>();
647 assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
648 });
649 }
650}