From 481369ea7cd7d2dc1b1381a58dec7b3c367760af Mon Sep 17 00:00:00 2001 From: bluepython508 <16466646+bluepython508@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:41:53 +0000 Subject: [PATCH] Improve error handling: render errors through typst --- .gitignore | 1 + Cargo.lock | 27 ++++++++ Cargo.toml | 1 + src/lib.rs | 24 +++---- src/typst.rs | 182 ++++++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 203 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 9c41696..3665c15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /.direnv *.mgc +*.typ diff --git a/Cargo.lock b/Cargo.lock index 37e6b50..2665834 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,6 +309,17 @@ dependencies = [ "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]] name = "codex" version = "0.2.0" @@ -2424,6 +2435,15 @@ dependencies = [ "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]] name = "thin-vec" version = "0.2.14" @@ -3082,6 +3102,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3533,6 +3559,7 @@ version = "0.1.0" dependencies = [ "bindgen", "cairo-rs", + "codespan-reporting", "pkg-config", "system-deps", "typst", diff --git a/Cargo.toml b/Cargo.toml index 1edd9df..485654f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ typst = "0.14.0" typst-render = "0.14.0" cairo-rs = "0.21.2" typst-kit = { version = "0.14.0", features = ["embed-fonts", "vendor-openssl"] } +codespan-reporting = "0.13.1" [build-dependencies] bindgen = "0.72.1" diff --git a/src/lib.rs b/src/lib.rs index 67c517e..0f80d4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,13 +4,11 @@ use std::{ path::Path, }; +use ::typst::layout::Page; use cairo::{Format, ImageSurface}; use crate::zathura::{ - ZathuraResult, 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_document, zathura_page_get_index, zathura_page_set_height, - zathura_page_set_width, zathura_page_t, + 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 }; 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(), )) }; - let Ok(document) = TypstDocument::load(path) else { - return ZathuraResult::InvalidArguments; - }; - let document = Box::new(document); + let document = Box::new(TypstDocument::load(path)); 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 _); } @@ -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 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 typst_page: &Page = &typst_doc.doc.pages[pageno as usize]; 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_height(page, typst_page.frame.height().to_pt()); }; @@ -105,10 +102,7 @@ unsafe extern "C" fn page_render_cairo( cairo: *mut cairo_t, _printing: bool, ) -> ZathuraResult { - let pageno = unsafe { zathura_page_get_index(page) }; - 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 typst_page: &Page = unsafe { &*(zathura_page_get_data(page) as *mut _)}; let context = unsafe { cairo::Context::from_raw_none(cairo) }; let surface = context.target(); @@ -153,9 +147,9 @@ unsafe extern "C" fn page_render_cairo( ZathuraResult::OK } -// TODO: handle compile errors // TODO: render warnings // 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) /* 0 string/cW /*\ doc:\ typst\ */ typst text document diff --git a/src/typst.rs b/src/typst.rs index da23458..130446d 100644 --- a/src/typst.rs +++ b/src/typst.rs @@ -2,15 +2,21 @@ use std::{ collections::{HashMap, hash_map::Entry}, hash::Hash, io::{self, ErrorKind}, + iter, path::{Path, PathBuf}, sync::{LazyLock, Mutex}, }; +use codespan_reporting::{ + diagnostic::{Diagnostic, Label}, + files::Files, + term::{Config, DisplayStyle, emit_to_write_style, termcolor::Ansi}, +}; use typst::{ - Library, LibraryExt, + Library, LibraryExt, WorldExt, diag::{FileError, FileResult, SourceDiagnostic, Warned}, foundations::{Bytes, Datetime}, - syntax::{FileId, Source, Span, VirtualPath}, + syntax::{FileId, Lines, Source, Span, VirtualPath}, text::{Font, FontBook}, utils::LazyHash, }; @@ -120,6 +126,16 @@ impl World { file_cache: Default::default(), }) } + + fn new_with_source(name: impl AsRef, 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 { @@ -127,21 +143,153 @@ pub struct TypstDocument { pub warnings: Vec, } -impl TypstDocument { - pub fn load(path: &Path) -> Result> { - let Warned { output, warnings } = typst::compile( - &World::new(path) - .map_err(|e| vec![SourceDiagnostic::error(Span::detached(), e.to_string())])?, - ); - match output { - Ok(doc) => Ok(Self { - doc, - warnings: warnings.to_vec(), - }), - Err(mut errors) => { - errors.extend(warnings); - Err(errors.to_vec()) - } +#[allow(clippy::result_large_err)] +fn compile(path: &Path) -> Result, Vec)> { + let world = World::new(path).map_err(|e| { + ( + None, + vec![SourceDiagnostic::error(Span::detached(), e.to_string())], + ) + })?; + let Warned { output, warnings } = typst::compile(&world); + match output { + Ok(doc) => Ok(TypstDocument { + doc, + warnings: warnings.to_vec(), + }), + Err(mut errors) => { + errors.extend(warnings); + 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; + + fn name(&self, id: Self::FileId) -> Result { + 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 { + 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 { + 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, 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) -> 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::() + }) + .map(|s| format!("==\n{s}")) + .collect::(); + + 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> { + Some(Label::primary(span.id()?, world.range(span)?)) +}