1use std::collections::HashMap;
4use std::sync::Arc;
5
6use ecow::{EcoString, EcoVec, eco_vec};
7use itertools::Itertools;
8use lsp_types::Position;
9use rayon::iter::{IntoParallelIterator, ParallelIterator};
10use serde::{Deserialize, Serialize};
11use typst::diag::StrResult;
12use typst::syntax::FileId;
13use typst::syntax::VirtualRoot;
14use typst::syntax::package::PackageSpec;
15use typst_shim::syntax::RootedPathExt;
16
17use crate::LocalContext;
18use crate::adt::interner::Interned;
19use crate::analysis::{Definition, SharedQueryCache};
20use crate::docs::file_id_repr;
21use crate::package::{PackageInfo, get_manifest_id, package_entrypoint_id};
22use crate::syntax::{Decl, DefKind, Expr, ExprInfo};
23
24use super::DefDocs;
25
26pub fn package_module_docs(ctx: &mut LocalContext, pkg: &PackageInfo) -> StrResult<PackageDefInfo> {
28 let toml_id = get_manifest_id(pkg)?;
29 let manifest = ctx.get_manifest(toml_id)?;
30
31 let entry_point = package_entrypoint_id(toml_id, &manifest.package.entrypoint);
32 module_docs(ctx, entry_point)
33}
34
35pub fn module_docs(ctx: &mut LocalContext, entry_point: FileId) -> StrResult<PackageDefInfo> {
37 let mut aliases = HashMap::new();
38 let mut extras = vec![];
39 let shared = ctx.shared().clone();
40
41 let mut scan_ctx = ScanDefCtx {
42 ctx,
43 root: entry_point,
44 for_spec: entry_point.package_compat(),
45 aliases: &mut aliases,
46 extras: &mut extras,
47 };
48
49 let ei = scan_ctx
50 .ctx
51 .expr_stage_by_id(entry_point)
52 .ok_or("entry point not found")?;
53 let docs_cache = SharedQueryCache::<Definition, Option<DefDocs>>::default();
54 let mut defs = enrich_def_docs_parallel(
55 shared.clone(),
56 docs_cache.clone(),
57 scan_ctx.defs(eco_vec![], ei),
58 );
59
60 let module_uses = aliases
61 .into_iter()
62 .map(|(fid, mut v)| {
63 v.sort_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)));
64 (file_id_repr(fid), v.into())
65 })
66 .collect();
67
68 crate::log_debug_ct!("module_uses: {module_uses:#?}",);
69
70 defs.children.extend(
71 extras
72 .into_par_iter()
73 .map(|extra| enrich_def_docs_parallel(shared.clone(), docs_cache.clone(), extra))
74 .collect::<Vec<_>>(),
75 );
76
77 Ok(PackageDefInfo {
78 root: defs,
79 module_uses,
80 })
81}
82
83fn enrich_def_docs_parallel(
84 shared: Arc<crate::analysis::SharedContext>,
85 docs_cache: SharedQueryCache<Definition, Option<DefDocs>>,
86 mut head: DefInfo,
87) -> DefInfo {
88 head.children = head
89 .children
90 .into_par_iter()
91 .map(|child| enrich_def_docs_parallel(shared.clone(), docs_cache.clone(), child))
92 .collect();
93
94 let def_docs = head
95 .decl
96 .as_ref()
97 .and_then(definition_for_docs)
98 .and_then(|definition| {
99 docs_cache.get_or_init(definition.clone(), || shared.def_docs(&definition))
100 });
101 head.docs = def_docs.as_ref().map(|docs| docs.docs().clone());
102 head.parsed_docs = def_docs;
103
104 if head.is_external {
105 head.oneliner = head.docs.as_ref().map(|docs| oneliner(docs).to_owned());
106 head.docs = None;
107 }
108
109 head
110}
111
112fn definition_for_docs(decl: &Interned<Decl>) -> Option<Definition> {
113 match decl.as_ref() {
114 Decl::Func(..) => Some(Definition::new(decl.clone(), None)),
115 _ => None,
116 }
117}
118
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121pub struct DefInfo {
122 pub id: EcoString,
124 pub name: EcoString,
126 pub kind: DefKind,
128 #[serde(skip)]
130 pub symbol: Option<String>,
131 #[serde(skip)]
133 pub loc: Option<(usize, usize, usize)>,
134 #[serde(skip)]
136 pub source: Option<SourceQuery>,
137 pub is_external: bool,
139 pub module_link: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub bundle_link: Option<String>,
144 pub symbol_link: Option<String>,
146 pub external_link: Option<String>,
148 #[serde(skip_serializing)]
150 pub oneliner: Option<String>,
151 #[serde(skip_serializing)]
153 pub docs: Option<EcoString>,
154 #[serde(skip_serializing)]
156 pub parsed_docs: Option<DefDocs>,
157 #[serde(skip)]
159 pub constant: Option<EcoString>,
160 #[serde(skip)]
163 pub decl: Option<Interned<Decl>>,
164 pub children: Vec<DefInfo>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct SourceQuery {
171 pub file: usize,
173 pub position: Position,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct PackageDefInfo {
180 #[serde(flatten)]
182 pub root: DefInfo,
183 pub module_uses: HashMap<String, EcoVec<String>>,
185}
186
187struct ScanDefCtx<'a> {
188 ctx: &'a mut LocalContext,
189 for_spec: Option<&'a PackageSpec>,
190 aliases: &'a mut HashMap<FileId, Vec<String>>,
191 extras: &'a mut Vec<DefInfo>,
192 root: FileId,
193}
194
195impl ScanDefCtx<'_> {
196 fn defs(&mut self, paths: EcoVec<&str>, ei: ExprInfo) -> DefInfo {
197 let module_decl = Decl::module(ei.fid);
198 let key = module_decl.name().clone();
199 let site = Some(self.root);
200 let paths = paths.clone();
201 self.def(&key, paths, site.as_ref(), &module_decl.into(), None)
202 }
203
204 fn expr(
205 &mut self,
206 key: &str,
207 path: EcoVec<&str>,
208 site: Option<&FileId>,
209 val: &Expr,
210 ) -> DefInfo {
211 match val {
212 Expr::Decl(decl) => self.def(key, path, site, decl, Some(val)),
213 Expr::Ref(r) if r.root.is_some() => {
214 self.expr(key, path, site, r.root.as_ref().unwrap())
215 }
216 Expr::Select(..) => {
218 let mut path = path.clone();
219 path.push(key);
220 DefInfo {
221 name: key.to_string().into(),
222 kind: DefKind::Module,
223 ..Default::default()
224 }
225 }
226 _ => {
228 let mut path = path.clone();
229 path.push(key);
230 DefInfo {
231 name: key.to_string().into(),
232 kind: DefKind::Constant,
233 ..Default::default()
234 }
235 }
236 }
237 }
238
239 fn def(
240 &mut self,
241 key: &str,
242 path: EcoVec<&str>,
243 site: Option<&FileId>,
244 decl: &Interned<Decl>,
245 expr: Option<&Expr>,
246 ) -> DefInfo {
247 let children = match decl.as_ref() {
248 Decl::Module(..) => decl.file_id().and_then(|fid| {
249 if !matches!(fid.root(), VirtualRoot::Package(package) if Some(package) == self.for_spec) {
251 return None;
252 }
253
254 let aliases_vec = self.aliases.entry(fid).or_default();
256 let is_fresh = aliases_vec.is_empty();
257 aliases_vec.push(path.iter().join("."));
258
259 if !is_fresh {
260 crate::log_debug_ct!("found module: {path:?} (reexport)");
261 return None;
262 }
263
264 crate::log_debug_ct!("found module: {path:?}");
265
266 let ei = self.ctx.expr_stage_by_id(fid)?;
267
268 let symbols = ei
269 .exports
270 .iter()
271 .map(|(name, val)| {
272 let mut path = path.clone();
273 path.push(name);
274 self.expr(name, path.clone(), Some(&fid), val)
275 })
276 .collect();
277 Some(symbols)
278 }),
279 _ => None,
280 };
281
282 let mut head = DefInfo {
283 id: EcoString::new(),
284 name: key.to_string().into(),
285 kind: decl.kind(),
286 constant: expr.map(|expr| expr.repr()),
287 docs: None,
288 parsed_docs: None,
289 decl: Some(decl.clone()),
290 children: children.unwrap_or_default(),
291 symbol: None,
292 loc: None,
293 source: None,
294 is_external: false,
295 module_link: None,
296 bundle_link: None,
297 symbol_link: None,
298 external_link: None,
299 oneliner: None,
300 };
301
302 if let Some((span, mod_fid)) = head.decl.as_ref().and_then(|decl| decl.file_id()).zip(site)
303 && span != *mod_fid
304 {
305 head.is_external = true;
306 }
307
308 if let Some(fid) = head.decl.as_ref().and_then(|del| del.file_id()) {
310 if matches!(fid.root(), VirtualRoot::Package(package) if Some(package) == self.for_spec)
312 {
313 let av = self.aliases.entry(fid).or_default();
314 if av.is_empty() {
315 let src = self.ctx.expr_stage_by_id(fid);
316 let mut path = path.clone();
317 path.push("-");
318 path.push(key);
319
320 crate::log_debug_ct!("found internal module: {path:?}");
321 if let Some(m) = src {
322 let msym = self.defs(path, m);
323 self.extras.push(msym)
324 }
325 }
326 }
327 }
328
329 head
330 }
331}
332
333fn oneliner(docs: &str) -> &str {
335 docs.lines().next().unwrap_or_default()
336}