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