mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
refactor: split entry resolver (#942)
* refactor: split entry resolver * refactor: naming
This commit is contained in:
parent
c0d20cd1b2
commit
00aecd9eae
6 changed files with 171 additions and 130 deletions
|
@ -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<ImmutPath>) {
|
||||
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(),
|
||||
);
|
||||
|
|
|
@ -49,7 +49,7 @@ use crate::{
|
|||
stats::{CompilerQueryStats, QueryStatGuard},
|
||||
task::{ExportTask, ExportUserConfig},
|
||||
world::{LspCompilerFeat, LspWorld},
|
||||
CompileConfig,
|
||||
CompileConfig, EntryResolver,
|
||||
};
|
||||
|
||||
type EditorSender = mpsc::UnboundedSender<EditorRequest>;
|
||||
|
@ -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<QuerySnapFut> {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ImmutPath>,
|
||||
/// The workspace roots from initialization.
|
||||
pub roots: Vec<ImmutPath>,
|
||||
/// Typst extra arguments.
|
||||
pub typst_extra_args: Option<CompileExtraOpts>,
|
||||
}
|
||||
|
||||
impl EntryResolver {
|
||||
/// Resolves the root directory for the entry file.
|
||||
pub fn root(&self, entry: Option<&ImmutPath>) -> Option<ImmutPath> {
|
||||
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<ImmutPath>) -> 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<ImmutPath> {
|
||||
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<ImmutPath>,
|
||||
/// 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<ImmutPath>,
|
||||
/// 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<ImmutPath> {
|
||||
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<ImmutPath>) -> 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<ImmutPath> {
|
||||
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!(
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<ImmutPath>) -> 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)))
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue