Skip to main content

duat_core/context/
log.rs

1//! Logging for Duat.
2//!
3//! This module defines types and functions for logging in Duat. It
4//! defines the [`debug!`], [`info!`], [`warn!`] and [`error!`] macros
5//! to directly log [`Text`] to Duat. They are essentially just
6//! wrappers around the [`txt!`] macro which log information. This
7//! module is also responsible for logging command results and panics.
8use std::{
9    panic::PanicHookInfo,
10    sync::{
11        Mutex,
12        atomic::{AtomicUsize, Ordering},
13    },
14};
15
16pub use log::{Level, Metadata};
17
18pub use self::macros::*;
19use crate::{hook, text::Text};
20
21mod macros {
22    #[doc(inline)]
23    pub use crate::{debug, error, info, warn};
24
25    /// Logs an error to Duat.
26    ///
27    /// Use this, as opposed to [`warn!`], [`info!`] or [`debug!`],
28    /// if you want to tell the user that something explicitely
29    /// failed, and they need to find a workaround, like failing
30    /// to write to/read from a buffer, for example.
31    ///
32    /// This error follows the same construction as the [`txt!`]
33    /// macro, and will create a [`Record`] inside of the [`Logs`],
34    /// which can be accessed by anyone, at any time.
35    ///
36    /// The [`Record`] added to the [`Logs`] is related to
37    /// [`log::Record`], from the [`log`] crate. But it differs in the
38    /// sense that it is always `'static`, and instead of having an
39    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
40    /// it a better fit for Duat.
41    ///
42    /// The connection to [`log::Record`] also means that external
43    /// libraries can log information using the [`log`] crate, and it
44    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
45    /// [`Text`] instead.
46    ///
47    /// # Custom location
48    ///
49    /// You can make use of a custom location by calling this macro
50    /// and surounding the arguments in a `()` pair.
51    ///
52    /// ```rust
53    /// # duat_core::doc_duat!(duat);
54    /// use duat::prelude::{context::Location, *};
55    ///
56    /// # fn test() {
57    /// context::error!(
58    ///     ("This is my {}", "error"),
59    ///     Location::new("some_file", 32, 10),
60    /// );
61    /// # }
62    /// ```
63    ///
64    /// [`txt!`]: crate::text::txt
65    /// [`Record`]: super::Record
66    /// [`Logs`]: super::Logs
67    /// [`Text`]: crate::text::Text
68    #[macro_export]
69    macro_rules! error {
70        (($($arg:tt)+), $location:expr $(,)?) => {
71            $crate::__log__!(
72                $crate::context::Level::Error,
73                $location,
74                $($arg)+
75            )
76        };
77        ($($arg:tt)+) => {
78            $crate::__log__!(
79                $crate::context::Level::Error,
80                $crate::context::Location::from_panic_location(::std::panic::Location::caller()),
81                $($arg)+
82            )
83        }
84    }
85
86    /// Logs an warning to Duat.
87    ///
88    /// Use this, as opposed to [`error!`], [`info!`] or [`debug!`],
89    /// if you want to tell the user that something was partially
90    /// successful, or that a failure happened, but
91    /// it's near inconsequential.
92    ///
93    /// This error follows the same construction as the [`txt!`]
94    /// macro, and will create a [`Record`] inside of the [`Logs`],
95    /// which can be accessed by anyone, at any time.
96    ///
97    /// The [`Record`] added to the [`Logs`] is related to
98    /// [`log::Record`], from the [`log`] crate. But it differs in the
99    /// sense that it is always `'static`, and instead of having an
100    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
101    /// it a better fit for Duat.
102    ///
103    /// The connection to [`log::Record`] also means that external
104    /// libraries can log information using the [`log`] crate, and it
105    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
106    /// [`Text`] instead.
107    ///
108    /// # Custom location
109    ///
110    /// You can make use of a custom location by calling this macro
111    /// and surounding the arguments in a `()` pair.
112    ///
113    /// ```rust
114    /// # duat_core::doc_duat!(duat);
115    /// use duat::prelude::{context::Location, *};
116    ///
117    /// # fn test() {
118    /// context::warn!(
119    ///     ("This is my {}", "warning"),
120    ///     Location::new("some_file", 32, 10),
121    /// );
122    /// # }
123    /// ```
124    ///
125    /// [`txt!`]: crate::text::txt
126    /// [`Record`]: super::Record
127    /// [`Logs`]: super::Logs
128    /// [`Text`]: crate::text::Text
129    #[macro_export]
130    macro_rules! warn {
131        (($($arg:tt)+), $location:expr $(,)?) => {
132            $crate::__log__!(
133                $crate::context::Level::Warn,
134                $location,
135                $($arg)+
136            )
137        };
138        ($($arg:tt)+) => {
139            $crate::__log__!(
140                $crate::context::Level::Warn,
141                $crate::context::Location::from_panic_location(::std::panic::Location::caller()),
142                $($arg)+
143            )
144        }
145    }
146
147    /// Logs an info to Duat.
148    ///
149    /// Use this, as opposed to [`error!`], [`warn!`] or [`debug!`],
150    /// when you want to tell the user that something was
151    /// successful, and it is important for them to know it was
152    /// successful.
153    ///
154    /// This error follows the same construction as the [`txt!`]
155    /// macro, and will create a [`Record`] inside of the [`Logs`],
156    /// which can be accessed by anyone, at any time.
157    ///
158    /// The [`Record`] added to the [`Logs`] is related to
159    /// [`log::Record`], from the [`log`] crate. But it differs in the
160    /// sense that it is always `'static`, and instead of having an
161    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
162    /// it a better fit for Duat.
163    ///
164    /// The connection to [`log::Record`] also means that external
165    /// libraries can log information using the [`log`] crate, and it
166    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
167    /// [`Text`] instead.
168    ///
169    /// # Custom location
170    ///
171    /// You can make use of a custom location by calling this macro
172    /// and surounding the arguments in a `()` pair.
173    ///
174    /// ```rust
175    /// # duat_core::doc_duat!(duat);
176    /// use duat::prelude::{context::Location, *};
177    ///
178    /// # fn test() {
179    /// context::info!(
180    ///     ("This is my {}", "info"),
181    ///     Location::new("some_file", 32, 10),
182    /// );
183    /// # }
184    /// ```
185    ///
186    /// [`txt!`]: crate::text::txt
187    /// [`Record`]: super::Record
188    /// [`Logs`]: super::Logs
189    /// [`Text`]: crate::text::Text
190    #[macro_export]
191    macro_rules! info {
192        (($($arg:tt)+), $location:expr $(,)?) => {
193            $crate::__log__!(
194                $crate::context::Level::Info,
195                $location,
196                $($arg)+
197            )
198        };
199        ($($arg:tt)+) => {
200            $crate::__log__!(
201                $crate::context::Level::Info,
202                $crate::context::Location::from_panic_location(::std::panic::Location::caller()),
203                $($arg)+
204            )
205        }
206    }
207
208    /// Logs an debug information to Duat.
209    ///
210    /// Use this, as opposed to [`error!`], [`warn!`] or [`info!`],
211    /// when you want to tell the user that something was
212    /// successful, but it is not that important, or the success is
213    /// only a smaller part of some bigger operation, or the success
214    /// is part of something that was done "silently".
215    ///
216    /// This error follows the same construction as the [`txt!`]
217    /// macro, and will create a [`Record`] inside of the [`Logs`],
218    /// which can be accessed by anyone, at any time.
219    ///
220    /// The [`Record`] added to the [`Logs`] is related to
221    /// [`log::Record`], from the [`log`] crate. But it differs in the
222    /// sense that it is always `'static`, and instead of having an
223    /// [`std::fmt::Arguments`] inside, it contains a [`Text`], making
224    /// it a better fit for Duat.
225    ///
226    /// The connection to [`log::Record`] also means that external
227    /// libraries can log information using the [`log`] crate, and it
228    /// will also show up in Duat's [`Logs`]s, but reformatted to be a
229    /// [`Text`] instead.
230    ///
231    /// # Custom location
232    ///
233    /// You can make use of a custom location by calling this macro
234    /// and surounding the arguments in a `()` pair.
235    ///
236    /// ```rust
237    /// # duat_core::doc_duat!(duat);
238    /// use duat::prelude::{context::Location, *};
239    ///
240    /// # fn test() {
241    /// let text = txt!("Some custom text I want to debug");
242    /// context::debug!(("text is {text:#?}"), Location::new("some_file", 32, 10));
243    /// # }
244    /// ```
245    ///
246    /// [`txt!`]: crate::text::txt
247    /// [`Record`]: super::Record
248    /// [`Logs`]: super::Logs
249    /// [`Text`]: crate::text::Text
250    #[macro_export]
251    macro_rules! debug {
252        (($($arg:tt)+), $location:expr $(,)?) => {
253            $crate::__log__!(
254                $crate::context::Level::Debug,
255                $location,
256                $($arg)+
257            )
258        };
259        ($($arg:tt)+) => {
260            $crate::__log__!(
261                $crate::context::Level::Debug,
262                $crate::context::Location::from_panic_location(::std::panic::Location::caller()),
263                $($arg)+
264            )
265        }
266    }
267}
268
269static LOGS: Logs = {
270    static LIST: Mutex<Vec<Record>> = Mutex::new(Vec::new());
271    static CUR_STATE: AtomicUsize = AtomicUsize::new(1);
272    Logs {
273        list: &LIST,
274        cur_state: &CUR_STATE,
275        read_state: AtomicUsize::new(0),
276    }
277};
278
279/// Notifications for duat.
280///
281/// This is a mutable, shareable, [`Send`]/[`Sync`] list of
282/// notifications in the form of [`Text`]s, you can read this,
283/// send new notifications, and check for updates, just like with
284/// [`RwData`]s and [`Handle`]s.
285///
286/// [`RwData`]: crate::data::RwData
287/// [`Handle`]: super::Handle
288pub fn logs() -> Logs {
289    LOGS.clone()
290}
291
292/// The notifications sent to Duat.
293///
294/// This can include command results, failed mappings,
295/// recompilation messages, and any other thing that you want
296/// to notify about. In order to set the level of severity for these
297/// messages, use the [`error!`], [`warn!`] and [`info!`] macros.
298#[derive(Debug)]
299pub struct Logs {
300    list: &'static Mutex<Vec<Record>>,
301    cur_state: &'static AtomicUsize,
302    read_state: AtomicUsize,
303}
304
305impl Clone for Logs {
306    fn clone(&self) -> Self {
307        Self {
308            list: self.list,
309            cur_state: self.cur_state,
310            read_state: AtomicUsize::new(self.cur_state.load(Ordering::Relaxed) - 1),
311        }
312    }
313}
314
315impl Logs {
316    /// Returns an owned valued of a [`SliceIndex`].
317    ///
318    /// - `&'static Log` for `usize`;
319    /// - [`Vec<&'static Log>`] for `impl RangeBounds<usize>`;
320    ///
321    /// [`SliceIndex`]: std::slice::SliceIndex
322    pub fn get<I>(&self, i: I) -> Option<<I::Output as ToOwned>::Owned>
323    where
324        I: std::slice::SliceIndex<[Record]>,
325        I::Output: ToOwned,
326    {
327        self.read_state
328            .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
329        self.list.lock().unwrap().get(i).map(ToOwned::to_owned)
330    }
331
332    /// Returns the last [`Record`], if there was one
333    pub fn last(&self) -> Option<(usize, Record)> {
334        self.read_state
335            .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
336        let list = self.list.lock().unwrap();
337        list.last().cloned().map(|last| (list.len() - 1, last))
338    }
339
340    /// Gets the last [`Record`] with a level from a list.
341    pub fn last_with_levels(&self, levels: &[Level]) -> Option<(usize, Record)> {
342        self.read_state
343            .store(self.cur_state.load(Ordering::Relaxed), Ordering::Relaxed);
344        self.list
345            .lock()
346            .unwrap()
347            .iter()
348            .enumerate()
349            .rev()
350            .find_map(|(i, rec)| levels.contains(&rec.level()).then(|| (i, rec.clone())))
351    }
352
353    /// Wether there are new notifications or not.
354    pub fn has_changed(&self) -> bool {
355        self.cur_state.load(Ordering::Relaxed) > self.read_state.load(Ordering::Relaxed)
356    }
357
358    /// Pushes a [`CmdResult`].
359    ///
360    /// [`CmdResult`]: crate::cmd::CmdResult
361    #[track_caller]
362    pub(crate) fn push_cmd_result(&self, result: Result<Option<Text>, Text>) {
363        let is_ok = result.is_ok();
364        let (Ok(Some(res)) | Err(res)) = result else {
365            return;
366        };
367
368        self.cur_state.fetch_add(1, Ordering::Relaxed);
369
370        let rec = Record {
371            metadata: log::MetadataBuilder::new()
372                .level(if is_ok { Level::Info } else { Level::Error })
373                .build(),
374            text: Box::leak(Box::new(res)),
375            location: Location::from_panic_location(std::panic::Location::caller()),
376        };
377
378        crate::context::queue(|pa| {
379            hook::trigger(pa, hook::MsgLogged(rec.clone()));
380            self.list.lock().unwrap().push(rec);
381        });
382    }
383
384    /// Pushes a new [`Record`] to Duat.
385    #[doc(hidden)]
386    pub fn push_record(&self, rec: Record) {
387        crate::context::queue(|pa| {
388            hook::trigger(pa, hook::MsgLogged(rec.clone()));
389            self.list.lock().unwrap().push(rec);
390        });
391
392        self.cur_state.fetch_add(1, Ordering::Relaxed);
393    }
394
395    /// Returns the number of [`Record`]s in the `Logs`.
396    pub fn len(&self) -> usize {
397        self.list.lock().unwrap().len()
398    }
399
400    /// Wether there are any [`Record`]s in the `Logs`.
401    ///
402    /// It's pretty much never `true`.
403    #[must_use]
404    pub fn is_empty(&self) -> bool {
405        self.len() == 0
406    }
407}
408
409impl log::Log for Logs {
410    fn enabled(&self, metadata: &log::Metadata) -> bool {
411        metadata.level() > log::Level::Debug
412    }
413
414    #[track_caller]
415    fn log(&self, rec: &log::Record) {
416        let rec = Record {
417            text: Box::leak(Box::new(Text::from(std::fmt::format(*rec.args())))),
418            metadata: log::MetadataBuilder::new()
419                .level(rec.level())
420                .target(rec.target().to_string().leak())
421                .build(),
422            location: Location::from_panic_location(std::panic::Location::caller()),
423        };
424
425        self.cur_state.fetch_add(1, Ordering::Relaxed);
426        self.list.lock().unwrap().push(rec)
427    }
428
429    fn flush(&self) {}
430}
431
432/// A record of something that happened in Duat.
433///
434/// Differs from [`log::Record`] in that its argument isn't an
435/// [`std::fmt::Arguments`], but a [`Text`] instead.
436#[derive(Clone, Debug)]
437pub struct Record {
438    text: &'static Text,
439    metadata: log::Metadata<'static>,
440    location: Location,
441}
442
443impl Record {
444    /// Creates a new [`Record`].
445    #[doc(hidden)]
446    pub fn new(text: Text, level: Level, location: Location) -> Self {
447        Self {
448            text: Box::leak(Box::new(text)),
449            metadata: log::MetadataBuilder::new().level(level).build(),
450            location,
451        }
452    }
453
454    /// The [`Text`] of this [`Record`].
455    #[inline]
456    pub fn text(&self) -> &Text {
457        self.text
458    }
459
460    /// Metadata about the log directive.
461    #[inline]
462    pub fn metadata(&self) -> log::Metadata<'static> {
463        self.metadata.clone()
464    }
465
466    /// The verbosity level of the message.
467    #[inline]
468    pub fn level(&self) -> Level {
469        self.metadata.level()
470    }
471
472    /// The name of the target of the directive.
473    #[inline]
474    pub fn target(&self) -> &'static str {
475        self.metadata.target()
476    }
477
478    /// The [`Location`] where the message was sent from.
479    #[inline]
480    pub fn location(&self) -> Location {
481        self.location
482    }
483}
484
485/// The location where a log came from
486#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
487pub struct Location {
488    filename: &'static str,
489    line: u32,
490    col: u32,
491}
492
493impl Location {
494    /// Returns a new custom `Location`.
495    ///
496    /// You can use this to add a more nuanced location for an error,
497    /// instead of just letting duat pick one automatically.
498    ///
499    /// An example of this is with treesitter query errors, where
500    /// [`std::panic::Location::caller`] would return an error within
501    /// a rust file, but a more appropriate position would be on the
502    /// actual query file.
503    pub fn new(filename: impl ToString, line: u32, col: u32) -> Self {
504        Self {
505            filename: filename.to_string().leak(),
506            line,
507            col,
508        }
509    }
510
511    /// Returns a new [`Location`] from a regular panic `Location`
512    pub fn from_panic_location(loc: &std::panic::Location) -> Self {
513        Self {
514            filename: loc.file().to_string().leak(),
515            line: loc.line(),
516            col: loc.column(),
517        }
518    }
519
520    /// Returns the name of the source file.
521    #[must_use]
522    pub const fn file(&self) -> &'static str {
523        self.filename
524    }
525
526    /// The line where the message originated from. 1 indexed.
527    #[must_use]
528    pub const fn line(&self) -> usize {
529        self.line as usize
530    }
531
532    /// The column where the message originated from. 1 indexed.
533    #[must_use]
534    pub const fn column(&self) -> usize {
535        self.col as usize
536    }
537}
538
539impl std::fmt::Display for Location {
540    #[inline]
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        write!(f, "{}:{}:{}", self.file(), self.line, self.col)
543    }
544}
545
546/// Log information about a panic that took place.
547#[doc(hidden)]
548pub fn log_panic(panic_info: &PanicHookInfo) {
549    let (Some(msg), Some(location)) = (panic_info.payload_as_str(), panic_info.location()) else {
550        return;
551    };
552
553    let rec = Record {
554        text: Box::leak(Box::new(Text::from(msg))),
555        metadata: Metadata::builder().level(Level::Error).build(),
556        location: Location::from_panic_location(location),
557    };
558
559    crate::context::queue(|pa| {
560        hook::trigger(pa, hook::MsgLogged(rec.clone()));
561        LOGS.list.lock().unwrap().push(rec);
562    });
563}