musubi/lib.rs
1//! Safe Rust wrapper for musubi diagnostic renderer
2//!
3//! This library provides a safe, ergonomic Rust API for the musubi C library,
4//! which renders beautiful diagnostic messages similar to rustc and other modern compilers.
5//!
6//! # Quick Start
7//!
8//! ```rust
9//! use musubi::{Report, Level};
10//!
11//! let report = Report::new()
12//! .with_title(Level::Error, "Invalid syntax")
13//! .with_code("E001")
14//! .with_label(8..10)
15//! .with_message("Answer to the Ultimate Question here")
16//! .render_to_string(("let x = 42;", "example.rs"))?;
17//!
18//! println!("{}", report);
19//! # Ok::<(), std::io::Error>(())
20//! ```
21//!
22//! # Core Concepts
23//!
24//! ## Sources and Cache
25//!
26//! A [`Source`] provides the text content for diagnostics. Sources are managed through
27//! a [`Cache`], which can store multiple sources and be reused across multiple reports:
28//!
29//! ```rust
30//! # use musubi::{Cache, Report, Level};
31//! let cache = Cache::new()
32//! .with_source(("let x = 42;", "main.rs"));
33//!
34//! let mut report = Report::new()
35//! .with_title(Level::Error, "Syntax error")
36//! .with_label(0..3);
37//! report.render_to_stdout(&cache)?;
38//! # Ok::<(), std::io::Error>(())
39//! ```
40//!
41//! Sources are registered in order and assigned IDs: first source is ID 0, second is ID 1, etc.
42//!
43//! For simple single-source diagnostics, you can pass content directly to rendering methods
44//! without creating an explicit [`Cache`]:
45//!
46//! ```rust
47//! # use musubi::{Report, Level};
48//! Report::new()
49//! .with_title(Level::Error, "Simple error")
50//! .with_label(0..3)
51//! .render_to_string(("let x", "main.rs"))?;
52//! # Ok::<(), std::io::Error>(())
53//! ```
54//!
55//! ### Lifetime Management
56//!
57//! By default, source content must outlive the [`Report`] (borrowed sources like `&str`).
58//! The [`Cache`] can also take ownership and manage the lifetime:
59//!
60//! - **Borrowed**: `cache.with_source("code")` - content must remain valid until rendering
61//! - **Owned**: `cache.with_source("code".to_string())` - `String` has built-in ownership
62//! - **Custom buffers**: Use [`OwnedSource`] for `Vec<u8>`, `Box<[u8]>`, etc.
63//!
64//! ```rust
65//! # use musubi::{Cache, OwnedSource};
66//! let cache = Cache::new()
67//! .with_source("static str") // Borrowed
68//! .with_source(("owned".to_string(), "file.rs")) // Owned by cache
69//! .with_source((OwnedSource::new(vec![b'x']), "buf")); // Custom buffer
70//! // Cache manages owned content lifetime until dropped
71//! ```
72//!
73//! ### Multiple Sources
74//!
75//! Display diagnostics that span multiple files:
76//!
77//! ```rust
78//! # use musubi::{Report, Level, Cache};
79//! let cache = Cache::new()
80//! .with_source(("import foo", "main.rs")) // Source ID 0
81//! .with_source(("pub fn foo() {}", "lib.rs")); // Source ID 1
82//!
83//! let report = Report::new()
84//! .with_title(Level::Error, "Import error")
85//! .with_label((7..10, 0)) // Label in main.rs
86//! .with_message("imported here")
87//! .with_label((7..10, 1)) // Label in lib.rs
88//! .with_message("defined here")
89//! .render_to_string(&cache)?;
90//! println!("{}", report);
91//! # Ok::<(), std::io::Error>(())
92//! ```
93//!
94//! ### Rendering Methods
95//!
96//! Three rendering methods are available:
97//! - [`Report::render_to_string()`] - Capture output as a String
98//! - [`Report::render_to_stdout()`] - Write directly to stdout (most efficient)
99//! - [`Report::render_to_writer()`] - Write to any `std::io::Write` implementation
100//!
101//! ## Labels
102//!
103//! Labels highlight specific spans in your source code. Each label can have:
104//! - A span (byte or character range)
105//! - A message explaining the issue
106//! - Custom colors
107//! - Display order and priority
108//!
109//! ```rust
110//! # use musubi::Report;
111//! let report = Report::new()
112//! // ...
113//! .with_label(0..3) // First label
114//! .with_message("expected type here")
115//! .with_label(4..5) // Second label
116//! .with_message("found here")
117//! // ...
118//! # ;
119//! ```
120//!
121//! ## Configuration
122//!
123//! Customize rendering with [`Config`]:
124//! - Character sets (ASCII vs Unicode)
125//! - Color schemes
126//! - Layout options (compact mode, tab width, line wrapping)
127//! - Label attachment (start/middle/end of spans)
128//!
129//! ```rust
130//! # use musubi::{Report, Config, CharSet};
131//! let config = Config::new()
132//! .with_char_set_unicode() // Use box-drawing characters
133//! .with_color_default() // Enable ANSI colors
134//! .with_compact(true) // Compact output
135//! .with_tab_width(4) // 4-space tabs
136//! // ...
137//! ;
138//!
139//! Report::new()
140//! .with_config(config)
141//! // ...
142//! # ;
143//! ```
144//!
145//! ## Custom Colors
146//!
147//! Implement the [`Color`] trait to provide custom color schemes:
148//!
149//! ```rust
150//! # use musubi::{Config, Color, ColorKind};
151//! # use std::io::Write;
152//! struct MyColors;
153//!
154//! impl Color for MyColors {
155//! fn color(&self, w: &mut dyn Write, kind: ColorKind) -> std::io::Result<()> {
156//! match kind {
157//! ColorKind::Error => write!(w, "\x1b[31m"), // Red
158//! ColorKind::Warning => write!(w, "\x1b[33m"), // Yellow
159//! ColorKind::Reset => write!(w, "\x1b[0m"), // Reset
160//! _ => Ok(()),
161//! }
162//! }
163//! }
164//!
165//! let config = Config::new().with_color(&MyColors);
166//! ```
167//!
168//! ## Custom Sources
169//!
170//! Implement the [`Source`] trait for lazy file loading or special formatting:
171//!
172//! ```rust
173//! # use musubi::{Source, Line};
174//! # use std::io;
175//! struct LazyFileSource {
176//! // ... your fields
177//! }
178//!
179//! impl Source for LazyFileSource {
180//! fn init(&mut self) -> io::Result<()> {
181//! // Initialize (e.g., open file, read metadata)
182//! Ok(())
183//! }
184//!
185//! fn get_line(&self, line_no: usize) -> &[u8] {
186//! // Return the requested line
187//! # b""
188//! }
189//!
190//! fn get_line_info(&self, line_no: usize) -> Line {
191//! // Return line metadata (offsets, lengths)
192//! # Line::default()
193//! }
194//!
195//! fn line_for_chars(&self, char_pos: usize) -> (usize, Line) {
196//! // Map character position to line
197//! # (0, Line::default())
198//! }
199//!
200//! fn line_for_bytes(&self, byte_pos: usize) -> (usize, Line) {
201//! // Map byte position to line
202//! # (0, Line::default())
203//! }
204//! }
205//! ```
206//!
207
208mod ffi;
209
210use std::ffi::{c_char, c_int, c_uint, c_void};
211use std::fmt::Debug;
212use std::io::{self, Write};
213use std::marker::PhantomData;
214use std::mem::MaybeUninit;
215use std::ptr;
216
217use crate::ffi::mu_Id;
218
219/// Diagnostic severity level
220///
221/// Represents the severity of a diagnostic message.
222/// These levels affect both the visual styling (colors, icons)
223/// and semantic meaning of the diagnostic.
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum Level {
226 /// Error level - indicates a compilation/execution failure
227 Error,
228 /// Warning level - indicates a potential problem
229 Warning,
230}
231
232impl From<Level> for ffi::mu_Level {
233 #[inline]
234 fn from(level: Level) -> Self {
235 match level {
236 Level::Error => ffi::mu_Level::MU_ERROR,
237 Level::Warning => ffi::mu_Level::MU_WARNING,
238 }
239 }
240}
241
242/// Where labels attach to their spans
243///
244/// Controls where the label's arrow/message attaches to the highlighted span.
245/// This affects the visual positioning of the label annotation.
246///
247/// # Example
248/// ```text
249/// Middle (default):
250/// foo(bar, baz)
251/// ---^---
252/// |
253/// label here
254///
255/// Start:
256/// foo(bar, baz)
257/// ^-------
258/// |
259/// label here
260///
261/// End:
262/// foo(bar, baz)
263/// -------^
264/// |
265/// label here
266/// ```
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
268pub enum LabelAttach {
269 /// Attach in the middle of the span (default)
270 #[default]
271 Middle,
272 /// Attach at the start of the span
273 Start,
274 /// Attach at the end of the span
275 End,
276}
277
278impl From<LabelAttach> for ffi::mu_LabelAttach {
279 #[inline]
280 fn from(attach: LabelAttach) -> Self {
281 match attach {
282 LabelAttach::Middle => ffi::mu_LabelAttach::MU_ATTACH_MIDDLE,
283 LabelAttach::Start => ffi::mu_LabelAttach::MU_ATTACH_START,
284 LabelAttach::End => ffi::mu_LabelAttach::MU_ATTACH_END,
285 }
286 }
287}
288
289/// Index type for span positions
290///
291/// Determines how span ranges are interpreted:
292/// - [`Byte`](IndexType::Byte) - Positions are byte offsets (faster, ASCII-friendly)
293/// - [`Char`](IndexType::Char) - Positions are character offsets (UTF-8 aware, default)
294///
295/// # Example
296/// ```text
297/// Source: "你好" (2 characters, 6 bytes in UTF-8)
298///
299/// With IndexType::Char:
300/// span 0..1 selects "你"
301/// span 1..2 selects "好"
302///
303/// With IndexType::Byte:
304/// span 0..3 selects "你" (3 bytes)
305/// span 3..6 selects "好" (3 bytes)
306/// ```
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
308pub enum IndexType {
309 /// Index by byte offset (0-indexed)
310 Byte,
311 /// Index by character offset (0-indexed, UTF-8 aware, default)
312 #[default]
313 Char,
314}
315
316impl From<IndexType> for ffi::mu_IndexType {
317 #[inline]
318 fn from(index_type: IndexType) -> Self {
319 match index_type {
320 IndexType::Byte => ffi::mu_IndexType::MU_INDEX_BYTE,
321 IndexType::Char => ffi::mu_IndexType::MU_INDEX_CHAR,
322 }
323 }
324}
325
326/// Color categories for diagnostic output
327///
328/// Each category represents a different part of the diagnostic rendering
329/// that can be styled independently.
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum ColorKind {
332 /// Reset all colors/styles to default
333 Reset,
334 /// Error severity level and error-related elements
335 Error,
336 /// Warning severity level and warning-related elements
337 Warning,
338 /// Custom severity level names (e.g., "Hint", "Note")
339 Kind,
340 /// Line number margin (gutter)
341 Margin,
342 /// Margin for skipped lines ("...")
343 SkippedMargin,
344 /// Less important text (e.g., source file paths)
345 Unimportant,
346 /// Note and help messages
347 Note,
348 /// Label highlights and arrows
349 Label,
350}
351
352impl From<ColorKind> for ffi::mu_ColorKind {
353 #[inline]
354 fn from(kind: ColorKind) -> Self {
355 match kind {
356 ColorKind::Reset => ffi::mu_ColorKind::MU_COLOR_RESET,
357 ColorKind::Error => ffi::mu_ColorKind::MU_COLOR_ERROR,
358 ColorKind::Warning => ffi::mu_ColorKind::MU_COLOR_WARNING,
359 ColorKind::Kind => ffi::mu_ColorKind::MU_COLOR_KIND,
360 ColorKind::Margin => ffi::mu_ColorKind::MU_COLOR_MARGIN,
361 ColorKind::SkippedMargin => ffi::mu_ColorKind::MU_COLOR_SKIPPED_MARGIN,
362 ColorKind::Unimportant => ffi::mu_ColorKind::MU_COLOR_UNIMPORTANT,
363 ColorKind::Note => ffi::mu_ColorKind::MU_COLOR_NOTE,
364 ColorKind::Label => ffi::mu_ColorKind::MU_COLOR_LABEL,
365 }
366 }
367}
368
369impl ColorKind {
370 #[inline]
371 fn from_ffi(kind: ffi::mu_ColorKind) -> Self {
372 match kind {
373 ffi::mu_ColorKind::MU_COLOR_RESET => ColorKind::Reset,
374 ffi::mu_ColorKind::MU_COLOR_ERROR => ColorKind::Error,
375 ffi::mu_ColorKind::MU_COLOR_WARNING => ColorKind::Warning,
376 ffi::mu_ColorKind::MU_COLOR_KIND => ColorKind::Kind,
377 ffi::mu_ColorKind::MU_COLOR_MARGIN => ColorKind::Margin,
378 ffi::mu_ColorKind::MU_COLOR_SKIPPED_MARGIN => ColorKind::SkippedMargin,
379 ffi::mu_ColorKind::MU_COLOR_UNIMPORTANT => ColorKind::Unimportant,
380 ffi::mu_ColorKind::MU_COLOR_NOTE => ColorKind::Note,
381 ffi::mu_ColorKind::MU_COLOR_LABEL => ColorKind::Label,
382 }
383 }
384}
385
386/// Internal representation of a title level for FFI.
387///
388/// This enables flexible title creation:
389/// - `.with_title(Level::Error, "message")` - standard level
390/// - `.with_title("Note", "message")` - custom level name
391pub struct TitleLevel<'a> {
392 level: ffi::mu_Level,
393 custom_name: ffi::mu_Slice,
394 _marker: PhantomData<&'a ()>,
395}
396
397/// Standard level
398impl From<Level> for TitleLevel<'_> {
399 #[inline]
400 fn from(level: Level) -> Self {
401 TitleLevel {
402 level: level.into(),
403 custom_name: Default::default(),
404 _marker: PhantomData,
405 }
406 }
407}
408
409/// Custom level: string name
410impl<'a> From<&'a str> for TitleLevel<'a> {
411 #[inline]
412 fn from(name: &'a str) -> Self {
413 TitleLevel {
414 level: ffi::mu_Level::MU_CUSTOM_LEVEL,
415 custom_name: name.into(),
416 _marker: PhantomData,
417 }
418 }
419}
420
421/// A label span with optional source ID.
422///
423/// The `src_id` is the registration order of sources (0 for first, 1 for second, etc.).
424///
425/// This enables flexible label creation:
426/// - `.with_label_at((0..10, 0))` - tuple of (range, src_id)
427#[derive(Debug, Clone, Copy)]
428pub struct LabelSpan {
429 start: usize,
430 end: usize,
431 src_id: ffi::mu_Id,
432}
433
434// Range<usize>
435impl From<std::ops::Range<usize>> for LabelSpan {
436 #[inline]
437 fn from(value: std::ops::Range<usize>) -> Self {
438 LabelSpan {
439 start: value.start,
440 end: value.end,
441 src_id: 0.into(),
442 }
443 }
444}
445
446// Range<i32>
447impl From<std::ops::Range<i32>> for LabelSpan {
448 #[inline]
449 fn from(value: std::ops::Range<i32>) -> Self {
450 LabelSpan {
451 start: value.start.max(0) as usize,
452 end: value.end.max(0) as usize,
453 src_id: 0.into(),
454 }
455 }
456}
457
458// (Range<usize>, usize) tuple
459impl<SrcId: Into<ffi::mu_Id>> From<(std::ops::Range<usize>, SrcId)> for LabelSpan {
460 #[inline]
461 fn from(value: (std::ops::Range<usize>, SrcId)) -> Self {
462 LabelSpan {
463 start: value.0.start,
464 end: value.0.end,
465 src_id: value.1.into(),
466 }
467 }
468}
469
470// (Range<i32>, usize) tuple
471impl<SrcId: Into<ffi::mu_Id>> From<(std::ops::Range<i32>, SrcId)> for LabelSpan {
472 #[inline]
473 fn from(value: (std::ops::Range<i32>, SrcId)) -> Self {
474 LabelSpan {
475 start: value.0.start.max(0) as usize,
476 end: value.0.end.max(0) as usize,
477 src_id: value.1.into(),
478 }
479 }
480}
481
482/// Character set for rendering diagnostic output
483///
484/// Defines all the box-drawing and decorative characters used in rendering.
485/// Two predefined sets are available:
486/// - [`CharSet::ascii()`] - Uses ASCII characters (`-`, `|`, `+`, etc.)
487/// - [`CharSet::unicode()`] - Uses Unicode box-drawing characters (`─`, `│`, `┬`, etc.)
488///
489/// You can also create custom character sets by modifying individual fields.
490///
491/// # Example
492/// ```rust
493/// # use musubi::CharSet;
494/// let custom = CharSet {
495/// hbar: '=',
496/// vbar: '!',
497/// ..CharSet::ascii()
498/// };
499/// ```
500#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
501pub struct CharSet {
502 /// Space character (usually ' ')
503 pub space: char,
504 /// Newline representation (usually visible as box character)
505 pub newline: char,
506 /// Left box bracket (e.g., '[')
507 pub lbox: char,
508 /// Right box bracket (e.g., ']')
509 pub rbox: char,
510 /// Colon separator (e.g., ':')
511 pub colon: char,
512 /// Horizontal bar (e.g., '-' or '─')
513 pub hbar: char,
514 /// Vertical bar (e.g., '|' or '│')
515 pub vbar: char,
516 /// Cross bar (both horizontal and vertical)
517 pub xbar: char,
518 /// Vertical bar with break
519 pub vbar_break: char,
520 /// Vertical bar with gap
521 pub vbar_gap: char,
522 /// Upward arrow (e.g., '^' or '↑')
523 pub uarrow: char,
524 /// Rightward arrow (e.g., '>' or '→')
525 pub rarrow: char,
526 /// Left top corner (e.g., ',' or '╭')
527 pub ltop: char,
528 /// Middle top connector (e.g., '^' or '┬')
529 pub mtop: char,
530 /// Right top corner (e.g., '.' or '╮')
531 pub rtop: char,
532 /// Left bottom corner (e.g., '`' or '╰')
533 pub lbot: char,
534 /// Middle bottom connector (e.g., 'v' or '┴')
535 pub mbot: char,
536 /// Right bottom corner (e.g., '\'' or '╯')
537 pub rbot: char,
538 /// Left cross connector (e.g., '+' or '├')
539 pub lcross: char,
540 /// Right cross connector (e.g., '+' or '┤')
541 pub rcross: char,
542 /// Underbar character (e.g., '_' or '─')
543 pub underbar: char,
544 /// Underline character for emphasis
545 pub underline: char,
546 /// Ellipsis for truncated text (e.g., '...' or '…')
547 pub ellipsis: char,
548}
549
550impl From<*const ffi::mu_Charset> for CharSet {
551 #[allow(clippy::not_unsafe_ptr_arg_deref)]
552 fn from(ptr: *const ffi::mu_Charset) -> Self {
553 fn slice_to_char(s: *const c_char) -> char {
554 if s.is_null() {
555 return ' ';
556 }
557 // SAFETY: Pointer is from C library, null-checked above.
558 // Length is stored in first byte, followed by valid UTF-8 data.
559 unsafe {
560 let len = *s as usize;
561 let bytes = std::slice::from_raw_parts(s.add(1) as *const u8, len);
562 std::str::from_utf8(bytes)
563 .unwrap_or(" ")
564 .chars()
565 .next()
566 .unwrap_or(' ')
567 }
568 }
569 // SAFETY: ptr is passed by calleree and assumed to be valid
570 let chars = unsafe { &*ptr };
571 CharSet {
572 space: slice_to_char(chars[0]),
573 newline: slice_to_char(chars[1]),
574 lbox: slice_to_char(chars[2]),
575 rbox: slice_to_char(chars[3]),
576 colon: slice_to_char(chars[4]),
577 hbar: slice_to_char(chars[5]),
578 vbar: slice_to_char(chars[6]),
579 xbar: slice_to_char(chars[7]),
580 vbar_break: slice_to_char(chars[8]),
581 vbar_gap: slice_to_char(chars[9]),
582 uarrow: slice_to_char(chars[10]),
583 rarrow: slice_to_char(chars[11]),
584 ltop: slice_to_char(chars[12]),
585 mtop: slice_to_char(chars[13]),
586 rtop: slice_to_char(chars[14]),
587 lbot: slice_to_char(chars[15]),
588 mbot: slice_to_char(chars[16]),
589 rbot: slice_to_char(chars[17]),
590 lcross: slice_to_char(chars[18]),
591 rcross: slice_to_char(chars[19]),
592 underbar: slice_to_char(chars[20]),
593 underline: slice_to_char(chars[21]),
594 ellipsis: slice_to_char(chars[22]),
595 }
596 }
597}
598
599impl CharSet {
600 /// Predefined ASCII character set
601 #[inline]
602 pub fn ascii() -> CharSet {
603 // SAFETY: mu_ascii() returns a valid static charset pointer
604 unsafe { ffi::mu_ascii() }.into()
605 }
606
607 /// Predefined Unicode character set
608 #[inline]
609 pub fn unicode() -> CharSet {
610 // SAFETY: mu_unicode() returns a valid static charset pointer
611 unsafe { ffi::mu_unicode() }.into()
612 }
613}
614
615/// Automatic color generator for creating visually distinct label colors.
616///
617/// ColorGenerator produces a sequence of pseudo-random colors that are
618/// perceptually distinct and readable. It's useful for assigning colors to
619/// multiple labels automatically.
620///
621/// # Examples
622///
623/// ```rust
624/// use musubi::{Report, ColorGenerator, Level};
625///
626/// let mut cg = ColorGenerator::new();
627///
628/// Report::new()
629/// // ...
630/// .with_label(0..3)
631/// .with_color(&cg.next_color()) // First color
632/// .with_label(4..5)
633/// .with_color(&cg.next_color()) // Second color (different)
634/// // ...
635/// # ;
636/// ```
637pub struct ColorGenerator {
638 base: ffi::mu_ColorGen,
639}
640
641/// Trait for types that can be used as raw color codes.
642///
643/// This trait is implemented for [`GenColor`] returned by [`ColorGenerator::next_color`].
644/// It allows efficiently passing pre-generated color codes to labels without
645/// the overhead of trait objects.
646pub trait IntoColor {
647 /// Apply this color to the most recently added label in the report.
648 ///
649 /// This method is called internally by [`Report::with_color`].
650 fn into_color(self, report: &mut Report);
651}
652
653/// A pre-generated ANSI color code.
654///
655/// This type wraps a raw color code buffer generated by [`ColorGenerator`].
656/// It can be applied to labels using [`Report::with_color`].
657///
658/// # Note
659///
660/// GenColor is more efficient than trait-object based colors because it
661/// avoids dynamic dispatch and stores the color code directly.
662pub struct GenColor(ffi::mu_ColorCode);
663
664impl IntoColor for &GenColor {
665 #[inline]
666 fn into_color(self, report: &mut Report) {
667 // SAFETY: mu_fromcolorcode is a valid C callback that reads from the color code array.
668 // The pointer to self.0 is valid for the duration of the mu_color call.
669 unsafe {
670 ffi::mu_color(
671 report.ptr,
672 Some(ffi::mu_fromcolorcode),
673 self.0.as_ptr() as *mut c_void,
674 );
675 }
676 }
677}
678
679impl Default for ColorGenerator {
680 #[inline]
681 fn default() -> Self {
682 Self::new()
683 }
684}
685
686impl ColorGenerator {
687 /// Create a new color generator with default brightness.
688 #[inline]
689 pub fn new() -> Self {
690 Self::new_with_brightness(0.5)
691 }
692
693 /// Create a new color generator with the specified brightness.
694 #[inline]
695 pub fn new_with_brightness(brightness: f32) -> Self {
696 let mut obj = MaybeUninit::uninit();
697 // SAFETY: mu_initcolorgen initializes all fields of the color generator
698 unsafe { ffi::mu_initcolorgen(obj.as_mut_ptr(), brightness) };
699 Self {
700 // SAFETY: obj has been fully initialized by mu_initcolorgen above
701 base: unsafe { obj.assume_init() },
702 }
703 }
704
705 /// Generate the next color in the sequence.
706 ///
707 /// Each call returns a different color code that is visually distinct from
708 /// previous colors. The sequence is deterministic based on the initial state.
709 ///
710 /// # Examples
711 ///
712 /// ```rust
713 /// use musubi::ColorGenerator;
714 ///
715 /// let mut cg = ColorGenerator::new();
716 /// let color1 = cg.next_color();
717 /// let color2 = cg.next_color(); // Different from color1
718 /// let color3 = cg.next_color(); // Different from color1 and color2
719 /// ```
720 #[inline]
721 pub fn next_color(&mut self) -> GenColor {
722 let mut rc = GenColor([0; ffi::sizes::COLOR_CODE]);
723 // SAFETY: &mut self ensures exclusive access to base.
724 // mu_gencolor always succeeds and fills the color code array.
725 unsafe { ffi::mu_gencolor(&mut self.base, &mut rc.0) };
726 rc
727 }
728}
729
730/// Trait for types that can provide color codes.
731///
732/// Similar to `Display`, this trait allows custom color implementations
733/// without heap allocation.
734///
735/// # Example
736/// ```rust
737/// # use musubi::{Config, ColorKind, Color};
738/// # use std::io::Write;
739/// struct MyColors;
740///
741/// impl Color for MyColors {
742/// fn color(&self, w: &mut dyn Write, kind: ColorKind) -> std::io::Result<()> {
743/// match kind {
744/// ColorKind::Error => w.write(b"[")?,
745/// ColorKind::Reset => w.write(b"]")?,
746/// _ => 0,
747/// };
748/// Ok(())
749/// }
750/// }
751///
752/// Config::new().with_color(&MyColors);
753/// ```
754pub trait Color {
755 /// Generate ANSI color code for the given color kind.
756 ///
757 /// This method is called during rendering to produce color escape sequences.
758 /// Write the ANSI escape sequence (e.g., `\x1b[31m` for red) to `w`.
759 ///
760 /// # Arguments
761 ///
762 /// * `w` - Output writer for the color code
763 /// * `kind` - The type of color needed (Error, Warning, etc.)
764 ///
765 /// # Returns
766 ///
767 /// `Ok(())` on success, or an I/O error if writing fails.
768 fn color(&self, w: &mut dyn Write, kind: ColorKind) -> std::io::Result<()>;
769}
770
771/// Internal userdata structure for color callbacks.
772///
773/// This structure is passed to C color callback functions via the `ud` pointer.
774/// It contains:
775/// - A type-erased pointer to the Rust `Color` trait object
776/// - A pointer to the shared color buffer for ANSI escape code output
777///
778/// # Safety
779///
780/// The pointers must remain valid for the entire duration of rendering.
781/// Memory safety is ensured by storing Color references and the buffer
782/// in the Report structure with appropriate lifetimes.
783struct ColorUd {
784 /// Pointer to the Color trait object (type-erased for FFI)
785 color_obj: *const c_void,
786 /// Pointer to the shared buffer for color escape codes
787 color_buf: *mut [u8; ffi::sizes::COLOR_CODE],
788}
789
790impl<C: Color> IntoColor for &C {
791 fn into_color(self, report: &mut Report) {
792 report.color_uds.push(Box::new(ColorUd {
793 color_obj: self as *const _ as *const c_void,
794 color_buf: &mut report.color_buf,
795 }));
796 extern "C" fn color_fn<C: Color>(
797 ud: *mut c_void,
798 kind: ffi::mu_ColorKind,
799 ) -> ffi::mu_Chunk {
800 // SAFETY: ud is a valid ColorUd pointer from color_uds vector
801 let ud = unsafe { &mut *(ud as *mut ColorUd) };
802 // SAFETY: color_obj points to a valid C reference with lifetime 'a
803 let color = unsafe { &*(ud.color_obj as *const C) };
804 // SAFETY: color_buf points to Report.color_buf, valid during render
805 let buf = unsafe { &mut *ud.color_buf };
806 let mut remain = &mut buf[1..];
807 match color.color(&mut remain, ColorKind::from_ffi(kind)) {
808 Ok(_) => {
809 let used = (ffi::sizes::COLOR_CODE - remain.len() - 1) as u8;
810 buf[0] = used;
811 buf.as_ptr() as *const c_char
812 }
813 Err(_) => c"".as_ptr(),
814 }
815 }
816 // SAFETY: self.ptr is valid, color_fn has correct signature, ud points to valid ColorUd
817 unsafe {
818 ffi::mu_color(
819 report.ptr,
820 Some(color_fn::<C>),
821 &**report.color_uds.last().unwrap() as *const ColorUd as *mut c_void,
822 )
823 };
824 }
825}
826
827/// Configuration for the diagnostic renderer
828pub struct Config<'a> {
829 inner: ffi::mu_Config,
830 color_ud: Option<Box<ColorUd>>,
831 char_set: Option<&'a CharSet>,
832}
833
834impl Debug for Config<'_> {
835 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
836 f.debug_struct("Config")
837 .field("cross_gap", &self.inner.cross_gap)
838 .field("compact", &self.inner.compact)
839 .field("underlines", &self.inner.underlines)
840 .field("column_order", &self.inner.column_order)
841 .field("align_messages", &self.inner.align_messages)
842 .field("multiline_arrows", &self.inner.multiline_arrows)
843 .field("tab_width", &self.inner.tab_width)
844 .field("limit_width", &self.inner.limit_width)
845 .field("ambi_width", &self.inner.ambiwidth)
846 .field("label_attach", &self.inner.label_attach)
847 .field("index_type", &self.inner.index_type)
848 .finish()
849 }
850}
851
852impl Clone for Config<'_> {
853 #[inline]
854 fn clone(&self) -> Self {
855 // SAFETY: mu_Config is a C struct with no Drop semantics, safe to copy
856 let new: ffi::mu_Config = unsafe { std::mem::transmute_copy(&self.inner) };
857 Self {
858 inner: new,
859 color_ud: None,
860 char_set: self.char_set,
861 }
862 }
863}
864
865impl Default for Config<'_> {
866 #[inline]
867 fn default() -> Self {
868 let mut obj = MaybeUninit::uninit();
869 // SAFETY: mu_initconfig initializes all fields of the config struct
870 unsafe {
871 ffi::mu_initconfig(obj.as_mut_ptr());
872 }
873 Self {
874 // SAFETY: obj has been fully initialized by mu_initconfig above
875 inner: unsafe { obj.assume_init() },
876 color_ud: None,
877 char_set: None,
878 }
879 }
880}
881
882impl<'a> Config<'a> {
883 /// Create a new config with default values.
884 #[inline]
885 pub fn new() -> Self {
886 Self::default()
887 }
888
889 /// Enable or disable cross gap rendering.
890 ///
891 /// When enabled, vertical bars between labels are drawn with gaps
892 /// for better visual clarity when labels overlap.
893 ///
894 /// Default: depends on C library default
895 #[inline]
896 pub fn with_cross_gap(mut self, enabled: bool) -> Self {
897 self.inner.cross_gap = enabled as c_int;
898 self
899 }
900
901 /// Enable or disable compact mode.
902 ///
903 /// In compact mode, the diagnostic output is more condensed:
904 /// - Underlines and arrows may be merged onto the same line
905 /// - Only meaningful label arrows are shown
906 ///
907 /// Works with underlines enabled or disabled.
908 ///
909 /// Default: `false`
910 #[inline]
911 pub fn with_compact(mut self, enabled: bool) -> Self {
912 self.inner.compact = enabled as c_int;
913 self
914 }
915
916 /// Enable or disable underlines for highlighted spans.
917 ///
918 /// When enabled, spans are underlined with characters like `^^^`.
919 /// When disabled, only label arrows are shown.
920 ///
921 /// Works with both compact and non-compact modes.
922 ///
923 /// Default: `true`
924 #[inline]
925 pub fn with_underlines(mut self, enabled: bool) -> Self {
926 self.inner.underlines = enabled as c_int;
927 self
928 }
929
930 /// Enable or disable natural label ordering.
931 ///
932 /// When disabled (default), labels are sorted to minimize line crossings:
933 /// - Inline labels appear first, ordered by reverse column position
934 /// - Multi-line labels follow, with tails before heads
935 ///
936 /// When enabled, labels are simply sorted by column position.
937 ///
938 /// Default: `false` (natural ordering enabled)
939 ///
940 /// # Example
941 /// ```rust
942 /// # use musubi::Config;
943 /// let config = Config::new().with_column_order(true); // Simple column order
944 /// ```
945 #[inline]
946 pub fn with_column_order(mut self, enabled: bool) -> Self {
947 self.inner.column_order = enabled as c_int;
948 self
949 }
950
951 /// Enable or disable aligned label messages.
952 ///
953 /// When enabled (default), label messages are aligned to the same column,
954 /// producing a more structured appearance with longer arrows.
955 ///
956 /// When disabled, messages are placed immediately after their arrows,
957 /// creating more compact output.
958 ///
959 /// Default: `true` (aligned)
960 ///
961 /// # Example
962 /// ```rust
963 /// # use musubi::Config;
964 /// let config = Config::new().with_align_messages(false); // Compact arrows
965 /// ```
966 #[inline]
967 pub fn with_align_messages(mut self, enabled: bool) -> Self {
968 self.inner.align_messages = enabled as c_int;
969 self
970 }
971
972 /// Enable or disable multiline arrows for labels.
973 ///
974 /// When enabled, labels that span multiple lines will have
975 /// arrows drawn across all covered lines.
976 ///
977 /// Default: `true`
978 #[inline]
979 pub fn with_multiline_arrows(mut self, enabled: bool) -> Self {
980 self.inner.multiline_arrows = enabled as c_int;
981 self
982 }
983
984 /// Set the tab width for rendering.
985 ///
986 /// Tab characters (`\t`) in source code are expanded to this many spaces.
987 ///
988 /// Default: `4`
989 ///
990 /// # Example
991 /// ```rust
992 /// # use musubi::Config;
993 /// let config = Config::new().with_tab_width(8); // 8-space tabs
994 /// ```
995 #[inline]
996 pub fn with_tab_width(mut self, width: i32) -> Self {
997 self.inner.tab_width = width;
998 self
999 }
1000
1001 /// Set the width limit for line wrapping.
1002 ///
1003 /// Lines longer than this width will be truncated with an ellipsis.
1004 /// Set to `0` for no limit (lines can be arbitrarily long).
1005 ///
1006 /// Default: `0` (no limit)
1007 ///
1008 /// # Example
1009 /// ```rust
1010 /// # use musubi::Config;
1011 /// let config = Config::new().with_limit_width(80); // Wrap at 80 columns
1012 /// ```
1013 #[inline]
1014 pub fn with_limit_width(mut self, width: i32) -> Self {
1015 self.inner.limit_width = width;
1016 self
1017 }
1018
1019 /// Set the ambiguous character width.
1020 ///
1021 /// Some Unicode characters have ambiguous width (e.g., East Asian characters).
1022 /// This setting determines their display width:
1023 /// - `1` - Treat as narrow (1 column)
1024 /// - `2` - Treat as wide (2 columns)
1025 ///
1026 /// Default: `1`
1027 ///
1028 /// # Example
1029 /// ```rust
1030 /// # use musubi::Config;
1031 /// let config = Config::new().with_ambi_width(2); // East Asian width
1032 /// ```
1033 #[inline]
1034 pub fn with_ambi_width(mut self, width: i32) -> Self {
1035 self.inner.ambiwidth = width;
1036 self
1037 }
1038
1039 /// Set where labels attach to spans.
1040 ///
1041 /// Controls the default attachment point for all labels.
1042 /// Individual labels can override this with [`Report::with_order`].
1043 ///
1044 /// Default: [`LabelAttach::Middle`]
1045 #[inline]
1046 pub fn with_label_attach(mut self, attach: LabelAttach) -> Self {
1047 self.inner.label_attach = attach.into();
1048 self
1049 }
1050
1051 /// Set the index type (character or byte).
1052 ///
1053 /// Determines how span ranges are interpreted.
1054 /// See [`IndexType`] for details.
1055 ///
1056 /// Default: [`IndexType::Char`]
1057 #[inline]
1058 pub fn with_index_type(mut self, index_type: IndexType) -> Self {
1059 self.inner.index_type = index_type.into();
1060 self
1061 }
1062
1063 /// Set ASCII character set for rendering.
1064 ///
1065 /// Uses ASCII characters (`-`, `|`, `+`, etc.) for box drawing.
1066 /// This is compatible with all terminals and file formats.
1067 ///
1068 /// # Example
1069 /// ```text
1070 /// Error: message
1071 /// ,-[ file.rs:1:1 ]
1072 /// |
1073 /// 1 | code here
1074 /// | ^^|^
1075 /// | `--- label
1076 /// ---'
1077 /// ```
1078 #[inline]
1079 pub fn with_char_set_ascii(mut self) -> Self {
1080 // SAFETY: mu_ascii() returns a valid static charset pointer
1081 self.inner.char_set = unsafe { ffi::mu_ascii() };
1082 self.char_set = None;
1083 self
1084 }
1085
1086 /// Set Unicode character set for rendering.
1087 ///
1088 /// Uses Unicode box-drawing characters (─, │, ┬, etc.) for prettier output.
1089 /// Requires a terminal that supports Unicode.
1090 ///
1091 /// # Example
1092 /// ```text
1093 /// Error: message
1094 /// ╭─[ file.rs:1:1 ]
1095 /// │
1096 /// 1 │ code here
1097 /// │ ──┬─
1098 /// │ ╰─── label
1099 /// ───╯
1100 /// ```
1101 #[inline]
1102 pub fn with_char_set_unicode(mut self) -> Self {
1103 // SAFETY: mu_unicode() returns a valid static charset pointer
1104 self.inner.char_set = unsafe { ffi::mu_unicode() };
1105 self.char_set = None;
1106 self
1107 }
1108
1109 /// Set a custom character set for rendering.
1110 ///
1111 /// Allows fine-grained control over all box-drawing characters.
1112 /// The character set must outlive the config.
1113 ///
1114 /// # Example
1115 /// ```rust
1116 /// # use musubi::{Config, CharSet};
1117 /// let custom = CharSet {
1118 /// hbar: '=',
1119 /// vbar: '!',
1120 /// ..CharSet::ascii()
1121 /// };
1122 /// let config = Config::new().with_char_set(&custom);
1123 /// ```
1124 #[inline]
1125 pub fn with_char_set(mut self, char_set: &'a CharSet) -> Self {
1126 self.char_set = Some(char_set);
1127 self
1128 }
1129
1130 /// Enable default ANSI colors.
1131 ///
1132 /// Uses the built-in color scheme with standard ANSI escape codes:
1133 /// - Errors in red
1134 /// - Warnings in yellow
1135 /// - Margins in blue
1136 /// - etc.
1137 ///
1138 /// This is appropriate for terminal output.
1139 #[inline]
1140 pub fn with_color_default(mut self) -> Self {
1141 self.inner.color = Some(ffi::mu_default_color);
1142 self.color_ud = None;
1143 self
1144 }
1145
1146 /// Disable color output.
1147 ///
1148 /// All output will be plain text without ANSI escape codes.
1149 /// This is appropriate for file output or non-color terminals.
1150 ///
1151 /// Default: colors are disabled
1152 #[inline]
1153 pub fn with_color_disabled(mut self) -> Self {
1154 self.inner.color = None;
1155 self.color_ud = None;
1156 self
1157 }
1158
1159 /// Set a custom color provider.
1160 pub fn with_color<C>(mut self, color: &'a C) -> Self
1161 where
1162 C: Color,
1163 {
1164 extern "C" fn color_fn<C: Color>(
1165 ud: *mut c_void,
1166 kind: ffi::mu_ColorKind,
1167 ) -> ffi::mu_Chunk {
1168 // SAFETY: ud is provided by the caller and assumed valid
1169 let ud = unsafe { &mut *(ud as *mut ColorUd) };
1170 // SAFETY: in color_fn's call lifetime, color_obj and color_buf are valid
1171 let color = unsafe { &*(ud.color_obj as *const C) };
1172 // SAFETY: color_buf is initialized by Report::render_to_writer and remains valid during callback
1173 let buf = unsafe { &mut *ud.color_buf };
1174 let mut remain = &mut buf[1..];
1175 match color.color(&mut remain, ColorKind::from_ffi(kind)) {
1176 Ok(_) => {
1177 let used = (ffi::sizes::COLOR_CODE - remain.len() - 1) as u8;
1178 buf[0] = used;
1179 buf.as_ptr() as *const c_char
1180 }
1181 Err(_) => b"\0" as *const u8 as *const c_char,
1182 }
1183 }
1184
1185 self.color_ud = Some(Box::new(ColorUd {
1186 color_obj: color as *const C as *mut c_void,
1187 color_buf: ptr::null_mut(),
1188 }));
1189 self.inner.color = Some(color_fn::<C>);
1190 self.inner.color_ud = self
1191 .color_ud
1192 .as_ref()
1193 .map_or(ptr::null_mut(), |ud| &**ud as *const ColorUd as *mut c_void);
1194 self
1195 }
1196}
1197
1198/// Trait for types that can be added to a cache.
1199///
1200/// This trait is automatically implemented for common types:
1201/// - `&str` - Borrowed string content
1202/// - `String` - Owned string content (stored in cache)
1203/// - `OwnedSource<S>` - Any type implementing `AsRef<[u8]>` (`Vec<u8>`, `Box<[u8]>`, etc.)
1204/// - Tuples with filename: `(&str, &str)`, `(String, &str)`
1205/// - Custom `Source` trait implementations
1206///
1207/// Users typically don't need to implement this trait directly.
1208pub trait AddToCache {
1209 /// Add this source to the cache.
1210 ///
1211 /// # Parameters
1212 /// - `cache`: Mutable reference to the C cache pointer
1213 ///
1214 /// # Returns
1215 /// Pointer to the created `mu_Source` in the C library
1216 fn add_to_cache(self, cache: &mut *mut ffi::mu_Cache) -> *mut ffi::mu_Source;
1217}
1218
1219/// Wrapper for owned source content.
1220///
1221/// `OwnedSource` wraps any type that can be viewed as bytes (`AsRef<[u8]>`),
1222/// such as `Vec<u8>`, `Box<[u8]>`, or custom buffer types. The content is
1223/// stored directly in the cache's internal memory managed by the C library.
1224///
1225/// # Example
1226/// ```rust
1227/// # use musubi::{Cache, OwnedSource, Report, Level};
1228/// let buffer = vec![b'c', b'o', b'd', b'e'];
1229/// let cache = Cache::new()
1230/// .with_source((OwnedSource::new(buffer), "data.bin"));
1231///
1232/// let mut report = Report::new()
1233/// .with_title(Level::Error, "Error in binary data")
1234/// .with_label(0..4)
1235/// .render_to_string(&cache)?;
1236/// # Ok::<(), std::io::Error>(())
1237/// ```
1238pub struct OwnedSource<S>(S);
1239
1240impl<S: AsRef<[u8]>> From<S> for OwnedSource<S> {
1241 #[inline]
1242 fn from(value: S) -> Self {
1243 Self(value)
1244 }
1245}
1246
1247impl<S: AsRef<[u8]>> OwnedSource<S> {
1248 /// Create a new owned source from any type implementing `AsRef<[u8]>`.
1249 #[inline]
1250 pub fn new(owned: S) -> Self {
1251 owned.into()
1252 }
1253}
1254
1255impl<S: AsRef<[u8]>> AddToCache for OwnedSource<S> {
1256 fn add_to_cache(self, cache: &mut *mut ffi::mu_Cache) -> *mut ffi::mu_Source {
1257 #[repr(C)]
1258 struct OwnedSource<S> {
1259 base: ffi::mu_Source,
1260 owned: S,
1261 }
1262 // SAFETY: mu_addmemory initializes the cache and source correctly
1263 let src =
1264 unsafe { ffi::mu_addsource(cache, size_of::<OwnedSource<S>>(), Default::default()) };
1265 // SAFETY: src is allocated by mu_addsource above and valid here
1266 let owned_src = unsafe { &mut *(src as *mut OwnedSource<S>) };
1267 owned_src.base.init = Some(init_fn::<S>);
1268 owned_src.base.free = Some(free_fn::<S>);
1269 owned_src.base.get_line = Some(get_line_fn::<S>);
1270 owned_src.owned = self.0;
1271
1272 unsafe extern "C" fn init_fn<S: AsRef<[u8]>>(src: *mut ffi::mu_Source) -> c_int {
1273 // SAFETY: src is a valid OwnedSource<S> pointer created in into_source below
1274 let src = unsafe { &mut *(src as *mut OwnedSource<S>) };
1275 // SAFETY: calling mu_updatelines is safe
1276 unsafe { ffi::mu_updatelines(&mut src.base, src.owned.as_ref().into()) };
1277 ffi::MU_OK
1278 }
1279
1280 unsafe extern "C" fn free_fn<S: AsRef<[u8]>>(src: *mut ffi::mu_Source) {
1281 let ud = src as *mut OwnedSource<S>;
1282 // SAFETY: ud was allocated by mu_addsource and is valid here
1283 // after this call, src will be freed by C library.
1284 unsafe { std::ptr::drop_in_place(ud) };
1285 }
1286
1287 unsafe extern "C" fn get_line_fn<S: AsRef<[u8]>>(
1288 src: *mut ffi::mu_Source,
1289 line_no: c_uint,
1290 ) -> ffi::mu_Slice {
1291 // SAFETY: src is a valid OwnedSource<S> pointer
1292 let src = unsafe { &mut *(src as *mut OwnedSource<S>) };
1293 // SAFETY: calling mu_getline is safe
1294 let line = unsafe { *ffi::mu_getline(&mut src.base, line_no) };
1295 src.owned.as_ref()[line.byte_offset as usize..][..line.byte_len as usize].into()
1296 }
1297
1298 src
1299 }
1300}
1301
1302impl AddToCache for String {
1303 #[inline]
1304 fn add_to_cache(self, cache: &mut *mut ffi::mu_Cache) -> *mut ffi::mu_Source {
1305 OwnedSource::new(self).add_to_cache(cache)
1306 }
1307}
1308
1309impl AddToCache for &str {
1310 #[inline]
1311 fn add_to_cache(self, cache: &mut *mut ffi::mu_Cache) -> *mut ffi::mu_Source {
1312 // SAFETY: mu_addmemory initializes the cache and source correctly
1313 unsafe { ffi::mu_addmemory(cache, self.into(), Default::default()) }
1314 }
1315}
1316
1317impl<S: Source> AddToCache for S {
1318 fn add_to_cache(self, cache: &mut *mut ffi::mu_Cache) -> *mut ffi::mu_Source {
1319 #[repr(C)]
1320 struct BoxedSource<S: Source> {
1321 base: ffi::mu_Source,
1322 rust_obj: S,
1323 line: ffi::mu_Line,
1324 err: Option<io::Error>,
1325 }
1326
1327 // SAFETY: mu_addsource initializes the cache and source correctly
1328 let src = unsafe {
1329 let src = ffi::mu_addsource(cache, size_of::<BoxedSource<S>>(), Default::default());
1330 &mut *(src as *mut BoxedSource<S>)
1331 };
1332 src.rust_obj = self;
1333 src.base.init = Some(init_fn::<S>);
1334 src.base.free = Some(free_fn::<S>);
1335 src.base.get_line = Some(get_line_fn::<S>);
1336 src.base.get_line_info = Some(get_line_info_fn::<S>);
1337 src.base.line_for_chars = Some(line_for_chars_fn::<S>);
1338 src.base.line_for_bytes = Some(line_for_bytes_fn::<S>);
1339
1340 extern "C" fn init_fn<S: Source>(src: *mut ffi::mu_Source) -> c_int {
1341 // SAFETY: src is a valid UdSource<S> pointer created in into_source below
1342 let src = unsafe { &mut (*(src as *mut BoxedSource<S>)) };
1343 match src.rust_obj.init() {
1344 Ok(_) => 0,
1345 Err(err) => {
1346 // SAFETY: report pointer is valid for the lifetime of the source
1347 src.err = Some(err);
1348 ffi::MU_ERR_SRCINIT
1349 }
1350 }
1351 }
1352
1353 unsafe extern "C" fn free_fn<S: Source>(src: *mut ffi::mu_Source) {
1354 let ud = src as *mut BoxedSource<S>;
1355 // SAFETY: ud was allocated by mu_addsource and is valid here
1356 // after this call, src will be freed by C library.
1357 unsafe { std::ptr::drop_in_place(ud) };
1358 }
1359
1360 extern "C" fn get_line_fn<S: Source>(
1361 src: *mut ffi::mu_Source,
1362 line_no: c_uint,
1363 ) -> ffi::mu_Slice {
1364 // SAFETY: src is a valid UdSource<S> pointer
1365 let src = unsafe { &mut *(src as *mut BoxedSource<S>) };
1366 src.rust_obj.get_line(line_no as usize).into()
1367 }
1368
1369 extern "C" fn get_line_info_fn<S: Source>(
1370 src: *mut ffi::mu_Source,
1371 line_no: c_uint,
1372 ) -> *const ffi::mu_Line {
1373 // SAFETY: src is a valid UdSource<S> pointer
1374 let src = unsafe { &mut *(src as *mut BoxedSource<S>) };
1375 let line_info = src.rust_obj.get_line_info(line_no as usize);
1376 src.line = line_info.into();
1377 &src.line
1378 }
1379
1380 extern "C" fn line_for_chars_fn<S: Source>(
1381 src: *mut ffi::mu_Source,
1382 char_pos: usize,
1383 out_line: *mut *const ffi::mu_Line,
1384 ) -> c_uint {
1385 // SAFETY: src is a valid UdSource<S> pointer
1386 let src = unsafe { &mut *(src as *mut BoxedSource<S>) };
1387 let (line_no, line_info) = src.rust_obj.line_for_chars(char_pos);
1388 if !out_line.is_null() {
1389 src.line = line_info.into();
1390 // SAFETY: out_line is checked
1391 unsafe { *out_line = &src.line };
1392 }
1393 line_no as c_uint
1394 }
1395
1396 extern "C" fn line_for_bytes_fn<S: Source>(
1397 src: *mut ffi::mu_Source,
1398 byte_pos: usize,
1399 out_line: *mut *const ffi::mu_Line,
1400 ) -> c_uint {
1401 // SAFETY: src is a valid UdSource<S> pointer
1402 let src = unsafe { &mut *(src as *mut BoxedSource<S>) };
1403 let (line_no, line_info) = src.rust_obj.line_for_bytes(byte_pos);
1404 if !out_line.is_null() {
1405 src.line = line_info.into();
1406 // SAFETY: out_line is checked
1407 unsafe { *out_line = &src.line };
1408 }
1409 line_no as c_uint
1410 }
1411
1412 &mut src.base
1413 }
1414}
1415
1416impl<S: AddToCache> AddToCache for (S, &str) {
1417 #[inline]
1418 fn add_to_cache(self, cache: &mut *mut ffi::mu_Cache) -> *mut ffi::mu_Source {
1419 let src = self.0.add_to_cache(cache);
1420 // SAFETY: src is a valid mu_Source pointer
1421 unsafe { (*src).name = self.1.into() };
1422 src
1423 }
1424}
1425
1426impl<S: AddToCache> AddToCache for (S, &str, i32) {
1427 #[inline]
1428 fn add_to_cache(self, cache: &mut *mut ffi::mu_Cache) -> *mut ffi::mu_Source {
1429 let src = self.0.add_to_cache(cache);
1430 // SAFETY: src is a valid mu_Source pointer
1431 unsafe {
1432 (*src).name = self.1.into();
1433 (*src).line_no_offset = self.2
1434 };
1435 src
1436 }
1437}
1438
1439/// Internal representation of a cache for rendering.
1440///
1441/// This enum manages the lifetime of the underlying C cache pointer:
1442/// - `Owned`: Cache was created for a single render and will be freed
1443/// - `Borrowed`: Cache is owned by user code and should not be freed
1444///
1445/// Users typically don't interact with this type directly; it's used
1446/// internally by the `render_to_*` methods.
1447pub enum RawCache {
1448 /// Temporary cache that will be freed when dropped
1449 Owned(*mut ffi::mu_Cache),
1450 /// Borrowed cache that remains owned by the caller
1451 Borrowed(*mut ffi::mu_Cache),
1452}
1453
1454impl Drop for RawCache {
1455 #[inline]
1456 fn drop(&mut self) {
1457 match self {
1458 RawCache::Owned(ptr) => {
1459 if !ptr.is_null() {
1460 // SAFETY: mu_delcache frees the cache allocated by mu_addmemory
1461 unsafe { ffi::mu_delcache(*ptr) };
1462 }
1463 }
1464 RawCache::Borrowed(_) => {
1465 // Do nothing for borrowed cache
1466 }
1467 }
1468 }
1469}
1470
1471impl RawCache {
1472 #[inline]
1473 fn as_ptr(&self) -> *mut ffi::mu_Cache {
1474 match self {
1475 RawCache::Owned(ptr) => *ptr,
1476 RawCache::Borrowed(ptr) => *ptr,
1477 }
1478 }
1479}
1480
1481impl<S: AddToCache> From<S> for RawCache {
1482 #[inline]
1483 fn from(value: S) -> RawCache {
1484 let mut cache = ptr::null_mut();
1485 value.add_to_cache(&mut cache);
1486 RawCache::Owned(cache)
1487 }
1488}
1489
1490/// A cache of diagnostic sources.
1491///
1492/// `Cache` manages multiple source files and their associated data,
1493/// allowing for efficient multi-source diagnostics. It can be reused
1494/// across multiple render operations.
1495///
1496/// # Source Lifetime Management
1497///
1498/// The cache automatically handles different source types:
1499/// - **Borrowed sources** (`&str`): Content must remain valid until rendering completes
1500/// - **Owned sources** (`String`, `Vec<u8>`, etc.): Content is stored in the cache's
1501/// internal memory managed by the C library
1502///
1503/// # Single Source Convenience
1504///
1505/// For simple single-source diagnostics, you can pass sources directly to
1506/// rendering methods without creating an explicit `Cache`. See [`Report::render_to_string()`]
1507/// for examples.
1508///
1509/// # Example
1510/// ```rust
1511/// use musubi::{Cache, Report, Level};
1512///
1513/// let cache = Cache::new()
1514/// .with_source(("let x = 42;", "main.rs")) // Source 0
1515/// .with_source(("fn foo() {}", "lib.rs")); // Source 1
1516///
1517/// let mut report = Report::new()
1518/// .with_title(Level::Error, "Multiple files")
1519/// .with_label((0..3, 0)) // Label in main.rs
1520/// .with_message("here")
1521/// .with_label((3..6, 1)) // Label in lib.rs
1522/// .with_message("and here");
1523///
1524/// report.render_to_stdout(&cache)?;
1525/// # Ok::<(), std::io::Error>(())
1526/// ```
1527#[derive(Default)]
1528pub struct Cache {
1529 inner: *mut ffi::mu_Cache,
1530}
1531
1532impl From<&Cache> for RawCache {
1533 #[inline]
1534 fn from(cache: &Cache) -> RawCache {
1535 RawCache::Borrowed(cache.inner)
1536 }
1537}
1538
1539impl Cache {
1540 /// Create a new empty cache.
1541 #[inline]
1542 pub fn new() -> Self {
1543 Default::default()
1544 }
1545
1546 /// Add a source to the cache.
1547 ///
1548 /// Accepts both borrowed (`&str`) and owned (`String`) content.
1549 /// For other byte buffers like `Vec<u8>`, use [`OwnedSource`].
1550 /// Borrowed content must remain valid until rendering completes.
1551 /// Owned content is stored in the cache's internal memory.
1552 ///
1553 /// # Example
1554 /// ```rust
1555 /// # use musubi::{Cache, OwnedSource};
1556 /// let cache = Cache::new()
1557 /// .with_source("let x = 42;") // &str - borrowed
1558 /// .with_source(("fn main() {}".to_string(), "main.rs")) // String - owned
1559 /// .with_source((OwnedSource::new(vec![b'a', b'b', b'c']), "data.bin")); // Vec<u8>
1560 /// ```
1561 #[inline]
1562 pub fn with_source<S: AddToCache>(mut self, content: S) -> Self {
1563 content.add_to_cache(&mut self.inner);
1564 self
1565 }
1566}
1567
1568/// A source of diagnostic content.
1569///
1570/// Sources can be created from in-memory strings or with custom line providers.
1571/// They are typically managed through a [`Cache`], but can also be passed directly
1572/// to rendering methods for single-source diagnostics.
1573///
1574/// # Example
1575/// ```rust
1576/// # use musubi::{Cache, Source, Line};
1577/// # use std::default::Default;
1578///
1579/// // implement a custom source
1580/// struct MySource { /* ... */ }
1581///
1582/// # impl MySource { fn new() -> Self { Self{ /* ... */ } } }
1583///
1584/// impl Source for MySource {
1585/// // ...
1586/// # fn init(&mut self) -> std::io::Result<()> { Ok(()) }
1587/// # fn get_line(&self, line_no: usize) -> &[u8] { b"" }
1588/// # fn get_line_info(&self, line_no: usize) -> musubi::Line { Line::new() }
1589/// # fn line_for_chars(&self, char_pos: usize) -> (usize, musubi::Line) { (0, Line::new()) }
1590/// # fn line_for_bytes(&self, byte_pos: usize) -> (usize, musubi::Line) { (0, Line::new()) }
1591/// }
1592///
1593/// // Use with Cache for multiple sources
1594/// let cache = Cache::new()
1595/// .with_source(("let x = 42;", "main.rs"))
1596/// .with_source((MySource::new(), "my_source.rs"));
1597///
1598/// // Or pass directly to render for single source
1599/// // report.render_to_string(("code", "file.rs"))?;
1600/// ```
1601pub trait Source {
1602 /// Initialize the source (e.g., read lines).
1603 fn init(&mut self) -> io::Result<()>;
1604
1605 /// Get a specific line by line number (0-based).
1606 /// Return last line data if line_no is out of range.
1607 fn get_line(&self, line_no: usize) -> &[u8];
1608
1609 /// Get line info struct by line number (0-based).
1610 /// Return last line info if line_no is out of range.
1611 fn get_line_info(&self, line_no: usize) -> Line;
1612
1613 /// Get the line number and line info for a given character position.
1614 /// Return last line number and info if char_pos is out of range.
1615 fn line_for_chars(&self, char_pos: usize) -> (usize, Line);
1616
1617 /// Get the line number and line info for a given byte position.
1618 /// Return last line number and info if byte_pos is out of range.
1619 fn line_for_bytes(&self, byte_pos: usize) -> (usize, Line);
1620}
1621
1622/// Information about a line in source code.
1623///
1624/// This structure describes a line's position and length in both
1625/// character and byte offsets, which is important for proper UTF-8 handling.
1626///
1627/// Returned by [`Source`] trait methods to provide line metadata.
1628#[derive(Default, Debug, Clone, Copy)]
1629pub struct Line {
1630 /// Character offset from the start of the source (0-based)
1631 pub offset: usize,
1632 /// Byte offset from the start of the source (0-based)
1633 pub byte_offset: usize,
1634 /// Line length in characters (excluding newline)
1635 pub len: u32,
1636 /// Line length in bytes (excluding newline)
1637 pub byte_len: u32,
1638 /// Newline sequence length in bytes (0, 1 for \n, 2 for \r\n)
1639 pub newline: u32,
1640}
1641
1642impl Line {
1643 /// Create a new empty Line with all fields set to zero.
1644 #[inline]
1645 pub fn new() -> Self {
1646 Self::default()
1647 }
1648}
1649
1650impl From<*const ffi::mu_Line> for Line {
1651 #[allow(clippy::not_unsafe_ptr_arg_deref)]
1652 #[inline]
1653 fn from(line: *const ffi::mu_Line) -> Self {
1654 // SAFETY: line pointer is provided by C library and assumed valid
1655 let line = unsafe { &*line };
1656 Line {
1657 offset: line.offset,
1658 byte_offset: line.byte_offset,
1659 len: line.len,
1660 byte_len: line.byte_len,
1661 newline: line.newline,
1662 }
1663 }
1664}
1665
1666impl From<Line> for ffi::mu_Line {
1667 #[inline]
1668 fn from(line: Line) -> Self {
1669 ffi::mu_Line {
1670 offset: line.offset,
1671 byte_offset: line.byte_offset,
1672 len: line.len,
1673 byte_len: line.byte_len,
1674 newline: line.newline,
1675 }
1676 }
1677}
1678
1679/// A diagnostic report builder.
1680///
1681/// The lifetime `'a` indicates that all string references passed to the report
1682/// must live at least as long as the report itself. This enables zero-copy
1683/// string passing to the underlying C library.
1684///
1685/// # Source Management
1686///
1687/// Sources are managed through a [`Cache`] and assigned IDs based on registration
1688/// order: first source is 0, second is 1, etc. The cache is then passed to rendering
1689/// methods.
1690///
1691/// # Example
1692/// ```rust
1693/// use musubi::{Report, Cache, Level};
1694///
1695/// let cache = Cache::new()
1696/// .with_source(("let x = 42;", "main.rs")) // src_id = 0
1697/// .with_source(("fn foo() {}", "lib.rs")); // src_id = 1
1698///
1699/// let mut report = Report::new()
1700/// .with_title(Level::Error, "Error")
1701/// .with_label((0..3, 0)) // label in source 0
1702/// .with_message("here")
1703/// .with_label((3..6, 1)) // label in source 1
1704/// .with_message("and here");
1705///
1706/// report.render_to_stdout(&cache)?;
1707/// # Ok::<(), std::io::Error>(())
1708/// ```
1709///
1710/// # Lifetime Safety
1711///
1712/// Source strings must outlive the report. This will not compile:
1713///
1714/// ```compile_fail
1715/// use musubi::{Report, Level};
1716///
1717/// fn bad() -> String {
1718/// let mut report = Report::new();
1719/// {
1720/// let code = String::from("let x = 42;");
1721/// report.with_source((code.as_str(), "test.rs"));
1722/// } // code dropped here, but report still holds reference
1723/// report.render_to_string(0, 0)
1724/// }
1725/// ```
1726pub struct Report<'a> {
1727 ptr: *mut ffi::mu_Report,
1728 config: Option<Config<'a>>,
1729 color_buf: [u8; ffi::sizes::COLOR_CODE],
1730 /// Box is necessary to ensure pointer stability when Vec grows
1731 #[allow(clippy::vec_box)]
1732 color_uds: Vec<Box<ColorUd>>,
1733 src_err: Option<io::Error>,
1734 _marker: PhantomData<&'a str>,
1735}
1736
1737impl Default for Report<'_> {
1738 #[inline]
1739 fn default() -> Self {
1740 Self::new()
1741 }
1742}
1743
1744impl Drop for Report<'_> {
1745 #[inline]
1746 fn drop(&mut self) {
1747 // SAFETY: self.ptr is a valid mu_Report pointer owned by this Report
1748 unsafe {
1749 ffi::mu_delete(self.ptr);
1750 }
1751 }
1752}
1753
1754impl<'a> Report<'a> {
1755 /// Create a new report.
1756 #[inline]
1757 pub fn new() -> Self {
1758 // SAFETY: mu_new allocates a new report, returns null on failure (checked below)
1759 let ptr = unsafe { ffi::mu_new(None, ptr::null_mut()) };
1760 assert!(!ptr.is_null(), "Failed to allocate report");
1761 Self {
1762 ptr,
1763 config: None,
1764 color_buf: [0; ffi::sizes::COLOR_CODE],
1765 color_uds: Vec::new(),
1766 src_err: None,
1767 _marker: PhantomData,
1768 }
1769 }
1770
1771 /// Configure the report.
1772 #[inline]
1773 pub fn with_config(mut self, config: Config<'a>) -> Self {
1774 self.config = Some(config);
1775 self
1776 }
1777
1778 /// Reset the report for reuse.
1779 ///
1780 /// Clears all labels, messages, and configuration, allowing the same
1781 /// Report instance to be used for rendering a different diagnostic.
1782 ///
1783 /// # Example
1784 /// ```rust
1785 /// # use musubi::{Report, Level};
1786 /// let mut report = Report::new()
1787 /// .with_title(Level::Error, "First error");
1788 /// // ... render ...
1789 /// report.render_to_string("")?;
1790 ///
1791 /// let mut report = report.reset()
1792 /// .with_title(Level::Warning, "Second warning");
1793 /// // ... render again ...
1794 /// report.render_to_string("")?;
1795 /// # Ok::<(), std::io::Error>(())
1796 /// ```
1797 #[inline]
1798 pub fn reset(self) -> Self {
1799 // SAFETY: self.ptr is a valid mu_Report pointer owned by this Report
1800 unsafe { ffi::mu_reset(self.ptr) };
1801 self
1802 }
1803
1804 /// Set the title and level.
1805 ///
1806 /// Accepts either a standard level or a custom level name:
1807 /// - `with_title(Level::Error, "message")` - standard level
1808 /// - `with_title("Note", "message")` - custom level name
1809 ///
1810 /// # Example
1811 /// ```rust
1812 /// # use musubi::{Report, Level};
1813 /// Report::new()
1814 /// .with_title(Level::Error, "Something went wrong")
1815 /// // Or with custom level:
1816 /// .with_title("Note", "Something to note")
1817 /// // ...
1818 /// # ;
1819 /// ```
1820 #[inline]
1821 pub fn with_title<L: Into<TitleLevel<'a>>>(self, level: L, message: &'a str) -> Self {
1822 let tl = level.into();
1823 // SAFETY: self.ptr is valid, message lifetime is bound to 'a
1824 unsafe { ffi::mu_title(self.ptr, tl.level, tl.custom_name, message.into()) };
1825 self
1826 }
1827
1828 /// Set the error code for this diagnostic.
1829 ///
1830 /// The error code is typically displayed in brackets before the title,
1831 /// like `[E0001]` or `[W123]`.
1832 ///
1833 /// # Example
1834 /// ```rust
1835 /// # use musubi::{Report, Level};
1836 /// Report::new()
1837 /// .with_title(Level::Error, "Type mismatch")
1838 /// .with_code("E0308") // Displayed as [E0308]
1839 /// // ...
1840 /// # ;
1841 /// ```
1842 #[inline]
1843 pub fn with_code(self, code: &'a str) -> Self {
1844 // SAFETY: self.ptr is valid, code lifetime is bound to 'a
1845 unsafe { ffi::mu_code(self.ptr, code.into()) };
1846 self
1847 }
1848
1849 /// Set the primary location for this diagnostic.
1850 ///
1851 /// This location is displayed in the diagnostic header, showing
1852 /// where the error occurred.
1853 ///
1854 /// # Parameters
1855 /// - `pos`: Byte or character position in the source (depending on `IndexType`)
1856 /// - `src_id`: Source ID (0 for first source, 1 for second, etc.)
1857 ///
1858 /// # Example
1859 /// ```rust
1860 /// # use musubi::{Report, Level};
1861 /// Report::new()
1862 /// .with_title(Level::Error, "Syntax error")
1863 /// .with_location(42, 0) // Position 42 in source 0
1864 /// // ...
1865 /// # ;
1866 /// ```
1867 #[inline]
1868 pub fn with_location(self, pos: usize, src_id: impl Into<mu_Id>) -> Self {
1869 // SAFETY: self.ptr is valid
1870 unsafe { ffi::mu_location(self.ptr, pos, src_id.into()) };
1871 self
1872 }
1873
1874 /// Add a label at the given byte range.
1875 ///
1876 /// The `src_id` is the source registration order (0 for first source, 1 for second, etc.).
1877 ///
1878 /// # Example
1879 /// ```rust
1880 /// # use musubi::{Report, Level};
1881 /// Report::new()
1882 /// .with_title(Level::Error, "Error")
1883 /// .with_label((0..3, 0)) // label in source 0
1884 /// .with_message("here")
1885 /// // ...
1886 /// # ;
1887 /// ```
1888 #[inline]
1889 pub fn with_label<L: Into<LabelSpan>>(self, span: L) -> Self {
1890 let span = span.into();
1891 // SAFETY: self.ptr is valid, span values are checked by C library
1892 unsafe { ffi::mu_label(self.ptr, span.start, span.end, span.src_id) };
1893 self
1894 }
1895
1896 /// Set the message for the last added label.
1897 ///
1898 /// The message is displayed next to the label's marker/arrow,
1899 /// providing explanation or context for the highlighted code.
1900 ///
1901 /// # Example
1902 /// ```rust
1903 /// # use musubi::{Report, Level};
1904 /// Report::new()
1905 /// .with_label(0..3)
1906 /// .with_message("expected identifier here") // ← message for this label
1907 /// .with_label(10..15)
1908 /// .with_message("found number instead") // ← message for next label
1909 /// // ...
1910 /// # ;
1911 /// ```
1912 #[inline]
1913 pub fn with_message(self, msg: &'a str) -> Self {
1914 let width = unicode_width(msg);
1915 // SAFETY: self.ptr is valid, msg lifetime is bound to 'a
1916 unsafe { ffi::mu_message(self.ptr, msg.into(), width) };
1917 self
1918 }
1919
1920 /// Set the color for the last added label.
1921 ///
1922 /// This method accepts anything that implements [`IntoColor`], including:
1923 /// - `&dyn Color` - Custom color trait objects
1924 /// - `&GenColor` - Pre-generated colors from [`ColorGenerator`]
1925 ///
1926 /// # Examples
1927 ///
1928 /// Using a custom color:
1929 /// ```rust
1930 /// # use musubi::{Report, Level, Color, ColorKind};
1931 /// # use std::io::Write;
1932 /// struct MyColor;
1933 /// impl Color for MyColor {
1934 /// fn color(&self, w: &mut dyn Write, kind: ColorKind) -> std::io::Result<()> {
1935 /// write!(w, "\x1b[31m") // Red
1936 /// }
1937 /// }
1938 ///
1939 /// let color = MyColor;
1940 /// Report::new()
1941 /// // ...
1942 /// .with_label(0..4)
1943 /// .with_color(&color)
1944 /// // ...
1945 /// # ;
1946 /// ```
1947 ///
1948 /// Using a color generator:
1949 /// ```rust
1950 /// # use musubi::{Report, Level, ColorGenerator};
1951 /// let mut cg = ColorGenerator::new();
1952 ///
1953 /// let report = Report::new()
1954 /// // ...
1955 /// .with_label(0..4)
1956 /// .with_color(&cg.next_color())
1957 /// // ...;
1958 /// # ;
1959 /// ```
1960 #[inline]
1961 pub fn with_color<C: IntoColor>(mut self, color: C) -> Self {
1962 color.into_color(&mut self);
1963 self
1964 }
1965
1966 /// Set the display order for the last added label.
1967 ///
1968 /// Labels with lower order values are displayed first (closer to the code).
1969 /// Labels with the same order are displayed in the order they were added.
1970 ///
1971 /// Default: `0`
1972 ///
1973 /// # Example
1974 /// ```rust
1975 /// # use musubi::{Report, Level};
1976 /// Report::new()
1977 /// // ...
1978 /// .with_label(0..4)
1979 /// .with_message("second")
1980 /// .with_order(1) // Display this label later
1981 /// .with_title(Level::Error, "Error")
1982 /// .with_label(0..4)
1983 /// .with_message("first")
1984 /// .with_order(-1) // Display this label first
1985 /// // ...
1986 /// # ;
1987 /// ```
1988 #[inline]
1989 pub fn with_order(self, order: i32) -> Self {
1990 // SAFETY: self.ptr is valid
1991 unsafe { ffi::mu_order(self.ptr, order) };
1992 self
1993 }
1994
1995 /// Set the priority for the last added label.
1996 ///
1997 /// Priority controls how overlapping labels are rendered when multiple
1998 /// labels cover the same source location. Labels with higher priority
1999 /// will be drawn on top, potentially obscuring lower-priority labels.
2000 ///
2001 /// Higher values = higher priority = drawn on top.
2002 ///
2003 /// Default: `0`
2004 ///
2005 /// # Example
2006 /// ```rust
2007 /// # use musubi::{Report, Level};
2008 /// Report::new()
2009 /// // ...
2010 /// .with_label(0..10)
2011 /// .with_message("low priority")
2012 /// .with_priority(0) // May be obscured by overlapping labels
2013 /// .with_label(5..15)
2014 /// .with_message("high priority")
2015 /// .with_priority(10) // Will be drawn on top
2016 /// // ...
2017 /// # ;
2018 /// ```
2019 #[inline]
2020 pub fn with_priority(self, priority: i32) -> Self {
2021 // SAFETY: self.ptr is valid
2022 unsafe { ffi::mu_priority(self.ptr, priority) };
2023 self
2024 }
2025
2026 /// Add a help message to the diagnostic.
2027 ///
2028 /// Help messages appear at the end of the diagnostic,
2029 /// providing suggestions or additional context.
2030 ///
2031 /// Multiple help messages can be added and will be displayed in order.
2032 ///
2033 /// # Example
2034 /// ```rust
2035 /// # use musubi::{Report, Level};
2036 /// Report::new()
2037 /// .with_title(Level::Error, "Type error")
2038 /// .with_label(0..4)
2039 /// .with_message("expected String")
2040 /// .with_help("try converting with .to_string()")
2041 /// // ...
2042 /// # ;
2043 /// ```
2044 #[inline]
2045 pub fn with_help(self, msg: &'a str) -> Self {
2046 // SAFETY: self.ptr is valid, msg lifetime is bound to 'a
2047 unsafe { ffi::mu_help(self.ptr, msg.into()) };
2048 self
2049 }
2050
2051 /// Add a note message to the diagnostic.
2052 ///
2053 /// Notes appear at the end of the diagnostic,
2054 /// providing additional information or context.
2055 ///
2056 /// Multiple notes can be added and will be displayed in order.
2057 ///
2058 /// # Example
2059 /// ```rust
2060 /// # use musubi::{Report, Level};
2061 /// Report::new()
2062 /// // ...
2063 /// .with_title(Level::Warning, "Unused variable")
2064 /// .with_label(0..4)
2065 /// .with_message("never used")
2066 /// .with_note("consider prefixing with an underscore: `_code`")
2067 /// // ...
2068 /// # ;
2069 /// ```
2070 #[inline]
2071 pub fn with_note(self, msg: &'a str) -> Self {
2072 // SAFETY: self.ptr is valid, msg lifetime is bound to 'a
2073 unsafe { ffi::mu_note(self.ptr, msg.into()) };
2074 self
2075 }
2076
2077 /// Render the report to a String.
2078 ///
2079 /// This is a convenience method that captures the rendered output
2080 /// into a String instead of writing to stdout or a file.
2081 ///
2082 /// # Parameters
2083 /// - `cache`: Source cache containing the code to display. Can be:
2084 /// - `&Cache` - A persistent cache with multiple sources
2085 /// - `&str` - A single source string (borrowed)
2086 /// - `(&str, &str)` - Source content and filename
2087 /// - `(&str, &str, i32)` - Source content, filename, and line offset for adjusting displayed line numbers
2088 /// - Custom types implementing `Source` trait
2089 ///
2090 /// # Example
2091 /// ```rust
2092 /// # use musubi::{Report, Level};
2093 /// let output = Report::new()
2094 /// .with_title(Level::Error, "Syntax error")
2095 /// .with_label(0..3)
2096 /// .with_message("unexpected token")
2097 /// .render_to_string(("let x", "main.rs"))?;
2098 /// println!("{}", output);
2099 /// # Ok::<(), std::io::Error>(())
2100 /// ```
2101 pub fn render_to_string(&mut self, cache: impl Into<RawCache>) -> io::Result<String> {
2102 let mut writer = Vec::new();
2103 unsafe extern "C" fn string_writer_callback(
2104 ud: *mut c_void,
2105 data: *const c_char,
2106 len: usize,
2107 ) -> c_int {
2108 // SAFETY: ud is a valid &mut Vec<u8> pointer passed to mu_writer below
2109 let writer = unsafe { &mut *(ud as *mut Vec<u8>) };
2110 // SAFETY: data and len are provided by C library, guaranteed to be valid
2111 let slice = unsafe { std::slice::from_raw_parts(data as *const u8, len) };
2112 writer.extend_from_slice(slice);
2113 ffi::MU_OK
2114 }
2115 // SAFETY: self.ptr is valid, callback has correct signature, writer is valid for this scope
2116 unsafe {
2117 ffi::mu_writer(
2118 self.ptr,
2119 Some(string_writer_callback),
2120 &mut writer as *mut Vec<u8> as *mut c_void,
2121 )
2122 };
2123 self.render(cache).map(|_| {
2124 String::from_utf8(writer)
2125 .unwrap_or_else(|e| String::from_utf8_lossy(&e.into_bytes()).into_owned())
2126 })
2127 }
2128
2129 /// Render the report directly to stdout.
2130 ///
2131 /// This is the most efficient way to display diagnostics,
2132 /// writing directly to the terminal without intermediate buffering.
2133 ///
2134 /// # Parameters
2135 /// - `cache`: Source cache or source content. Can be `&Cache`, `&str`,
2136 /// `(&str, &str)`, `(&str, &str, i32)`, or custom `Source` implementations.
2137 /// The third element (if present) is a line offset for adjusting displayed line numbers.
2138 ///
2139 /// # Example
2140 /// ```no_run
2141 /// # use musubi::{Report, Level};
2142 /// Report::new()
2143 /// .with_title(Level::Error, "Error message")
2144 /// .with_label(0..5)
2145 /// .render_to_stdout(("let x = 42;", "main.rs"))?;
2146 /// # Ok::<(), std::io::Error>(())
2147 /// ```
2148 pub fn render_to_stdout(&mut self, cache: impl Into<RawCache>) -> io::Result<()> {
2149 unsafe extern "C" fn stdout_writer_callback(
2150 _ud: *mut c_void,
2151 data: *const c_char,
2152 len: usize,
2153 ) -> c_int {
2154 // SAFETY: data and len are provided by C library, guaranteed to be valid
2155 let slice = unsafe { std::slice::from_raw_parts(data as *const u8, len) };
2156 let mut stdout = io::stdout();
2157 if stdout.write_all(slice).is_ok() && stdout.flush().is_ok() {
2158 ffi::MU_OK
2159 } else {
2160 ffi::MU_ERRPARAM
2161 }
2162 }
2163
2164 // SAFETY: self.ptr is valid, callback has correct signature
2165 unsafe { ffi::mu_writer(self.ptr, Some(stdout_writer_callback), ptr::null_mut()) };
2166 self.render(cache)
2167 }
2168
2169 /// Render the report to any type implementing `Write`.
2170 ///
2171 /// This allows rendering to files, buffers, or any custom writer.
2172 ///
2173 /// # Parameters
2174 /// - `writer`: Mutable reference to any type implementing `std::io::Write`
2175 /// - `cache`: Source cache or source content. Can be `&Cache`, `&str`,
2176 /// `(&str, &str)`, `(&str, &str, i32)`, or custom `Source` implementations.
2177 /// The third element (if present) is a line offset for adjusting displayed line numbers.
2178 ///
2179 /// # Example
2180 /// ```rust
2181 /// # use musubi::{Report, Level};
2182 /// # use std::io::Write;
2183 /// let mut buffer = Vec::new();
2184 /// Report::new()
2185 /// .with_title(Level::Warning, "Deprecated")
2186 /// .with_label(0..3)
2187 /// .render_to_writer(&mut buffer, "let x = 1;")?;
2188 /// assert!(!buffer.is_empty());
2189 /// # Ok::<(), std::io::Error>(())
2190 /// ```
2191 pub fn render_to_writer<'b, W: Write>(
2192 &'b mut self,
2193 writer: &'b mut W,
2194 cache: impl Into<RawCache>,
2195 ) -> io::Result<()> {
2196 struct WriterWrapper<'a, W: Write> {
2197 writer: &'a mut W,
2198 report: *mut Report<'a>,
2199 }
2200
2201 unsafe extern "C" fn writer_callback<W: Write>(
2202 ud: *mut c_void,
2203 data: *const c_char,
2204 len: usize,
2205 ) -> c_int {
2206 // SAFETY: ud is a valid WriterWrapper<W> pointer passed to mu_writer below
2207 let w = unsafe { &mut *(ud as *mut WriterWrapper<W>) };
2208 // SAFETY: data and len are provided by C library, guaranteed to be valid
2209 let slice = unsafe { std::slice::from_raw_parts(data as *const u8, len) };
2210 match w.writer.write_all(slice) {
2211 Ok(_) => ffi::MU_OK,
2212 Err(e) => {
2213 // SAFETY: report pointer is setted below, and this function only called during render()
2214 unsafe { &mut *w.report }.src_err = Some(e);
2215 ffi::MU_ERR_WRITER
2216 }
2217 }
2218 }
2219 #[allow(clippy::unnecessary_cast)]
2220 let mut wrapper = WriterWrapper {
2221 writer,
2222 report: self as *mut Report<'a> as *mut Report<'b>,
2223 };
2224 // SAFETY: mu_writer expects a valid Report pointer and writer callback
2225 unsafe {
2226 ffi::mu_writer(
2227 self.ptr,
2228 Some(writer_callback::<W>),
2229 &mut wrapper as *mut _ as *mut c_void,
2230 );
2231 }
2232 self.render(cache)
2233 }
2234
2235 fn render(&mut self, cache: impl Into<RawCache>) -> io::Result<()> {
2236 let mut buf = [0u8; ffi::sizes::COLOR_CODE];
2237 let cs_buf: CharSetBuf;
2238 let cs: ffi::mu_Charset;
2239 if let Some(config) = &mut self.config
2240 && let Some(char_set) = config.char_set
2241 {
2242 cs_buf = (*char_set).into();
2243 cs = cs_buf.into();
2244 config.inner.char_set = &cs as *const ffi::mu_Charset;
2245 }
2246 if let Some(cfg) = self.config.as_mut()
2247 && let Some(color_ud) = cfg.color_ud.as_mut()
2248 {
2249 color_ud.color_buf = &mut buf as *mut [u8; ffi::sizes::COLOR_CODE];
2250 }
2251 for color_ud in &mut self.color_uds {
2252 color_ud.color_buf = &mut buf as *mut [u8; ffi::sizes::COLOR_CODE];
2253 }
2254 if let Some(cfg) = &self.config {
2255 // SAFETY: self.ptr is valid, cfg.inner is a valid config with lifetime guarantees
2256 unsafe { ffi::mu_config(self.ptr, &cfg.inner) };
2257 }
2258 // SAFETY: self.ptr is valid, all sources and labels have been properly registered
2259 match unsafe { ffi::mu_render(self.ptr, cache.into().as_ptr()) } {
2260 ffi::MU_OK => Ok(()),
2261 ffi::MU_ERR_SRCINIT => {
2262 if let Some(err) = self.src_err.take() {
2263 return Err(err);
2264 }
2265 Err(io::Error::other("Source init error during rendering"))
2266 }
2267 ffi::MU_ERR_WRITER => {
2268 if let Some(err) = self.src_err.take() {
2269 return Err(err);
2270 }
2271 Err(io::Error::other("Writer error during rendering"))
2272 }
2273 err_code => Err(io::Error::other(format!(
2274 "Rendering failed with error code {}",
2275 err_code
2276 ))),
2277 }
2278 }
2279}
2280
2281/// Internal buffer for character set conversion to C representation.
2282///
2283/// Converts Rust [`CharSet`] into a C-compatible array of chunk pointers.
2284/// Each character is encoded as: `[length_byte, utf8_byte1, utf8_byte2, ...]`
2285///
2286/// The buffer contains 23 entries (one for each CharSet field), each up to
2287/// 8 bytes (1 length byte + up to 7 UTF-8 bytes, though most characters are 1-3 bytes).
2288struct CharSetBuf {
2289 /// 23 characters × 8 bytes each (length prefix + UTF-8 data)
2290 buf: [[u8; 8]; 23],
2291}
2292
2293impl From<CharSetBuf> for ffi::mu_Charset {
2294 #[inline]
2295 fn from(value: CharSetBuf) -> Self {
2296 let mut chars: ffi::mu_Charset = [ptr::null(); 23];
2297 for (i, slice) in value.buf.iter().enumerate() {
2298 chars[i] = slice.as_ptr() as *const c_char;
2299 }
2300 chars
2301 }
2302}
2303
2304impl From<CharSet> for CharSetBuf {
2305 fn from(char_set: CharSet) -> Self {
2306 #[inline]
2307 fn char_to_slice(c: char) -> [u8; 8] {
2308 if c == '.' {
2309 return [3, b'.', b'.', b'.', 0, 0, 0, 0];
2310 }
2311 let mut buf = [0u8; 8];
2312 let s = c.encode_utf8(&mut buf);
2313 let len = s.len() as u8;
2314 let mut result = [0u8; 8];
2315 result[0] = len;
2316 result[1..(len as usize + 1)].copy_from_slice(s.as_bytes());
2317 result
2318 }
2319 CharSetBuf {
2320 buf: [
2321 char_to_slice(char_set.space),
2322 char_to_slice(char_set.newline),
2323 char_to_slice(char_set.lbox),
2324 char_to_slice(char_set.rbox),
2325 char_to_slice(char_set.colon),
2326 char_to_slice(char_set.hbar),
2327 char_to_slice(char_set.vbar),
2328 char_to_slice(char_set.xbar),
2329 char_to_slice(char_set.vbar_break),
2330 char_to_slice(char_set.vbar_gap),
2331 char_to_slice(char_set.uarrow),
2332 char_to_slice(char_set.rarrow),
2333 char_to_slice(char_set.ltop),
2334 char_to_slice(char_set.mtop),
2335 char_to_slice(char_set.rtop),
2336 char_to_slice(char_set.lbot),
2337 char_to_slice(char_set.mbot),
2338 char_to_slice(char_set.rbot),
2339 char_to_slice(char_set.lcross),
2340 char_to_slice(char_set.rcross),
2341 char_to_slice(char_set.underbar),
2342 char_to_slice(char_set.underline),
2343 char_to_slice(char_set.ellipsis),
2344 ],
2345 }
2346 }
2347}
2348
2349/// Calculate the display width of a string (simple ASCII version).
2350/// For full Unicode support, consider using the unicode-width crate.
2351fn unicode_width(s: &str) -> i32 {
2352 s.chars().count() as i32
2353}
2354
2355#[cfg(test)]
2356mod tests {
2357 use super::*;
2358 use insta::assert_snapshot;
2359
2360 fn remove_trailing_whitespace(s: &str) -> String {
2361 s.lines()
2362 .map(|line| line.trim_end())
2363 .collect::<Vec<&str>>()
2364 .join("\n")
2365 }
2366
2367 #[test]
2368 fn test_basic_report() {
2369 let mut report = Report::new()
2370 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
2371 .with_title(Level::Error, "Test error")
2372 .with_code("E001")
2373 .with_label(0..3)
2374 .with_message("this is a test");
2375
2376 let output = report.render_to_string(("let x = 42;", "test.rs")).unwrap();
2377 assert_snapshot!(
2378 remove_trailing_whitespace(&output),
2379 @r##"
2380 [E001] Error: Test error
2381 ,-[ test.rs:1:1 ]
2382 |
2383 1 | let x = 42;
2384 | ^|^
2385 | `--- this is a test
2386 ---'
2387 "##
2388 );
2389 }
2390
2391 #[test]
2392 fn test_config() {
2393 let config = Config::new()
2394 .with_compact(true)
2395 .with_char_set_ascii()
2396 .with_color_disabled();
2397
2398 let mut report = Report::new()
2399 .with_config(config)
2400 .with_title(Level::Warning, "Test warning")
2401 .with_label(0..5)
2402 .with_message("test");
2403
2404 let output = report.render_to_string(("hello", "test.rs")).unwrap();
2405 assert_snapshot!(
2406 remove_trailing_whitespace(&output),
2407 @r##"
2408 Warning: Test warning
2409 ,-[ test.rs:1:1 ]
2410 1 |hello
2411 |^^|^^
2412 | `--- test
2413 "##
2414 );
2415 }
2416
2417 #[test]
2418 fn test_custom_level() {
2419 let mut report = Report::new()
2420 .with_config(Config::new().with_color_disabled())
2421 .with_title("Hint", "Consider this")
2422 .with_label(0..4)
2423 .with_message("here");
2424
2425 let output = report.render_to_string(("code", "test.rs")).unwrap();
2426 assert_snapshot!(
2427 remove_trailing_whitespace(&output),
2428 @r##"
2429 Hint: Consider this
2430 ╭─[ test.rs:1:1 ]
2431 │
2432 1 │ code
2433 │ ──┬─
2434 │ ╰─── here
2435 ───╯
2436 "##
2437 );
2438 }
2439
2440 #[test]
2441 fn test_multiple_sources() {
2442 let cache = Cache::new()
2443 .with_source(("import foo", "main.rs")) // src_id = 0
2444 .with_source(("pub fn foo() {}".to_string(), "foo.rs")); // src_id = 1
2445 let mut report = Report::new()
2446 .with_config(Config::new().with_color_disabled())
2447 .with_title(Level::Error, "Import error")
2448 .with_label((7..10, 0))
2449 .with_message("imported here")
2450 .with_label((7..10, 1))
2451 .with_message("defined here");
2452
2453 let output = report.render_to_string(&cache).unwrap();
2454 assert_snapshot!(
2455 remove_trailing_whitespace(&output),
2456 @r##"
2457 Error: Import error
2458 ╭─[ main.rs:1:8 ]
2459 │
2460 1 │ import foo
2461 │ ─┬─
2462 │ ╰─── imported here
2463 │
2464 │─[ foo.rs:1:8 ]
2465 │
2466 1 │ pub fn foo() {}
2467 │ ─┬─
2468 │ ╰─── defined here
2469 ───╯
2470 "##
2471 );
2472 }
2473
2474 #[test]
2475 fn test_owned_source() {
2476 // Test OwnedSource with various types
2477 let vec_data = vec![
2478 b'h', b'e', b'l', b'l', b'o', b'\n', b'w', b'o', b'r', b'l', b'd',
2479 ];
2480 let cache = Cache::new()
2481 .with_source((OwnedSource::new(vec_data), "vec.txt")) // Vec<u8>
2482 .with_source(("static str".to_string(), "string.txt")); // String
2483
2484 let mut report = Report::new()
2485 .with_config(Config::new().with_color_disabled())
2486 .with_title(Level::Error, "Owned source test")
2487 .with_label((0..5, 0))
2488 .with_message("from Vec<u8>")
2489 .with_label((7..12, 1))
2490 .with_message("from String");
2491
2492 let output = report.render_to_string(&cache).unwrap();
2493 assert_snapshot!(
2494 remove_trailing_whitespace(&output),
2495 @r##"
2496 Error: Owned source test
2497 ╭─[ vec.txt:1:1 ]
2498 │
2499 1 │ hello
2500 │ ──┬──
2501 │ ╰──── from Vec<u8>
2502 │
2503 │─[ string.txt:1:8 ]
2504 │
2505 1 │ static str
2506 │ ─┬─
2507 │ ╰─── from String
2508 ───╯
2509 "##
2510 );
2511 }
2512
2513 #[test]
2514 fn test_source_new() {
2515 let mut report = Report::new()
2516 .with_config(Config::new().with_color_disabled())
2517 .with_title(Level::Error, "Error")
2518 .with_label((0..4, 0))
2519 .with_message("here");
2520
2521 let output = report.render_to_string(("test code", "file.rs")).unwrap();
2522 assert_snapshot!(
2523 remove_trailing_whitespace(&output),
2524 @r##"
2525 Error: Error
2526 ╭─[ file.rs:1:1 ]
2527 │
2528 1 │ test code
2529 │ ──┬─
2530 │ ╰─── here
2531 ───╯
2532 "##
2533 );
2534 }
2535
2536 #[test]
2537 fn test_label_at() {
2538 let cache = Cache::new()
2539 .with_source(("code1", "a.rs")) // src_id = 0
2540 .with_source(("code2", "b.rs")); // src_id = 1
2541 let mut report = Report::new()
2542 .with_config(Config::new().with_color_disabled())
2543 .with_title(Level::Error, "Error")
2544 .with_label((0..4, 0usize))
2545 .with_message("in a")
2546 .with_label((0..4, 1usize))
2547 .with_message("in b");
2548
2549 let output = report.render_to_string(&cache).unwrap();
2550 assert_snapshot!(
2551 remove_trailing_whitespace(&output),
2552 @r##"
2553 Error: Error
2554 ╭─[ a.rs:1:1 ]
2555 │
2556 1 │ code1
2557 │ ──┬─
2558 │ ╰─── in a
2559 │
2560 │─[ b.rs:1:1 ]
2561 │
2562 1 │ code2
2563 │ ──┬─
2564 │ ╰─── in b
2565 ───╯
2566 "##
2567 );
2568 }
2569
2570 #[test]
2571 fn test_custom_charset() {
2572 // Custom charset with different characters
2573 let custom = CharSet {
2574 hbar: '=',
2575 vbar: '!',
2576 ltop: '<',
2577 rtop: '>',
2578 lbot: '[',
2579 rbot: ']',
2580 ..CharSet::ascii()
2581 };
2582
2583 let config = Config::new().with_char_set(&custom).with_color_disabled();
2584
2585 let mut report = Report::new()
2586 .with_config(config)
2587 .with_title(Level::Error, "Test")
2588 .with_label(0..5usize)
2589 .with_message("here");
2590
2591 let output = report.render_to_string(("hello", "test.rs")).unwrap();
2592 assert_snapshot!(
2593 remove_trailing_whitespace(&output),
2594 @r##"
2595 Error: Test
2596 <=[ test.rs:1:1 ]
2597 !
2598 1 ! hello
2599 ! ^^|^^
2600 ! [==== here
2601 ===]
2602 "##
2603 );
2604 }
2605
2606 #[test]
2607 fn test_custom_color() {
2608 struct CustomColor;
2609 impl Color for CustomColor {
2610 fn color(&self, w: &mut dyn Write, kind: ColorKind) -> std::io::Result<()> {
2611 match kind {
2612 ColorKind::Reset => w.write(b"}")?,
2613 _ => w.write(b"{")?,
2614 };
2615 Ok(())
2616 }
2617 }
2618
2619 let mut report = Report::new()
2620 .with_config(Config::new().with_char_set_ascii().with_color(&CustomColor))
2621 .with_title(Level::Error, "test colors")
2622 .with_label(0..6usize)
2623 .with_message("here");
2624
2625 let output = report.render_to_string("klmnop").unwrap();
2626 assert_snapshot!(
2627 remove_trailing_whitespace(&output),
2628 @r##"
2629 {Error:} test colors
2630 {,-[} <unknown>:1:1 {]}
2631 {|}
2632 {1 |} {klmnop}
2633 {|} {^^^|^^}
2634 {|} {`----} here
2635 {---'}
2636 "##
2637 );
2638 }
2639
2640 #[test]
2641 fn test_color_gen() {
2642 let mut cg = ColorGenerator::new();
2643 let label1 = cg.next_color();
2644
2645 let mut report = Report::new()
2646 .with_config(Config::new().with_char_set_ascii())
2647 .with_title(Level::Error, "test colors")
2648 .with_label(0..6usize)
2649 .with_message("here")
2650 .with_color(&label1);
2651
2652 let output = report.render_to_string("klmnop").unwrap();
2653 assert_snapshot!(
2654 remove_trailing_whitespace(&output).replace('\x1b', "ESC"),
2655 @r##"
2656 ESC[31mError:ESC[0m test colors
2657 ESC[38;5;246m,-[ESC[0m <unknown>:1:1 ESC[38;5;246m]ESC[0m
2658 ESC[38;5;246m|ESC[0m
2659 ESC[38;5;246m1 |ESC[0m ESC[38;5;201mklmnopESC[0m
2660 ESC[38;5;240m|ESC[0m ESC[38;5;201m^^^|^^ESC[0m
2661 ESC[38;5;240m|ESC[0m ESC[38;5;201m`----ESC[0m here
2662 ESC[38;5;246m---'ESC[0m
2663 "##
2664 );
2665 }
2666
2667 #[test]
2668 fn test_custom_label_color() {
2669 struct CustomColor;
2670 impl Color for CustomColor {
2671 fn color(&self, w: &mut dyn Write, kind: ColorKind) -> std::io::Result<()> {
2672 match kind {
2673 ColorKind::Reset => w.write(b"}").map(|_| ()),
2674 _ => w.write(b"{").map(|_| ()),
2675 }
2676 }
2677 }
2678
2679 let mut report = Report::new()
2680 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
2681 .with_title(Level::Error, "test label colors")
2682 .with_label(0..6usize)
2683 .with_color(&CustomColor)
2684 .with_message("here");
2685
2686 let output = report.render_to_string("abcdef").unwrap();
2687 assert_snapshot!(
2688 remove_trailing_whitespace(&output),
2689 @r##"
2690 Error: test label colors
2691 ,-[ <unknown>:1:1 ]
2692 |
2693 1 | {abcdef}
2694 | {^^^|^^}
2695 | {`----} here
2696 ---'
2697 "##
2698 );
2699 }
2700
2701 #[test]
2702 fn test_source_with_line_offset() {
2703 let mut report = Report::new()
2704 .with_config(Config::new().with_color_disabled())
2705 .with_title(Level::Error, "Error")
2706 .with_label(0..4usize)
2707 .with_message("here");
2708
2709 let output = report
2710 // Line numbers start at 100
2711 .render_to_string(("some code here", "file.rs", 99))
2712 .unwrap();
2713 assert_snapshot!(
2714 remove_trailing_whitespace(&output),
2715 @r##"
2716 Error: Error
2717 ╭─[ file.rs:100:1 ]
2718 │
2719 100 │ some code here
2720 │ ──┬─
2721 │ ╰─── here
2722 ─────╯
2723 "##
2724 );
2725 }
2726
2727 #[test]
2728 fn custom_source() {
2729 struct MySource;
2730
2731 impl Source for MySource {
2732 fn init(&mut self) -> io::Result<()> {
2733 Ok(())
2734 }
2735
2736 fn get_line(&self, _line_no: usize) -> &[u8] {
2737 b"some code here"
2738 }
2739
2740 fn get_line_info(&self, line_no: usize) -> Line {
2741 Line {
2742 offset: 15 * line_no,
2743 byte_offset: 15 * line_no,
2744 len: 14,
2745 byte_len: 14,
2746 newline: 1,
2747 }
2748 }
2749
2750 fn line_for_bytes(&self, byte_pos: usize) -> (usize, Line) {
2751 let line_no = byte_pos / 15;
2752 (
2753 line_no,
2754 Line {
2755 offset: 15 * line_no,
2756 byte_offset: 15 * line_no,
2757 len: 14,
2758 byte_len: 14,
2759 newline: 1,
2760 },
2761 )
2762 }
2763
2764 fn line_for_chars(&self, char_pos: usize) -> (usize, Line) {
2765 let line_no = char_pos / 15;
2766 (
2767 line_no,
2768 Line {
2769 offset: 15 * line_no,
2770 byte_offset: 15 * line_no,
2771 len: 14,
2772 byte_len: 14,
2773 newline: 1,
2774 },
2775 )
2776 }
2777 }
2778
2779 let mut report = Report::new()
2780 .with_config(Config::new().with_color_disabled())
2781 .with_location(1485, 0)
2782 .with_title(Level::Error, "Error")
2783 .with_label(1485..1489usize)
2784 .with_message("here");
2785
2786 let output = report.render_to_string((MySource, "file.rs")).unwrap();
2787 assert_snapshot!(
2788 remove_trailing_whitespace(&output),
2789 @r##"
2790 Error: Error
2791 ╭─[ file.rs:100:1 ]
2792 │
2793 100 │ some code here
2794 │ ──┬─
2795 │ ╰─── here
2796 ─────╯
2797 "##
2798 );
2799 }
2800
2801 #[test]
2802 fn test_config_options() {
2803 // Test various config options
2804 let config = Config::new()
2805 .with_cross_gap(false)
2806 .with_compact(false)
2807 .with_underlines(true)
2808 .with_multiline_arrows(true)
2809 .with_tab_width(2)
2810 .with_limit_width(40)
2811 .with_ambi_width(2)
2812 .with_label_attach(LabelAttach::Start)
2813 .with_index_type(IndexType::Char)
2814 .with_char_set_ascii()
2815 .with_color_disabled();
2816
2817 let mut report = Report::new()
2818 .with_config(config)
2819 .with_title(Level::Error, "Test")
2820 .with_label(0..5)
2821 .with_message("here");
2822
2823 let output = report
2824 .render_to_string(("hello\tworld", "test.rs"))
2825 .unwrap();
2826 assert!(output.contains("hello"));
2827 }
2828
2829 #[test]
2830 fn test_index_type_byte() {
2831 let config = Config::new()
2832 .with_index_type(IndexType::Byte)
2833 .with_char_set_ascii()
2834 .with_color_disabled();
2835
2836 let mut report = Report::new()
2837 .with_config(config)
2838 .with_title(Level::Error, "Test")
2839 .with_label(0..5)
2840 .with_message("bytes");
2841
2842 let output = report.render_to_string(("hello", "test.rs")).unwrap();
2843 assert_snapshot!(
2844 remove_trailing_whitespace(&output),
2845 @r##"
2846 Error: Test
2847 ,-[ test.rs:1:1 ]
2848 |
2849 1 | hello
2850 | ^^|^^
2851 | `---- bytes
2852 ---'
2853 "##
2854 );
2855 }
2856
2857 #[test]
2858 fn test_label_attach_start() {
2859 let config = Config::new()
2860 .with_label_attach(LabelAttach::Start)
2861 .with_char_set_ascii()
2862 .with_color_disabled();
2863
2864 let mut report = Report::new()
2865 .with_config(config)
2866 .with_title(Level::Error, "Test")
2867 .with_label(0..5)
2868 .with_message("start");
2869
2870 let output = report.render_to_string(("hello world", "test.rs")).unwrap();
2871 assert!(output.contains("start"));
2872 }
2873
2874 #[test]
2875 fn test_label_attach_end() {
2876 let config = Config::new()
2877 .with_label_attach(LabelAttach::End)
2878 .with_char_set_ascii()
2879 .with_color_disabled();
2880
2881 let mut report = Report::new()
2882 .with_config(config)
2883 .with_title(Level::Error, "Test")
2884 .with_label(0..5)
2885 .with_message("end");
2886
2887 let output = report.render_to_string(("hello world", "test.rs")).unwrap();
2888 assert!(output.contains("end"));
2889 }
2890
2891 #[test]
2892 fn test_with_order() {
2893 let mut report = Report::new()
2894 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
2895 .with_title(Level::Error, "Test")
2896 .with_label(0..4)
2897 .with_message("second")
2898 .with_order(1)
2899 .with_label(0..4)
2900 .with_message("first")
2901 .with_order(-1);
2902
2903 let output = report.render_to_string(("code here", "test.rs")).unwrap();
2904 // Verify both labels appear
2905 assert!(output.contains("first"));
2906 assert!(output.contains("second"));
2907 }
2908
2909 #[test]
2910 fn test_with_priority() {
2911 let mut report = Report::new()
2912 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
2913 .with_title(Level::Error, "Test")
2914 .with_label(0..4)
2915 .with_message("high priority")
2916 .with_priority(10)
2917 .with_label(5..9)
2918 .with_message("low priority")
2919 .with_priority(0);
2920
2921 let output = report.render_to_string(("code here", "test.rs")).unwrap();
2922 assert!(output.contains("high priority"));
2923 assert!(output.contains("low priority"));
2924 }
2925
2926 #[test]
2927 fn test_with_help() {
2928 let mut report = Report::new()
2929 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
2930 .with_title(Level::Error, "Type error")
2931 .with_label(0..4)
2932 .with_message("wrong type")
2933 .with_help("try using .to_string()");
2934
2935 let output = report.render_to_string(("code", "test.rs")).unwrap();
2936 assert_snapshot!(
2937 remove_trailing_whitespace(&output),
2938 @r##"
2939 Error: Type error
2940 ,-[ test.rs:1:1 ]
2941 |
2942 1 | code
2943 | ^^|^
2944 | `--- wrong type
2945 |
2946 | Help: try using .to_string()
2947 ---'
2948 "##
2949 );
2950 }
2951
2952 #[test]
2953 fn test_with_note() {
2954 let mut report = Report::new()
2955 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
2956 .with_title(Level::Warning, "Unused variable")
2957 .with_label(0..4)
2958 .with_message("never used")
2959 .with_note("consider prefixing with `_`");
2960
2961 let output = report.render_to_string(("code", "test.rs")).unwrap();
2962 assert_snapshot!(
2963 remove_trailing_whitespace(&output),
2964 @r##"
2965 Warning: Unused variable
2966 ,-[ test.rs:1:1 ]
2967 |
2968 1 | code
2969 | ^^|^
2970 | `--- never used
2971 |
2972 | Note: consider prefixing with `_`
2973 ---'
2974 "##
2975 );
2976 }
2977
2978 #[test]
2979 fn test_multiple_help_and_notes() {
2980 let mut report = Report::new()
2981 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
2982 .with_title(Level::Error, "Error")
2983 .with_label(0..4)
2984 .with_message("problem")
2985 .with_help("first help")
2986 .with_help("second help")
2987 .with_note("first note")
2988 .with_note("second note");
2989
2990 let output = report.render_to_string(("code", "test.rs")).unwrap();
2991 assert_snapshot!(
2992 remove_trailing_whitespace(&output),
2993 @r##"
2994 Error: Error
2995 ,-[ test.rs:1:1 ]
2996 |
2997 1 | code
2998 | ^^|^
2999 | `--- problem
3000 |
3001 | Help 1: first help
3002 |
3003 | Help 2: second help
3004 |
3005 | Note 1: first note
3006 |
3007 | Note 2: second note
3008 ---'
3009 "##
3010 );
3011 }
3012
3013 #[test]
3014 fn test_empty_source() {
3015 let mut report = Report::new()
3016 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
3017 .with_title(Level::Error, "Empty file")
3018 .with_label(0..0)
3019 .with_message("empty");
3020
3021 // Should not panic
3022 let output = report.render_to_string(("", "empty.rs")).unwrap();
3023 assert_snapshot!(
3024 remove_trailing_whitespace(&output),
3025 @r##"
3026 Error: Empty file
3027 ,-[ empty.rs:1:1 ]
3028 |
3029 1 |
3030 | |
3031 | `- empty
3032 ---'
3033 "##
3034 );
3035 }
3036
3037 #[test]
3038 fn test_render_to_stdout() {
3039 let mut report = Report::new()
3040 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
3041 .with_title(Level::Error, "Test")
3042 .with_label(0..4)
3043 .with_message("test");
3044
3045 // Should not panic (output goes to stdout)
3046 let result = report.render_to_stdout(("code", "test.rs"));
3047 assert!(result.is_ok());
3048 }
3049
3050 #[test]
3051 fn test_render_to_writer() {
3052 let mut report = Report::new()
3053 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
3054 .with_title(Level::Error, "Test")
3055 .with_label(0..4)
3056 .with_message("test");
3057
3058 let mut buffer = Vec::new();
3059 {
3060 let buf = &mut buffer;
3061 let result = report.render_to_writer(buf, ("code", "test.rs"));
3062 assert!(result.is_ok());
3063 assert_snapshot!(
3064 remove_trailing_whitespace(&String::from_utf8_lossy(buf)),
3065 @r##"
3066 Error: Test
3067 ,-[ test.rs:1:1 ]
3068 |
3069 1 | code
3070 | ^^|^
3071 | `--- test
3072 ---'
3073 "##
3074 );
3075 }
3076
3077 let output = String::from_utf8(buffer).unwrap();
3078 assert_snapshot!(
3079 remove_trailing_whitespace(&output),
3080 @r##"
3081 Error: Test
3082 ,-[ test.rs:1:1 ]
3083 |
3084 1 | code
3085 | ^^|^
3086 | `--- test
3087 ---'
3088 "##
3089 );
3090 }
3091
3092 #[test]
3093 fn test_reset() {
3094 let report = Report::new()
3095 .with_config(Config::new().with_char_set_ascii().with_color_disabled())
3096 .with_title(Level::Error, "Test")
3097 .with_label(0..4)
3098 .with_message("test");
3099
3100 // Reset and reuse
3101 let mut report = report
3102 .reset()
3103 .with_title(Level::Warning, "New")
3104 .with_label(0..4)
3105 .with_message("new");
3106
3107 let output = report.render_to_string(("code", "new.rs")).unwrap();
3108 assert_snapshot!(
3109 remove_trailing_whitespace(&output),
3110 @r##"
3111 Warning: New
3112 ,-[ new.rs:1:1 ]
3113 |
3114 1 | code
3115 | ^^|^
3116 | `--- new
3117 ---'
3118 "##
3119 );
3120 }
3121
3122 #[test]
3123 fn test_char_set_conversion() {
3124 let ascii = CharSet::ascii();
3125 let unicode = CharSet::unicode();
3126
3127 // ASCII should use simple characters
3128 assert_eq!(ascii.hbar, '-');
3129 assert_eq!(ascii.vbar, '|');
3130
3131 // Unicode should use box-drawing characters
3132 assert_ne!(unicode.hbar, '-');
3133 assert_ne!(unicode.vbar, '|');
3134 }
3135}