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 name = {
123            let stem = ei.fid.vpath().as_rooted_path().file_stem();
124            stem.and_then(|s| Some(Interned::new_str(s.to_str()?)))
125                .unwrap_or_default()
126        };
127        let module_decl = Decl::module(name.clone(), ei.fid).into();
128        let site = Some(self.root);
129        let paths = paths.clone();
130        self.def(&name, paths, site.as_ref(), &module_decl, None)
131    }
132
133    fn expr(
134        &mut self,
135        key: &str,
136        path: EcoVec<&str>,
137        site: Option<&FileId>,
138        val: &Expr,
139    ) -> DefInfo {
140        match val {
141            Expr::Decl(decl) => self.def(key, path, site, decl, Some(val)),
142            Expr::Ref(r) if r.root.is_some() => {
143                self.expr(key, path, site, r.root.as_ref().unwrap())
144            }
145            // todo: select
146            Expr::Select(..) => {
147                let mut path = path.clone();
148                path.push(key);
149                DefInfo {
150                    name: key.to_string().into(),
151                    kind: DefKind::Module,
152                    ..Default::default()
153                }
154            }
155            // v => panic!("unexpected export: {key} -> {v}"),
156            _ => {
157                let mut path = path.clone();
158                path.push(key);
159                DefInfo {
160                    name: key.to_string().into(),
161                    kind: DefKind::Constant,
162                    ..Default::default()
163                }
164            }
165        }
166    }
167
168    fn def(
169        &mut self,
170        key: &str,
171        path: EcoVec<&str>,
172        site: Option<&FileId>,
173        decl: &Interned<Decl>,
174        expr: Option<&Expr>,
175    ) -> DefInfo {
176        let def = self.ctx.def_of_decl(decl);
177        let def_docs = def.and_then(|def| self.ctx.def_docs(&def));
178        let docs = def_docs.as_ref().map(|docs| docs.docs().clone());
179        let children = match decl.as_ref() {
180            Decl::Module(..) => decl.file_id().and_then(|fid| {
181                // only generate docs for the same package
182                if fid.package() != self.for_spec {
183                    return None;
184                }
185
186                // !aliases.insert(fid)
187                let aliases_vec = self.aliases.entry(fid).or_default();
188                let is_fresh = aliases_vec.is_empty();
189                aliases_vec.push(path.iter().join("."));
190
191                if !is_fresh {
192                    crate::log_debug_ct!("found module: {path:?} (reexport)");
193                    return None;
194                }
195
196                crate::log_debug_ct!("found module: {path:?}");
197
198                let ei = self.ctx.expr_stage_by_id(fid)?;
199
200                let symbols = ei
201                    .exports
202                    .iter()
203                    .map(|(name, val)| {
204                        let mut path = path.clone();
205                        path.push(name);
206                        self.expr(name, path.clone(), Some(&fid), val)
207                    })
208                    .collect();
209                Some(symbols)
210            }),
211            _ => None,
212        };
213
214        let mut head = DefInfo {
215            id: EcoString::new(),
216            name: key.to_string().into(),
217            kind: decl.kind(),
218            constant: expr.map(|expr| expr.repr()),
219            docs,
220            parsed_docs: def_docs,
221            decl: Some(decl.clone()),
222            children: children.unwrap_or_default(),
223            loc: None,
224            is_external: false,
225            module_link: None,
226            symbol_link: None,
227            external_link: None,
228            oneliner: None,
229        };
230
231        if let Some((span, mod_fid)) = head.decl.as_ref().and_then(|decl| decl.file_id()).zip(site)
232            && span != *mod_fid
233        {
234            head.is_external = true;
235            head.oneliner = head.docs.map(|docs| oneliner(&docs).to_owned());
236            head.docs = None;
237        }
238
239        // Insert module that is not exported
240        if let Some(fid) = head.decl.as_ref().and_then(|del| del.file_id()) {
241            // only generate docs for the same package
242            if fid.package() == self.for_spec {
243                let av = self.aliases.entry(fid).or_default();
244                if av.is_empty() {
245                    let src = self.ctx.expr_stage_by_id(fid);
246                    let mut path = path.clone();
247                    path.push("-");
248                    path.push(key);
249
250                    crate::log_debug_ct!("found internal module: {path:?}");
251                    if let Some(m) = src {
252                        let msym = self.defs(path, m);
253                        self.extras.push(msym)
254                    }
255                }
256            }
257        }
258
259        head
260    }
261}
262
263/// Extract the first line of documentation.
264fn oneliner(docs: &str) -> &str {
265    docs.lines().next().unwrap_or_default()
266}