tinymist_project/
lsp.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use tinymist_std::ImmutPath;
5use tinymist_std::error::prelude::*;
6use tinymist_task::ExportTarget;
7use tinymist_world::package::RegistryPathMapper;
8#[cfg(all(not(feature = "system"), feature = "web"))]
9use tinymist_world::package::registry::ProxyContext;
10use tinymist_world::vfs::Vfs;
11use tinymist_world::{
12    CompileSnapshot, CompilerFeat, CompilerUniverse, CompilerWorld, EntryOpts, EntryState,
13};
14use tinymist_world::{WorldComputeGraph, args::*};
15use typst::Features;
16use typst::diag::FileResult;
17use typst::foundations::{Bytes, Dict};
18use typst::utils::LazyHash;
19
20use crate::world::font::FontResolverImpl;
21use crate::{CompiledArtifact, Interrupt};
22
23/// Compiler feature for LSP universe and worlds without typst.ts to implement
24/// more for tinymist. type trait of [`CompilerUniverse`].
25#[derive(Debug, Clone, Copy)]
26pub struct LspCompilerFeat;
27
28impl CompilerFeat for LspCompilerFeat {
29    /// Uses [`FontResolverImpl`] directly.
30    type FontResolver = FontResolverImpl;
31    /// It accesses a physical file system.
32    type AccessModel = DynAccessModel;
33    /// It performs:
34    /// - native HTTP requests for fetching package data in system environment
35    /// - js proxied requests to browser environment
36    /// - no package registry in other environments
37    type Registry = LspRegistry;
38}
39
40/// LSP universe that spawns LSP worlds.
41pub type LspUniverse = CompilerUniverse<LspCompilerFeat>;
42/// LSP world that holds compilation resources
43pub type LspWorld = CompilerWorld<LspCompilerFeat>;
44/// LSP compile snapshot.
45pub type LspCompileSnapshot = CompileSnapshot<LspCompilerFeat>;
46/// LSP compiled artifact.
47pub type LspCompiledArtifact = CompiledArtifact<LspCompilerFeat>;
48/// LSP compute graph.
49pub type LspComputeGraph = Arc<WorldComputeGraph<LspCompilerFeat>>;
50/// LSP interrupt.
51pub type LspInterrupt = Interrupt<LspCompilerFeat>;
52/// Immutable prehashed reference to dictionary.
53pub type ImmutDict = Arc<LazyHash<Dict>>;
54
55/// World provider for LSP universe and worlds.
56pub trait WorldProvider {
57    /// Get the entry options from the arguments.
58    fn entry(&self) -> Result<EntryOpts>;
59    /// Get a universe instance from the given arguments.
60    fn resolve(&self) -> Result<LspUniverse>;
61}
62
63#[cfg(feature = "system")]
64impl WorldProvider for CompileOnceArgs {
65    fn resolve(&self) -> Result<LspUniverse> {
66        let entry = self.entry()?.try_into()?;
67        let inputs = self.resolve_inputs().unwrap_or_default();
68        let fonts = Arc::new(LspUniverseBuilder::resolve_fonts(self.font.clone())?);
69        let packages = LspUniverseBuilder::resolve_package(
70            self.cert.as_deref().map(From::from),
71            Some(&self.package),
72        );
73
74        // todo: more export targets
75        Ok(LspUniverseBuilder::build(
76            entry,
77            ExportTarget::Paged,
78            self.resolve_features(),
79            inputs,
80            packages,
81            fonts,
82            self.creation_timestamp,
83            DynAccessModel(Arc::new(tinymist_world::vfs::system::SystemAccessModel {})),
84        ))
85    }
86
87    fn entry(&self) -> Result<EntryOpts> {
88        let mut cwd = None;
89        let mut cwd = move || {
90            cwd.get_or_insert_with(|| {
91                std::env::current_dir().context("failed to get current directory")
92            })
93            .clone()
94        };
95
96        let main = {
97            let input = self.input.as_ref().context("entry file must be provided")?;
98            let input = Path::new(&input);
99            if input.is_absolute() {
100                input.to_owned()
101            } else {
102                cwd()?.join(input)
103            }
104        };
105
106        let root = if let Some(root) = &self.root {
107            if root.is_absolute() {
108                root.clone()
109            } else {
110                cwd()?.join(root)
111            }
112        } else {
113            main.parent()
114                .context("entry file don't have a valid parent as root")?
115                .to_owned()
116        };
117
118        let relative_main = match main.strip_prefix(&root) {
119            Ok(relative_main) => relative_main,
120            Err(_) => {
121                log::error!("entry file must be inside the root, file: {main:?}, root: {root:?}");
122                bail!("entry file must be inside the root, file: {main:?}, root: {root:?}");
123            }
124        };
125
126        Ok(EntryOpts::new_rooted(
127            root.clone(),
128            Some(relative_main.to_owned()),
129        ))
130    }
131}
132
133// todo: merge me with the above impl
134#[cfg(feature = "system")]
135impl WorldProvider for (crate::ProjectInput, ImmutPath) {
136    fn resolve(&self) -> Result<LspUniverse> {
137        use typst::foundations::{Str, Value};
138
139        let (proj, lock_dir) = self;
140        let entry = self.entry()?.try_into()?;
141        let inputs = proj
142            .inputs
143            .iter()
144            .map(|(k, v)| (Str::from(k.as_str()), Value::Str(Str::from(v.as_str()))))
145            .collect();
146        let fonts = LspUniverseBuilder::resolve_fonts(CompileFontArgs {
147            font_paths: {
148                proj.font_paths
149                    .iter()
150                    .flat_map(|p| p.to_abs_path(lock_dir))
151                    .collect::<Vec<_>>()
152            },
153            ignore_system_fonts: !proj.system_fonts,
154        })?;
155        let packages = LspUniverseBuilder::resolve_package(
156            // todo: recover certificate path
157            None,
158            Some(&CompilePackageArgs {
159                package_path: proj
160                    .package_path
161                    .as_ref()
162                    .and_then(|p| p.to_abs_path(lock_dir)),
163                package_cache_path: proj
164                    .package_cache_path
165                    .as_ref()
166                    .and_then(|p| p.to_abs_path(lock_dir)),
167            }),
168        );
169
170        // todo: more export targets
171        Ok(LspUniverseBuilder::build(
172            entry,
173            ExportTarget::Paged,
174            // todo: features
175            Features::default(),
176            Arc::new(LazyHash::new(inputs)),
177            packages,
178            Arc::new(fonts),
179            None, // creation_timestamp - not available in project file context
180            DynAccessModel(Arc::new(tinymist_world::vfs::system::SystemAccessModel {})),
181        ))
182    }
183
184    fn entry(&self) -> Result<EntryOpts> {
185        let (proj, lock_dir) = self;
186
187        let entry = proj
188            .main
189            .to_abs_path(lock_dir)
190            .context("failed to resolve entry file")?;
191
192        let root = if let Some(root) = &proj.root {
193            root.to_abs_path(lock_dir)
194                .context("failed to resolve root")?
195        } else {
196            lock_dir.as_ref().to_owned()
197        };
198
199        if !entry.starts_with(&root) {
200            bail!("entry file must be in the root directory, {entry:?}, {root:?}");
201        }
202
203        let relative_entry = match entry.strip_prefix(&root) {
204            Ok(relative_entry) => relative_entry,
205            Err(_) => bail!("entry path must be inside the root: {}", entry.display()),
206        };
207
208        Ok(EntryOpts::new_rooted(
209            root.clone(),
210            Some(relative_entry.to_owned()),
211        ))
212    }
213}
214
215#[cfg(all(not(feature = "system"), feature = "web"))]
216type LspRegistry = tinymist_world::package::registry::JsRegistry;
217#[cfg(feature = "system")]
218type LspRegistry = tinymist_world::package::registry::HttpRegistry;
219#[cfg(not(any(feature = "system", feature = "web")))]
220type LspRegistry = tinymist_world::package::registry::DummyRegistry;
221
222/// Builder for LSP universe.
223pub struct LspUniverseBuilder;
224
225impl LspUniverseBuilder {
226    /// Create [`LspUniverse`] with the given options.
227    /// See [`LspCompilerFeat`] for instantiation details.
228    #[allow(clippy::too_many_arguments)]
229    pub fn build(
230        entry: EntryState,
231        export_target: ExportTarget,
232        features: Features,
233        inputs: ImmutDict,
234        package_registry: LspRegistry,
235        font_resolver: Arc<FontResolverImpl>,
236        creation_timestamp: Option<i64>,
237        access_model: DynAccessModel,
238    ) -> LspUniverse {
239        let package_registry = Arc::new(package_registry);
240        let resolver = Arc::new(RegistryPathMapper::new(package_registry.clone()));
241
242        // todo: typst doesn't allow to merge features
243        let features = if matches!(export_target, ExportTarget::Html) {
244            Features::from_iter([typst::Feature::Html])
245        } else {
246            features
247        };
248
249        LspUniverse::new_raw(
250            entry,
251            features,
252            Some(inputs),
253            Vfs::new(resolver, access_model),
254            package_registry,
255            font_resolver,
256            creation_timestamp,
257        )
258    }
259
260    /// Resolve fonts from given options.
261    #[cfg(feature = "system")]
262    pub fn only_embedded_fonts() -> Result<FontResolverImpl> {
263        let mut searcher = tinymist_world::font::system::SystemFontSearcher::new();
264        searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
265            font_paths: vec![],
266            no_system_fonts: true,
267            with_embedded_fonts: typst_assets::fonts()
268                .map(std::borrow::Cow::Borrowed)
269                .collect(),
270        })?;
271        Ok(searcher.build())
272    }
273
274    /// Resolve fonts from given options.
275    #[cfg(feature = "system")]
276    pub fn resolve_fonts(args: CompileFontArgs) -> Result<FontResolverImpl> {
277        let mut searcher = tinymist_world::font::system::SystemFontSearcher::new();
278        searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
279            font_paths: args.font_paths,
280            no_system_fonts: args.ignore_system_fonts,
281            with_embedded_fonts: typst_assets::fonts()
282                .map(std::borrow::Cow::Borrowed)
283                .collect(),
284        })?;
285        Ok(searcher.build())
286    }
287
288    /// Resolve fonts from given options.
289    #[cfg(all(not(feature = "system"), feature = "web"))]
290    pub fn resolve_fonts(args: CompileFontArgs) -> Result<FontResolverImpl> {
291        let mut searcher = tinymist_world::font::web::BrowserFontSearcher::new();
292        searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
293            font_paths: args.font_paths,
294            no_system_fonts: args.ignore_system_fonts,
295            with_embedded_fonts: typst_assets::fonts()
296                .map(std::borrow::Cow::Borrowed)
297                .collect(),
298        })?;
299        Ok(searcher.build())
300    }
301
302    /// Resolve fonts from given options.
303    #[cfg(not(any(feature = "system", feature = "web")))]
304    pub fn resolve_fonts(_args: CompileFontArgs) -> Result<FontResolverImpl> {
305        let mut searcher = tinymist_world::font::memory::MemoryFontSearcher::default();
306        searcher.add_memory_fonts(typst_assets::fonts().map(Bytes::new).collect::<Vec<_>>());
307        Ok(searcher.build())
308    }
309
310    /// Resolves package registry from given options.
311    #[cfg(feature = "system")]
312    pub fn resolve_package(
313        cert_path: Option<ImmutPath>,
314        args: Option<&CompilePackageArgs>,
315    ) -> tinymist_world::package::registry::HttpRegistry {
316        tinymist_world::package::registry::HttpRegistry::new(
317            cert_path,
318            args.and_then(|args| Some(args.package_path.clone()?.into())),
319            args.and_then(|args| Some(args.package_cache_path.clone()?.into())),
320        )
321    }
322
323    /// Resolves package registry from given options.
324    #[cfg(all(not(feature = "system"), feature = "web"))]
325    pub fn resolve_package(
326        _cert_path: Option<ImmutPath>,
327        _args: Option<&CompilePackageArgs>,
328        resolve_fn: js_sys::Function,
329    ) -> tinymist_world::package::registry::JsRegistry {
330        tinymist_world::package::registry::JsRegistry {
331            context: ProxyContext::new(wasm_bindgen::JsValue::NULL),
332            real_resolve_fn: resolve_fn,
333        }
334    }
335
336    /// Resolves package registry from given options.
337    #[cfg(not(any(feature = "system", feature = "web")))]
338    pub fn resolve_package(
339        _cert_path: Option<ImmutPath>,
340        _args: Option<&CompilePackageArgs>,
341    ) -> tinymist_world::package::registry::DummyRegistry {
342        tinymist_world::package::registry::DummyRegistry
343    }
344}
345
346/// Access model for LSP universe and worlds.
347pub trait LspAccessModel: Send + Sync {
348    /// Returns the content of a file entry.
349    fn content(&self, src: &Path) -> FileResult<Bytes>;
350}
351
352impl<T> LspAccessModel for T
353where
354    T: tinymist_world::vfs::PathAccessModel + Send + Sync + 'static,
355{
356    fn content(&self, src: &Path) -> FileResult<Bytes> {
357        self.content(src)
358    }
359}
360
361/// Access model for LSP universe and worlds.
362#[derive(Clone)]
363pub struct DynAccessModel(pub Arc<dyn LspAccessModel>);
364
365impl DynAccessModel {
366    /// Create a new dynamic access model from the given access model.
367    pub fn new(access_model: Arc<dyn LspAccessModel>) -> Self {
368        Self(access_model)
369    }
370}
371
372impl tinymist_world::vfs::PathAccessModel for DynAccessModel {
373    fn content(&self, src: &Path) -> FileResult<Bytes> {
374        self.0.content(src)
375    }
376
377    fn reset(&mut self) {}
378}