Move parts of the messages to hints

This commit is contained in:
Neven Villani 2025-07-24 09:25:58 +02:00
parent 3b84ce91e3
commit 26f0a1358c
4 changed files with 111 additions and 69 deletions

View File

@ -29,8 +29,9 @@ pub fn init(command: &InitCommand) -> StrResult<()> {
})?; })?;
// Find or download the package. // Find or download the package.
let package_path = let package_path = package_storage
package_storage.prepare_package(&spec, &mut PrintDownload(&spec))?; .prepare_package(&spec, &mut PrintDownload(&spec))
.map_err(|e| eco_format!("{e}"))?;
// Parse the manifest. // Parse the manifest.
let manifest = parse_manifest(&package_path)?; let manifest = parse_manifest(&package_path)?;

View File

@ -121,7 +121,7 @@ impl PackageStorage {
let namespace_dir = packages_dir.join(format!("{}", spec.namespace)); let namespace_dir = packages_dir.join(format!("{}", spec.namespace));
if !namespace_dir.exists() { if !namespace_dir.exists() {
return not_found(eco_format!( return not_found(eco_format!(
"namespace @{} should be located at {}", "the namespace @{} should be located at {}",
spec.namespace, spec.namespace,
namespace_dir.display() namespace_dir.display()
)); ));
@ -129,7 +129,7 @@ impl PackageStorage {
let package_dir = namespace_dir.join(format!("{}", spec.name)); let package_dir = namespace_dir.join(format!("{}", spec.name));
if !package_dir.exists() { if !package_dir.exists() {
return not_found(eco_format!( return not_found(eco_format!(
"{} does not have package '{}'", "the registry at {} does not have package '{}'",
namespace_dir.display(), namespace_dir.display(),
spec.name spec.name
)); ));
@ -220,7 +220,7 @@ impl PackageStorage {
return Err(PackageError::NotFound( return Err(PackageError::NotFound(
spec.clone(), spec.clone(),
eco_format!( eco_format!(
"{namespace_url} does not have package '{}'", "the registry at {namespace_url} does not have package '{}'",
spec.name spec.name
), ),
)); ));

View File

@ -315,6 +315,24 @@ impl<T> Trace<T> for SourceResult<T> {
/// A result type with a string error message. /// A result type with a string error message.
pub type StrResult<T> = Result<T, EcoString>; pub type StrResult<T> = Result<T, EcoString>;
/// 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<S> ErrAt for S
where
S: Into<EcoString>,
{
fn err_at(self, span: Span) -> SourceDiagnostic {
SourceDiagnostic::error(span, self)
}
}
/// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by /// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by
/// adding span information. /// adding span information.
pub trait At<T> { pub trait At<T> {
@ -324,18 +342,10 @@ pub trait At<T> {
impl<T, S> At<T> for Result<T, S> impl<T, S> At<T> for Result<T, S>
where where
S: Into<EcoString>, S: ErrAt,
{ {
fn at(self, span: Span) -> SourceResult<T> { fn at(self, span: Span) -> SourceResult<T> {
self.map_err(|message| { self.map_err(|e| eco_vec![e.err_at(span)])
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]
})
} }
} }
@ -469,6 +479,29 @@ impl FileError {
_ => Self::Other(Some(eco_format!("{err}"))), _ => Self::Other(Some(eco_format!("{err}"))),
} }
} }
fn write_hints(&self) -> EcoVec<EcoString> {
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 {} impl std::error::Error for FileError {}
@ -479,23 +512,28 @@ impl Display for FileError {
Self::NotFound(path) => { Self::NotFound(path) => {
write!(f, "file not found (searched at {})", path.display()) 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) => { Self::NotSource(path) => {
write!(f, "{} is not a typst source file", path.display()) write!(f, "{} is not a typst source file", path.display())
} }
Self::InvalidUtf8(Some(path)) => { Self::InvalidUtf8(_) => write!(f, "file is not valid utf-8"),
write!(f, "file {} is not valid utf-8", path.display()) 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::AccessDenied(path) => {
Self::Package(error) => error.fmt(f), write!(f, "failed to load file {} (access denied)", path.display())
Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
Self::Other(None) => f.pad("failed to load file"),
} }
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)
} }
} }
@ -517,12 +555,6 @@ impl From<PackageError> for FileError {
} }
} }
impl From<FileError> for EcoString {
fn from(err: FileError) -> Self {
eco_format!("{err}")
}
}
/// A result type with a package-related error. /// A result type with a package-related error.
pub type PackageResult<T> = Result<T, PackageError>; pub type PackageResult<T> = Result<T, PackageError>;
@ -545,46 +577,56 @@ pub enum PackageError {
Other(Option<EcoString>), Other(Option<EcoString>),
} }
impl PackageError {
fn write_hints(&self) -> EcoVec<EcoString> {
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 std::error::Error for PackageError {}
impl Display for PackageError { impl Display for PackageError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::NotFound(spec, detail) => { Self::NotFound(spec, _) => write!(f, "package {spec} not found"),
write!(f, "package not found: {detail} (searching for {spec})",) Self::VersionNotFound(spec, _, _) => {
write!(f, "package version {spec} not found")
} }
Self::VersionNotFound(spec, latest, registry) => { Self::NetworkFailed(_) => write!(f, "failed to download package"),
write!( Self::MalformedArchive(_) => write!(f, "failed to decompress package"),
f, Self::Other(_) => f.pad("failed to load package"),
"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"),
}
}
}
impl From<PackageError> for EcoString {
fn from(err: PackageError) -> Self {
eco_format!("{err}")
}
} }
/// A result type with a data-loading-related error. /// A result type with a data-loading-related error.

View File

@ -41,9 +41,9 @@ pub use typst_utils as utils;
use std::collections::HashSet; use std::collections::HashSet;
use comemo::{Track, Tracked, Validate}; use comemo::{Track, Tracked, Validate};
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use ecow::{eco_format, eco_vec, EcoVec};
use typst_library::diag::{ 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::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{StyleChain, Styles, Value}; use typst_library::foundations::{StyleChain, Styles, Value};
@ -191,8 +191,7 @@ fn hint_invalid_main_file(
input: FileId, input: FileId,
) -> EcoVec<SourceDiagnostic> { ) -> EcoVec<SourceDiagnostic> {
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8(_)); let is_utf8_error = matches!(file_error, FileError::InvalidUtf8(_));
let mut diagnostic = let mut diagnostic = file_error.err_at(Span::detached());
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
// Attempt to provide helpful hints for UTF-8 errors. Perhaps the user // Attempt to provide helpful hints for UTF-8 errors. Perhaps the user
// mistyped the filename. For example, they could have written "file.pdf" // mistyped the filename. For example, they could have written "file.pdf"