tinymist_query/syntax/
module.rs

1use std::sync::Once;
2
3use regex::RegexSet;
4
5use crate::prelude::*;
6
7/// The dependency information of a module (file).
8#[derive(Debug, Clone)]
9pub struct ModuleDependency {
10    /// The dependencies of this module.
11    pub dependencies: EcoVec<TypstFileId>,
12    /// The dependents of this module.
13    pub dependents: EcoVec<TypstFileId>,
14}
15
16/// Construct the module dependencies of the given context.
17///
18/// It will scan all the files in the context, using
19/// [`LocalContext::source_files`], and find the dependencies and dependents
20/// of each file.
21#[typst_macros::time]
22pub fn construct_module_dependencies(
23    ctx: &mut LocalContext,
24) -> HashMap<TypstFileId, ModuleDependency> {
25    let mut dependencies = HashMap::new();
26    let mut dependents = HashMap::new();
27
28    for file_id in ctx.source_files().clone() {
29        let source = match ctx.shared.source_by_id(file_id) {
30            Ok(source) => source,
31            Err(err) => {
32                static WARN_ONCE: Once = Once::new();
33                WARN_ONCE.call_once(|| {
34                    log::warn!("construct_module_dependencies: {err:?}");
35                });
36                continue;
37            }
38        };
39
40        let file_id = source.id();
41        let ei = ctx.shared.expr_stage(&source);
42
43        dependencies
44            .entry(file_id)
45            .or_insert_with(|| ModuleDependency {
46                dependencies: ei.imports.keys().cloned().collect(),
47                dependents: EcoVec::default(),
48            });
49        for (dep, _) in ei.imports.clone() {
50            dependents
51                .entry(dep)
52                .or_insert_with(EcoVec::new)
53                .push(file_id);
54        }
55    }
56
57    for (file_id, dependents) in dependents {
58        if let Some(dep) = dependencies.get_mut(&file_id) {
59            dep.dependents = dependents;
60        }
61    }
62
63    dependencies
64}
65
66fn is_hidden(entry: &walkdir::DirEntry) -> bool {
67    entry
68        .file_name()
69        .to_str()
70        .map(|s| s.starts_with('.'))
71        .unwrap_or(false)
72}
73
74/// Scan the files in the workspace and return the file ids.
75///
76/// Note: this function will touch the physical file system.
77pub(crate) fn scan_workspace_files<T>(
78    root: &Path,
79    ext: &RegexSet,
80    f: impl Fn(&Path) -> T,
81) -> Vec<T> {
82    let mut res = vec![];
83    let mut it = walkdir::WalkDir::new(root).follow_links(false).into_iter();
84    loop {
85        let de = match it.next() {
86            None => break,
87            Some(Err(_err)) => continue,
88            Some(Ok(entry)) => entry,
89        };
90        if is_hidden(&de) {
91            if de.file_type().is_dir() {
92                it.skip_current_dir();
93            }
94            continue;
95        }
96
97        /// this is a temporary solution to ignore some common build directories
98        static IGNORE_REGEX: LazyLock<RegexSet> = LazyLock::new(|| {
99            RegexSet::new([
100                r#"^build$"#,
101                r#"^target$"#,
102                r#"^node_modules$"#,
103                r#"^out$"#,
104                r#"^dist$"#,
105            ])
106            .unwrap()
107        });
108        if de
109            .path()
110            .file_name()
111            .and_then(|s| s.to_str())
112            .is_some_and(|s| IGNORE_REGEX.is_match(s))
113        {
114            if de.file_type().is_dir() {
115                it.skip_current_dir();
116            }
117            continue;
118        }
119
120        if !de.file_type().is_file() {
121            continue;
122        }
123        if !de
124            .path()
125            .extension()
126            .and_then(|err| err.to_str())
127            .is_some_and(|err| ext.is_match(err))
128        {
129            continue;
130        }
131
132        let path = de.path();
133        let relative_path = match path.strip_prefix(root) {
134            Ok(path) => path,
135            Err(err) => {
136                log::warn!("failed to strip prefix, path: {path:?}, root: {root:?}: {err}");
137                continue;
138            }
139        };
140
141        res.push(f(relative_path));
142
143        // two times of max number of typst file ids
144        if res.len() >= (u16::MAX as usize) {
145            break;
146        }
147    }
148
149    res
150}