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}