tinymist_std/
error.rs

1//! Error handling utilities for the `tinymist` crate.
2
3use core::fmt;
4
5use ecow::EcoString;
6use serde::{Deserialize, Serialize};
7#[cfg(feature = "typst")]
8use typst::diag::SourceDiagnostic;
9
10use lsp_types::Range as LspRange;
11
12/// The severity of a diagnostic message, following the LSP specification.
13#[derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Clone)]
14#[repr(u8)]
15pub enum DiagSeverity {
16    /// An error message.
17    Error = 1,
18    /// A warning message.
19    Warning = 2,
20    /// An information message.
21    Information = 3,
22    /// A hint message.
23    Hint = 4,
24}
25
26impl fmt::Display for DiagSeverity {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            DiagSeverity::Error => write!(f, "error"),
30            DiagSeverity::Warning => write!(f, "warning"),
31            DiagSeverity::Information => write!(f, "information"),
32            DiagSeverity::Hint => write!(f, "hint"),
33        }
34    }
35}
36
37/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic>
38/// The `owner` and `source` fields are not included in the struct, but they
39/// could be added to `ErrorImpl::arguments`.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct DiagMessage {
42    /// The typst package specifier.
43    pub package: String,
44    /// The file path relative to the root of the workspace or the package.
45    pub path: String,
46    /// The diagnostic message.
47    pub message: EcoString,
48    /// The severity of the diagnostic message.
49    pub severity: DiagSeverity,
50    /// The char range in the file. The position encoding must be negotiated.
51    pub range: Option<LspRange>,
52}
53
54impl DiagMessage {}
55
56/// ALl kind of errors that can occur in the `tinymist` crate.
57#[derive(Debug, Clone)]
58#[non_exhaustive]
59pub enum ErrKind {
60    /// No message.
61    None,
62    /// A string message.
63    Msg(EcoString),
64    /// A source diagnostic message.
65    #[cfg(feature = "typst")]
66    RawDiag(ecow::EcoVec<SourceDiagnostic>),
67    /// A source diagnostic message.
68    Diag(Box<DiagMessage>),
69    /// An inner error.
70    Inner(Error),
71}
72
73/// A trait to convert an error kind into an error kind.
74pub trait ErrKindExt {
75    /// Convert the error kind into an error kind.
76    fn to_error_kind(self) -> ErrKind;
77}
78
79impl ErrKindExt for ErrKind {
80    fn to_error_kind(self) -> Self {
81        self
82    }
83}
84
85impl ErrKindExt for std::io::Error {
86    fn to_error_kind(self) -> ErrKind {
87        ErrKind::Msg(self.to_string().into())
88    }
89}
90
91impl ErrKindExt for std::str::Utf8Error {
92    fn to_error_kind(self) -> ErrKind {
93        ErrKind::Msg(self.to_string().into())
94    }
95}
96
97impl ErrKindExt for String {
98    fn to_error_kind(self) -> ErrKind {
99        ErrKind::Msg(self.into())
100    }
101}
102
103impl ErrKindExt for &str {
104    fn to_error_kind(self) -> ErrKind {
105        ErrKind::Msg(self.into())
106    }
107}
108
109impl ErrKindExt for &String {
110    fn to_error_kind(self) -> ErrKind {
111        ErrKind::Msg(self.into())
112    }
113}
114
115impl ErrKindExt for EcoString {
116    fn to_error_kind(self) -> ErrKind {
117        ErrKind::Msg(self)
118    }
119}
120
121impl ErrKindExt for &dyn std::fmt::Display {
122    fn to_error_kind(self) -> ErrKind {
123        ErrKind::Msg(self.to_string().into())
124    }
125}
126
127impl ErrKindExt for serde_json::Error {
128    fn to_error_kind(self) -> ErrKind {
129        ErrKind::Msg(self.to_string().into())
130    }
131}
132
133impl ErrKindExt for anyhow::Error {
134    fn to_error_kind(self) -> ErrKind {
135        ErrKind::Msg(self.to_string().into())
136    }
137}
138
139impl ErrKindExt for Error {
140    fn to_error_kind(self) -> ErrKind {
141        ErrKind::Msg(self.to_string().into())
142    }
143}
144
145/// The internal error implementation.
146#[derive(Debug, Clone)]
147pub struct ErrorImpl {
148    /// A static error identifier.
149    loc: &'static str,
150    /// The kind of error.
151    kind: ErrKind,
152    /// Additional extractable arguments for the error.
153    args: Option<Box<[(&'static str, String)]>>,
154}
155
156/// This type represents all possible errors that can occur in typst.ts
157#[derive(Debug, Clone)]
158pub struct Error {
159    /// This `Box` allows us to keep the size of `Error` as small as possible. A
160    /// larger `Error` type was substantially slower due to all the functions
161    /// that pass around `Result<T, Error>`.
162    err: Box<ErrorImpl>,
163}
164
165impl Error {
166    /// Creates a new error.
167    pub fn new(
168        loc: &'static str,
169        kind: ErrKind,
170        args: Option<Box<[(&'static str, String)]>>,
171    ) -> Self {
172        Self {
173            err: Box::new(ErrorImpl { loc, kind, args }),
174        }
175    }
176
177    /// Returns the location of the error.
178    pub fn loc(&self) -> &'static str {
179        self.err.loc
180    }
181
182    /// Returns the kind of the error.
183    pub fn kind(&self) -> &ErrKind {
184        &self.err.kind
185    }
186
187    /// Returns the arguments of the error.
188    pub fn arguments(&self) -> &[(&'static str, String)] {
189        self.err.args.as_deref().unwrap_or_default()
190    }
191}
192
193impl fmt::Display for Error {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        let err = &self.err;
196
197        if err.loc.is_empty() {
198            match &err.kind {
199                ErrKind::Msg(msg) => write!(f, "{msg} with {:?}", err.args),
200                #[cfg(feature = "typst")]
201                ErrKind::RawDiag(diag) => {
202                    write!(f, "{diag:?} with {:?}", err.args)
203                }
204                ErrKind::Diag(diag) => {
205                    write!(f, "{} with {:?}", diag.message, err.args)
206                }
207                ErrKind::Inner(e) => write!(f, "{e} with {:?}", err.args),
208                ErrKind::None => write!(f, "error with {:?}", err.args),
209            }
210        } else {
211            match &err.kind {
212                ErrKind::Msg(msg) => write!(f, "{}: {} with {:?}", err.loc, msg, err.args),
213                #[cfg(feature = "typst")]
214                ErrKind::RawDiag(diag) => {
215                    write!(f, "{}: {diag:?} with {:?}", err.loc, err.args)
216                }
217                ErrKind::Diag(diag) => {
218                    write!(f, "{}: {} with {:?}", err.loc, diag.message, err.args)
219                }
220                ErrKind::Inner(e) => write!(f, "{}: {} with {:?}", err.loc, e, err.args),
221                ErrKind::None => write!(f, "{}: with {:?}", err.loc, err.args),
222            }
223        }
224    }
225}
226
227impl From<anyhow::Error> for Error {
228    fn from(e: anyhow::Error) -> Self {
229        Error::new("", e.to_string().to_error_kind(), None)
230    }
231}
232
233#[cfg(feature = "typst")]
234impl From<ecow::EcoVec<SourceDiagnostic>> for Error {
235    fn from(e: ecow::EcoVec<SourceDiagnostic>) -> Self {
236        Error::new("", ErrKind::RawDiag(e), None)
237    }
238}
239
240impl std::error::Error for Error {}
241
242#[cfg(feature = "web")]
243impl ErrKindExt for wasm_bindgen::JsValue {
244    fn to_error_kind(self) -> ErrKind {
245        ErrKind::Msg(ecow::eco_format!("{self:?}"))
246    }
247}
248
249#[cfg(feature = "web")]
250impl From<Error> for wasm_bindgen::JsValue {
251    fn from(e: Error) -> Self {
252        js_sys::Error::new(&e.to_string()).into()
253    }
254}
255
256#[cfg(feature = "web")]
257impl From<&Error> for wasm_bindgen::JsValue {
258    fn from(e: &Error) -> Self {
259        js_sys::Error::new(&e.to_string()).into()
260    }
261}
262
263/// The result type used in the `tinymist` crate.
264pub type Result<T, Err = Error> = std::result::Result<T, Err>;
265
266/// A trait to add context to a result.
267pub trait IgnoreLogging<T>: Sized {
268    /// Log an error message and return `None`.
269    fn log_error(self, msg: &str) -> Option<T>;
270    /// Log an error message and return `None`.
271    fn log_error_with(self, f: impl FnOnce() -> String) -> Option<T>;
272}
273
274impl<T, E: std::fmt::Display> IgnoreLogging<T> for Result<T, E> {
275    fn log_error(self, msg: &str) -> Option<T> {
276        self.inspect_err(|e| log::error!("{msg}: {e}")).ok()
277    }
278
279    fn log_error_with(self, f: impl FnOnce() -> String) -> Option<T> {
280        self.inspect_err(|e| log::error!("{}: {e}", f())).ok()
281    }
282}
283
284/// A trait to add context to a result.
285pub trait WithContext<T>: Sized {
286    /// Add a context to the result.
287    fn context(self, loc: &'static str) -> Result<T>;
288
289    /// Add a context to the result with additional arguments.
290    fn with_context<F>(self, loc: &'static str, f: F) -> Result<T>
291    where
292        F: FnOnce() -> Option<Box<[(&'static str, String)]>>;
293}
294
295impl<T, E: ErrKindExt> WithContext<T> for Result<T, E> {
296    fn context(self, loc: &'static str) -> Result<T> {
297        self.map_err(|e| Error::new(loc, e.to_error_kind(), None))
298    }
299
300    fn with_context<F>(self, loc: &'static str, f: F) -> Result<T>
301    where
302        F: FnOnce() -> Option<Box<[(&'static str, String)]>>,
303    {
304        self.map_err(|e| Error::new(loc, e.to_error_kind(), f()))
305    }
306}
307
308impl<T> WithContext<T> for Option<T> {
309    fn context(self, loc: &'static str) -> Result<T> {
310        self.ok_or_else(|| Error::new(loc, ErrKind::None, None))
311    }
312
313    fn with_context<F>(self, loc: &'static str, f: F) -> Result<T>
314    where
315        F: FnOnce() -> Option<Box<[(&'static str, String)]>>,
316    {
317        self.ok_or_else(|| Error::new(loc, ErrKind::None, f()))
318    }
319}
320
321/// A trait to add context to a result without a specific error type.
322pub trait WithContextUntyped<T>: Sized {
323    /// Add a context to the result.
324    fn context_ut(self, loc: &'static str) -> Result<T>;
325
326    /// Add a context to the result with additional arguments.
327    fn with_context_ut<F>(self, loc: &'static str, f: F) -> Result<T>
328    where
329        F: FnOnce() -> Option<Box<[(&'static str, String)]>>;
330}
331
332impl<T, E: std::fmt::Display> WithContextUntyped<T> for Result<T, E> {
333    fn context_ut(self, loc: &'static str) -> Result<T> {
334        self.map_err(|e| Error::new(loc, ErrKind::Msg(ecow::eco_format!("{e}")), None))
335    }
336
337    fn with_context_ut<F>(self, loc: &'static str, f: F) -> Result<T>
338    where
339        F: FnOnce() -> Option<Box<[(&'static str, String)]>>,
340    {
341        self.map_err(|e| Error::new(loc, ErrKind::Msg(ecow::eco_format!("{e}")), f()))
342    }
343}
344
345/// The error prelude.
346pub mod prelude {
347    #![allow(missing_docs)]
348
349    use super::ErrKindExt;
350    use crate::Error;
351
352    pub use super::{IgnoreLogging, WithContext, WithContextUntyped};
353    pub use crate::{bail, Result};
354
355    pub fn map_string_err<T: ToString>(loc: &'static str) -> impl Fn(T) -> Error {
356        move |e| Error::new(loc, e.to_string().to_error_kind(), None)
357    }
358
359    pub fn map_into_err<S: ErrKindExt, T: Into<S>>(loc: &'static str) -> impl Fn(T) -> Error {
360        move |e| Error::new(loc, e.into().to_error_kind(), None)
361    }
362
363    pub fn map_err<T: ErrKindExt>(loc: &'static str) -> impl Fn(T) -> Error {
364        move |e| Error::new(loc, e.to_error_kind(), None)
365    }
366
367    pub fn wrap_err(loc: &'static str) -> impl Fn(Error) -> Error {
368        move |e| Error::new(loc, crate::ErrKind::Inner(e), None)
369    }
370
371    pub fn map_string_err_with_args<
372        T: ToString,
373        Args: IntoIterator<Item = (&'static str, String)>,
374    >(
375        loc: &'static str,
376        args: Args,
377    ) -> impl FnOnce(T) -> Error {
378        move |e| {
379            Error::new(
380                loc,
381                e.to_string().to_error_kind(),
382                Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
383            )
384        }
385    }
386
387    pub fn map_into_err_with_args<
388        S: ErrKindExt,
389        T: Into<S>,
390        Args: IntoIterator<Item = (&'static str, String)>,
391    >(
392        loc: &'static str,
393        args: Args,
394    ) -> impl FnOnce(T) -> Error {
395        move |e| {
396            Error::new(
397                loc,
398                e.into().to_error_kind(),
399                Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
400            )
401        }
402    }
403
404    pub fn map_err_with_args<T: ErrKindExt, Args: IntoIterator<Item = (&'static str, String)>>(
405        loc: &'static str,
406        args: Args,
407    ) -> impl FnOnce(T) -> Error {
408        move |e| {
409            Error::new(
410                loc,
411                e.to_error_kind(),
412                Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
413            )
414        }
415    }
416
417    pub fn wrap_err_with_args<Args: IntoIterator<Item = (&'static str, String)>>(
418        loc: &'static str,
419        args: Args,
420    ) -> impl FnOnce(Error) -> Error {
421        move |e| {
422            Error::new(
423                loc,
424                crate::ErrKind::Inner(e),
425                Some(args.into_iter().collect::<Vec<_>>().into_boxed_slice()),
426            )
427        }
428    }
429
430    pub fn _error_once(loc: &'static str, args: Box<[(&'static str, String)]>) -> Error {
431        Error::new(loc, crate::ErrKind::None, Some(args))
432    }
433
434    pub fn _msg(loc: &'static str, msg: EcoString) -> Error {
435        Error::new(loc, crate::ErrKind::Msg(msg), None)
436    }
437
438    pub use ecow::eco_format as _eco_format;
439
440    #[macro_export]
441    macro_rules! bail {
442        ($($arg:tt)+) => {{
443            let args = $crate::error::prelude::_eco_format!($($arg)+);
444            return Err($crate::error::prelude::_msg(file!(), args))
445        }};
446    }
447
448    #[macro_export]
449    macro_rules! error_once {
450        ($loc:expr, $($arg_key:ident: $arg:expr),+ $(,)?) => {
451            $crate::error::prelude::_error_once($loc, Box::new([$((stringify!($arg_key), $arg.to_string())),+]))
452        };
453        ($loc:expr $(,)?) => {
454            $crate::error::prelude::_error_once($loc, Box::new([]))
455        };
456    }
457
458    #[macro_export]
459    macro_rules! error_once_map {
460        ($loc:expr, $($arg_key:ident: $arg:expr),+ $(,)?) => {
461            $crate::error::prelude::map_err_with_args($loc, [$((stringify!($arg_key), $arg.to_string())),+])
462        };
463        ($loc:expr $(,)?) => {
464            $crate::error::prelude::map_err($loc)
465        };
466    }
467
468    #[macro_export]
469    macro_rules! error_once_map_string {
470        ($loc:expr, $($arg_key:ident: $arg:expr),+ $(,)?) => {
471            $crate::error::prelude::map_string_err_with_args($loc, [$((stringify!($arg_key), $arg.to_string())),+])
472        };
473        ($loc:expr $(,)?) => {
474            $crate::error::prelude::map_string_err($loc)
475        };
476    }
477
478    use ecow::EcoString;
479    pub use error_once;
480    pub use error_once_map;
481    pub use error_once_map_string;
482}
483
484#[test]
485fn test_send() {
486    fn is_send<T: Send>() {}
487    is_send::<Error>();
488}