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