From 00aecd9eae5dca00b8199ca2d17ade6247788338 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:05:28 +0800 Subject: [PATCH] refactor: split entry resolver (#942) * refactor: split entry resolver * refactor: naming --- crates/tinymist/src/actor/mod.rs | 4 +- crates/tinymist/src/actor/typ_client.rs | 11 +- crates/tinymist/src/cmd.rs | 2 +- crates/tinymist/src/init.rs | 258 +++++++++++++----------- crates/tinymist/src/main.rs | 15 +- crates/tinymist/src/server.rs | 11 +- 6 files changed, 171 insertions(+), 130 deletions(-) diff --git a/crates/tinymist/src/actor/mod.rs b/crates/tinymist/src/actor/mod.rs index 2a47d0a6..ba38252b 100644 --- a/crates/tinymist/src/actor/mod.rs +++ b/crates/tinymist/src/actor/mod.rs @@ -28,7 +28,7 @@ use typ_server::{CompileServerActor, CompileServerOpts}; impl LanguageState { /// Restart the primary server. pub fn restart_primary(&mut self) { - let entry = self.compile_config().determine_default_entry_path(); + let entry = self.entry_resolver().resolve_default(); self.restart_server("primary", entry); } @@ -41,7 +41,7 @@ impl LanguageState { fn restart_server(&mut self, group: &str, entry: Option) { let server = self.server( group.to_owned(), - self.compile_config().determine_entry(entry), + self.entry_resolver().resolve(entry), self.compile_config().determine_inputs(), self.vfs_snapshot(), ); diff --git a/crates/tinymist/src/actor/typ_client.rs b/crates/tinymist/src/actor/typ_client.rs index e72ba5e8..f337aae5 100644 --- a/crates/tinymist/src/actor/typ_client.rs +++ b/crates/tinymist/src/actor/typ_client.rs @@ -49,7 +49,7 @@ use crate::{ stats::{CompilerQueryStats, QueryStatGuard}, task::{ExportTask, ExportUserConfig}, world::{LspCompilerFeat, LspWorld}, - CompileConfig, + CompileConfig, EntryResolver, }; type EditorSender = mpsc::UnboundedSender; @@ -287,6 +287,11 @@ impl CompileClientActor { self.handle.clone().snapshot() } + /// Get the entry resolver. + pub fn entry_resolver(&self) -> &EntryResolver { + &self.config.entry_resolver + } + /// Snapshot the compiler thread for language queries pub fn query_snapshot(&self) -> ZResult { self.handle.clone().query_snapshot(None) @@ -321,7 +326,7 @@ impl CompileClientActor { let OnExportRequest { path, kind, open } = req; let snap = self.snapshot()?; - let entry = self.config.determine_entry(Some(path.as_path().into())); + let entry = self.entry_resolver().resolve(Some(path.as_path().into())); let export = self.handle.export.oneshot(snap, Some(entry), kind); just_future(async move { let res = export.await?; @@ -369,7 +374,7 @@ impl CompileClientActor { return Err(error_once!("entry file must be absolute", path: path.unwrap().display())); } - let next_entry = self.config.determine_entry(path); + let next_entry = self.entry_resolver().resolve(path); if next_entry == self.entry { return Ok(false); } diff --git a/crates/tinymist/src/cmd.rs b/crates/tinymist/src/cmd.rs index d4560b40..7f56c3da 100644 --- a/crates/tinymist/src/cmd.rs +++ b/crates/tinymist/src/cmd.rs @@ -456,7 +456,7 @@ impl LanguageState { let self_path = std::env::current_exe() .map_err(|e| internal_error(format!("Cannot get typst compiler {e}")))?; - let entry = self.config.compile.determine_entry(Some(path)); + let entry = self.entry_resolver().resolve(Some(path)); let snap = self.primary().snapshot().map_err(z_internal_error)?; let user_action = self.user_action; diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index f0e67910..4d25376a 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -70,22 +70,26 @@ impl Initializer for RegularInit { /// Errors if the configuration could not be updated. fn initialize(mut self, params: InitializeParams) -> (LanguageState, AnySchedulableResponse) { // Initialize configurations + let roots = match params.workspace_folders.as_ref() { + Some(roots) => roots + .iter() + .filter_map(|root| root.uri.to_file_path().ok().map(ImmutPath::from)) + .collect(), + #[allow(deprecated)] // `params.root_path` is marked as deprecated + None => params + .root_uri + .as_ref() + .and_then(|uri| uri.to_file_path().ok().map(ImmutPath::from)) + .or_else(|| Some(Path::new(¶ms.root_path.as_ref()?).into())) + .into_iter() + .collect(), + }; let mut config = Config { const_config: ConstConfig::from(¶ms), compile: CompileConfig { - roots: match params.workspace_folders.as_ref() { - Some(roots) => roots - .iter() - .filter_map(|root| root.uri.to_file_path().ok().map(ImmutPath::from)) - .collect(), - #[allow(deprecated)] // `params.root_path` is marked as deprecated - None => params - .root_uri - .as_ref() - .and_then(|uri| uri.to_file_path().ok().map(ImmutPath::from)) - .or_else(|| Some(Path::new(¶ms.root_path?).into())) - .into_iter() - .collect(), + entry_resolver: EntryResolver { + roots, + ..Default::default() }, font_opts: std::mem::take(&mut self.font_opts), ..CompileConfig::default() @@ -465,17 +469,125 @@ impl From<&InitializeParams> for ConstConfig { } } +/// Entry resolver +#[derive(Debug, Default, Clone)] +pub struct EntryResolver { + /// Specifies the root path of the project manually. + pub root_path: Option, + /// The workspace roots from initialization. + pub roots: Vec, + /// Typst extra arguments. + pub typst_extra_args: Option, +} + +impl EntryResolver { + /// Resolves the root directory for the entry file. + pub fn root(&self, entry: Option<&ImmutPath>) -> Option { + if let Some(path) = &self.root_path { + return Some(path.clone()); + } + + if let Some(root) = try_(|| self.typst_extra_args.as_ref()?.root_dir.as_ref()) { + return Some(root.clone()); + } + + if let Some(entry) = entry { + for root in self.roots.iter() { + if entry.starts_with(root) { + return Some(root.clone()); + } + } + + if !self.roots.is_empty() { + log::warn!("entry is not in any set root directory"); + } + + if let Some(parent) = entry.parent() { + return Some(parent.into()); + } + } + + if !self.roots.is_empty() { + return Some(self.roots[0].clone()); + } + + None + } + + /// Resolves the entry state. + pub fn resolve(&self, entry: Option) -> EntryState { + // todo: formalize untitled path + // let is_untitled = entry.as_ref().is_some_and(|p| p.starts_with("/untitled")); + // let root_dir = self.determine_root(if is_untitled { None } else { + // entry.as_ref() }); + let root_dir = self.root(entry.as_ref()); + + let entry = match (entry, root_dir) { + // (Some(entry), Some(root)) if is_untitled => Some(EntryState::new_rooted( + // root, + // Some(FileId::new(None, VirtualPath::new(entry))), + // )), + (Some(entry), Some(root)) => match entry.strip_prefix(&root) { + Ok(stripped) => Some(EntryState::new_rooted( + root, + Some(FileId::new(None, VirtualPath::new(stripped))), + )), + Err(err) => { + log::info!("Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}"); + EntryState::new_rootless(entry) + } + }, + (Some(entry), None) => EntryState::new_rootless(entry), + (None, Some(root)) => Some(EntryState::new_workspace(root)), + (None, None) => None, + }; + + entry.unwrap_or_else(|| match self.root(None) { + Some(root) => EntryState::new_workspace(root), + None => EntryState::new_detached(), + }) + } + + /// Determines the default entry path. + pub fn resolve_default(&self) -> Option { + let extras = self.typst_extra_args.as_ref()?; + // todo: pre-compute this when updating config + if let Some(entry) = &extras.entry { + if entry.is_relative() { + let root = self.root(None)?; + return Some(root.join(entry).as_path().into()); + } + } + extras.entry.clone() + } + + /// Validates the configuration. + pub fn validate(&self) -> anyhow::Result<()> { + if let Some(root) = &self.root_path { + if !root.is_absolute() { + bail!("rootPath must be an absolute path: {root:?}"); + } + } + + if let Some(extra_args) = &self.typst_extra_args { + if let Some(root) = &extra_args.root_dir { + if !root.is_absolute() { + bail!("typstExtraArgs.root must be an absolute path: {root:?}"); + } + } + } + + Ok(()) + } +} + /// The user configuration read from the editor. #[derive(Debug, Default, Clone)] pub struct CompileConfig { - /// The workspace roots from initialization. - pub roots: Vec, /// The output directory for PDF export. pub output_path: PathPattern, /// The mode of PDF export. pub export_pdf: ExportMode, - /// Specifies the root path of the project manually. - pub root_path: Option, /// Specifies the cli font options pub font_opts: CompileFontArgs, /// Whether to ignore system fonts @@ -496,6 +608,8 @@ pub struct CompileConfig { pub has_default_entry_path: bool, /// The inputs for the language server protocol. pub lsp_inputs: ImmutDict, + /// The entry resolver. + pub entry_resolver: EntryResolver, } impl CompileConfig { @@ -518,7 +632,6 @@ impl CompileConfig { self.output_path = deser_or_default!("outputPath", PathPattern); self.export_pdf = deser_or_default!("exportPdf", ExportMode); - self.root_path = try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())); self.notify_status = match try_(|| update.get("compileStatus")?.as_str()) { Some("enable") => true, Some("disable") | None => false, @@ -585,7 +698,10 @@ impl CompileConfig { self.font_paths = try_or_default(|| Vec::<_>::deserialize(update.get("fontPaths")?).ok()); self.system_fonts = try_(|| update.get("systemFonts")?.as_bool()); - self.has_default_entry_path = self.determine_default_entry_path().is_some(); + self.entry_resolver.root_path = + try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())); + self.entry_resolver.typst_extra_args = self.typst_extra_args.clone(); + self.has_default_entry_path = self.entry_resolver.resolve_default().is_some(); self.lsp_inputs = { let mut dict = TypstDict::default(); @@ -612,86 +728,6 @@ impl CompileConfig { self.validate() } - /// Determines the root directory for the entry file. - pub fn determine_root(&self, entry: Option<&ImmutPath>) -> Option { - if let Some(path) = &self.root_path { - return Some(path.clone()); - } - - if let Some(root) = try_(|| self.typst_extra_args.as_ref()?.root_dir.as_ref()) { - return Some(root.clone()); - } - - if let Some(entry) = entry { - for root in self.roots.iter() { - if entry.starts_with(root) { - return Some(root.clone()); - } - } - - if !self.roots.is_empty() { - log::warn!("entry is not in any set root directory"); - } - - if let Some(parent) = entry.parent() { - return Some(parent.into()); - } - } - - if !self.roots.is_empty() { - return Some(self.roots[0].clone()); - } - - None - } - - /// Determines the entry state. - pub fn determine_entry(&self, entry: Option) -> EntryState { - // todo: formalize untitled path - // let is_untitled = entry.as_ref().is_some_and(|p| p.starts_with("/untitled")); - // let root_dir = self.determine_root(if is_untitled { None } else { - // entry.as_ref() }); - let root_dir = self.determine_root(entry.as_ref()); - - let entry = match (entry, root_dir) { - // (Some(entry), Some(root)) if is_untitled => Some(EntryState::new_rooted( - // root, - // Some(FileId::new(None, VirtualPath::new(entry))), - // )), - (Some(entry), Some(root)) => match entry.strip_prefix(&root) { - Ok(stripped) => Some(EntryState::new_rooted( - root, - Some(FileId::new(None, VirtualPath::new(stripped))), - )), - Err(err) => { - log::info!("Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}"); - EntryState::new_rootless(entry) - } - }, - (Some(entry), None) => EntryState::new_rootless(entry), - (None, Some(root)) => Some(EntryState::new_workspace(root)), - (None, None) => None, - }; - - entry.unwrap_or_else(|| match self.determine_root(None) { - Some(root) => EntryState::new_workspace(root), - None => EntryState::new_detached(), - }) - } - - /// Determines the default entry path. - pub fn determine_default_entry_path(&self) -> Option { - let extras = self.typst_extra_args.as_ref()?; - // todo: pre-compute this when updating config - if let Some(entry) = &extras.entry { - if entry.is_relative() { - let root = self.determine_root(None)?; - return Some(root.join(entry).as_path().into()); - } - } - extras.entry.clone() - } - /// Determines the font options. pub fn determine_font_opts(&self) -> CompileFontArgs { let mut opts = self.font_opts.clone(); @@ -714,7 +750,7 @@ impl CompileConfig { let root = OnceCell::new(); for path in opts.font_paths.iter_mut() { if path.is_relative() { - if let Some(root) = root.get_or_init(|| self.determine_root(None)) { + if let Some(root) = root.get_or_init(|| self.entry_resolver.root(None)) { let p = std::mem::take(path); *path = root.join(p); } @@ -800,25 +836,14 @@ impl CompileConfig { self.system_fonts, &self.font_paths, self.typst_extra_args.as_ref().map(|e| &e.font), - self.determine_root(self.determine_default_entry_path().as_ref()), + self.entry_resolver + .root(self.entry_resolver.resolve_default().as_ref()), ) } /// Validates the configuration. pub fn validate(&self) -> anyhow::Result<()> { - if let Some(root) = &self.root_path { - if !root.is_absolute() { - bail!("rootPath must be an absolute path: {root:?}"); - } - } - - if let Some(extra_args) = &self.typst_extra_args { - if let Some(root) = &extra_args.root_dir { - if !root.is_absolute() { - bail!("typstExtraArgs.root must be an absolute path: {root:?}"); - } - } - } + self.entry_resolver.validate()?; Ok(()) } @@ -1000,7 +1025,10 @@ mod tests { assert_eq!(config.compile.output_path, PathPattern::new("out")); assert_eq!(config.compile.export_pdf, ExportMode::OnSave); - assert_eq!(config.compile.root_path, Some(ImmutPath::from(root_path))); + assert_eq!( + config.compile.entry_resolver.root_path, + Some(ImmutPath::from(root_path)) + ); assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable); assert_eq!(config.formatter_mode, FormatterMode::Typstyle); assert_eq!( diff --git a/crates/tinymist/src/main.rs b/crates/tinymist/src/main.rs index 96c046f1..ce2499cb 100644 --- a/crates/tinymist/src/main.rs +++ b/crates/tinymist/src/main.rs @@ -24,7 +24,9 @@ use sync_lsp::{ transport::{with_stdio_transport, MirrorArgs}, LspBuilder, LspClientRoot, LspResult, }; -use tinymist::{CompileConfig, Config, LanguageState, RegularInit, SuperInit, UserActionTask}; +use tinymist::{ + CompileConfig, Config, EntryResolver, LanguageState, RegularInit, SuperInit, UserActionTask, +}; use tinymist_query::package::PackageInfo; use typst::foundations::IntoValue; use typst_shim::utils::LazyHash; @@ -163,10 +165,13 @@ pub fn trace_lsp_main(args: TraceLspArgs) -> anyhow::Result<()> { with_stdio_transport(args.mirror.clone(), |conn| { let client_root = LspClientRoot::new(RUNTIMES.tokio_runtime.handle().clone(), conn.sender); let client = client_root.weak(); - + let roots = vec![ImmutPath::from(root_path)]; let config = Config { compile: CompileConfig { - roots: vec![ImmutPath::from(root_path)], + entry_resolver: EntryResolver { + roots, + ..Default::default() + }, font_opts: args.compile.font, ..CompileConfig::default() }, @@ -205,9 +210,7 @@ pub fn trace_lsp_main(args: TraceLspArgs) -> anyhow::Result<()> { let state = service.state_mut().unwrap(); - let entry = state - .compile_config() - .determine_entry(Some(input.as_path().into())); + let entry = state.entry_resolver().resolve(Some(input.as_path().into())); let snap = state.primary().snapshot().unwrap(); diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index 8e54effb..332f6c47 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -165,6 +165,11 @@ impl LanguageState { &self.config.compile } + /// Get the entry resolver. + pub fn entry_resolver(&self) -> &EntryResolver { + &self.compile_config().entry_resolver + } + /// Get the primary compile server for those commands without task context. pub fn primary(&self) -> &CompileClientActor { self.primary.as_ref().expect("primary") @@ -849,7 +854,7 @@ impl LanguageState { pub fn pin_entry(&mut self, new_entry: Option) -> Result<(), Error> { self.pinning = new_entry.is_some(); let entry = new_entry - .or_else(|| self.config.compile.determine_default_entry_path()) + .or_else(|| self.entry_resolver().resolve_default()) .or_else(|| self.focusing.clone()); self.do_change_entry(entry).map(|_| ()) } @@ -1047,9 +1052,9 @@ impl LanguageState { let fut_stat = client.query_snapshot_with_stat(&query)?; let entry = query .associated_path() - .map(|path| client.config.determine_entry(Some(path.into()))) + .map(|path| client.entry_resolver().resolve(Some(path.into()))) .or_else(|| { - let root = client.config.determine_root(None)?; + let root = client.entry_resolver().root(None)?; Some(EntryState::new_rooted(root, Some(*DETACHED_ENTRY))) });