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}