Improve error handling: render errors through typst

This commit is contained in:
bluepython508
2025-10-31 16:41:53 +00:00
parent 01474ad6a0
commit 481369ea7c
5 changed files with 203 additions and 32 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target /target
/.direnv /.direnv
*.mgc *.mgc
*.typ

27
Cargo.lock generated
View File

@@ -309,6 +309,17 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "codespan-reporting"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
"unicode-width",
]
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.2.0" version = "0.2.0"
@@ -2424,6 +2435,15 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "thin-vec" name = "thin-vec"
version = "0.2.14" version = "0.2.14"
@@ -3082,6 +3102,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "unsafe-libyaml" name = "unsafe-libyaml"
version = "0.2.11" version = "0.2.11"
@@ -3533,6 +3559,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bindgen", "bindgen",
"cairo-rs", "cairo-rs",
"codespan-reporting",
"pkg-config", "pkg-config",
"system-deps", "system-deps",
"typst", "typst",

View File

@@ -8,6 +8,7 @@ typst = "0.14.0"
typst-render = "0.14.0" typst-render = "0.14.0"
cairo-rs = "0.21.2" cairo-rs = "0.21.2"
typst-kit = { version = "0.14.0", features = ["embed-fonts", "vendor-openssl"] } typst-kit = { version = "0.14.0", features = ["embed-fonts", "vendor-openssl"] }
codespan-reporting = "0.13.1"
[build-dependencies] [build-dependencies]
bindgen = "0.72.1" bindgen = "0.72.1"

View File

@@ -4,13 +4,11 @@ use std::{
path::Path, path::Path,
}; };
use ::typst::layout::Page;
use cairo::{Format, ImageSurface}; use cairo::{Format, ImageSurface};
use crate::zathura::{ use crate::zathura::{
ZathuraResult, cairo_t, zathura_document_get_data, zathura_document_get_path, cairo_t, zathura_document_get_data, zathura_document_get_path, zathura_document_s, zathura_document_set_data, zathura_document_set_number_of_pages, zathura_page_get_data, zathura_page_get_document, zathura_page_get_index, zathura_page_set_data, zathura_page_set_height, zathura_page_set_width, zathura_page_t, ZathuraResult
zathura_document_s, zathura_document_set_data, zathura_document_set_number_of_pages,
zathura_page_get_document, zathura_page_get_index, zathura_page_set_height,
zathura_page_set_width, zathura_page_t,
}; };
mod typst; mod typst;
@@ -61,13 +59,10 @@ unsafe extern "C" fn document_open(doc: *mut zathura_document_s) -> ZathuraResul
CStr::from_ptr(zathura_document_get_path(doc)).to_bytes(), CStr::from_ptr(zathura_document_get_path(doc)).to_bytes(),
)) ))
}; };
let Ok(document) = TypstDocument::load(path) else { let document = Box::new(TypstDocument::load(path));
return ZathuraResult::InvalidArguments;
};
let document = Box::new(document);
unsafe { unsafe {
zathura_document_set_number_of_pages(doc, document.doc.pages.len() as _); zathura_document_set_number_of_pages(doc, document.doc.pages.len() as u32);
zathura_document_set_data(doc, Box::into_raw(document) as _); zathura_document_set_data(doc, Box::into_raw(document) as _);
} }
@@ -85,9 +80,11 @@ unsafe extern "C" fn page_init(page: *mut zathura_page_t) -> ZathuraResult {
let pageno = unsafe { zathura_page_get_index(page) }; let pageno = unsafe { zathura_page_get_index(page) };
let doc = unsafe { zathura_page_get_document(page) }; let doc = unsafe { zathura_page_get_document(page) };
let typst_doc = unsafe { &mut *(zathura_document_get_data(doc) as *mut TypstDocument) }; let typst_doc = unsafe { &mut *(zathura_document_get_data(doc) as *mut TypstDocument) };
let typst_page = &mut typst_doc.doc.pages[pageno as usize];
let typst_page: &Page = &typst_doc.doc.pages[pageno as usize];
unsafe { unsafe {
zathura_page_set_data(page, typst_page as *const _ as _);
zathura_page_set_width(page, typst_page.frame.width().to_pt()); zathura_page_set_width(page, typst_page.frame.width().to_pt());
zathura_page_set_height(page, typst_page.frame.height().to_pt()); zathura_page_set_height(page, typst_page.frame.height().to_pt());
}; };
@@ -105,10 +102,7 @@ unsafe extern "C" fn page_render_cairo(
cairo: *mut cairo_t, cairo: *mut cairo_t,
_printing: bool, _printing: bool,
) -> ZathuraResult { ) -> ZathuraResult {
let pageno = unsafe { zathura_page_get_index(page) }; let typst_page: &Page = unsafe { &*(zathura_page_get_data(page) as *mut _)};
let doc = unsafe { zathura_page_get_document(page) };
let typst_doc = unsafe { &mut *(zathura_document_get_data(doc) as *mut TypstDocument) };
let typst_page = &mut typst_doc.doc.pages[pageno as usize];
let context = unsafe { cairo::Context::from_raw_none(cairo) }; let context = unsafe { cairo::Context::from_raw_none(cairo) };
let surface = context.target(); let surface = context.target();
@@ -153,9 +147,9 @@ unsafe extern "C" fn page_render_cairo(
ZathuraResult::OK ZathuraResult::OK
} }
// TODO: handle compile errors
// TODO: render warnings // TODO: render warnings
// TODO: text/link/... handling // TODO: text/link/... handling
// TODO: better caching
// TODO: nixify compilation of magicdb (file -Cm ~/.magic:/nix/store/vi7ya34k19nid2m0dmkljqip5572g0bi-file-5.45/share/misc/magic.mgc) // TODO: nixify compilation of magicdb (file -Cm ~/.magic:/nix/store/vi7ya34k19nid2m0dmkljqip5572g0bi-file-5.45/share/misc/magic.mgc)
/* /*
0 string/cW /*\ doc:\ typst\ */ typst text document 0 string/cW /*\ doc:\ typst\ */ typst text document

View File

@@ -2,15 +2,21 @@ use std::{
collections::{HashMap, hash_map::Entry}, collections::{HashMap, hash_map::Entry},
hash::Hash, hash::Hash,
io::{self, ErrorKind}, io::{self, ErrorKind},
iter,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{LazyLock, Mutex}, sync::{LazyLock, Mutex},
}; };
use codespan_reporting::{
diagnostic::{Diagnostic, Label},
files::Files,
term::{Config, DisplayStyle, emit_to_write_style, termcolor::Ansi},
};
use typst::{ use typst::{
Library, LibraryExt, Library, LibraryExt, WorldExt,
diag::{FileError, FileResult, SourceDiagnostic, Warned}, diag::{FileError, FileResult, SourceDiagnostic, Warned},
foundations::{Bytes, Datetime}, foundations::{Bytes, Datetime},
syntax::{FileId, Source, Span, VirtualPath}, syntax::{FileId, Lines, Source, Span, VirtualPath},
text::{Font, FontBook}, text::{Font, FontBook},
utils::LazyHash, utils::LazyHash,
}; };
@@ -120,6 +126,16 @@ impl World {
file_cache: Default::default(), file_cache: Default::default(),
}) })
} }
fn new_with_source(name: impl AsRef<Path>, contents: String) -> Self {
let main = FileId::new_fake(VirtualPath::new(name));
Self {
main,
root: PathBuf::new(),
source_cache: Mutex::new(HashMap::from([(main, Source::new(main, contents.clone()))])),
file_cache: Mutex::new(HashMap::from([(main, Bytes::from_string(contents))])),
}
}
} }
pub struct TypstDocument { pub struct TypstDocument {
@@ -127,21 +143,153 @@ pub struct TypstDocument {
pub warnings: Vec<SourceDiagnostic>, pub warnings: Vec<SourceDiagnostic>,
} }
impl TypstDocument { #[allow(clippy::result_large_err)]
pub fn load(path: &Path) -> Result<Self, Vec<SourceDiagnostic>> { fn compile(path: &Path) -> Result<TypstDocument, (Option<World>, Vec<SourceDiagnostic>)> {
let Warned { output, warnings } = typst::compile( let world = World::new(path).map_err(|e| {
&World::new(path) (
.map_err(|e| vec![SourceDiagnostic::error(Span::detached(), e.to_string())])?, None,
); vec![SourceDiagnostic::error(Span::detached(), e.to_string())],
)
})?;
let Warned { output, warnings } = typst::compile(&world);
match output { match output {
Ok(doc) => Ok(Self { Ok(doc) => Ok(TypstDocument {
doc, doc,
warnings: warnings.to_vec(), warnings: warnings.to_vec(),
}), }),
Err(mut errors) => { Err(mut errors) => {
errors.extend(warnings); errors.extend(warnings);
Err(errors.to_vec()) Err((Some(world), errors.to_vec()))
}
} }
} }
} }
impl TypstDocument {
pub fn load(path: &Path) -> Self {
let compiled = compile(path);
match compiled {
Ok(doc) => doc,
Err((world, errors)) => diagnostics(
&world.unwrap_or_else(|| World::new_with_source("error", String::new())),
errors,
),
}
}
}
impl Files<'_> for World {
type FileId = FileId;
type Name = String;
type Source = Lines<String>;
fn name(&self, id: Self::FileId) -> Result<Self::Name, codespan_reporting::files::Error> {
let vpath = id.vpath();
Ok(format!(
"{}{}",
id.package()
.map(|p| format!("{}/", p.name))
.unwrap_or_default(),
vpath.as_rootless_path().display()
))
}
fn source(&self, id: Self::FileId) -> Result<Self::Source, codespan_reporting::files::Error> {
typst::World::source(self, id)
.map(|s| s.lines().clone())
.map_err(|_| codespan_reporting::files::Error::FileMissing)
}
fn line_index(
&self,
id: Self::FileId,
byte_index: usize,
) -> Result<usize, codespan_reporting::files::Error> {
let source = Files::source(self, id)?;
source.byte_to_line(byte_index).ok_or_else(|| {
codespan_reporting::files::Error::IndexTooLarge {
given: byte_index,
max: source.len_bytes(),
}
})
}
fn line_range(
&self,
id: Self::FileId,
line_index: usize,
) -> Result<std::ops::Range<usize>, codespan_reporting::files::Error> {
let source = Files::source(self, id)?;
source.line_to_range(line_index).ok_or_else(|| {
codespan_reporting::files::Error::LineTooLarge {
given: line_index,
max: source.len_lines(),
}
})
}
}
fn diagnostics(world: &World, errors: impl IntoIterator<Item = SourceDiagnostic>) -> TypstDocument {
let config = &Config {
display_style: DisplayStyle::Rich,
tab_width: 2,
..Default::default()
};
let mut binaries = HashMap::new();
let mut i = 0;
let errors = errors
.into_iter()
.map(|diag| {
iter::once(
match diag.severity {
typst::diag::Severity::Error => Diagnostic::error(),
typst::diag::Severity::Warning => Diagnostic::warning(),
}
.with_message(diag.message)
.with_notes(
diag.hints
.into_iter()
.map(|e| format!("hint: {e}"))
.collect(),
)
.with_labels(label(world, diag.span).into_iter().collect()),
)
.chain(diag.trace.into_iter().map(|point| {
Diagnostic::help()
.with_message(point.v)
.with_labels(label(world, point.span).into_iter().collect())
}))
.map(|out_diag| {
let mut buf = Vec::new();
emit_to_write_style(&mut Ansi::new(&mut buf), config, world, &out_diag)
.expect("Failed to format diagnostic");
i += 1;
binaries.insert(
FileId::new(None, VirtualPath::new(format!("/error-{i}"))),
Bytes::new(buf),
);
format!("#ansi-render(read(\"error-{i}\"), theme: terminal-themes.one-half-light, font: none)\n")
})
.collect::<String>()
})
.map(|s| format!("==\n{s}"))
.collect::<String>();
let world = World::new_with_source("errors.typ", format!("#import \"@preview/ansi-render:0.8.0\": *\n{errors}"));
world.file_cache.lock().unwrap().extend(binaries);
let Warned {
output,
warnings: _,
} = typst::compile(&world);
TypstDocument {
doc: output.expect("Should be valid syntax"),
warnings: vec![],
}
}
fn label(world: &World, span: Span) -> Option<Label<FileId>> {
Some(Label::primary(span.id()?, world.range(span)?))
}