tinymist_query/syntax/
module.rs1use std::sync::Once;
2
3use regex::RegexSet;
4
5use crate::prelude::*;
6
7#[derive(Debug, Clone)]
9pub struct ModuleDependency {
10 pub dependencies: EcoVec<TypstFileId>,
12 pub dependents: EcoVec<TypstFileId>,
14}
15
16#[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
74pub(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 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 if res.len() >= (u16::MAX as usize) {
145 break;
146 }
147 }
148
149 res
150}