diff --git a/crates/typst-cli/src/init.rs b/crates/typst-cli/src/init.rs index 9a77fb470..2806c7eae 100644 --- a/crates/typst-cli/src/init.rs +++ b/crates/typst-cli/src/init.rs @@ -29,8 +29,9 @@ pub fn init(command: &InitCommand) -> StrResult<()> { })?; // Find or download the package. - let package_path = - package_storage.prepare_package(&spec, &mut PrintDownload(&spec))?; + let package_path = package_storage + .prepare_package(&spec, &mut PrintDownload(&spec)) + .map_err(|e| eco_format!("{e}"))?; // Parse the manifest. let manifest = parse_manifest(&package_path)?; diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index 5b5c23c42..0a794d49b 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -121,7 +121,7 @@ impl PackageStorage { let namespace_dir = packages_dir.join(format!("{}", spec.namespace)); if !namespace_dir.exists() { return not_found(eco_format!( - "namespace @{} should be located at {}", + "the namespace @{} should be located at {}", spec.namespace, namespace_dir.display() )); @@ -129,7 +129,7 @@ impl PackageStorage { let package_dir = namespace_dir.join(format!("{}", spec.name)); if !package_dir.exists() { return not_found(eco_format!( - "{} does not have package '{}'", + "the registry at {} does not have package '{}'", namespace_dir.display(), spec.name )); @@ -220,7 +220,7 @@ impl PackageStorage { return Err(PackageError::NotFound( spec.clone(), eco_format!( - "{namespace_url} does not have package '{}'", + "the registry at {namespace_url} does not have package '{}'", spec.name ), )); diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index 2214b63f4..682ef2650 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -315,6 +315,24 @@ impl Trace for SourceResult { /// A result type with a string error message. pub type StrResult = Result; +/// A common trait to add a span to any error +pub trait ErrAt { + /// Attach a span to a more specialized error and turn it into + /// a generic error, optionally with hints. + fn err_at(self, span: Span) -> SourceDiagnostic; +} + +/// Blanket implementation for anything that doesn't implement its own +/// convertion to a SourceDiagnostic. +impl ErrAt for S +where + S: Into, +{ + fn err_at(self, span: Span) -> SourceDiagnostic { + SourceDiagnostic::error(span, self) + } +} + /// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by /// adding span information. pub trait At { @@ -324,18 +342,10 @@ pub trait At { impl At for Result where - S: Into, + S: ErrAt, { fn at(self, span: Span) -> SourceResult { - self.map_err(|message| { - let mut diagnostic = SourceDiagnostic::error(span, message); - if diagnostic.message.contains("(access denied)") { - diagnostic.hint("cannot read file outside of project root"); - diagnostic - .hint("you can adjust the project root with the --root argument"); - } - eco_vec![diagnostic] - }) + self.map_err(|e| eco_vec![e.err_at(span)]) } } @@ -469,6 +479,29 @@ impl FileError { _ => Self::Other(Some(eco_format!("{err}"))), } } + + fn write_hints(&self) -> EcoVec { + match self { + Self::NotFound(_) => eco_vec![], + Self::AccessDenied(_) => { + eco_vec![ + eco_format!("cannot read file outside of project root"), + eco_format!( + "you can adjust the project root with the --root argument" + ), + ] + } + Self::IsDirectory(_) => eco_vec![], + Self::NotSource(_) => eco_vec![], + Self::InvalidUtf8(Some(path)) => { + eco_vec![eco_format!("tried to read {}", path.display())] + } + Self::InvalidUtf8(None) => eco_vec![], + Self::Package(error) => error.write_hints(), + Self::Other(Some(err)) => eco_vec![eco_format!("{err}")], + Self::Other(None) => eco_vec![], + } + } } impl std::error::Error for FileError {} @@ -479,26 +512,31 @@ impl Display for FileError { Self::NotFound(path) => { write!(f, "file not found (searched at {})", path.display()) } - Self::AccessDenied(path) => { - write!(f, "failed to load file {} (access denied)", path.display()) - } - Self::IsDirectory(path) => { - write!(f, "failed to load file {} (is a directory)", path.display()) - } Self::NotSource(path) => { write!(f, "{} is not a typst source file", path.display()) } - Self::InvalidUtf8(Some(path)) => { - write!(f, "file {} is not valid utf-8", path.display()) + Self::InvalidUtf8(_) => write!(f, "file is not valid utf-8"), + Self::IsDirectory(path) => { + write!(f, "failed to load file {} (is a directory)", path.display()) } - Self::InvalidUtf8(None) => f.pad("file is not valid utf-8"), - Self::Package(error) => error.fmt(f), - Self::Other(Some(err)) => write!(f, "failed to load file ({err})"), - Self::Other(None) => f.pad("failed to load file"), + Self::AccessDenied(path) => { + write!(f, "failed to load file {} (access denied)", path.display()) + } + Self::Other(_) => { + write!(f, "failed to load file") + } + Self::Package(error) => write!(f, "{error}"), } } } +impl ErrAt for FileError { + fn err_at(self, span: Span) -> SourceDiagnostic { + let hints = self.write_hints(); + SourceDiagnostic::error(span, eco_format!("{}", self)).with_hints(hints) + } +} + impl From for FileError { fn from(_: Utf8Error) -> Self { Self::InvalidUtf8(None) @@ -517,12 +555,6 @@ impl From for FileError { } } -impl From for EcoString { - fn from(err: FileError) -> Self { - eco_format!("{err}") - } -} - /// A result type with a package-related error. pub type PackageResult = Result; @@ -545,48 +577,58 @@ pub enum PackageError { Other(Option), } +impl PackageError { + fn write_hints(&self) -> EcoVec { + match self { + Self::NotFound(_, detail) => { + eco_vec![eco_format!("{detail}")] + } + Self::VersionNotFound(spec, latest, registry) => { + if let Some(version) = latest { + eco_vec![ + eco_format!( + "the package {} exists, but not with version {}", + spec.name, + spec.version + ), + eco_format!( + "the registry at {registry} provides up to version {version}" + ), + ] + } else { + eco_vec![eco_format!( + "the registry at {registry} contains no versions for this package" + )] + } + } + Self::NetworkFailed(Some(err)) => eco_vec![eco_format!("{err}")], + Self::NetworkFailed(None) => eco_vec![], + Self::MalformedArchive(Some(err)) => eco_vec![eco_format!("{err}")], + Self::MalformedArchive(None) => { + eco_vec![eco_format!("the archive is malformed")] + } + Self::Other(Some(err)) => eco_vec![eco_format!("{err}")], + Self::Other(None) => eco_vec![], + } + } +} + impl std::error::Error for PackageError {} impl Display for PackageError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Self::NotFound(spec, detail) => { - write!(f, "package not found: {detail} (searching for {spec})",) + Self::NotFound(spec, _) => write!(f, "package {spec} not found"), + Self::VersionNotFound(spec, _, _) => { + write!(f, "package version {spec} not found") } - Self::VersionNotFound(spec, latest, registry) => { - write!( - f, - "package '{}' found, but version {} does not exist", - spec.name, spec.version - )?; - if let Some(version) = latest { - write!(f, " (latest version provided by {registry} is {version})") - } else { - write!(f, " ({registry} contains no versions for this package)") - } - } - Self::NetworkFailed(Some(err)) => { - write!(f, "failed to download package ({err})") - } - Self::NetworkFailed(None) => f.pad("failed to download package"), - Self::MalformedArchive(Some(err)) => { - write!(f, "failed to decompress package ({err})") - } - Self::MalformedArchive(None) => { - f.pad("failed to decompress package (archive malformed)") - } - Self::Other(Some(err)) => write!(f, "failed to load package ({err})"), - Self::Other(None) => f.pad("failed to load package"), + Self::NetworkFailed(_) => write!(f, "failed to download package"), + Self::MalformedArchive(_) => write!(f, "failed to decompress package"), + Self::Other(_) => f.pad("failed to load package"), } } } -impl From for EcoString { - fn from(err: PackageError) -> Self { - eco_format!("{err}") - } -} - /// A result type with a data-loading-related error. pub type LoadResult = Result; diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 9d388f3bb..8d37f8dae 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -41,9 +41,9 @@ pub use typst_utils as utils; use std::collections::HashSet; use comemo::{Track, Tracked, Validate}; -use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use ecow::{eco_format, eco_vec, EcoVec}; use typst_library::diag::{ - bail, warning, FileError, SourceDiagnostic, SourceResult, Warned, + bail, warning, ErrAt, FileError, SourceDiagnostic, SourceResult, Warned, }; use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{StyleChain, Styles, Value}; @@ -191,8 +191,7 @@ fn hint_invalid_main_file( input: FileId, ) -> EcoVec { let is_utf8_error = matches!(file_error, FileError::InvalidUtf8(_)); - let mut diagnostic = - SourceDiagnostic::error(Span::detached(), EcoString::from(file_error)); + let mut diagnostic = file_error.err_at(Span::detached()); // Attempt to provide helpful hints for UTF-8 errors. Perhaps the user // mistyped the filename. For example, they could have written "file.pdf"