tinymist_query/
package.rs1use std::path::PathBuf;
4
5use ecow::eco_format;
6#[cfg(feature = "local-registry")]
7use ecow::{EcoVec, eco_vec};
8use serde::{Deserialize, Serialize};
10use tinymist_world::package::PackageSpec;
11use tinymist_world::package::registry::PackageIndexEntry;
12use typst::World;
13use typst::diag::{EcoString, StrResult};
14use typst::syntax::package::PackageManifest;
15use typst::syntax::{FileId, VirtualPath};
16
17use crate::LocalContext;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PackageInfo {
22 pub path: PathBuf,
24 pub namespace: EcoString,
26 pub name: EcoString,
28 pub version: String,
30}
31
32impl From<PackageIndexEntry> for PackageInfo {
33 fn from(entry: PackageIndexEntry) -> Self {
34 let spec = entry.spec();
35 Self {
36 path: entry.path.unwrap_or_default(),
37 namespace: spec.namespace,
38 name: spec.name,
39 version: spec.version.to_string(),
40 }
41 }
42}
43
44pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
46 Ok(FileId::new(
47 Some(PackageSpec {
48 namespace: spec.namespace.clone(),
49 name: spec.name.clone(),
50 version: spec.version.parse()?,
51 }),
52 VirtualPath::new("typst.toml"),
53 ))
54}
55
56pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
58 let toml_data = world
59 .file(toml_id)
60 .map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
61
62 let string = std::str::from_utf8(&toml_data)
63 .map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
64
65 toml::from_str(string)
66 .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
67}
68
69pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
71 let toml_id = get_manifest_id(spec)?;
72 let manifest = ctx.get_manifest(toml_id)?;
73
74 let entry_point = toml_id.join(&manifest.package.entrypoint);
75
76 ctx.shared_().preload_package(entry_point);
77 Ok(())
78}
79
80#[cfg(feature = "local-registry")]
82pub enum PackageFilter {
83 For(EcoString),
85 ExceptFor(EcoString),
87 All,
89}
90
91#[cfg(feature = "local-registry")]
92pub fn list_package(
94 world: &tinymist_project::LspWorld,
95 filter: PackageFilter,
96) -> EcoVec<PackageIndexEntry> {
97 trait IsDirFollowLinks {
98 fn is_dir_follow_links(&self) -> bool;
99 }
100
101 impl IsDirFollowLinks for PathBuf {
102 fn is_dir_follow_links(&self) -> bool {
103 self.canonicalize()
106 .map(|meta| meta.is_dir())
107 .unwrap_or(false)
108 }
109 }
110
111 let registry = &world.registry;
112
113 let mut packages = eco_vec![];
117
118 let paths = registry.paths();
119 log::info!("searching for packages in paths {paths:?}");
120
121 let mut search_in_dir = |local_path: PathBuf, ns: EcoString| {
122 if !local_path.exists() || !local_path.is_dir_follow_links() {
123 return;
124 }
125 let Some(package_names) = once_log(std::fs::read_dir(local_path), "read local package")
128 else {
129 return;
130 };
131 for package in package_names {
132 let Some(package) = once_log(package, "read package name") else {
133 continue;
134 };
135 let package_name = EcoString::from(package.file_name().to_string_lossy());
136 if package_name.starts_with('.') {
137 continue;
138 }
139
140 let package_path = package.path();
141 if !package_path.is_dir_follow_links() {
142 continue;
143 }
144 let Some(versions) = once_log(std::fs::read_dir(package_path), "read package versions")
146 else {
147 continue;
148 };
149 for version in versions {
150 let Some(version_entry) = once_log(version, "read package version") else {
151 continue;
152 };
153 if version_entry.file_name().to_string_lossy().starts_with('.') {
154 continue;
155 }
156 let package_version_path = version_entry.path();
157 if !package_version_path.is_dir_follow_links() {
158 continue;
159 }
160 let Some(version) = once_log(
161 version_entry.file_name().to_string_lossy().parse(),
162 "parse package version",
163 ) else {
164 continue;
165 };
166 let spec = PackageSpec {
167 namespace: ns.clone(),
168 name: package_name.clone(),
169 version,
170 };
171 let manifest_id = typst::syntax::FileId::new(
172 Some(spec.clone()),
173 typst::syntax::VirtualPath::new("typst.toml"),
174 );
175 let Some(manifest) =
176 once_log(get_manifest(world, manifest_id), "read package manifest")
177 else {
178 continue;
179 };
180 packages.push(PackageIndexEntry {
181 namespace: ns.clone(),
182 package: manifest.package,
183 template: manifest.template,
184 updated_at: None,
185 path: Some(package_version_path),
186 });
187 }
188 }
189 };
190
191 for dir in paths {
192 let matching_ns = match &filter {
193 PackageFilter::For(ns) => {
194 let local_path = dir.join(ns.as_str());
195 search_in_dir(local_path, ns.clone());
196
197 continue;
198 }
199 PackageFilter::ExceptFor(ns) => Some(ns),
200 PackageFilter::All => None,
201 };
202
203 let Some(namespaces) = once_log(std::fs::read_dir(dir), "read package directory") else {
204 continue;
205 };
206 for dir in namespaces {
207 let Some(dir) = once_log(dir, "read ns directory") else {
208 continue;
209 };
210 let ns = dir.file_name();
211 let ns = ns.to_string_lossy();
212 if let Some(matching_ns) = &matching_ns
213 && matching_ns.as_str() == ns.as_ref()
214 {
215 continue;
216 }
217 let local_path = dir.path();
218 search_in_dir(local_path, ns.into());
219 }
220 }
221
222 packages
223}
224
225#[cfg(feature = "local-registry")]
226fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
227 use std::collections::HashSet;
228 use std::sync::OnceLock;
229
230 use parking_lot::Mutex;
231
232 let err = match result {
233 Ok(value) => return Some(value),
234 Err(err) => err,
235 };
236
237 static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
238 let mut once = ONCE.get_or_init(Default::default).lock();
239 if once.insert(site) {
240 log::error!("failed to perform {site}: {err}");
241 }
242
243 None
244}