tinymist_query/docs/
module.rs

1//! Module documentation.
2
3use 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
26/// Get documentation of definitions in a package.
27pub 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
35/// Get documentation of definitions in a module.
36pub 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/// Information about a definition.
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121pub struct DefInfo {
122    /// The raw documentation of the definition.
123    pub id: EcoString,
124    /// The name of the definition.
125    pub name: EcoString,
126    /// The kind of the definition.
127    pub kind: DefKind,
128    /// The SCIP symbol for index-backed queries.
129    #[serde(skip)]
130    pub symbol: Option<String>,
131    /// The location (file, start, end) of the definition.
132    #[serde(skip)]
133    pub loc: Option<(usize, usize, usize)>,
134    /// The source position for index-backed queries.
135    #[serde(skip)]
136    pub source: Option<SourceQuery>,
137    /// Whether the definition external to the module.
138    pub is_external: bool,
139    /// The module link to the definition
140    pub module_link: Option<String>,
141    /// The bundle-mode link to the definition.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub bundle_link: Option<String>,
144    /// The symbol link to the definition
145    pub symbol_link: Option<String>,
146    /// The link to the definition if it is external.
147    pub external_link: Option<String>,
148    /// The one-line documentation of the definition.
149    #[serde(skip_serializing)]
150    pub oneliner: Option<String>,
151    /// The raw documentation of the definition.
152    #[serde(skip_serializing)]
153    pub docs: Option<EcoString>,
154    /// The parsed documentation of the definition.
155    #[serde(skip_serializing)]
156    pub parsed_docs: Option<DefDocs>,
157    /// The value of the definition.
158    #[serde(skip)]
159    pub constant: Option<EcoString>,
160    /// The name range of the definition.
161    /// The value of the definition.
162    #[serde(skip)]
163    pub decl: Option<Interned<Decl>>,
164    /// The children of the definition.
165    pub children: Vec<DefInfo>,
166}
167
168/// A source position that can be used to query a package index.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct SourceQuery {
171    /// The source file index in the package document file table.
172    pub file: usize,
173    /// The source position for a `textDocument/definition` query.
174    pub position: Position,
175}
176
177/// Information about the definitions in a package.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct PackageDefInfo {
180    /// The root module information.
181    #[serde(flatten)]
182    pub root: DefInfo,
183    /// The module accessible paths.
184    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            // todo: select
217            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            // v => panic!("unexpected export: {key} -> {v}"),
227            _ => {
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                // only generate docs for the same package
250                if !matches!(fid.root(), VirtualRoot::Package(package) if Some(package) == self.for_spec) {
251                    return None;
252                }
253
254                // !aliases.insert(fid)
255                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        // Insert module that is not exported
309        if let Some(fid) = head.decl.as_ref().and_then(|del| del.file_id()) {
310            // only generate docs for the same package
311            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
333/// Extract the first line of documentation.
334fn oneliner(docs: &str) -> &str {
335    docs.lines().next().unwrap_or_default()
336}