tinymist_query/docs/
module.rs

1//! Module documentation.
2
3use std::collections::HashMap;
4
5use ecow::{EcoString, EcoVec, eco_vec};
6use itertools::Itertools;
7use serde::{Deserialize, Serialize};
8use typst::diag::StrResult;
9use typst::syntax::FileId;
10use typst::syntax::package::PackageSpec;
11
12use crate::LocalContext;
13use crate::adt::interner::Interned;
14use crate::docs::file_id_repr;
15use crate::package::{PackageInfo, get_manifest_id};
16use crate::syntax::{Decl, DefKind, Expr, ExprInfo};
17
18use super::DefDocs;
19
20/// Get documentation of definitions in a package.
21pub fn package_module_docs(ctx: &mut LocalContext, pkg: &PackageInfo) -> StrResult<PackageDefInfo> {
22    let toml_id = get_manifest_id(pkg)?;
23    let manifest = ctx.get_manifest(toml_id)?;
24
25    let entry_point = toml_id.join(&manifest.package.entrypoint);
26    module_docs(ctx, entry_point)
27}
28
29/// Get documentation of definitions in a module.
30pub fn module_docs(ctx: &mut LocalContext, entry_point: FileId) -> StrResult<PackageDefInfo> {
31    let mut aliases = HashMap::new();
32    let mut extras = vec![];
33
34    let mut scan_ctx = ScanDefCtx {
35        ctx,
36        root: entry_point,
37        for_spec: entry_point.package(),
38        aliases: &mut aliases,
39        extras: &mut extras,
40    };
41
42    let ei = scan_ctx
43        .ctx
44        .expr_stage_by_id(entry_point)
45        .ok_or("entry point not found")?;
46    let mut defs = scan_ctx.defs(eco_vec![], ei);
47
48    let module_uses = aliases
49        .into_iter()
50        .map(|(fid, mut v)| {
51            v.sort_by(|a, b| a.len().cmp(&b.len()).then(a.cmp(b)));
52            (file_id_repr(fid), v.into())
53        })
54        .collect();
55
56    crate::log_debug_ct!("module_uses: {module_uses:#?}",);
57
58    defs.children.extend(extras);
59
60    Ok(PackageDefInfo {
61        root: defs,
62        module_uses,
63    })
64}
65
66/// Information about a definition.
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct DefInfo {
69    /// The raw documentation of the definition.
70    pub id: EcoString,
71    /// The name of the definition.
72    pub name: EcoString,
73    /// The kind of the definition.
74    pub kind: DefKind,
75    /// The location (file, start, end) of the definition.
76    pub loc: Option<(usize, usize, usize)>,
77    /// Whether the definition external to the module.
78    pub is_external: bool,
79    /// The module link to the definition
80    pub module_link: Option<String>,
81    /// The symbol link to the definition
82    pub symbol_link: Option<String>,
83    /// The link to the definition if it is external.
84    pub external_link: Option<String>,
85    /// The one-line documentation of the definition.
86    pub oneliner: Option<String>,
87    /// The raw documentation of the definition.
88    pub docs: Option<EcoString>,
89    /// The parsed documentation of the definition.
90    pub parsed_docs: Option<DefDocs>,
91    /// The value of the definition.
92    #[serde(skip)]
93    pub constant: Option<EcoString>,
94    /// The name range of the definition.
95    /// The value of the definition.
96    #[serde(skip)]
97    pub decl: Option<Interned<Decl>>,
98    /// The children of the definition.
99    pub children: Vec<DefInfo>,
100}
101
102/// Information about the definitions in a package.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PackageDefInfo {
105    /// The root module information.
106    #[serde(flatten)]
107    pub root: DefInfo,
108    /// The module accessible paths.
109    pub module_uses: HashMap<String, EcoVec<String>>,
110}
111
112struct ScanDefCtx<'a> {
113    ctx: &'a mut LocalContext,
114    for_spec: Option<&'a PackageSpec>,
115    aliases: &'a mut HashMap<FileId, Vec<String>>,
116    extras: &'a mut Vec<DefInfo>,
117    root: FileId,
118}
119
120impl ScanDefCtx<'_> {
121    fn defs(&mut self, paths: EcoVec<&str>, ei: ExprInfo) -> DefInfo {
122        let module_decl = Decl::module(ei.fid);
123        let key = module_decl.name().clone();
124        let site = Some(self.root);
125        let paths = paths.clone();
126        self.def(&key, paths, site.as_ref(), &module_decl.into(), None)
127    }
128
129    fn expr(
130        &mut self,
131        key: &str,
132        path: EcoVec<&str>,
133        site: Option<&FileId>,
134        val: &Expr,
135    ) -> DefInfo {
136        match val {
137            Expr::Decl(decl) => self.def(key, path, site, decl, Some(val)),
138            Expr::Ref(r) if r.root.is_some() => {
139                self.expr(key, path, site, r.root.as_ref().unwrap())
140            }
141            // todo: select
142            Expr::Select(..) => {
143                let mut path = path.clone();
144                path.push(key);
145                DefInfo {
146                    name: key.to_string().into(),
147                    kind: DefKind::Module,
148                    ..Default::default()
149                }
150            }
151            // v => panic!("unexpected export: {key} -> {v}"),
152            _ => {
153                let mut path = path.clone();
154                path.push(key);
155                DefInfo {
156                    name: key.to_string().into(),
157                    kind: DefKind::Constant,
158                    ..Default::default()
159                }
160            }
161        }
162    }
163
164    fn def(
165        &mut self,
166        key: &str,
167        path: EcoVec<&str>,
168        site: Option<&FileId>,
169        decl: &Interned<Decl>,
170        expr: Option<&Expr>,
171    ) -> DefInfo {
172        let def = self.ctx.def_of_decl(decl);
173        let def_docs = def.and_then(|def| self.ctx.def_docs(&def));
174        let docs = def_docs.as_ref().map(|docs| docs.docs().clone());
175        let children = match decl.as_ref() {
176            Decl::Module(..) => decl.file_id().and_then(|fid| {
177                // only generate docs for the same package
178                if fid.package() != self.for_spec {
179                    return None;
180                }
181
182                // !aliases.insert(fid)
183                let aliases_vec = self.aliases.entry(fid).or_default();
184                let is_fresh = aliases_vec.is_empty();
185                aliases_vec.push(path.iter().join("."));
186
187                if !is_fresh {
188                    crate::log_debug_ct!("found module: {path:?} (reexport)");
189                    return None;
190                }
191
192                crate::log_debug_ct!("found module: {path:?}");
193
194                let ei = self.ctx.expr_stage_by_id(fid)?;
195
196                let symbols = ei
197                    .exports
198                    .iter()
199                    .map(|(name, val)| {
200                        let mut path = path.clone();
201                        path.push(name);
202                        self.expr(name, path.clone(), Some(&fid), val)
203                    })
204                    .collect();
205                Some(symbols)
206            }),
207            _ => None,
208        };
209
210        let mut head = DefInfo {
211            id: EcoString::new(),
212            name: key.to_string().into(),
213            kind: decl.kind(),
214            constant: expr.map(|expr| expr.repr()),
215            docs,
216            parsed_docs: def_docs,
217            decl: Some(decl.clone()),
218            children: children.unwrap_or_default(),
219            loc: None,
220            is_external: false,
221            module_link: None,
222            symbol_link: None,
223            external_link: None,
224            oneliner: None,
225        };
226
227        if let Some((span, mod_fid)) = head.decl.as_ref().and_then(|decl| decl.file_id()).zip(site)
228            && span != *mod_fid
229        {
230            head.is_external = true;
231            head.oneliner = head.docs.map(|docs| oneliner(&docs).to_owned());
232            head.docs = None;
233        }
234
235        // Insert module that is not exported
236        if let Some(fid) = head.decl.as_ref().and_then(|del| del.file_id()) {
237            // only generate docs for the same package
238            if fid.package() == self.for_spec {
239                let av = self.aliases.entry(fid).or_default();
240                if av.is_empty() {
241                    let src = self.ctx.expr_stage_by_id(fid);
242                    let mut path = path.clone();
243                    path.push("-");
244                    path.push(key);
245
246                    crate::log_debug_ct!("found internal module: {path:?}");
247                    if let Some(m) = src {
248                        let msym = self.defs(path, m);
249                        self.extras.push(msym)
250                    }
251                }
252            }
253        }
254
255        head
256    }
257}
258
259/// Extract the first line of documentation.
260fn oneliner(docs: &str) -> &str {
261    docs.lines().next().unwrap_or_default()
262}