diff --git a/Cargo.lock b/Cargo.lock index 251248b3..b581575d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3218,9 +3218,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rpds" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0e15515d3ce3313324d842629ea4905c25a13f81953eadb88f85516f59290a4" +checksum = "a7f89f654d51fffdd6026289d07d1fd523244d46ae0a8bc22caa6dd7f9e8cb0b" dependencies = [ "archery", ] @@ -4294,7 +4294,6 @@ dependencies = [ "hex", "if_chain", "indexmap 2.9.0", - "insta", "itertools 0.13.0", "log", "lsp-types", @@ -4318,6 +4317,7 @@ dependencies = [ "tinymist-lint", "tinymist-project", "tinymist-std", + "tinymist-tests", "tinymist-world", "toml", "ttf-parser", @@ -4413,6 +4413,19 @@ dependencies = [ "typst-svg", ] +[[package]] +name = "tinymist-tests" +version = "0.13.12" +dependencies = [ + "comemo", + "insta", + "rayon", + "tinymist-analysis", + "tinymist-project", + "tinymist-std", + "typst", +] + [[package]] name = "tinymist-vfs" version = "0.13.12" diff --git a/Cargo.toml b/Cargo.toml index 6550b8a2..f7ca9c8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -186,6 +186,8 @@ tinymist-project = { path = "./crates/tinymist-project/", version = "0.13.12" } tinymist-task = { path = "./crates/tinymist-task/", version = "0.13.12" } typst-shim = { path = "./crates/typst-shim", version = "0.13.12" } +tinymist-tests = { path = "./crates/tinymist-tests/" } + tinymist = { path = "./crates/tinymist/", version = "0.13.12" } tinymist-analysis = { path = "./crates/tinymist-analysis/", version = "0.13.12" } tinymist-core = { path = "./crates/tinymist-core/", version = "0.13.12", default-features = false } diff --git a/crates/tinymist-analysis/src/location.rs b/crates/tinymist-analysis/src/location.rs index a775ad4b..f40eaa70 100644 --- a/crates/tinymist-analysis/src/location.rs +++ b/crates/tinymist-analysis/src/location.rs @@ -6,9 +6,9 @@ use std::ops::Range; use typst::syntax::Source; /// An LSP Position encoded by [`PositionEncoding`]. -type LspPosition = tinymist_world::debug_loc::LspPosition; +pub type LspPosition = tinymist_world::debug_loc::LspPosition; /// An LSP range encoded by [`PositionEncoding`]. -type LspRange = tinymist_world::debug_loc::LspRange; +pub type LspRange = tinymist_world::debug_loc::LspRange; /// What counts as "1 character" for string indexing. We should always prefer /// UTF-8, but support UTF-16 as long as it is standard. For more background on diff --git a/crates/tinymist-query/Cargo.toml b/crates/tinymist-query/Cargo.toml index acaf8abd..995a4e61 100644 --- a/crates/tinymist-query/Cargo.toml +++ b/crates/tinymist-query/Cargo.toml @@ -57,10 +57,10 @@ walkdir.workspace = true yaml-rust2.workspace = true [dev-dependencies] -insta.workspace = true serde.workspace = true serde_json.workspace = true typst-assets = { workspace = true, features = ["fonts"] } +tinymist-tests = { workspace = true } sha2 = { version = "0.10" } hex = { version = "0.4" } diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs index 914990c9..ddbe1d1b 100644 --- a/crates/tinymist-query/src/analysis.rs +++ b/crates/tinymist-query/src/analysis.rs @@ -33,19 +33,16 @@ mod prelude; mod global; pub use global::*; -use std::path::Path; use std::sync::Arc; -use ecow::{eco_format, EcoVec}; +use ecow::eco_format; use lsp_types::Url; use tinymist_project::LspComputeGraph; -use tinymist_std::{bail, ImmutPath, Result}; -use tinymist_world::vfs::WorkspaceResolver; -use tinymist_world::{EntryReader, TaskInputs, WorldDeps}; +use tinymist_std::{bail, Result}; +use tinymist_world::{EntryReader, TaskInputs}; use typst::diag::{FileError, FileResult}; use typst::foundations::{Func, Value}; -use typst::syntax::{FileId, Source}; -use typst::World; +use typst::syntax::FileId; use crate::{path_res_to_url, CompilerQueryResponse, SemanticRequest, StatefulRequest}; @@ -65,65 +62,17 @@ impl ToFunc for Value { /// Extension trait for `typst::World`. pub trait LspWorldExt { - /// Get file's id by its path - fn file_id_by_path(&self, path: &Path) -> FileResult; - - /// Get the source of a file by file path. - fn source_by_path(&self, path: &Path) -> FileResult; - /// Resolve the uri for a file id. fn uri_for_id(&self, fid: FileId) -> FileResult; - - /// Get all depended file ids of a compilation, inclusively. - /// Note: must be called after compilation. - fn depended_files(&self) -> EcoVec; - - /// Get all depended paths in file system of a compilation, inclusively. - /// Note: must be called after compilation. - fn depended_fs_paths(&self) -> EcoVec; } impl LspWorldExt for tinymist_project::LspWorld { - fn file_id_by_path(&self, path: &Path) -> FileResult { - // todo: source in packages - match self.id_for_path(path) { - Some(id) => Ok(id), - None => WorkspaceResolver::file_with_parent_root(path).ok_or_else(|| { - let reason = eco_format!("invalid path: {path:?}"); - FileError::Other(Some(reason)) - }), - } - } - - fn source_by_path(&self, path: &Path) -> FileResult { - // todo: source cache - self.source(self.file_id_by_path(path)?) - } - fn uri_for_id(&self, fid: FileId) -> Result { let res = path_res_to_url(self.path_for_id(fid)?); crate::log_debug_ct!("uri_for_id: {fid:?} -> {res:?}"); res.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}")))) } - - fn depended_files(&self) -> EcoVec { - let mut deps = EcoVec::new(); - self.iter_dependencies(&mut |file_id| { - deps.push(file_id); - }); - deps - } - - fn depended_fs_paths(&self) -> EcoVec { - let mut deps = EcoVec::new(); - self.iter_dependencies(&mut |file_id| { - if let Ok(path) = self.path_for_id(file_id) { - deps.push(path.as_path().into()); - } - }); - deps - } } /// A snapshot for LSP queries. @@ -448,7 +397,6 @@ mod type_check_tests { #[cfg(test)] mod post_type_check_tests { - use insta::with_settings; use typst::syntax::LinkedNode; use typst_shim::syntax::LinkedNodeExt; @@ -484,7 +432,6 @@ mod post_type_check_tests { #[cfg(test)] mod type_describe_tests { - use insta::with_settings; use typst::syntax::LinkedNode; use typst_shim::syntax::LinkedNodeExt; diff --git a/crates/tinymist-query/src/analysis/bib.rs b/crates/tinymist-query/src/analysis/bib.rs index 024d6e1a..a557df18 100644 --- a/crates/tinymist-query/src/analysis/bib.rs +++ b/crates/tinymist-query/src/analysis/bib.rs @@ -213,6 +213,8 @@ mod tests { use typst::syntax::{FileId, VirtualPath}; + use crate::tests::*; + // This is a workaround for slashes in the path on Windows and Linux // are different fn bib_snap(snap: &impl fmt::Debug) -> String { @@ -234,8 +236,8 @@ Euclid2: FileId::new_fake(VirtualPath::new(Path::new("test.yml"))), ); assert_eq!(bib.entries.len(), 2); - insta::assert_snapshot!(bib_snap(&bib.entries[0]), @r###"("Euclid", BibEntry { file_id: /test.yml, name_range: 1..7, range: 1..63, raw_entry: None })"###); - insta::assert_snapshot!(bib_snap(&bib.entries[1]), @r###"("Euclid2", BibEntry { file_id: /test.yml, name_range: 63..70, range: 63..126, raw_entry: None })"###); + assert_snapshot!(bib_snap(&bib.entries[0]), @r###"("Euclid", BibEntry { file_id: /test.yml, name_range: 1..7, range: 1..63, raw_entry: None })"###); + assert_snapshot!(bib_snap(&bib.entries[1]), @r###"("Euclid2", BibEntry { file_id: /test.yml, name_range: 63..70, range: 63..126, raw_entry: None })"###); } #[test] diff --git a/crates/tinymist-query/src/code_action.rs b/crates/tinymist-query/src/code_action.rs index e752eb57..52e52203 100644 --- a/crates/tinymist-query/src/code_action.rs +++ b/crates/tinymist-query/src/code_action.rs @@ -98,7 +98,7 @@ mod tests { let result = request.request(ctx); - insta::with_settings!({ + with_settings!({ description => format!("Code Action on {})", make_range_annoation(&source)), }, { assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC)); diff --git a/crates/tinymist-query/src/completion.rs b/crates/tinymist-query/src/completion.rs index 8f74e71c..c135e77b 100644 --- a/crates/tinymist-query/src/completion.rs +++ b/crates/tinymist-query/src/completion.rs @@ -100,8 +100,6 @@ impl StatefulRequest for CompletionRequest { mod tests { use std::collections::HashSet; - use insta::with_settings; - use super::*; use crate::{completion::proto::CompletionItem, syntax::find_module_level_docs, tests::*}; diff --git a/crates/tinymist-query/src/diagnostics.rs b/crates/tinymist-query/src/diagnostics.rs index a0897241..4304405b 100644 --- a/crates/tinymist-query/src/diagnostics.rs +++ b/crates/tinymist-query/src/diagnostics.rs @@ -4,7 +4,7 @@ use tinymist_project::LspWorld; use tinymist_world::vfs::WorkspaceResolver; use typst::syntax::Span; -use crate::{analysis::Analysis, prelude::*, LspWorldExt}; +use crate::{analysis::Analysis, prelude::*}; use regex::RegexSet; diff --git a/crates/tinymist-query/src/jump.rs b/crates/tinymist-query/src/jump.rs index 5916cefa..22213132 100644 --- a/crates/tinymist-query/src/jump.rs +++ b/crates/tinymist-query/src/jump.rs @@ -249,7 +249,7 @@ mod tests { }) .join("\n"); - insta::with_settings!({ + with_settings!({ description => format!("Jump cursor on {})", make_range_annoation(&source)), }, { assert_snapshot!(results); diff --git a/crates/tinymist-query/src/lsp_typst_boundary.rs b/crates/tinymist-query/src/lsp_typst_boundary.rs index 912f4ff4..78afc051 100644 --- a/crates/tinymist-query/src/lsp_typst_boundary.rs +++ b/crates/tinymist-query/src/lsp_typst_boundary.rs @@ -6,9 +6,9 @@ use tinymist_world::vfs::PathResolution; use crate::prelude::*; /// An LSP Position encoded by [`PositionEncoding`]. -pub type LspPosition = lsp_types::Position; +pub use tinymist_analysis::location::LspPosition; /// An LSP range encoded by [`PositionEncoding`]. -pub type LspRange = lsp_types::Range; +pub use tinymist_analysis::location::LspRange; pub use tinymist_analysis::location::*; diff --git a/crates/tinymist-query/src/on_enter.rs b/crates/tinymist-query/src/on_enter.rs index 3d06bde2..d52a7379 100644 --- a/crates/tinymist-query/src/on_enter.rs +++ b/crates/tinymist-query/src/on_enter.rs @@ -218,7 +218,7 @@ mod tests { format!("{window_before}|{window_line}|{window_after}") }; - insta::with_settings!({ + with_settings!({ description => format!("On Enter on {annotated})"), }, { assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC)); diff --git a/crates/tinymist-query/src/tests.rs b/crates/tinymist-query/src/tests.rs index 933f7c70..02b54ee7 100644 --- a/crates/tinymist-query/src/tests.rs +++ b/crates/tinymist-query/src/tests.rs @@ -9,45 +9,30 @@ use std::{ }; use serde_json::{ser::PrettyFormatter, Serializer, Value}; -use tinymist_project::{CompileFontArgs, ExportTarget, LspCompileSnapshot, LspComputeGraph}; +use tinymist_project::{LspCompileSnapshot, LspComputeGraph}; use tinymist_std::path::unix_slash; use tinymist_std::typst::TypstDocument; use tinymist_world::debug_loc::LspRange; use tinymist_world::package::PackageSpec; use tinymist_world::vfs::WorkspaceResolver; -use tinymist_world::{EntryManager, EntryReader, EntryState, ShadowApi, TaskInputs}; -use typst::foundations::Bytes; +use tinymist_world::{EntryReader, ShadowApi, TaskInputs}; use typst::syntax::ast::{self, AstNode}; use typst::syntax::{LinkedNode, Source, SyntaxKind, VirtualPath}; use typst_shim::syntax::LinkedNodeExt; -pub use crate::syntax::find_module_level_docs; -pub use insta::assert_snapshot; pub use serde::Serialize; pub use serde_json::json; -pub use tinymist_project::{LspUniverse, LspUniverseBuilder}; +pub use tinymist_project::LspUniverse; +pub use tinymist_tests::{assert_snapshot, run_with_sources, with_settings}; pub use tinymist_world::WorldComputeGraph; +pub use crate::syntax::find_module_level_docs; use crate::{analysis::Analysis, prelude::LocalContext, LspPosition, PositionEncoding}; -use crate::{to_lsp_position, CompletionFeat, LspWorldExt}; +use crate::{to_lsp_position, CompletionFeat}; pub fn snapshot_testing(name: &str, f: &impl Fn(&mut LocalContext, PathBuf)) { - let name = if name.is_empty() { "playground" } else { name }; - - let mut settings = insta::Settings::new(); - settings.set_prepend_module_to_snapshot(false); - settings.set_snapshot_path(format!("fixtures/{name}/snaps")); - settings.bind(|| { - let glob_path = format!("fixtures/{name}/*.typ"); - insta::glob!(&glob_path, |path| { - let contents = std::fs::read_to_string(path).unwrap(); - #[cfg(windows)] - let contents = contents.replace("\r\n", "\n"); - - run_with_sources(&contents, |verse, path| { - run_with_ctx(verse, path, f); - }); - }); + tinymist_tests::snapshot_testing!(name, |verse, path| { + run_with_ctx(verse, path, f); }); } @@ -143,64 +128,6 @@ pub fn compile_doc_for_test( WorldComputeGraph::new(snap) } -pub fn run_with_sources(source: &str, f: impl FnOnce(&mut LspUniverse, PathBuf) -> T) -> T { - let root = if cfg!(windows) { - PathBuf::from("C:\\root") - } else { - PathBuf::from("/root") - }; - let mut verse = LspUniverseBuilder::build( - EntryState::new_rooted(root.as_path().into(), None), - ExportTarget::Paged, - Default::default(), - Default::default(), - LspUniverseBuilder::resolve_package(None, None), - Arc::new( - LspUniverseBuilder::resolve_fonts(CompileFontArgs { - ignore_system_fonts: true, - ..Default::default() - }) - .unwrap(), - ), - ); - let sources = source.split("-----"); - - let mut last_pw = None; - for (idx, source) in sources.enumerate() { - // find prelude - let mut source = source.trim_start(); - let mut path = None; - - if source.starts_with("//") { - let first_line = source.lines().next().unwrap(); - let content = first_line.trim_start_matches("/").trim(); - - if let Some(path_attr) = content.strip_prefix("path:") { - source = source.strip_prefix(first_line).unwrap().trim(); - path = Some(path_attr.trim().to_owned()) - } - }; - - let path = path.unwrap_or_else(|| format!("/s{idx}.typ")); - let path = path.strip_prefix("/").unwrap_or(path.as_str()); - - let pw = root.join(Path::new(&path)); - verse - .map_shadow(&pw, Bytes::from_string(source.to_owned())) - .unwrap(); - last_pw = Some(pw); - } - - let pw = last_pw.unwrap(); - verse - .mutate_entry(EntryState::new_rooted( - root.as_path().into(), - Some(VirtualPath::new(pw.strip_prefix(root).unwrap())), - )) - .unwrap(); - f(&mut verse, pw) -} - pub fn find_test_range(s: &Source) -> LspRange { let range = find_test_range_(s); crate::to_lsp_range(range, s, PositionEncoding::Utf16) diff --git a/crates/tinymist-tests/Cargo.toml b/crates/tinymist-tests/Cargo.toml new file mode 100644 index 00000000..f477b271 --- /dev/null +++ b/crates/tinymist-tests/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tinymist-tests" +description = "Test support for Tinymist." +authors.workspace = true +version.workspace = true +license.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +typst.workspace = true +tinymist-analysis.workspace = true +tinymist-project = { workspace = true, features = ["lsp"] } +tinymist-std.workspace = true +comemo.workspace = true +rayon.workspace = true +insta.workspace = true + +[lints] +workspace = true diff --git a/crates/tinymist-tests/src/lib.rs b/crates/tinymist-tests/src/lib.rs new file mode 100644 index 00000000..47e8a85c --- /dev/null +++ b/crates/tinymist-tests/src/lib.rs @@ -0,0 +1,103 @@ +//! Tests support for tinymist crates. + +use std::{ + path::{Path, PathBuf}, + sync::{Arc, LazyLock}, +}; + +use tinymist_project::{ + base::ShadowApi, font::FontResolverImpl, CompileFontArgs, EntryManager, EntryState, + ExportTarget, LspUniverse, LspUniverseBuilder, +}; +use typst::{foundations::Bytes, syntax::VirtualPath}; + +pub use insta::{assert_debug_snapshot, assert_snapshot, glob, with_settings, Settings}; + +/// Runs snapshot tests. +#[macro_export] +macro_rules! snapshot_testing { + ($name:expr, $f:expr) => { + let name = $name; + let name = if name.is_empty() { "playground" } else { name }; + let mut settings = $crate::Settings::new(); + settings.set_prepend_module_to_snapshot(false); + settings.set_snapshot_path(format!("fixtures/{name}/snaps")); + settings.bind(|| { + let glob_path = format!("fixtures/{name}/*.typ"); + $crate::glob!(&glob_path, |path| { + let contents = std::fs::read_to_string(path).unwrap(); + #[cfg(windows)] + let contents = contents.replace("\r\n", "\n"); + + $crate::run_with_sources(&contents, $f); + }); + }); + }; +} + +/// A test that runs a function with a given source string and returns the +/// result. +/// +/// Multiple sources can be provided, separated by `-----`. The last source +/// is used as the entry point. +pub fn run_with_sources(source: &str, f: impl FnOnce(&mut LspUniverse, PathBuf) -> T) -> T { + static FONT_RESOLVER: LazyLock> = LazyLock::new(|| { + Arc::new( + LspUniverseBuilder::resolve_fonts(CompileFontArgs { + ignore_system_fonts: true, + ..Default::default() + }) + .unwrap(), + ) + }); + + let root = if cfg!(windows) { + PathBuf::from("C:\\root") + } else { + PathBuf::from("/root") + }; + let mut verse = LspUniverseBuilder::build( + EntryState::new_rooted(root.as_path().into(), None), + ExportTarget::Paged, + Default::default(), + Default::default(), + LspUniverseBuilder::resolve_package(None, None), + FONT_RESOLVER.clone(), + ); + let sources = source.split("-----"); + + let mut last_pw = None; + for (idx, source) in sources.enumerate() { + // find prelude + let mut source = source.trim_start(); + let mut path = None; + + if source.starts_with("//") { + let first_line = source.lines().next().unwrap(); + let content = first_line.trim_start_matches("/").trim(); + + if let Some(path_attr) = content.strip_prefix("path:") { + source = source.strip_prefix(first_line).unwrap().trim(); + path = Some(path_attr.trim().to_owned()) + } + }; + + let path = path.unwrap_or_else(|| format!("/s{idx}.typ")); + let path = path.strip_prefix("/").unwrap_or(path.as_str()); + + let pw = root.join(Path::new(&path)); + verse + .map_shadow(&pw, Bytes::from_string(source.to_owned())) + .unwrap(); + last_pw = Some(pw); + } + + let pw = last_pw.unwrap(); + verse + .mutate_entry(EntryState::new_rooted( + root.as_path().into(), + Some(VirtualPath::new(pw.strip_prefix(root).unwrap())), + )) + .unwrap(); + f(&mut verse, pw) +} diff --git a/crates/tinymist-world/src/world.rs b/crates/tinymist-world/src/world.rs index 30a4069d..28321a30 100644 --- a/crates/tinymist-world/src/world.rs +++ b/crates/tinymist-world/src/world.rs @@ -6,7 +6,8 @@ use std::{ sync::{Arc, LazyLock, OnceLock}, }; -use tinymist_std::error::prelude::*; +use ecow::EcoVec; +use tinymist_std::{error::prelude::*, ImmutPath}; use tinymist_vfs::{ FileId, FsProvider, PathResolution, RevisingVfs, SourceCache, Vfs, WorkspaceResolver, }; @@ -545,6 +546,20 @@ impl CompilerWorld { self.inputs.clone() } + pub fn revision(&self) -> NonZeroUsize { + self.revision + } + + pub fn evict_vfs(&mut self, threshold: usize) { + self.vfs.evict(threshold); + } + + pub fn evict_source_cache(&mut self, threshold: usize) { + self.vfs + .clone_source_cache() + .evict(self.vfs.revision(), threshold); + } + /// Resolve the real path for a file id. pub fn path_for_id(&self, id: FileId) -> Result { self.vfs.file_path(id) @@ -559,18 +574,37 @@ impl CompilerWorld { )) } - pub fn revision(&self) -> NonZeroUsize { - self.revision + pub fn file_id_by_path(&self, path: &Path) -> FileResult { + // todo: source in packages + match self.id_for_path(path) { + Some(id) => Ok(id), + None => WorkspaceResolver::file_with_parent_root(path).ok_or_else(|| { + let reason = eco_format!("invalid path: {path:?}"); + FileError::Other(Some(reason)) + }), + } } - pub fn evict_vfs(&mut self, threshold: usize) { - self.vfs.evict(threshold); + pub fn source_by_path(&self, path: &Path) -> FileResult { + self.source(self.file_id_by_path(path)?) } - pub fn evict_source_cache(&mut self, threshold: usize) { - self.vfs - .clone_source_cache() - .evict(self.vfs.revision(), threshold); + pub fn depended_files(&self) -> EcoVec { + let mut deps = EcoVec::new(); + self.iter_dependencies(&mut |file_id| { + deps.push(file_id); + }); + deps + } + + pub fn depended_fs_paths(&self) -> EcoVec { + let mut deps = EcoVec::new(); + self.iter_dependencies(&mut |file_id| { + if let Ok(path) = self.path_for_id(file_id) { + deps.push(path.as_path().into()); + } + }); + deps } /// A list of all available packages and optionally descriptions for them. diff --git a/crates/tinymist/src/route.rs b/crates/tinymist/src/route.rs index 33632cd1..0d8c44ab 100644 --- a/crates/tinymist/src/route.rs +++ b/crates/tinymist/src/route.rs @@ -2,7 +2,6 @@ use std::{path::Path, sync::Arc}; use reflexo_typst::{path::unix_slash, typst::prelude::EcoVec, LazyHash}; use rpds::RedBlackTreeMapSync; -use tinymist_query::LspWorldExt; use tinymist_std::{hash::FxHashMap, ImmutPath}; use typst::diag::EcoString; diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index 2d4efc18..c438dc06 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -7,7 +7,7 @@ use lsp_types::request::ShowMessageRequest; use lsp_types::*; use reflexo::debug_loc::LspPosition; use sync_ls::*; -use tinymist_query::{LspWorldExt, OnExportRequest, ServerInfoResponse}; +use tinymist_query::{OnExportRequest, ServerInfoResponse}; use tinymist_std::error::prelude::*; use tinymist_std::ImmutPath; use tinymist_task::ProjectTask;