Skip to main content

miden_debug_types/
source_file.rs

1use alloc::{
2    boxed::Box,
3    string::{String, ToString},
4    sync::Arc,
5    vec::Vec,
6};
7use core::{fmt, num::NonZeroU32, ops::Range};
8
9#[cfg(feature = "arbitrary")]
10use proptest::prelude::*;
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14use super::{FileLineCol, Position, Selection, SourceId, SourceSpan, Uri};
15
16// SOURCE LANGUAGE
17// ================================================================================================
18
19#[derive(Debug, Copy, Clone, PartialEq, Eq)]
20pub enum SourceLanguage {
21    Masm,
22    Rust,
23    Other(&'static str),
24}
25
26#[cfg(feature = "arbitrary")]
27impl Arbitrary for SourceLanguage {
28    type Parameters = ();
29    type Strategy = BoxedStrategy<Self>;
30
31    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
32        prop_oneof![
33            Just(Self::Masm),
34            Just(Self::Rust),
35            Just(Self::Other("other")),
36            Just(Self::Other("unknown")),
37        ]
38        .boxed()
39    }
40}
41
42impl AsRef<str> for SourceLanguage {
43    fn as_ref(&self) -> &str {
44        match self {
45            Self::Masm => "masm",
46            Self::Rust => "rust",
47            Self::Other(other) => other,
48        }
49    }
50}
51
52// SOURCE FILE
53// ================================================================================================
54
55/// A [SourceFile] represents a single file stored in a [super::SourceManager]
56#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
58pub struct SourceFile {
59    /// The unique identifier allocated for this [SourceFile] by its owning [super::SourceManager]
60    id: SourceId,
61    /// The file content
62    #[cfg_attr(
63        feature = "serde",
64        serde(deserialize_with = "SourceContent::deserialize_and_recompute_line_starts")
65    )]
66    content: SourceContent,
67}
68
69impl miette::SourceCode for SourceFile {
70    fn read_span<'a>(
71        &'a self,
72        span: &miette::SourceSpan,
73        context_lines_before: usize,
74        context_lines_after: usize,
75    ) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
76        let mut start =
77            u32::try_from(span.offset()).map_err(|_| miette::MietteError::OutOfBounds)?;
78        let len = u32::try_from(span.len()).map_err(|_| miette::MietteError::OutOfBounds)?;
79        let mut end = start.checked_add(len).ok_or(miette::MietteError::OutOfBounds)?;
80        if context_lines_before > 0 {
81            let line_index = self.content.line_index(start.into());
82            let start_line_index = line_index.saturating_sub(context_lines_before as u32);
83            start = self.content.line_start(start_line_index).map(ByteIndex::to_u32).unwrap_or(0);
84        }
85        if context_lines_after > 0 {
86            let line_index = self.content.line_index(end.into());
87            let end_line_index = line_index
88                .checked_add(context_lines_after as u32)
89                .ok_or(miette::MietteError::OutOfBounds)?;
90            end = self
91                .content
92                .line_range(end_line_index)
93                .map(|range| range.end.to_u32())
94                .unwrap_or_else(|| self.content.source_range().end.to_u32());
95        }
96        Ok(Box::new(ScopedSourceFileRef {
97            file: self,
98            span: miette::SourceSpan::new((start as usize).into(), end.abs_diff(start) as usize),
99        }))
100    }
101}
102
103impl SourceFile {
104    /// Create a new [SourceFile] from its raw components
105    pub fn new(id: SourceId, lang: SourceLanguage, uri: Uri, content: impl Into<Box<str>>) -> Self {
106        let content = SourceContent::new(lang, uri, content.into());
107        Self { id, content }
108    }
109
110    /// This function is intended for use by [super::SourceManager] implementations that need to
111    /// construct a [SourceFile] from its raw components (i.e. the identifier for the source file
112    /// and its content).
113    ///
114    /// Since the only entity that should be constructing a [SourceId] is a [super::SourceManager],
115    /// it is only valid to call this function in one of two scenarios:
116    ///
117    /// 1. You are a [super::SourceManager] constructing a [SourceFile] after allocating a
118    ///    [SourceId]
119    /// 2. You pass [`SourceId::default()`], i.e. [`SourceId::UNKNOWN`] for the source identifier.
120    ///    The resulting [SourceFile] will be valid and safe to use in a context where there isn't a
121    ///    [super::SourceManager] present. If there is a source manager in use, then constructing
122    ///    detached [SourceFile]s is _not_ recommended, because it will make it confusing to
123    ///    determine whether a given [SourceFile] reference is safe to use.
124    ///
125    /// You should rarely, if ever, fall in camp 2 - but it can be handy in some narrow cases
126    pub fn from_raw_parts(id: SourceId, content: SourceContent) -> Self {
127        Self { id, content }
128    }
129
130    /// Get the [SourceId] associated with this file
131    pub const fn id(&self) -> SourceId {
132        self.id
133    }
134
135    /// Get the name of this source file
136    pub fn uri(&self) -> &Uri {
137        self.content.uri()
138    }
139
140    /// Returns a reference to the underlying [SourceContent]
141    pub fn content(&self) -> &SourceContent {
142        &self.content
143    }
144
145    /// Returns a mutable reference to the underlying [SourceContent]
146    pub fn content_mut(&mut self) -> &mut SourceContent {
147        &mut self.content
148    }
149
150    /// Returns the number of lines in this file
151    pub fn line_count(&self) -> usize {
152        self.content.line_starts.len()
153    }
154
155    /// Returns the number of bytes in this file
156    pub fn len(&self) -> usize {
157        self.content.len()
158    }
159
160    /// Returns true if this file is empty
161    pub fn is_empty(&self) -> bool {
162        self.content.is_empty()
163    }
164
165    /// Get the underlying content of this file
166    #[inline(always)]
167    pub fn as_str(&self) -> &str {
168        self.content.as_str()
169    }
170
171    /// Get the underlying content of this file as a byte slice
172    #[inline(always)]
173    pub fn as_bytes(&self) -> &[u8] {
174        self.content.as_bytes()
175    }
176
177    /// Returns a [SourceSpan] covering the entirety of this file
178    #[inline]
179    pub fn source_span(&self) -> SourceSpan {
180        let range = self.content.source_range();
181        SourceSpan::new(self.id, range.start.0..range.end.0)
182    }
183
184    /// Returns a subset of the underlying content as a string slice.
185    ///
186    /// The bounds of the given span are byte indices, _not_ character indices.
187    ///
188    /// Returns `None` if the given span is out of bounds, or if the bounds do not
189    /// fall on valid UTF-8 character boundaries.
190    #[inline(always)]
191    pub fn source_slice(&self, span: impl Into<Range<usize>>) -> Option<&str> {
192        self.content.source_slice(span)
193    }
194
195    /// Returns a [SourceFileRef] corresponding to the bytes contained in the specified span.
196    pub fn slice(self: &Arc<Self>, span: impl Into<Range<u32>>) -> SourceFileRef {
197        SourceFileRef::new(Arc::clone(self), span)
198    }
199
200    /// Get a [SourceSpan] which points to the first byte of the character at `column` on `line`
201    ///
202    /// Returns `None` if the given line/column is out of bounds for this file.
203    pub fn line_column_to_span(
204        &self,
205        line: LineNumber,
206        column: ColumnNumber,
207    ) -> Option<SourceSpan> {
208        let offset = self.content.line_column_to_offset(line.into(), column.into())?;
209        Some(SourceSpan::at(self.id, offset.0))
210    }
211
212    /// Get a [FileLineCol] equivalent to the start of the given [SourceSpan]
213    pub fn location(&self, span: SourceSpan) -> FileLineCol {
214        assert_eq!(span.source_id(), self.id, "mismatched source ids");
215
216        self.content
217            .location(ByteIndex(span.into_range().start))
218            .expect("invalid source span: starting byte is out of bounds")
219    }
220}
221
222impl AsRef<str> for SourceFile {
223    #[inline(always)]
224    fn as_ref(&self) -> &str {
225        self.as_str()
226    }
227}
228
229impl AsRef<[u8]> for SourceFile {
230    #[inline(always)]
231    fn as_ref(&self) -> &[u8] {
232        self.as_bytes()
233    }
234}
235
236// SOURCE FILE REF
237// ================================================================================================
238
239/// A reference to a specific spanned region of a [SourceFile], that provides access to the actual
240/// [SourceFile], but scoped to the span it was created with.
241///
242/// This is useful in error types that implement [miette::Diagnostic], as it contains all of the
243/// data necessary to render the source code being referenced, without a [super::SourceManager] on
244/// hand.
245#[derive(Debug, Clone)]
246pub struct SourceFileRef {
247    file: Arc<SourceFile>,
248    span: SourceSpan,
249}
250
251impl SourceFileRef {
252    /// Create a [SourceFileRef] from a [SourceFile] and desired span (in bytes)
253    ///
254    /// The given span will be constrained to the bytes of `file`, so a span that reaches out of
255    /// bounds will have its end bound set to the last byte of the file.
256    pub fn new(file: Arc<SourceFile>, span: impl Into<Range<u32>>) -> Self {
257        let span = span.into();
258        let end = core::cmp::min(span.end, file.len() as u32);
259        let span = SourceSpan::new(file.id(), span.start..end);
260        Self { file, span }
261    }
262
263    /// Returns a ref-counted handle to the underlying [SourceFile]
264    pub fn source_file(&self) -> Arc<SourceFile> {
265        self.file.clone()
266    }
267
268    /// Returns the URI of the file this [SourceFileRef] is selecting
269    pub fn uri(&self) -> &Uri {
270        self.file.uri()
271    }
272
273    /// Returns the [SourceSpan] selected by this [SourceFileRef]
274    pub const fn span(&self) -> SourceSpan {
275        self.span
276    }
277
278    /// Returns the underlying `str` selected by this [SourceFileRef]
279    pub fn as_str(&self) -> &str {
280        self.file.source_slice(self.span).unwrap()
281    }
282
283    /// Returns the underlying bytes selected by this [SourceFileRef]
284    #[inline]
285    pub fn as_bytes(&self) -> &[u8] {
286        self.as_str().as_bytes()
287    }
288
289    /// Returns the number of bytes represented by the subset of the underlying file that is covered
290    /// by this [SourceFileRef]
291    pub fn len(&self) -> usize {
292        self.span.len()
293    }
294
295    /// Returns true if this selection is empty
296    pub fn is_empty(&self) -> bool {
297        self.len() == 0
298    }
299}
300
301impl Eq for SourceFileRef {}
302
303impl PartialEq for SourceFileRef {
304    fn eq(&self, other: &Self) -> bool {
305        self.as_str() == other.as_str()
306    }
307}
308
309impl Ord for SourceFileRef {
310    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
311        self.as_str().cmp(other.as_str())
312    }
313}
314
315impl PartialOrd for SourceFileRef {
316    #[inline(always)]
317    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
318        Some(self.cmp(other))
319    }
320}
321
322impl core::hash::Hash for SourceFileRef {
323    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
324        self.as_str().hash(state);
325    }
326}
327
328impl AsRef<str> for SourceFileRef {
329    #[inline(always)]
330    fn as_ref(&self) -> &str {
331        self.as_str()
332    }
333}
334
335impl AsRef<[u8]> for SourceFileRef {
336    #[inline(always)]
337    fn as_ref(&self) -> &[u8] {
338        self.as_bytes()
339    }
340}
341
342impl From<&SourceFileRef> for miette::SourceSpan {
343    fn from(source: &SourceFileRef) -> Self {
344        source.span.into()
345    }
346}
347
348/// Used to implement [miette::SpanContents] for [SourceFile] and [SourceFileRef]
349struct ScopedSourceFileRef<'a> {
350    file: &'a SourceFile,
351    span: miette::SourceSpan,
352}
353
354impl<'a> miette::SpanContents<'a> for ScopedSourceFileRef<'a> {
355    #[inline]
356    fn data(&self) -> &'a [u8] {
357        let start = self.span.offset();
358        let end = start + self.span.len();
359        &self.file.as_bytes()[start..end]
360    }
361
362    #[inline]
363    fn span(&self) -> &miette::SourceSpan {
364        &self.span
365    }
366
367    fn line(&self) -> usize {
368        let offset = self.span.offset() as u32;
369        self.file.content.line_index(offset.into()).to_usize()
370    }
371
372    fn column(&self) -> usize {
373        let start = self.span.offset() as u32;
374        let end = start + self.span.len() as u32;
375        let span = SourceSpan::new(self.file.id(), start..end);
376        let loc = self.file.location(span);
377        loc.column.to_index().to_usize()
378    }
379
380    #[inline]
381    fn line_count(&self) -> usize {
382        self.file.line_count()
383    }
384
385    #[inline]
386    fn name(&self) -> Option<&str> {
387        Some(self.file.uri().as_ref())
388    }
389
390    #[inline]
391    fn language(&self) -> Option<&str> {
392        None
393    }
394}
395
396impl miette::SourceCode for SourceFileRef {
397    #[inline]
398    fn read_span<'a>(
399        &'a self,
400        span: &miette::SourceSpan,
401        context_lines_before: usize,
402        context_lines_after: usize,
403    ) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
404        self.file.read_span(span, context_lines_before, context_lines_after)
405    }
406}
407
408// SOURCE CONTENT
409// ================================================================================================
410
411/// Represents key information about a source file and its content:
412///
413/// * The path to the file (or its name, in the case of virtual files)
414/// * The content of the file
415/// * The byte offsets of every line in the file, for use in looking up line/column information
416#[derive(Clone)]
417#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
418pub struct SourceContent {
419    /// The language identifier for this source file
420    language: Box<str>,
421    /// The path (or name) of this file
422    uri: Uri,
423    /// The underlying content of this file
424    content: String,
425    /// The byte offsets for each line in this file
426    #[cfg_attr(feature = "serde", serde(default, skip))]
427    line_starts: Vec<ByteIndex>,
428    /// The document version
429    #[cfg_attr(feature = "serde", serde(default))]
430    version: i32,
431}
432
433impl fmt::Debug for SourceContent {
434    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
435        let Self {
436            language,
437            uri,
438            content,
439            line_starts,
440            version,
441        } = self;
442        f.debug_struct("SourceContent")
443            .field("version", version)
444            .field("language", language)
445            .field("uri", uri)
446            .field("size_in_bytes", &content.len())
447            .field("line_count", &line_starts.len())
448            .field("content", content)
449            .finish()
450    }
451}
452
453impl Eq for SourceContent {}
454
455impl PartialEq for SourceContent {
456    #[inline]
457    fn eq(&self, other: &Self) -> bool {
458        self.language == other.language && self.uri == other.uri && self.content == other.content
459    }
460}
461
462impl Ord for SourceContent {
463    #[inline]
464    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
465        self.uri.cmp(&other.uri).then_with(|| self.content.cmp(&other.content))
466    }
467}
468
469impl PartialOrd for SourceContent {
470    #[inline]
471    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
472        Some(self.cmp(other))
473    }
474}
475
476impl core::hash::Hash for SourceContent {
477    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
478        self.language.hash(state);
479        self.uri.hash(state);
480        self.content.hash(state);
481    }
482}
483
484#[derive(Debug, thiserror::Error)]
485pub enum SourceContentUpdateError {
486    #[error("invalid content selection: start position of {}:{} is out of bounds", .0.line, .0.character)]
487    InvalidSelectionStart(Position),
488    #[error("invalid content selection: end position of {}:{} is out of bounds", .0.line, .0.character)]
489    InvalidSelectionEnd(Position),
490}
491
492impl SourceContent {
493    /// Create a new [SourceContent] from the (possibly virtual) file path, and its content as a
494    /// UTF-8 string.
495    ///
496    /// When created, the line starts for this file will be computed, which requires scanning the
497    /// file content once.
498    pub fn new(language: impl AsRef<str>, uri: impl Into<Uri>, content: impl Into<String>) -> Self {
499        let language = language.as_ref().to_string().into_boxed_str();
500        let content: String = content.into();
501        let bytes = content.as_bytes();
502
503        assert!(
504            bytes.len() < u32::MAX as usize,
505            "unsupported source file: current maximum supported length in bytes is 2^32"
506        );
507
508        let line_starts = compute_line_starts(&content, None);
509
510        Self {
511            language,
512            uri: uri.into(),
513            content,
514            line_starts,
515            version: 0,
516        }
517    }
518
519    /// Get the language identifier of this source file
520    pub fn language(&self) -> &str {
521        &self.language
522    }
523
524    /// Get the current version of this source file's content
525    pub fn version(&self) -> i32 {
526        self.version
527    }
528
529    /// Set the current version of this content
530    #[inline(always)]
531    pub fn set_version(&mut self, version: i32) {
532        self.version = version;
533    }
534
535    /// Get the URI of this source file
536    #[inline]
537    pub fn uri(&self) -> &Uri {
538        &self.uri
539    }
540
541    /// Returns the underlying content as a string slice
542    #[inline(always)]
543    pub fn as_str(&self) -> &str {
544        self.content.as_ref()
545    }
546
547    /// Returns the underlying content as a byte slice
548    #[inline(always)]
549    pub fn as_bytes(&self) -> &[u8] {
550        self.content.as_bytes()
551    }
552
553    /// Returns the size in bytes of the underlying content
554    #[inline(always)]
555    pub fn len(&self) -> usize {
556        self.content.len()
557    }
558
559    /// Returns true if the underlying content is empty
560    #[inline(always)]
561    pub fn is_empty(&self) -> bool {
562        self.content.is_empty()
563    }
564
565    /// Returns the range of valid byte indices for this file
566    #[inline]
567    pub fn source_range(&self) -> Range<ByteIndex> {
568        ByteIndex(0)..ByteIndex(self.content.len() as u32)
569    }
570
571    /// Returns a subset of the underlying content as a string slice.
572    ///
573    /// The bounds of the given span are byte indices, _not_ character indices.
574    ///
575    /// Returns `None` if the given span is out of bounds, or if the bounds do not
576    /// fall on valid UTF-8 character boundaries.
577    #[inline(always)]
578    pub fn source_slice(&self, span: impl Into<Range<usize>>) -> Option<&str> {
579        self.as_str().get(span.into())
580    }
581
582    /// Returns a subset of the underlying content as a byte slice.
583    ///
584    /// Returns `None` if the given span is out of bounds
585    #[inline(always)]
586    pub fn byte_slice(&self, span: impl Into<Range<ByteIndex>>) -> Option<&[u8]> {
587        let Range { start, end } = span.into();
588        self.as_bytes().get(start.to_usize()..end.to_usize())
589    }
590
591    /// Like [Self::source_slice], but the slice is computed like a selection in an editor, i.e.
592    /// based on line/column positions, rather than raw character indices.
593    ///
594    /// This is useful when mapping LSP operations to content in the source file.
595    pub fn select(&self, mut range: Selection) -> Option<&str> {
596        range.canonicalize();
597
598        let start = self.line_column_to_offset(range.start.line, range.start.character)?;
599        let end = self.line_column_to_offset(range.end.line, range.end.character)?;
600
601        Some(&self.as_str()[start.to_usize()..end.to_usize()])
602    }
603
604    /// Returns the number of lines in the source content
605    pub fn line_count(&self) -> usize {
606        self.line_starts.len()
607    }
608
609    /// Returns the byte index at which the line corresponding to `line_index` starts
610    ///
611    /// Returns `None` if the given index is out of bounds
612    pub fn line_start(&self, line_index: LineIndex) -> Option<ByteIndex> {
613        self.line_starts.get(line_index.to_usize()).copied()
614    }
615
616    /// Returns the index of the last line in this file
617    pub fn last_line_index(&self) -> LineIndex {
618        LineIndex(self.line_count().saturating_sub(1).try_into().expect("too many lines in file"))
619    }
620
621    /// Get the range of byte indices covered by the given line
622    pub fn line_range(&self, line_index: LineIndex) -> Option<Range<ByteIndex>> {
623        let line_start = self.line_start(line_index)?;
624        match self.line_start(line_index + 1) {
625            Some(line_end) => Some(line_start..line_end),
626            None => Some(line_start..ByteIndex(self.content.len() as u32)),
627        }
628    }
629
630    /// Get the index of the line to which `byte_index` belongs
631    pub fn line_index(&self, byte_index: ByteIndex) -> LineIndex {
632        match self.line_starts.binary_search(&byte_index) {
633            Ok(line) => LineIndex(line as u32),
634            Err(next_line) => LineIndex(next_line as u32 - 1),
635        }
636    }
637
638    /// Get the [ByteIndex] corresponding to the given line and column indices.
639    ///
640    /// Returns `None` if the line or column indices are out of bounds.
641    pub fn line_column_to_offset(
642        &self,
643        line_index: LineIndex,
644        column_index: ColumnIndex,
645    ) -> Option<ByteIndex> {
646        let column_index = column_index.to_usize();
647        let line_span = self.line_range(line_index)?;
648        let line_src = self
649            .content
650            .get(line_span.start.to_usize()..line_span.end.to_usize())
651            .expect("invalid line boundaries: invalid utf-8");
652        if line_src.len() < column_index {
653            return None;
654        }
655        let (pre, _) = line_src.split_at(column_index);
656        let start = line_span.start;
657        Some(start + ByteOffset::from_str_len(pre))
658    }
659
660    /// Get a [FileLineCol] corresponding to the line/column in this file at which `byte_index`
661    /// occurs
662    pub fn location(&self, byte_index: ByteIndex) -> Option<FileLineCol> {
663        let line_index = self.line_index(byte_index);
664        let line_start_index = self.line_start(line_index)?;
665        let line_src = self.content.get(line_start_index.to_usize()..byte_index.to_usize())?;
666        let column_index = ColumnIndex::from(line_src.chars().count() as u32);
667        Some(FileLineCol {
668            uri: self.uri.clone(),
669            line: line_index.number(),
670            column: column_index.number(),
671        })
672    }
673
674    /// Update the source document after being notified of a change event.
675    ///
676    /// The `version` indicates the new version of the document
677    ///
678    /// NOTE: This is intended to update a [super::SourceManager]'s view of the content of the
679    /// document, _not_ to perform an update against the actual file, wherever it may be.
680    pub fn update(
681        &mut self,
682        text: String,
683        range: Option<Selection>,
684        version: i32,
685    ) -> Result<(), SourceContentUpdateError> {
686        match range {
687            Some(range) => {
688                let start = self
689                    .line_column_to_offset(range.start.line, range.start.character)
690                    .ok_or(SourceContentUpdateError::InvalidSelectionStart(range.start))?
691                    .to_usize();
692                let end = self
693                    .line_column_to_offset(range.end.line, range.end.character)
694                    .ok_or(SourceContentUpdateError::InvalidSelectionEnd(range.end))?
695                    .to_usize();
696                assert!(start <= end, "start of range must be less than end, got {start}..{end}",);
697                self.content.replace_range(start..end, &text);
698
699                let added_line_starts = compute_line_starts(&text, Some(start as u32));
700                let num_added = added_line_starts.len();
701                let splice_start = range.start.line.to_usize() + 1;
702                // Determine deletion range in line_starts to respect Selection semantics.
703                // For multi-line edits, remove line starts from (start.line + 1) up to end.line
704                // inclusive, since all intervening newlines are removed by the
705                // replacement, regardless of end.character.
706                enum Deletion {
707                    Empty,
708                    Inclusive(usize), // inclusive end index
709                }
710                let deletion = if range.start.line == range.end.line {
711                    Deletion::Empty
712                } else {
713                    let mut end_line_for_splice = range.end.line.to_usize();
714                    if !self.line_starts.is_empty() {
715                        let max_idx = self.line_starts.len() - 1;
716                        if end_line_for_splice > max_idx {
717                            end_line_for_splice = max_idx;
718                        }
719                    }
720                    if end_line_for_splice >= splice_start {
721                        Deletion::Inclusive(end_line_for_splice)
722                    } else {
723                        Deletion::Empty
724                    }
725                };
726
727                match deletion {
728                    Deletion::Empty => {
729                        self.line_starts.splice(splice_start..splice_start, added_line_starts);
730                    },
731                    Deletion::Inclusive(end_idx) => {
732                        self.line_starts.splice(splice_start..=end_idx, added_line_starts);
733                    },
734                }
735
736                let diff =
737                    (text.len() as i32).saturating_sub_unsigned((end as u32) - (start as u32));
738                if diff != 0 {
739                    for i in (splice_start + num_added)..self.line_starts.len() {
740                        self.line_starts[i] =
741                            ByteIndex(self.line_starts[i].to_u32().saturating_add_signed(diff));
742                    }
743                }
744            },
745            None => {
746                self.line_starts = compute_line_starts(&text, None);
747                self.content = text;
748            },
749        }
750
751        self.version = version;
752
753        Ok(())
754    }
755}
756
757#[cfg(feature = "serde")]
758impl SourceContent {
759    fn deserialize_and_recompute_line_starts<'de, D>(deserializer: D) -> Result<Self, D::Error>
760    where
761        D: serde::Deserializer<'de>,
762    {
763        let mut content = SourceContent::deserialize(deserializer)?;
764        content.line_starts = compute_line_starts(&content.content, None);
765        Ok(content)
766    }
767}
768
769fn compute_line_starts(text: &str, text_offset: Option<u32>) -> Vec<ByteIndex> {
770    let bytes = text.as_bytes();
771    let initial_line_offset = match text_offset {
772        Some(_) => None,
773        None => Some(ByteIndex(0)),
774    };
775    let text_offset = text_offset.unwrap_or(0);
776    initial_line_offset
777        .into_iter()
778        .chain(
779            memchr::memchr_iter(b'\n', bytes)
780                .map(|offset| ByteIndex(text_offset + (offset + 1) as u32)),
781        )
782        .collect()
783}
784
785// SOURCE CONTENT INDICES
786// ================================================================================================
787
788/// An index representing the offset in bytes from the start of a source file
789#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
790#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
791#[cfg_attr(feature = "serde", serde(transparent))]
792#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
793pub struct ByteIndex(pub u32);
794
795impl ByteIndex {
796    /// Create a [ByteIndex] from a raw `u32` index
797    pub const fn new(index: u32) -> Self {
798        Self(index)
799    }
800
801    /// Get the raw index as a usize
802    #[inline(always)]
803    pub const fn to_usize(self) -> usize {
804        self.0 as usize
805    }
806
807    /// Get the raw index as a u32
808    #[inline(always)]
809    pub const fn to_u32(self) -> u32 {
810        self.0
811    }
812}
813
814impl core::ops::Add<ByteOffset> for ByteIndex {
815    type Output = ByteIndex;
816
817    fn add(self, rhs: ByteOffset) -> Self {
818        Self((self.0 as i64 + rhs.0) as u32)
819    }
820}
821
822impl core::ops::Add<u32> for ByteIndex {
823    type Output = ByteIndex;
824
825    fn add(self, rhs: u32) -> Self {
826        Self(self.0 + rhs)
827    }
828}
829
830impl core::ops::AddAssign<ByteOffset> for ByteIndex {
831    fn add_assign(&mut self, rhs: ByteOffset) {
832        *self = *self + rhs;
833    }
834}
835
836impl core::ops::AddAssign<u32> for ByteIndex {
837    fn add_assign(&mut self, rhs: u32) {
838        self.0 += rhs;
839    }
840}
841
842impl core::ops::Sub<ByteOffset> for ByteIndex {
843    type Output = ByteIndex;
844
845    fn sub(self, rhs: ByteOffset) -> Self {
846        Self((self.0 as i64 - rhs.0) as u32)
847    }
848}
849
850impl core::ops::Sub<u32> for ByteIndex {
851    type Output = ByteIndex;
852
853    fn sub(self, rhs: u32) -> Self {
854        Self(self.0 - rhs)
855    }
856}
857
858impl core::ops::SubAssign<ByteOffset> for ByteIndex {
859    fn sub_assign(&mut self, rhs: ByteOffset) {
860        *self = *self - rhs;
861    }
862}
863
864impl core::ops::SubAssign<u32> for ByteIndex {
865    fn sub_assign(&mut self, rhs: u32) {
866        self.0 -= rhs;
867    }
868}
869
870impl From<u32> for ByteIndex {
871    fn from(index: u32) -> Self {
872        Self(index)
873    }
874}
875
876impl From<ByteIndex> for u32 {
877    fn from(index: ByteIndex) -> Self {
878        index.0
879    }
880}
881
882impl fmt::Display for ByteIndex {
883    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
884        fmt::Display::fmt(&self.0, f)
885    }
886}
887
888#[cfg(feature = "arbitrary")]
889impl Arbitrary for ByteIndex {
890    type Parameters = ();
891    type Strategy = BoxedStrategy<Self>;
892
893    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
894        any::<u32>().prop_map(Self).boxed()
895    }
896}
897
898/// An offset in bytes relative to some [ByteIndex]
899#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
900pub struct ByteOffset(i64);
901
902impl ByteOffset {
903    /// Compute the offset in bytes represented by the given `char`
904    pub fn from_char_len(c: char) -> ByteOffset {
905        Self(c.len_utf8() as i64)
906    }
907
908    /// Compute the offset in bytes represented by the given `str`
909    pub fn from_str_len(s: &str) -> ByteOffset {
910        Self(s.len() as i64)
911    }
912}
913
914impl core::ops::Add for ByteOffset {
915    type Output = ByteOffset;
916
917    fn add(self, rhs: Self) -> Self {
918        Self(self.0 + rhs.0)
919    }
920}
921
922impl core::ops::AddAssign for ByteOffset {
923    fn add_assign(&mut self, rhs: Self) {
924        self.0 += rhs.0;
925    }
926}
927
928impl core::ops::Sub for ByteOffset {
929    type Output = ByteOffset;
930
931    fn sub(self, rhs: Self) -> Self {
932        Self(self.0 - rhs.0)
933    }
934}
935
936impl core::ops::SubAssign for ByteOffset {
937    fn sub_assign(&mut self, rhs: Self) {
938        self.0 -= rhs.0;
939    }
940}
941
942impl fmt::Display for ByteOffset {
943    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944        fmt::Display::fmt(&self.0, f)
945    }
946}
947
948macro_rules! declare_dual_number_and_index_type {
949    ($name:ident, $description:literal) => {
950        paste::paste! {
951            declare_dual_number_and_index_type!([<$name Index>], [<$name Number>], $description);
952        }
953    };
954
955    ($index_name:ident, $number_name:ident, $description:literal) => {
956        #[doc = concat!("A zero-indexed ", $description, " number")]
957        #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
958        #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
959        #[cfg_attr(feature = "serde", serde(transparent))]
960        #[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
961        pub struct $index_name(pub u32);
962
963        impl $index_name {
964            #[doc = concat!("Convert to a [", stringify!($number_name), "]")]
965            pub const fn number(self) -> $number_name {
966                $number_name(unsafe { NonZeroU32::new_unchecked(self.0 + 1) })
967            }
968
969            /// Get the raw index value as a usize
970            #[inline(always)]
971            pub const fn to_usize(self) -> usize {
972                self.0 as usize
973            }
974
975            /// Get the raw index value as a u32
976            #[inline(always)]
977            pub const fn to_u32(self) -> u32 {
978                self.0
979            }
980
981            /// Add `offset` to this index, returning `None` on overflow
982            pub fn checked_add(self, offset: u32) -> Option<Self> {
983                self.0.checked_add(offset).map(Self)
984            }
985
986            /// Add a signed `offset` to this index, returning `None` on overflow
987            pub fn checked_add_signed(self, offset: i32) -> Option<Self> {
988                self.0.checked_add_signed(offset).map(Self)
989            }
990
991            /// Subtract `offset` from this index, returning `None` on underflow
992            pub fn checked_sub(self, offset: u32) -> Option<Self> {
993                self.0.checked_sub(offset).map(Self)
994            }
995
996            /// Add `offset` to this index, saturating to `u32::MAX` on overflow
997            pub const fn saturating_add(self, offset: u32) -> Self {
998                Self(self.0.saturating_add(offset))
999            }
1000
1001            /// Add a signed `offset` to this index, saturating to `0` on underflow, and `u32::MAX`
1002            /// on overflow.
1003            pub const fn saturating_add_signed(self, offset: i32) -> Self {
1004                Self(self.0.saturating_add_signed(offset))
1005            }
1006
1007            /// Subtract `offset` from this index, saturating to `0` on overflow
1008            pub const fn saturating_sub(self, offset: u32) -> Self {
1009                Self(self.0.saturating_sub(offset))
1010            }
1011        }
1012
1013        impl From<u32> for $index_name {
1014            #[inline]
1015            fn from(index: u32) -> Self {
1016                Self(index)
1017            }
1018        }
1019
1020        impl From<$number_name> for $index_name {
1021            #[inline]
1022            fn from(index: $number_name) -> Self {
1023                Self(index.to_u32() - 1)
1024            }
1025        }
1026
1027        impl core::ops::Add<u32> for $index_name {
1028            type Output = Self;
1029
1030            #[inline]
1031            fn add(self, rhs: u32) -> Self {
1032                Self(self.0 + rhs)
1033            }
1034        }
1035
1036        impl core::ops::AddAssign<u32> for $index_name {
1037            fn add_assign(&mut self, rhs: u32) {
1038                let result = *self + rhs;
1039                *self = result;
1040            }
1041        }
1042
1043        impl core::ops::Add<i32> for $index_name {
1044            type Output = Self;
1045
1046            fn add(self, rhs: i32) -> Self {
1047                self.checked_add_signed(rhs).expect("invalid offset: overflow occurred")
1048            }
1049        }
1050
1051        impl core::ops::AddAssign<i32> for $index_name {
1052            fn add_assign(&mut self, rhs: i32) {
1053                let result = *self + rhs;
1054                *self = result;
1055            }
1056        }
1057
1058        impl core::ops::Sub<u32> for $index_name {
1059            type Output = Self;
1060
1061            #[inline]
1062            fn sub(self, rhs: u32) -> Self {
1063                Self(self.0 - rhs)
1064            }
1065        }
1066
1067        impl core::ops::SubAssign<u32> for $index_name {
1068            fn sub_assign(&mut self, rhs: u32) {
1069                let result = *self - rhs;
1070                *self = result;
1071            }
1072        }
1073
1074        impl fmt::Display for $index_name {
1075            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1076                fmt::Display::fmt(&self.0, f)
1077            }
1078        }
1079
1080        #[cfg(feature = "arbitrary")]
1081        impl Arbitrary for $index_name {
1082            type Parameters = ();
1083            type Strategy = BoxedStrategy<Self>;
1084
1085            fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1086                any::<u32>().prop_map(Self).boxed()
1087            }
1088        }
1089
1090        #[doc = concat!("A one-indexed ", $description, " number")]
1091        #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1092        #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1093        #[cfg_attr(feature = "serde", serde(transparent))]
1094        #[cfg_attr(
1095            all(feature = "arbitrary", test),
1096            miden_test_serde_macros::serde_test(binary_serde(true))
1097        )]
1098        pub struct $number_name(NonZeroU32);
1099
1100        impl Default for $number_name {
1101            fn default() -> Self {
1102                Self(unsafe { NonZeroU32::new_unchecked(1) })
1103            }
1104        }
1105
1106        impl $number_name {
1107            pub const fn new(number: u32) -> Option<Self> {
1108                match NonZeroU32::new(number) {
1109                    Some(num) => Some(Self(num)),
1110                    None => None,
1111                }
1112            }
1113
1114            #[doc = concat!("Convert to a [", stringify!($index_name), "]")]
1115            pub const fn to_index(self) -> $index_name {
1116                $index_name(self.to_u32().saturating_sub(1))
1117            }
1118
1119            /// Get the raw value as a usize
1120            #[inline(always)]
1121            pub const fn to_usize(self) -> usize {
1122                self.0.get() as usize
1123            }
1124
1125            /// Get the raw value as a u32
1126            #[inline(always)]
1127            pub const fn to_u32(self) -> u32 {
1128                self.0.get()
1129            }
1130
1131            /// Add `offset` to this index, returning `None` on overflow
1132            pub fn checked_add(self, offset: u32) -> Option<Self> {
1133                self.0.checked_add(offset).map(Self)
1134            }
1135
1136            /// Add a signed `offset` to this index, returning `None` on overflow
1137            pub fn checked_add_signed(self, offset: i32) -> Option<Self> {
1138                self.0.get().checked_add_signed(offset).and_then(Self::new)
1139            }
1140
1141            /// Subtract `offset` from this index, returning `None` on underflow
1142            pub fn checked_sub(self, offset: u32) -> Option<Self> {
1143                self.0.get().checked_sub(offset).and_then(Self::new)
1144            }
1145
1146            /// Add `offset` to this index, saturating to `u32::MAX` on overflow
1147            pub const fn saturating_add(self, offset: u32) -> Self {
1148                Self(unsafe { NonZeroU32::new_unchecked(self.0.get().saturating_add(offset)) })
1149            }
1150
1151            /// Add a signed `offset` to this index, saturating to `0` on underflow, and `u32::MAX`
1152            /// on overflow.
1153            pub fn saturating_add_signed(self, offset: i32) -> Self {
1154                Self::new(self.to_u32().saturating_add_signed(offset)).unwrap_or_default()
1155            }
1156
1157            /// Subtract `offset` from this index, saturating to `0` on overflow
1158            pub fn saturating_sub(self, offset: u32) -> Self {
1159                Self::new(self.to_u32().saturating_sub(offset)).unwrap_or_default()
1160            }
1161        }
1162
1163        impl From<NonZeroU32> for $number_name {
1164            #[inline]
1165            fn from(index: NonZeroU32) -> Self {
1166                Self(index)
1167            }
1168        }
1169
1170        impl From<$index_name> for $number_name {
1171            #[inline]
1172            fn from(index: $index_name) -> Self {
1173                Self(unsafe { NonZeroU32::new_unchecked(index.to_u32() + 1) })
1174            }
1175        }
1176
1177        impl core::ops::Add<u32> for $number_name {
1178            type Output = Self;
1179
1180            #[inline]
1181            fn add(self, rhs: u32) -> Self {
1182                Self(unsafe { NonZeroU32::new_unchecked(self.0.get() + rhs) })
1183            }
1184        }
1185
1186        impl core::ops::AddAssign<u32> for $number_name {
1187            fn add_assign(&mut self, rhs: u32) {
1188                let result = *self + rhs;
1189                *self = result;
1190            }
1191        }
1192
1193        impl core::ops::Add<i32> for $number_name {
1194            type Output = Self;
1195
1196            fn add(self, rhs: i32) -> Self {
1197                self.to_u32()
1198                    .checked_add_signed(rhs)
1199                    .and_then(Self::new)
1200                    .expect("invalid offset: overflow occurred")
1201            }
1202        }
1203
1204        impl core::ops::AddAssign<i32> for $number_name {
1205            fn add_assign(&mut self, rhs: i32) {
1206                let result = *self + rhs;
1207                *self = result;
1208            }
1209        }
1210
1211        impl core::ops::Sub<u32> for $number_name {
1212            type Output = Self;
1213
1214            #[inline]
1215            fn sub(self, rhs: u32) -> Self {
1216                self.to_u32()
1217                    .checked_sub(rhs)
1218                    .and_then(Self::new)
1219                    .expect("invalid offset: overflow occurred")
1220            }
1221        }
1222
1223        impl core::ops::SubAssign<u32> for $number_name {
1224            fn sub_assign(&mut self, rhs: u32) {
1225                let result = *self - rhs;
1226                *self = result;
1227            }
1228        }
1229
1230        impl fmt::Display for $number_name {
1231            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1232                fmt::Display::fmt(&self.0, f)
1233            }
1234        }
1235    };
1236}
1237
1238declare_dual_number_and_index_type!(Line, "line");
1239declare_dual_number_and_index_type!(Column, "column");
1240
1241// SERIALIZATION FOR LINE/COLUMN NUMBERS
1242// ================================================================================================
1243
1244use miden_crypto::utils::{
1245    ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
1246};
1247
1248impl Serializable for LineNumber {
1249    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1250        target.write_u32(self.to_u32());
1251    }
1252}
1253
1254impl Deserializable for LineNumber {
1255    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1256        let value = source.read_u32()?;
1257        Self::new(value)
1258            .ok_or_else(|| DeserializationError::InvalidValue("line number cannot be zero".into()))
1259    }
1260}
1261
1262impl Serializable for ColumnNumber {
1263    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1264        target.write_u32(self.to_u32());
1265    }
1266}
1267
1268impl Deserializable for ColumnNumber {
1269    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1270        let value = source.read_u32()?;
1271        Self::new(value).ok_or_else(|| {
1272            DeserializationError::InvalidValue("column number cannot be zero".into())
1273        })
1274    }
1275}
1276
1277#[cfg(feature = "arbitrary")]
1278impl Arbitrary for LineNumber {
1279    type Parameters = ();
1280    type Strategy = BoxedStrategy<Self>;
1281
1282    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1283        (1..=u32::MAX)
1284            .prop_map(|value| Self::new(value).expect("non-zero value"))
1285            .boxed()
1286    }
1287}
1288
1289#[cfg(feature = "arbitrary")]
1290impl Arbitrary for ColumnNumber {
1291    type Parameters = ();
1292    type Strategy = BoxedStrategy<Self>;
1293
1294    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1295        (1..=u32::MAX)
1296            .prop_map(|value| Self::new(value).expect("non-zero value"))
1297            .boxed()
1298    }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303    use super::*;
1304
1305    #[test]
1306    fn source_content_line_starts() {
1307        const CONTENT: &str = "\
1308begin
1309  push.1
1310  push.2
1311  add
1312end
1313";
1314        let content = SourceContent::new("masm", "foo.masm", CONTENT);
1315
1316        assert_eq!(content.line_count(), 6);
1317        assert_eq!(
1318            content
1319                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1320                .expect("invalid byte range"),
1321            "begin\n".as_bytes()
1322        );
1323        assert_eq!(
1324            content
1325                .byte_slice(content.line_range(LineIndex(1)).expect("invalid line"))
1326                .expect("invalid byte range"),
1327            "  push.1\n".as_bytes()
1328        );
1329        assert_eq!(
1330            content
1331                .byte_slice(content.line_range(content.last_line_index()).expect("invalid line"))
1332                .expect("invalid byte range"),
1333            "".as_bytes()
1334        );
1335    }
1336
1337    #[test]
1338    fn source_content_line_starts_after_update() {
1339        const CONTENT: &str = "\
1340begin
1341  push.1
1342  push.2
1343  add
1344end
1345";
1346        const FRAGMENT: &str = "  push.2
1347  mul
1348end
1349";
1350        let mut content = SourceContent::new("masm", "foo.masm", CONTENT);
1351        content
1352            .update(FRAGMENT.to_string(), Some(Selection::from(LineIndex(4)..LineIndex(5))), 1)
1353            .expect("update failed");
1354
1355        assert_eq!(
1356            content.as_str(),
1357            "\
1358begin
1359  push.1
1360  push.2
1361  add
1362  push.2
1363  mul
1364end
1365"
1366        );
1367        assert_eq!(content.line_count(), 8);
1368        assert_eq!(
1369            content
1370                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1371                .expect("invalid byte range"),
1372            "begin\n".as_bytes()
1373        );
1374        assert_eq!(
1375            content
1376                .byte_slice(content.line_range(LineIndex(3)).expect("invalid line"))
1377                .expect("invalid byte range"),
1378            "  add\n".as_bytes()
1379        );
1380        assert_eq!(
1381            content
1382                .byte_slice(content.line_range(LineIndex(4)).expect("invalid line"))
1383                .expect("invalid byte range"),
1384            "  push.2\n".as_bytes()
1385        );
1386        assert_eq!(
1387            content
1388                .byte_slice(content.line_range(content.last_line_index()).expect("invalid line"))
1389                .expect("invalid byte range"),
1390            "".as_bytes()
1391        );
1392    }
1393
1394    /// Test that backslash-before-newline is NOT treated as a line continuation.
1395    #[test]
1396    fn source_content_line_starts_with_trailing_backslash() {
1397        const CONTENT: &str =
1398            "//! Build with:\n//!   cargo build \\\n//!     --release\nfn main() {}\n";
1399
1400        let content = SourceContent::new("rust", "example.rs", CONTENT);
1401
1402        // Should have 5 lines (4 lines of content + 1 empty line after final newline)
1403        // Line 0: "//! Build with:\n"
1404        // Line 1: "//!   cargo build \\\n"
1405        // Line 2: "//!     --release\n"
1406        // Line 3: "fn main() {}\n"
1407        // Line 4: "" (empty line after final newline)
1408        assert_eq!(content.line_count(), 5);
1409
1410        // Verify each line's content
1411        assert_eq!(
1412            content
1413                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1414                .expect("invalid byte range"),
1415            "//! Build with:\n".as_bytes()
1416        );
1417        assert_eq!(
1418            content
1419                .byte_slice(content.line_range(LineIndex(1)).expect("invalid line"))
1420                .expect("invalid byte range"),
1421            "//!   cargo build \\\n".as_bytes()
1422        );
1423        assert_eq!(
1424            content
1425                .byte_slice(content.line_range(LineIndex(2)).expect("invalid line"))
1426                .expect("invalid byte range"),
1427            "//!     --release\n".as_bytes()
1428        );
1429        assert_eq!(
1430            content
1431                .byte_slice(content.line_range(LineIndex(3)).expect("invalid line"))
1432                .expect("invalid byte range"),
1433            "fn main() {}\n".as_bytes()
1434        );
1435
1436        // Verify line_column_to_offset works for all lines, including those after
1437        // backslash-ended lines.
1438        let offset_line0 = content.line_column_to_offset(LineIndex(0), ColumnIndex(0));
1439        let offset_line1 = content.line_column_to_offset(LineIndex(1), ColumnIndex(0));
1440        let offset_line2 = content.line_column_to_offset(LineIndex(2), ColumnIndex(0));
1441        let offset_line3 = content.line_column_to_offset(LineIndex(3), ColumnIndex(0));
1442
1443        assert!(offset_line0.is_some(), "line 0 should be accessible");
1444        assert!(offset_line1.is_some(), "line 1 should be accessible");
1445        assert!(offset_line2.is_some(), "line 2 should be accessible");
1446        assert!(offset_line3.is_some(), "line 3 should be accessible");
1447
1448        // Verify the offsets are at the expected byte positions
1449        assert_eq!(offset_line0.unwrap().to_u32(), 0);
1450        assert_eq!(offset_line1.unwrap().to_u32(), 16); // After "//! Build with:\n"
1451        assert_eq!(offset_line2.unwrap().to_u32(), 36); // After "//!   cargo build \\\n"
1452        assert_eq!(offset_line3.unwrap().to_u32(), 54); // After "//!     --release\n"
1453    }
1454
1455    /// Test with multiple consecutive backslash-ended lines
1456    #[test]
1457    fn source_content_line_starts_multiple_trailing_backslashes() {
1458        // Multiple lines ending with backslashes
1459        const CONTENT: &str = "line1 \\\nline2 \\\nline3 \\\nline4\n";
1460
1461        let content = SourceContent::new("text", "test.txt", CONTENT);
1462
1463        // Should have 5 lines (4 lines of content + 1 empty line after final newline)
1464        assert_eq!(content.line_count(), 5);
1465
1466        // Verify each line is correctly separated
1467        assert_eq!(
1468            content
1469                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1470                .expect("invalid byte range"),
1471            "line1 \\\n".as_bytes()
1472        );
1473        assert_eq!(
1474            content
1475                .byte_slice(content.line_range(LineIndex(1)).expect("invalid line"))
1476                .expect("invalid byte range"),
1477            "line2 \\\n".as_bytes()
1478        );
1479        assert_eq!(
1480            content
1481                .byte_slice(content.line_range(LineIndex(2)).expect("invalid line"))
1482                .expect("invalid byte range"),
1483            "line3 \\\n".as_bytes()
1484        );
1485        assert_eq!(
1486            content
1487                .byte_slice(content.line_range(LineIndex(3)).expect("invalid line"))
1488                .expect("invalid byte range"),
1489            "line4\n".as_bytes()
1490        );
1491    }
1492}