From c3fa0c5cb2248cfa14dd152d6b79e589d59f35c7 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Thu, 7 Aug 2025 07:42:02 +0800 Subject: [PATCH] fix: ensure that the lockfile is respected (#2000) --- .github/workflows/ci.yml | 4 ++ Dockerfile | 6 +- crates/tinymist-project/src/args.rs | 39 ++++++---- crates/tinymist-project/src/lock.rs | 42 +++++++---- crates/tinymist-project/src/model.rs | 21 +++++- crates/tinymist-task/src/model.rs | 16 +++++ crates/tinymist-task/src/primitives.rs | 59 +++++++++++++-- crates/tinymist/src/route.rs | 45 ++++++++++-- crates/tinymist/src/server.rs | 6 +- crates/tinymist/src/task/export.rs | 15 ++-- crates/tinymist/src/tool/project.rs | 39 ++++++---- editors/neovim/samples/lazyvim-dev/Dockerfile | 21 ++++-- editors/neovim/spec/lockfile_spec.lua | 71 +++++++++++++++++++ editors/neovim/spec/main.py | 26 +++++++ package.json | 1 + scripts/test-lock.sh | 3 + tests/workspaces/book/tinymist.lock | 21 ++++++ 17 files changed, 372 insertions(+), 63 deletions(-) create mode 100644 editors/neovim/spec/lockfile_spec.lua create mode 100755 scripts/test-lock.sh create mode 100644 tests/workspaces/book/tinymist.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a00bcf75..621f7cfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,10 @@ jobs: path: tinymist-completions.tar.gz - name: Test tinymist run: cargo test --workspace -- --skip=e2e + - name: Test Lockfile (Prepare) + run: ./scripts/test-lock.sh + - name: Test Lockfile (Check) + run: cargo test --package tinymist --lib -- route::tests --show-output --ignored checks-windows: name: Check Minimum Rust version and Tests (Windows) diff --git a/Dockerfile b/Dockerfile index e4bd35e3..e2eb4fc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN cargo install cargo-chef COPY . . RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ - cargo chef prepare --recipe-path recipe.json + cargo +${RUST_VERSION} chef prepare --recipe-path recipe.json FROM base as builder WORKDIR app @@ -36,11 +36,11 @@ RUN cargo install cargo-chef COPY --from=planner /app/recipe.json recipe.json RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ - cargo chef cook --release --recipe-path recipe.json + cargo +${RUST_VERSION} chef cook --release --recipe-path recipe.json COPY . . RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ - cargo build -p tinymist --release + cargo +${RUST_VERSION} build -p tinymist --release FROM debian:12 WORKDIR /app/ diff --git a/crates/tinymist-project/src/args.rs b/crates/tinymist-project/src/args.rs index 43c864d5..fcb99448 100644 --- a/crates/tinymist-project/src/args.rs +++ b/crates/tinymist-project/src/args.rs @@ -37,36 +37,37 @@ pub struct DocNewArgs { impl DocNewArgs { /// Converts to project input. - pub fn to_input(&self) -> ProjectInput { - let id: Id = (&self.id).into(); + pub fn to_input(&self, ctx: CtxPath) -> ProjectInput { + let id: Id = self.id.id(ctx); let root = self .root .as_ref() - .map(|root| ResourcePath::from_user_sys(Path::new(root))); - let main = ResourcePath::from_user_sys(Path::new(&self.id.input)); + .map(|root| ResourcePath::from_user_sys(Path::new(root), ctx)); + let main = ResourcePath::from_user_sys(Path::new(&self.id.input), ctx); let font_paths = self .font .font_paths .iter() - .map(|p| ResourcePath::from_user_sys(p)) + .map(|p| ResourcePath::from_user_sys(p, ctx)) .collect::>(); let package_path = self .package .package_path .as_ref() - .map(|p| ResourcePath::from_user_sys(p)); + .map(|p| ResourcePath::from_user_sys(p, ctx)); let package_cache_path = self .package .package_cache_path .as_ref() - .map(|p| ResourcePath::from_user_sys(p)); + .map(|p| ResourcePath::from_user_sys(p, ctx)); ProjectInput { id: id.clone(), + lock_dir: Some(ctx.1.to_path_buf()), root, main, // todo: inputs @@ -92,12 +93,13 @@ pub struct DocIdArgs { pub input: String, } -impl From<&DocIdArgs> for Id { - fn from(args: &DocIdArgs) -> Self { - if let Some(id) = &args.name { +impl DocIdArgs { + /// Converts to a document ID. + pub fn id(&self, ctx: CtxPath) -> Id { + if let Some(id) = &self.name { Id::new(id.clone()) } else { - (&ResourcePath::from_user_sys(Path::new(&args.input))).into() + (&ResourcePath::from_user_sys(Path::new(&self.input), ctx)).into() } } } @@ -172,7 +174,7 @@ pub struct TaskCompileArgs { impl TaskCompileArgs { /// Convert the arguments to a project task. - pub fn to_task(self, doc_id: Id) -> Result { + pub fn to_task(self, doc_id: Id, cwd: &Path) -> Result { let new_task_id = self.task_name.map(Id::new); let task_id = new_task_id.unwrap_or(doc_id.clone()); @@ -195,6 +197,17 @@ impl TaskCompileArgs { OutputFormat::Pdf }; + let output = self.output.as_ref().map(|output| { + let output = Path::new(output); + let output = if output.is_absolute() { + output.to_path_buf() + } else { + cwd.join(output) + }; + + PathPattern::new(&output.with_extension("").to_string_lossy()) + }); + let when = self.when.unwrap_or(TaskWhen::Never); let mut transforms = vec![]; @@ -207,7 +220,7 @@ impl TaskCompileArgs { let export = ExportTask { when, - output: self.output.as_deref().map(PathPattern::new), + output, transform: transforms, }; diff --git a/crates/tinymist-project/src/lock.rs b/crates/tinymist-project/src/lock.rs index 74cd2736..7b274c4b 100644 --- a/crates/tinymist-project/src/lock.rs +++ b/crates/tinymist-project/src/lock.rs @@ -8,6 +8,7 @@ use ecow::{eco_vec, EcoVec}; use tinymist_std::error::prelude::*; use tinymist_std::path::unix_slash; use tinymist_std::{bail, ImmutPath}; +use tinymist_task::CtxPath; use typst::diag::EcoString; use typst::World; @@ -25,7 +26,9 @@ impl LockFile { self.task.iter().find(|i| &i.id == id) } - pub fn replace_document(&mut self, input: ProjectInput) { + pub fn replace_document(&mut self, mut input: ProjectInput) { + input.lock_dir = None; + let input = input; let id = input.id.clone(); let index = self.document.iter().position(|i| i.id == id); if let Some(index) = index { @@ -35,7 +38,14 @@ impl LockFile { } } - pub fn replace_task(&mut self, task: ApplyProjectTask) { + pub fn replace_task(&mut self, mut task: ApplyProjectTask) { + if let Some(pat) = task.task.as_export_mut().and_then(|t| t.output.as_mut()) { + let rel = pat.clone().relative_to(self.lock_dir.as_ref().unwrap()); + *pat = rel; + } + + let task = task; + let id = task.id().clone(); let index = self.task.iter().position(|i| *i.id() == id); if let Some(index) = index { @@ -146,6 +156,8 @@ impl LockFile { let mut state = if old_data.trim().is_empty() { LockFile { + // todo: reduce cost + lock_dir: Some(ImmutPath::from(cwd)), document: vec![], task: vec![], route: eco_vec![], @@ -169,7 +181,9 @@ impl LockFile { } } - old_state.migrate()? + let mut lf = old_state.migrate()?; + lf.lock_dir = Some(ImmutPath::from(cwd)); + lf }; f(&mut state)?; @@ -213,7 +227,9 @@ impl LockFile { let state = toml::from_str::(data) .context_ut("tinymist.lock file is not a valid TOML file")?; - state.migrate() + let mut lf = state.migrate()?; + lf.lock_dir = Some(dir.into()); + Ok(lf) } } @@ -238,17 +254,18 @@ pub struct LockFileUpdate { } impl LockFileUpdate { - pub fn compiled(&mut self, world: &LspWorld) -> Option { - let id = Id::from_world(world)?; + pub fn compiled(&mut self, world: &LspWorld, ctx: CtxPath) -> Option { + let id = Id::from_world(world, ctx)?; - let root = ResourcePath::from_user_sys(Path::new(".")); - let main = ResourcePath::from_user_sys(world.path_for_id(world.main()).ok()?.as_path()); + let root = ResourcePath::from_user_sys(Path::new("."), ctx); + let main = + ResourcePath::from_user_sys(world.path_for_id(world.main()).ok()?.as_path(), ctx); let font_resolver = &world.font_resolver; let font_paths = font_resolver .font_paths() .iter() - .map(|p| ResourcePath::from_user_sys(p)) + .map(|p| ResourcePath::from_user_sys(p, ctx)) .collect::>(); // let system_font = font_resolver.system_font(); @@ -256,10 +273,10 @@ impl LockFileUpdate { let registry = &world.registry; let package_path = registry .package_path() - .map(|p| ResourcePath::from_user_sys(p)); + .map(|p| ResourcePath::from_user_sys(p, ctx)); let package_cache_path = registry .package_cache_path() - .map(|p| ResourcePath::from_user_sys(p)); + .map(|p| ResourcePath::from_user_sys(p, ctx)); // todo: freeze the package paths let _ = package_cache_path; @@ -269,6 +286,7 @@ impl LockFileUpdate { let input = ProjectInput { id: id.clone(), + lock_dir: Some(ctx.1.to_path_buf()), root: Some(root), main, inputs: vec![], @@ -325,7 +343,7 @@ impl LockFileUpdate { let id_hi = id >> 12; let hash_str = - format!("{root_lo:03x}/{root_hi:013x}/{id_lo:03x}/{id_hi:016x}"); + format!("{root_lo:03x}/{root_hi:013x}/{id_lo:03x}/{id_hi:013x}"); let cache_dir = cache_dir.join("tinymist/projects").join(hash_str); let _ = std::fs::create_dir_all(&cache_dir); diff --git a/crates/tinymist-project/src/model.rs b/crates/tinymist-project/src/model.rs index fb61e59a..235067de 100644 --- a/crates/tinymist-project/src/model.rs +++ b/crates/tinymist-project/src/model.rs @@ -1,5 +1,5 @@ use std::hash::Hash; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use ecow::EcoVec; use tinymist_std::error::prelude::*; @@ -53,6 +53,9 @@ impl LockFileCompat { /// A lock file storing project information. #[derive(Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct LockFile { + /// The directory where stores the lock file. + #[serde(skip)] + pub lock_dir: Option, // The lock file version. // version: String, /// The project's document (input). @@ -72,6 +75,9 @@ pub struct LockFile { pub struct ProjectInput { /// The project's ID. pub id: Id, + /// The cwd of the project when relative paths will be resolved. + #[serde(skip_serializing_if = "Option::is_none")] + pub lock_dir: Option, /// The path to the root directory of the project. #[serde(skip_serializing_if = "Option::is_none")] pub root: Option, @@ -93,6 +99,19 @@ pub struct ProjectInput { pub package_cache_path: Option, } +impl ProjectInput { + /// Returns a new project input relative to the provided lock directory. + pub fn relative_to(&self, that: &Path) -> Self { + if let Some(lock_dir) = &self.lock_dir { + if lock_dir == that { + return self.clone(); + } + } + + todo!() + } +} + /// A project route specifier. #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case")] diff --git a/crates/tinymist-task/src/model.rs b/crates/tinymist-task/src/model.rs index 1f6fe75b..1510ad9f 100644 --- a/crates/tinymist-task/src/model.rs +++ b/crates/tinymist-task/src/model.rs @@ -116,6 +116,22 @@ impl ProjectTask { }) } + /// Returns the export configuration of a task. + pub fn as_export_mut(&mut self) -> Option<&mut ExportTask> { + Some(match self { + Self::Preview(..) => return None, + Self::ExportPdf(task) => &mut task.export, + Self::ExportPng(task) => &mut task.export, + Self::ExportSvg(task) => &mut task.export, + Self::ExportHtml(task) => &mut task.export, + Self::ExportSvgHtml(task) => &mut task.export, + Self::ExportTeX(task) => &mut task.export, + Self::ExportMd(task) => &mut task.export, + Self::ExportText(task) => &mut task.export, + Self::Query(task) => &mut task.export, + }) + } + /// Returns extension of the artifact. pub fn extension(&self) -> &str { match self { diff --git a/crates/tinymist-task/src/primitives.rs b/crates/tinymist-task/src/primitives.rs index 922985b6..f98b6536 100644 --- a/crates/tinymist-task/src/primitives.rs +++ b/crates/tinymist-task/src/primitives.rs @@ -77,11 +77,12 @@ impl Id { } /// Creates a new project Id from a world. - pub fn from_world(world: &CompilerWorld) -> Option { + pub fn from_world(world: &CompilerWorld, ctx: CtxPath) -> Option { let entry = world.entry_state(); let id = unix_slash(entry.main()?.vpath().as_rootless_path()); - let path = &ResourcePath::from_user_sys(Path::new(&id)); + // todo: entry root may not be set, so we should use the cwd + let path = &ResourcePath::from_user_sys(Path::new(&id), ctx); Some(path.into()) } } @@ -121,6 +122,25 @@ impl PathPattern { Self(pattern.into()) } + /// Creates a new path pattern from a string. + pub fn relative_to(self, base: &Path) -> Self { + if self.0.is_empty() { + return self; + } + + let path = Path::new(self.0.as_str()); + if path.is_absolute() { + let rel_path = tinymist_std::path::diff(path, base); + + match rel_path { + Some(rel) => PathPattern(unix_slash(&rel).into()), + None => self, + } + } else { + self + } + } + /// Substitutes the path pattern with `$root`, and `$dir/$name`. pub fn substitute(&self, entry: &EntryState) -> Option { self.substitute_impl(entry.root(), entry.main()) @@ -300,18 +320,29 @@ impl<'de> serde::Deserialize<'de> for ResourcePath { } } +// todo: The ctx path looks not quite maintainable. But we only target to make +// things correct, then back to make code good. +pub type CtxPath<'a, 'b> = (/* cwd */ &'a Path, /* lock_dir */ &'b Path); + impl ResourcePath { /// Creates a new resource path from a user passing system path. - pub fn from_user_sys(inp: &Path) -> Self { - let rel = if inp.is_relative() { + pub fn from_user_sys(inp: &Path, (cwd, lock_dir): CtxPath) -> Self { + let abs = if inp.is_absolute() { inp.to_path_buf() } else { - let cwd = std::env::current_dir().unwrap(); - tinymist_std::path::diff(inp, &cwd).unwrap() + cwd.join(inp) }; - let rel = unix_slash(&rel); + let resource_path = if let Some(rel) = tinymist_std::path::diff(&abs, lock_dir) { + rel + } else { + abs + }; + // todo: clean is not posix compatible, + // for example /symlink/../file is not equivalent to /file + let rel = unix_slash(&resource_path.clean()); ResourcePath("file".into(), rel.to_string()) } + /// Creates a new resource path from a file id. pub fn from_file_id(id: FileId) -> Self { let package = id.package(); @@ -327,6 +358,20 @@ impl ResourcePath { } } + pub fn relative_to(&self, base: &Path) -> Option { + if self.0 == "file" { + let path = Path::new(&self.1); + if path.is_absolute() { + let rel_path = tinymist_std::path::diff(path, base)?; + Some(ResourcePath(self.0.clone(), unix_slash(&rel_path))) + } else { + Some(ResourcePath(self.0.clone(), self.1.clone())) + } + } else { + Some(self.clone()) + } + } + /// Converts the resource path to a path relative to the `base` (usually the /// directory storing the lockfile). pub fn to_rel_path(&self, base: &Path) -> Option { diff --git a/crates/tinymist/src/route.rs b/crates/tinymist/src/route.rs index 0d8c44ab..5639b875 100644 --- a/crates/tinymist/src/route.rs +++ b/crates/tinymist/src/route.rs @@ -1,6 +1,6 @@ use std::{path::Path, sync::Arc}; -use reflexo_typst::{path::unix_slash, typst::prelude::EcoVec, LazyHash}; +use reflexo_typst::{path::unix_slash, typst::prelude::EcoVec, EntryReader, LazyHash}; use rpds::RedBlackTreeMapSync; use tinymist_std::{hash::FxHashMap, ImmutPath}; use typst::diag::EcoString; @@ -12,6 +12,7 @@ pub struct ProjectRouteState { path_routes: FxHashMap, } +#[derive(Debug)] pub struct ProjectResolution { pub lock_dir: ImmutPath, pub project_id: Id, @@ -95,8 +96,10 @@ impl ProjectRouteState { snap: &LspCompileSnapshot, ) -> Option<()> { let path_route = self.path_routes.get_mut(&lock_dir)?; + // todo: rootless + let root = snap.world.entry_state().root()?; - let id = Id::from_world(&snap.world)?; + let id = Id::from_world(&snap.world, (&root, &lock_dir))?; let deps = snap.world.depended_fs_paths(); let material = ProjectPathMaterial::from_deps(id, deps); @@ -121,7 +124,7 @@ impl ProjectRouteState { return None; } }); - log::info!("loaded lock at {path:?}"); + log::debug!("loaded lock at {path:?}"); let root: EcoString = unix_slash(path).into(); let root_hash = tinymist_std::hash::hash128(&root); @@ -151,7 +154,7 @@ impl ProjectRouteState { } fn read_material(&self, entry_path: &Path) -> Option { - log::info!("check material at {entry_path:?}"); + log::debug!("check material at {entry_path:?}"); let name = entry_path.file_name().unwrap_or(entry_path.as_os_str()); if name != "path-material.json" { return None; @@ -193,3 +196,37 @@ struct RoutePathState { routes: Arc>, cache_dir: Option, } + +#[cfg(test)] +mod tests { + use reflexo::path::PathClean; + + use super::*; + + // todo: enable me + #[test] + #[ignore] + fn test_resolve_chapter() { + let mut state = ProjectRouteState::default(); + + let lock_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/workspaces/book/"); + let lock_dir = lock_dir.clean(); + + let leaf = lock_dir.join("chapters/chapter1.typ").into(); + + // Resolve the path + let resolution = state.resolve(&leaf); + assert!(resolution.is_some(), "Resolution should not be None"); + let resolution = resolution.unwrap(); + assert_eq!( + resolution.lock_dir, + ImmutPath::from(lock_dir), + "Lock directory should match" + ); + assert_eq!( + resolution.project_id, + Id::new("file:main.typ".to_owned()), + "Project ID should match" + ); + } +} diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index 1107d931..21c471e3 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -454,9 +454,11 @@ impl ServerState { let update_dep = lock_dir.clone().map(|lock_dir| { |snap: LspComputeGraph| async move { - let mut updater = update_lock(lock_dir); + let mut updater = update_lock(lock_dir.clone()); let world = snap.world(); - let doc_id = updater.compiled(world)?; + // todo: rootless. + let root_dir = world.entry_state().root()?; + let doc_id = updater.compiled(world, (&root_dir, &lock_dir))?; updater.update_materials(doc_id.clone(), world.depended_fs_paths()); updater.route(doc_id, PROJECT_ROUTE_USER_ACTION_PRIORITY); diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs index b75a632a..56946345 100644 --- a/crates/tinymist/src/task/export.rs +++ b/crates/tinymist/src/task/export.rs @@ -7,7 +7,7 @@ use std::sync::{Arc, OnceLock}; use reflexo::ImmutPath; use reflexo_typst::{Bytes, CompilationTask, ExportComputation}; -use tinymist_project::LspWorld; +use tinymist_project::{LspWorld, PROJECT_ROUTE_USER_ACTION_PRIORITY}; use tinymist_std::error::prelude::*; use tinymist_std::fs::paths::write_atomic; use tinymist_std::path::PathClean; @@ -196,7 +196,9 @@ impl ExportTask { static EXPORT_ID: AtomicUsize = AtomicUsize::new(0); let export_id = EXPORT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - log::debug!("ExportTask({export_id}): exporting {entry:?} to {write_to:?}"); + log::debug!( + "ExportTask({export_id},lock={lock_dir:?}): exporting {entry:?} to {write_to:?}" + ); if let Some(e) = write_to.parent() { if !e.exists() { std::fs::create_dir_all(e).context("failed to create directory")?; @@ -204,15 +206,18 @@ impl ExportTask { } let _: Option<()> = lock_dir.and_then(|lock_dir| { - let mut updater = crate::project::update_lock(lock_dir); + let mut updater = crate::project::update_lock(lock_dir.clone()); + let root = graph.world().entry_state().root()?; - let doc_id = updater.compiled(graph.world())?; + let doc_id = updater.compiled(graph.world(), (&root, &lock_dir))?; updater.task(ApplyProjectTask { id: doc_id.clone(), - document: doc_id, + document: doc_id.clone(), task: task.clone(), }); + updater.update_materials(doc_id.clone(), graph.world().depended_fs_paths()); + updater.route(doc_id, PROJECT_ROUTE_USER_ACTION_PRIORITY); updater.commit(); Some(()) diff --git a/crates/tinymist/src/tool/project.rs b/crates/tinymist/src/tool/project.rs index c29c3269..504ab662 100644 --- a/crates/tinymist/src/tool/project.rs +++ b/crates/tinymist/src/tool/project.rs @@ -111,22 +111,33 @@ impl LockFileExt for LockFile { /// Runs project compilation(s) pub async fn compile_main(args: CompileArgs) -> Result<()> { - // Identifies the input and output - let input = args.compile.declare.to_input(); - let output = args.compile.to_task(input.id.clone())?; + let cwd = std::env::current_dir().context("cannot get cwd")?; + // todo: respect the name of the lock file // Saves the lock file if the flags are set let save_lock = args.save_lock || args.lockfile.is_some(); - // todo: respect the name of the lock file + let lock_dir: ImmutPath = if let Some(lockfile) = args.lockfile { - lockfile.parent().context("no parent")?.into() + let lockfile = if lockfile.is_absolute() { + lockfile + } else { + cwd.join(lockfile) + }; + lockfile + .parent() + .context("lock file must have a parent directory")? + .into() } else { - std::env::current_dir().context("lock directory")?.into() + cwd.as_path().into() }; + // Identifies the input and output + let input = args.compile.declare.to_input((&cwd, &lock_dir)); + let output = args.compile.to_task(input.id.clone(), &cwd)?; + if save_lock { LockFile::update(&lock_dir, |state| { - state.replace_document(input.clone()); + state.replace_document(input.relative_to(&lock_dir)); state.replace_task(output.clone()); Ok(()) @@ -365,13 +376,15 @@ fn shell_build_script(shell: Shell) -> Result { /// Project document commands' main pub fn project_main(args: DocCommands) -> Result<()> { - LockFile::update(Path::new("."), |state| { + let cwd = std::env::current_dir().context("cannot get cwd")?; + LockFile::update(&cwd, |state| { + let ctx: (&Path, &Path) = (&cwd, &cwd); match args { DocCommands::New(args) => { - state.replace_document(args.to_input()); + state.replace_document(args.to_input(ctx)); } DocCommands::Configure(args) => { - let id: Id = (&args.id).into(); + let id: Id = args.id.id(ctx); state.route.push(ProjectRoute { id: id.clone(), @@ -386,12 +399,14 @@ pub fn project_main(args: DocCommands) -> Result<()> { /// Project task commands' main pub fn task_main(args: TaskCommands) -> Result<()> { - LockFile::update(Path::new("."), |state| { + let cwd = std::env::current_dir().context("cannot get cwd")?; + LockFile::update(&cwd, |state| { + let ctx: (&Path, &Path) = (&cwd, &cwd); let _ = state; match args { #[cfg(feature = "preview")] TaskCommands::Preview(args) => { - let input = args.declare.to_input(); + let input = args.declare.to_input(ctx); let id = input.id.clone(); state.replace_document(input); let _ = state.preview(id, &args); diff --git a/editors/neovim/samples/lazyvim-dev/Dockerfile b/editors/neovim/samples/lazyvim-dev/Dockerfile index 74324f5e..83674dcd 100644 --- a/editors/neovim/samples/lazyvim-dev/Dockerfile +++ b/editors/neovim/samples/lazyvim-dev/Dockerfile @@ -3,7 +3,10 @@ FROM debian:12 AS builder -RUN apt-get update && apt-get install -y \ +RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ + --mount=target=/var/cache/apt,type=cache,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get update && apt-get install -y \ git \ file \ ninja-build gettext cmake unzip curl build-essential @@ -17,9 +20,19 @@ FROM myriaddreamin/tinymist:0.13.22 as tinymist FROM debian:12 COPY --from=builder /neovim/build/nvim-linux-x86_64.deb /tmp/nvim-linux-x86_64.deb -RUN apt-get update && apt-get install -y curl git ripgrep build-essential unzip -RUN apt-get update && apt-get install -y python3 -RUN apt-get install -y /tmp/nvim-linux-x86_64.deb \ + +RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ + --mount=target=/var/cache/apt,type=cache,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get update && apt-get install -y curl git ripgrep build-essential unzip +RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ + --mount=target=/var/cache/apt,type=cache,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get update && apt-get install -y python3 +RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ + --mount=target=/var/cache/apt,type=cache,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + apt-get install -y /tmp/nvim-linux-x86_64.deb \ && rm /tmp/nvim-linux-x86_64.deb RUN useradd --create-home --shell /bin/bash runner diff --git a/editors/neovim/spec/lockfile_spec.lua b/editors/neovim/spec/lockfile_spec.lua new file mode 100644 index 00000000..0872bf54 --- /dev/null +++ b/editors/neovim/spec/lockfile_spec.lua @@ -0,0 +1,71 @@ +---@brief [[ +--- Tests for export functionalities. +---@brief ]] + +local fixtures = require 'spec.fixtures' +local helpers = require 'spec.helpers' + +-- async async +local util = require "plenary.async.util" +local async = require('plenary.async') + +local async_tests = require "plenary.async.tests" + +require('tinymist').setup { + lsp = { + init_options = { + projectResolution = 'lockDatabase', + exportPdf = 'onType', + outputPath = '/home/runner/test/$name', + development = true, + systemFonts = false, + }, + } +} + +local defer_swapped = function(timeout, callback) + vim.defer_fn(callback, timeout) +end + +async_tests.describe('Lockfile', function() + assert.is.empty(vim.lsp.get_clients { bufnr = 0, name = 'tinymist', _uninitialized = true }) + + async_tests.it('pdf of main is created onType', function() + local pdf_path = '/home/runner/test/main.pdf' + local pdf_sub_path = '/home/runner/test/chapter1.pdf' + assert.is.same(nil, vim.uv.fs_stat(pdf_path), 'PDF file should not be created before testing') + assert.is.same(nil, vim.uv.fs_stat(pdf_sub_path), 'PDF sub file should not be created before testing') + + local pdf_exported = async.wrap(function(cb) + require('tinymist').subscribeDevEvent( + function(result) + if result.type == 'export' and result.needExport + then + cb(result) -- resolve the promise when the export event is received + return true -- unregister the callback after receiving the event + end + end) + + -- defer 2000ms and resolve a nil + defer_swapped(2000, function() + cb(nil) -- resolve the promise after 2 seconds + end) + + vim.cmd.edit(fixtures.project.some_nested_existing_file) + assert.is.same(1, #vim.lsp.get_clients { bufnr = 0, name = 'tinymist', _uninitialized = true }) + --- append a text to current buffer + helpers.insert('This is a test export.\n') + vim.cmd.sleep('30m') + --- append a text to current buffer + helpers.insert('This is a test export.\n') + vim.cmd.sleep('30m') + + end, 1)() + + assert.is_not.same(nil, pdf_exported, 'PDF export should be triggered on type') + assert.is.same('onType', pdf_exported.when, 'Export is when = onType') + + assert.is.same(nil, vim.uv.fs_stat(pdf_sub_path), 'PDF file should not be created because of the lockfile') + assert.is_not.same(nil, vim.uv.fs_stat(pdf_path), 'PDF file should be created after typing') + end) +end) diff --git a/editors/neovim/spec/main.py b/editors/neovim/spec/main.py index a9ae7c3d..fcc82663 100644 --- a/editors/neovim/spec/main.py +++ b/editors/neovim/spec/main.py @@ -34,6 +34,31 @@ def run_tests(test_files=None): subprocess.run(command, check=True) +def prepare(): + bookdir = os.environ.get("BOOKDIR", "/home/runner/dev/workspaces/book") + if not os.path.exists(bookdir): + print(f"Book directory {bookdir} does not exist.") + sys.exit(1) + + # compile + compile_command = [ + "tinymist", + "compile", + "--lockfile", + os.path.join(bookdir, "tinymist.lock"), + os.path.join(bookdir, "main.typ"), + os.path.join(bookdir, "book.pdf"), + ] + + try: + subprocess.run(compile_command, check=True) + except subprocess.CalledProcessError as e: + print(f"Compilation failed: {e}") + sys.exit(1) + + print("Compilation completed successfully.") + + if __name__ == "__main__": # Check if any test files are provided as command line arguments if len(sys.argv) > 1: @@ -41,4 +66,5 @@ if __name__ == "__main__": else: test_files = None + prepare() run_tests(test_files) diff --git a/package.json b/package.json index 15e66f40..90dc6c52 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "lint-fix": "eslint editors/vscode/src --fix", "benches": "cargo bench --workspace", "bench": "cargo bench --workspace --bench", + "test:nvim": "cd editors/neovim && ./bootstrap.sh test", "test:grammar": "cd syntaxes/textmate && yarn run test", "build:typlite": "cargo build --bin typlite", "typlite": "target/debug/typlite", diff --git a/scripts/test-lock.sh b/scripts/test-lock.sh new file mode 100755 index 00000000..0c4a65c8 --- /dev/null +++ b/scripts/test-lock.sh @@ -0,0 +1,3 @@ +bookdir=tests/workspaces/book +# typst compile ${bookdir}/main.typ ${bookdir}/book.pdf +cargo run --bin tinymist -- compile --lockfile ${bookdir}/tinymist.lock ${bookdir}/main.typ ${bookdir}/book.pdf diff --git a/tests/workspaces/book/tinymist.lock b/tests/workspaces/book/tinymist.lock new file mode 100644 index 00000000..ef8c9b3e --- /dev/null +++ b/tests/workspaces/book/tinymist.lock @@ -0,0 +1,21 @@ +# This file is automatically @generated by tinymist. +# It is not intended for manual editing. +version = "0.1.0-beta0" + +[[document]] +id = "file:main.typ" +inputs = [] +main = "file:main.typ" +root = "file:." +system-fonts = true + +[[route]] +id = "file:main.typ" +priority = 256 + +[[task]] +document = "file:main.typ" +id = "file:main.typ" +output = "book" +type = "export-pdf" +when = "never"