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        let target_feature = match export_target {
243            ExportTarget::Html => Some(typst::Feature::Html),
244            ExportTarget::Bundle => Some(typst::Feature::Bundle),
245            ExportTarget::Paged => None,
246        };
247        let features = [
248            typst::Feature::Html,
249            typst::Feature::A11yExtras,
250            typst::Feature::Bundle,
251        ]
252        .into_iter()
253        .filter(|feature| features.is_enabled(*feature) || Some(*feature) == target_feature)
254        .collect();
255
256        LspUniverse::new_raw(
257            entry,
258            features,
259            Some(inputs),
260            Vfs::new(resolver, access_model),
261            package_registry,
262            font_resolver,
263            creation_timestamp,
264        )
265    }
266
267    /// Resolve fonts from given options.
268    #[cfg(feature = "system")]
269    pub fn only_embedded_fonts() -> Result<FontResolverImpl> {
270        let mut searcher = tinymist_world::font::system::SystemFontSearcher::new();
271        searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
272            font_paths: vec![],
273            no_system_fonts: true,
274            with_embedded_fonts: typst_assets::fonts()
275                .map(std::borrow::Cow::Borrowed)
276                .collect(),
277        })?;
278        Ok(searcher.build())
279    }
280
281    /// Resolve fonts from given options.
282    #[cfg(feature = "system")]
283    pub fn resolve_fonts(args: CompileFontArgs) -> Result<FontResolverImpl> {
284        let mut searcher = tinymist_world::font::system::SystemFontSearcher::new();
285        searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
286            font_paths: args.font_paths,
287            no_system_fonts: args.ignore_system_fonts,
288            with_embedded_fonts: typst_assets::fonts()
289                .map(std::borrow::Cow::Borrowed)
290                .collect(),
291        })?;
292        Ok(searcher.build())
293    }
294
295    /// Resolve fonts from given options.
296    #[cfg(all(not(feature = "system"), feature = "web"))]
297    pub fn resolve_fonts(args: CompileFontArgs) -> Result<FontResolverImpl> {
298        let mut searcher = tinymist_world::font::web::BrowserFontSearcher::new();
299        searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
300            font_paths: args.font_paths,
301            no_system_fonts: args.ignore_system_fonts,
302            with_embedded_fonts: typst_assets::fonts()
303                .map(std::borrow::Cow::Borrowed)
304                .collect(),
305        })?;
306        Ok(searcher.build())
307    }
308
309    /// Resolve fonts from given options.
310    #[cfg(not(any(feature = "system", feature = "web")))]
311    pub fn resolve_fonts(_args: CompileFontArgs) -> Result<FontResolverImpl> {
312        let mut searcher = tinymist_world::font::memory::MemoryFontSearcher::default();
313        searcher.add_memory_fonts(typst_assets::fonts().map(Bytes::new).collect::<Vec<_>>());
314        Ok(searcher.build())
315    }
316
317    /// Resolves package registry from given options.
318    #[cfg(feature = "system")]
319    pub fn resolve_package(
320        cert_path: Option<ImmutPath>,
321        args: Option<&CompilePackageArgs>,
322    ) -> tinymist_world::package::registry::HttpRegistry {
323        tinymist_world::package::registry::HttpRegistry::new(
324            cert_path,
325            args.and_then(|args| Some(args.package_path.clone()?.into())),
326            args.and_then(|args| Some(args.package_cache_path.clone()?.into())),
327        )
328    }
329
330    /// Resolves package registry from given options.
331    #[cfg(all(not(feature = "system"), feature = "web"))]
332    pub fn resolve_package(
333        _cert_path: Option<ImmutPath>,
334        _args: Option<&CompilePackageArgs>,
335        resolve_fn: js_sys::Function,
336    ) -> tinymist_world::package::registry::JsRegistry {
337        tinymist_world::package::registry::JsRegistry {
338            context: ProxyContext::new(wasm_bindgen::JsValue::NULL),
339            real_resolve_fn: resolve_fn,
340        }
341    }
342
343    /// Resolves package registry from given options.
344    #[cfg(not(any(feature = "system", feature = "web")))]
345    pub fn resolve_package(
346        _cert_path: Option<ImmutPath>,
347        _args: Option<&CompilePackageArgs>,
348    ) -> tinymist_world::package::registry::DummyRegistry {
349        tinymist_world::package::registry::DummyRegistry
350    }
351}
352
353/// Access model for LSP universe and worlds.
354pub trait LspAccessModel: Send + Sync {
355    /// Returns the content of a file entry.
356    fn content(&self, src: &Path) -> FileResult<Bytes>;
357}
358
359impl<T> LspAccessModel for T
360where
361    T: tinymist_world::vfs::PathAccessModel + Send + Sync + 'static,
362{
363    fn content(&self, src: &Path) -> FileResult<Bytes> {
364        self.content(src)
365    }
366}
367
368/// Access model for LSP universe and worlds.
369#[derive(Clone)]
370pub struct DynAccessModel(pub Arc<dyn LspAccessModel>);
371
372impl DynAccessModel {
373    /// Create a new dynamic access model from the given access model.
374    pub fn new(access_model: Arc<dyn LspAccessModel>) -> Self {
375        Self(access_model)
376    }
377}
378
379impl tinymist_world::vfs::PathAccessModel for DynAccessModel {
380    fn content(&self, src: &Path) -> FileResult<Bytes> {
381        self.0.content(src)
382    }
383
384    fn reset(&mut self) {}
385}