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 ]
24312432             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 ]
24592460             1 │ import foo
2461               │        ─┬─
2462               │         ╰─── imported here
24632464               │─[ foo.rs:1:8 ]
24652466             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 ]
24982499             1 │ hello
2500               │ ──┬──
2501               │   ╰──── from Vec<u8>
25022503               │─[ string.txt:1:8 ]
25042505             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 ]
25272528             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 ]
25552556             1 │ code1
2557               │ ──┬─
2558               │   ╰─── in a
25592560               │─[ b.rs:1:1 ]
25612562             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 ]
27182719             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 ]
27922793             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}