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(
790    Default,
791    Debug,
792    Copy,
793    Clone,
794    PartialEq,
795    Eq,
796    PartialOrd,
797    Ord,
798    Hash,
799    zerocopy::FromBytes,
800    zerocopy::Immutable,
801    zerocopy::IntoBytes,
802    zerocopy::KnownLayout,
803)]
804#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
805#[cfg_attr(feature = "serde", serde(transparent))]
806#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
807pub struct ByteIndex(pub u32);
808
809impl ByteIndex {
810    /// Create a [ByteIndex] from a raw `u32` index
811    pub const fn new(index: u32) -> Self {
812        Self(index)
813    }
814
815    /// Get the raw index as a usize
816    #[inline(always)]
817    pub const fn to_usize(self) -> usize {
818        self.0 as usize
819    }
820
821    /// Get the raw index as a u32
822    #[inline(always)]
823    pub const fn to_u32(self) -> u32 {
824        self.0
825    }
826}
827
828impl core::ops::Add<ByteOffset> for ByteIndex {
829    type Output = ByteIndex;
830
831    fn add(self, rhs: ByteOffset) -> Self {
832        Self((self.0 as i64 + rhs.0) as u32)
833    }
834}
835
836impl core::ops::Add<u32> for ByteIndex {
837    type Output = ByteIndex;
838
839    fn add(self, rhs: u32) -> Self {
840        Self(self.0 + rhs)
841    }
842}
843
844impl core::ops::AddAssign<ByteOffset> for ByteIndex {
845    fn add_assign(&mut self, rhs: ByteOffset) {
846        *self = *self + rhs;
847    }
848}
849
850impl core::ops::AddAssign<u32> for ByteIndex {
851    fn add_assign(&mut self, rhs: u32) {
852        self.0 += rhs;
853    }
854}
855
856impl core::ops::Sub<ByteOffset> for ByteIndex {
857    type Output = ByteIndex;
858
859    fn sub(self, rhs: ByteOffset) -> Self {
860        Self((self.0 as i64 - rhs.0) as u32)
861    }
862}
863
864impl core::ops::Sub<u32> for ByteIndex {
865    type Output = ByteIndex;
866
867    fn sub(self, rhs: u32) -> Self {
868        Self(self.0 - rhs)
869    }
870}
871
872impl core::ops::SubAssign<ByteOffset> for ByteIndex {
873    fn sub_assign(&mut self, rhs: ByteOffset) {
874        *self = *self - rhs;
875    }
876}
877
878impl core::ops::SubAssign<u32> for ByteIndex {
879    fn sub_assign(&mut self, rhs: u32) {
880        self.0 -= rhs;
881    }
882}
883
884impl From<u32> for ByteIndex {
885    fn from(index: u32) -> Self {
886        Self(index)
887    }
888}
889
890impl From<ByteIndex> for u32 {
891    fn from(index: ByteIndex) -> Self {
892        index.0
893    }
894}
895
896impl fmt::Display for ByteIndex {
897    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
898        fmt::Display::fmt(&self.0, f)
899    }
900}
901
902#[cfg(feature = "arbitrary")]
903impl Arbitrary for ByteIndex {
904    type Parameters = ();
905    type Strategy = BoxedStrategy<Self>;
906
907    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
908        any::<u32>().prop_map(Self).boxed()
909    }
910}
911
912/// An offset in bytes relative to some [ByteIndex]
913#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
914pub struct ByteOffset(i64);
915
916impl ByteOffset {
917    /// Compute the offset in bytes represented by the given `char`
918    pub fn from_char_len(c: char) -> ByteOffset {
919        Self(c.len_utf8() as i64)
920    }
921
922    /// Compute the offset in bytes represented by the given `str`
923    pub fn from_str_len(s: &str) -> ByteOffset {
924        Self(s.len() as i64)
925    }
926}
927
928impl core::ops::Add for ByteOffset {
929    type Output = ByteOffset;
930
931    fn add(self, rhs: Self) -> Self {
932        Self(self.0 + rhs.0)
933    }
934}
935
936impl core::ops::AddAssign for ByteOffset {
937    fn add_assign(&mut self, rhs: Self) {
938        self.0 += rhs.0;
939    }
940}
941
942impl core::ops::Sub for ByteOffset {
943    type Output = ByteOffset;
944
945    fn sub(self, rhs: Self) -> Self {
946        Self(self.0 - rhs.0)
947    }
948}
949
950impl core::ops::SubAssign for ByteOffset {
951    fn sub_assign(&mut self, rhs: Self) {
952        self.0 -= rhs.0;
953    }
954}
955
956impl fmt::Display for ByteOffset {
957    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
958        fmt::Display::fmt(&self.0, f)
959    }
960}
961
962macro_rules! declare_dual_number_and_index_type {
963    ($name:ident, $description:literal) => {
964        paste::paste! {
965            declare_dual_number_and_index_type!([<$name Index>], [<$name Number>], $description);
966        }
967    };
968
969    ($index_name:ident, $number_name:ident, $description:literal) => {
970        #[doc = concat!("A zero-indexed ", $description, " number")]
971        #[derive(
972            Default,
973            Debug,
974            Copy,
975            Clone,
976            PartialEq,
977            Eq,
978            PartialOrd,
979            Ord,
980            Hash,
981            zerocopy::FromBytes,
982            zerocopy::Immutable,
983            zerocopy::IntoBytes,
984            zerocopy::KnownLayout,
985        )]
986        #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
987        #[cfg_attr(feature = "serde", serde(transparent))]
988        #[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
989        pub struct $index_name(pub u32);
990
991        impl $index_name {
992            #[doc = concat!("Convert to a [", stringify!($number_name), "]")]
993            pub const fn number(self) -> $number_name {
994                $number_name(unsafe { NonZeroU32::new_unchecked(self.0 + 1) })
995            }
996
997            /// Get the raw index value as a usize
998            #[inline(always)]
999            pub const fn to_usize(self) -> usize {
1000                self.0 as usize
1001            }
1002
1003            /// Get the raw index value as a u32
1004            #[inline(always)]
1005            pub const fn to_u32(self) -> u32 {
1006                self.0
1007            }
1008
1009            /// Add `offset` to this index, returning `None` on overflow
1010            pub fn checked_add(self, offset: u32) -> Option<Self> {
1011                self.0.checked_add(offset).map(Self)
1012            }
1013
1014            /// Add a signed `offset` to this index, returning `None` on overflow
1015            pub fn checked_add_signed(self, offset: i32) -> Option<Self> {
1016                self.0.checked_add_signed(offset).map(Self)
1017            }
1018
1019            /// Subtract `offset` from this index, returning `None` on underflow
1020            pub fn checked_sub(self, offset: u32) -> Option<Self> {
1021                self.0.checked_sub(offset).map(Self)
1022            }
1023
1024            /// Add `offset` to this index, saturating to `u32::MAX` on overflow
1025            pub const fn saturating_add(self, offset: u32) -> Self {
1026                Self(self.0.saturating_add(offset))
1027            }
1028
1029            /// Add a signed `offset` to this index, saturating to `0` on underflow, and `u32::MAX`
1030            /// on overflow.
1031            pub const fn saturating_add_signed(self, offset: i32) -> Self {
1032                Self(self.0.saturating_add_signed(offset))
1033            }
1034
1035            /// Subtract `offset` from this index, saturating to `0` on overflow
1036            pub const fn saturating_sub(self, offset: u32) -> Self {
1037                Self(self.0.saturating_sub(offset))
1038            }
1039        }
1040
1041        impl From<u32> for $index_name {
1042            #[inline]
1043            fn from(index: u32) -> Self {
1044                Self(index)
1045            }
1046        }
1047
1048        impl From<$number_name> for $index_name {
1049            #[inline]
1050            fn from(index: $number_name) -> Self {
1051                Self(index.to_u32() - 1)
1052            }
1053        }
1054
1055        impl core::ops::Add<u32> for $index_name {
1056            type Output = Self;
1057
1058            #[inline]
1059            fn add(self, rhs: u32) -> Self {
1060                Self(self.0 + rhs)
1061            }
1062        }
1063
1064        impl core::ops::AddAssign<u32> for $index_name {
1065            fn add_assign(&mut self, rhs: u32) {
1066                let result = *self + rhs;
1067                *self = result;
1068            }
1069        }
1070
1071        impl core::ops::Add<i32> for $index_name {
1072            type Output = Self;
1073
1074            fn add(self, rhs: i32) -> Self {
1075                self.checked_add_signed(rhs).expect("invalid offset: overflow occurred")
1076            }
1077        }
1078
1079        impl core::ops::AddAssign<i32> for $index_name {
1080            fn add_assign(&mut self, rhs: i32) {
1081                let result = *self + rhs;
1082                *self = result;
1083            }
1084        }
1085
1086        impl core::ops::Sub<u32> for $index_name {
1087            type Output = Self;
1088
1089            #[inline]
1090            fn sub(self, rhs: u32) -> Self {
1091                Self(self.0 - rhs)
1092            }
1093        }
1094
1095        impl core::ops::SubAssign<u32> for $index_name {
1096            fn sub_assign(&mut self, rhs: u32) {
1097                let result = *self - rhs;
1098                *self = result;
1099            }
1100        }
1101
1102        impl fmt::Display for $index_name {
1103            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1104                fmt::Display::fmt(&self.0, f)
1105            }
1106        }
1107
1108        #[cfg(feature = "arbitrary")]
1109        impl Arbitrary for $index_name {
1110            type Parameters = ();
1111            type Strategy = BoxedStrategy<Self>;
1112
1113            fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1114                any::<u32>().prop_map(Self).boxed()
1115            }
1116        }
1117
1118        #[doc = concat!("A one-indexed ", $description, " number")]
1119        #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1120        #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1121        #[cfg_attr(feature = "serde", serde(transparent))]
1122        #[cfg_attr(
1123            all(feature = "arbitrary", test),
1124            miden_test_serde_macros::serde_test(binary_serde(true))
1125        )]
1126        pub struct $number_name(NonZeroU32);
1127
1128        impl Default for $number_name {
1129            fn default() -> Self {
1130                Self(unsafe { NonZeroU32::new_unchecked(1) })
1131            }
1132        }
1133
1134        impl $number_name {
1135            pub const fn new(number: u32) -> Option<Self> {
1136                match NonZeroU32::new(number) {
1137                    Some(num) => Some(Self(num)),
1138                    None => None,
1139                }
1140            }
1141
1142            #[doc = concat!("Convert to a [", stringify!($index_name), "]")]
1143            pub const fn to_index(self) -> $index_name {
1144                $index_name(self.to_u32().saturating_sub(1))
1145            }
1146
1147            /// Get the raw value as a usize
1148            #[inline(always)]
1149            pub const fn to_usize(self) -> usize {
1150                self.0.get() as usize
1151            }
1152
1153            /// Get the raw value as a u32
1154            #[inline(always)]
1155            pub const fn to_u32(self) -> u32 {
1156                self.0.get()
1157            }
1158
1159            /// Add `offset` to this index, returning `None` on overflow
1160            pub fn checked_add(self, offset: u32) -> Option<Self> {
1161                self.0.checked_add(offset).map(Self)
1162            }
1163
1164            /// Add a signed `offset` to this index, returning `None` on overflow
1165            pub fn checked_add_signed(self, offset: i32) -> Option<Self> {
1166                self.0.get().checked_add_signed(offset).and_then(Self::new)
1167            }
1168
1169            /// Subtract `offset` from this index, returning `None` on underflow
1170            pub fn checked_sub(self, offset: u32) -> Option<Self> {
1171                self.0.get().checked_sub(offset).and_then(Self::new)
1172            }
1173
1174            /// Add `offset` to this index, saturating to `u32::MAX` on overflow
1175            pub const fn saturating_add(self, offset: u32) -> Self {
1176                Self(unsafe { NonZeroU32::new_unchecked(self.0.get().saturating_add(offset)) })
1177            }
1178
1179            /// Add a signed `offset` to this index, saturating to `0` on underflow, and `u32::MAX`
1180            /// on overflow.
1181            pub fn saturating_add_signed(self, offset: i32) -> Self {
1182                Self::new(self.to_u32().saturating_add_signed(offset)).unwrap_or_default()
1183            }
1184
1185            /// Subtract `offset` from this index, saturating to `0` on overflow
1186            pub fn saturating_sub(self, offset: u32) -> Self {
1187                Self::new(self.to_u32().saturating_sub(offset)).unwrap_or_default()
1188            }
1189        }
1190
1191        impl From<NonZeroU32> for $number_name {
1192            #[inline]
1193            fn from(index: NonZeroU32) -> Self {
1194                Self(index)
1195            }
1196        }
1197
1198        impl From<$index_name> for $number_name {
1199            #[inline]
1200            fn from(index: $index_name) -> Self {
1201                Self(unsafe { NonZeroU32::new_unchecked(index.to_u32() + 1) })
1202            }
1203        }
1204
1205        impl core::ops::Add<u32> for $number_name {
1206            type Output = Self;
1207
1208            #[inline]
1209            fn add(self, rhs: u32) -> Self {
1210                Self(unsafe { NonZeroU32::new_unchecked(self.0.get() + rhs) })
1211            }
1212        }
1213
1214        impl core::ops::AddAssign<u32> for $number_name {
1215            fn add_assign(&mut self, rhs: u32) {
1216                let result = *self + rhs;
1217                *self = result;
1218            }
1219        }
1220
1221        impl core::ops::Add<i32> for $number_name {
1222            type Output = Self;
1223
1224            fn add(self, rhs: i32) -> Self {
1225                self.to_u32()
1226                    .checked_add_signed(rhs)
1227                    .and_then(Self::new)
1228                    .expect("invalid offset: overflow occurred")
1229            }
1230        }
1231
1232        impl core::ops::AddAssign<i32> for $number_name {
1233            fn add_assign(&mut self, rhs: i32) {
1234                let result = *self + rhs;
1235                *self = result;
1236            }
1237        }
1238
1239        impl core::ops::Sub<u32> for $number_name {
1240            type Output = Self;
1241
1242            #[inline]
1243            fn sub(self, rhs: u32) -> Self {
1244                self.to_u32()
1245                    .checked_sub(rhs)
1246                    .and_then(Self::new)
1247                    .expect("invalid offset: overflow occurred")
1248            }
1249        }
1250
1251        impl core::ops::SubAssign<u32> for $number_name {
1252            fn sub_assign(&mut self, rhs: u32) {
1253                let result = *self - rhs;
1254                *self = result;
1255            }
1256        }
1257
1258        impl fmt::Display for $number_name {
1259            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1260                fmt::Display::fmt(&self.0, f)
1261            }
1262        }
1263    };
1264}
1265
1266declare_dual_number_and_index_type!(Line, "line");
1267declare_dual_number_and_index_type!(Column, "column");
1268
1269// SERIALIZATION FOR LINE/COLUMN NUMBERS
1270// ================================================================================================
1271
1272use miden_crypto::utils::{
1273    ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
1274};
1275
1276impl Serializable for LineNumber {
1277    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1278        target.write_u32(self.to_u32());
1279    }
1280}
1281
1282impl Deserializable for LineNumber {
1283    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1284        let value = source.read_u32()?;
1285        Self::new(value)
1286            .ok_or_else(|| DeserializationError::InvalidValue("line number cannot be zero".into()))
1287    }
1288}
1289
1290impl Serializable for ColumnNumber {
1291    fn write_into<W: ByteWriter>(&self, target: &mut W) {
1292        target.write_u32(self.to_u32());
1293    }
1294}
1295
1296impl Deserializable for ColumnNumber {
1297    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1298        let value = source.read_u32()?;
1299        Self::new(value).ok_or_else(|| {
1300            DeserializationError::InvalidValue("column number cannot be zero".into())
1301        })
1302    }
1303}
1304
1305#[cfg(feature = "arbitrary")]
1306impl Arbitrary for LineNumber {
1307    type Parameters = ();
1308    type Strategy = BoxedStrategy<Self>;
1309
1310    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1311        (1..=u32::MAX)
1312            .prop_map(|value| Self::new(value).expect("non-zero value"))
1313            .boxed()
1314    }
1315}
1316
1317#[cfg(feature = "arbitrary")]
1318impl Arbitrary for ColumnNumber {
1319    type Parameters = ();
1320    type Strategy = BoxedStrategy<Self>;
1321
1322    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1323        (1..=u32::MAX)
1324            .prop_map(|value| Self::new(value).expect("non-zero value"))
1325            .boxed()
1326    }
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331    use super::*;
1332
1333    #[test]
1334    fn source_content_line_starts() {
1335        const CONTENT: &str = "\
1336begin
1337  push.1
1338  push.2
1339  add
1340end
1341";
1342        let content = SourceContent::new("masm", "foo.masm", CONTENT);
1343
1344        assert_eq!(content.line_count(), 6);
1345        assert_eq!(
1346            content
1347                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1348                .expect("invalid byte range"),
1349            "begin\n".as_bytes()
1350        );
1351        assert_eq!(
1352            content
1353                .byte_slice(content.line_range(LineIndex(1)).expect("invalid line"))
1354                .expect("invalid byte range"),
1355            "  push.1\n".as_bytes()
1356        );
1357        assert_eq!(
1358            content
1359                .byte_slice(content.line_range(content.last_line_index()).expect("invalid line"))
1360                .expect("invalid byte range"),
1361            "".as_bytes()
1362        );
1363    }
1364
1365    #[test]
1366    fn source_content_line_starts_after_update() {
1367        const CONTENT: &str = "\
1368begin
1369  push.1
1370  push.2
1371  add
1372end
1373";
1374        const FRAGMENT: &str = "  push.2
1375  mul
1376end
1377";
1378        let mut content = SourceContent::new("masm", "foo.masm", CONTENT);
1379        content
1380            .update(FRAGMENT.to_string(), Some(Selection::from(LineIndex(4)..LineIndex(5))), 1)
1381            .expect("update failed");
1382
1383        assert_eq!(
1384            content.as_str(),
1385            "\
1386begin
1387  push.1
1388  push.2
1389  add
1390  push.2
1391  mul
1392end
1393"
1394        );
1395        assert_eq!(content.line_count(), 8);
1396        assert_eq!(
1397            content
1398                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1399                .expect("invalid byte range"),
1400            "begin\n".as_bytes()
1401        );
1402        assert_eq!(
1403            content
1404                .byte_slice(content.line_range(LineIndex(3)).expect("invalid line"))
1405                .expect("invalid byte range"),
1406            "  add\n".as_bytes()
1407        );
1408        assert_eq!(
1409            content
1410                .byte_slice(content.line_range(LineIndex(4)).expect("invalid line"))
1411                .expect("invalid byte range"),
1412            "  push.2\n".as_bytes()
1413        );
1414        assert_eq!(
1415            content
1416                .byte_slice(content.line_range(content.last_line_index()).expect("invalid line"))
1417                .expect("invalid byte range"),
1418            "".as_bytes()
1419        );
1420    }
1421
1422    /// Test that backslash-before-newline is NOT treated as a line continuation.
1423    #[test]
1424    fn source_content_line_starts_with_trailing_backslash() {
1425        const CONTENT: &str =
1426            "//! Build with:\n//!   cargo build \\\n//!     --release\nfn main() {}\n";
1427
1428        let content = SourceContent::new("rust", "example.rs", CONTENT);
1429
1430        // Should have 5 lines (4 lines of content + 1 empty line after final newline)
1431        // Line 0: "//! Build with:\n"
1432        // Line 1: "//!   cargo build \\\n"
1433        // Line 2: "//!     --release\n"
1434        // Line 3: "fn main() {}\n"
1435        // Line 4: "" (empty line after final newline)
1436        assert_eq!(content.line_count(), 5);
1437
1438        // Verify each line's content
1439        assert_eq!(
1440            content
1441                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1442                .expect("invalid byte range"),
1443            "//! Build with:\n".as_bytes()
1444        );
1445        assert_eq!(
1446            content
1447                .byte_slice(content.line_range(LineIndex(1)).expect("invalid line"))
1448                .expect("invalid byte range"),
1449            "//!   cargo build \\\n".as_bytes()
1450        );
1451        assert_eq!(
1452            content
1453                .byte_slice(content.line_range(LineIndex(2)).expect("invalid line"))
1454                .expect("invalid byte range"),
1455            "//!     --release\n".as_bytes()
1456        );
1457        assert_eq!(
1458            content
1459                .byte_slice(content.line_range(LineIndex(3)).expect("invalid line"))
1460                .expect("invalid byte range"),
1461            "fn main() {}\n".as_bytes()
1462        );
1463
1464        // Verify line_column_to_offset works for all lines, including those after
1465        // backslash-ended lines.
1466        let offset_line0 = content.line_column_to_offset(LineIndex(0), ColumnIndex(0));
1467        let offset_line1 = content.line_column_to_offset(LineIndex(1), ColumnIndex(0));
1468        let offset_line2 = content.line_column_to_offset(LineIndex(2), ColumnIndex(0));
1469        let offset_line3 = content.line_column_to_offset(LineIndex(3), ColumnIndex(0));
1470
1471        assert!(offset_line0.is_some(), "line 0 should be accessible");
1472        assert!(offset_line1.is_some(), "line 1 should be accessible");
1473        assert!(offset_line2.is_some(), "line 2 should be accessible");
1474        assert!(offset_line3.is_some(), "line 3 should be accessible");
1475
1476        // Verify the offsets are at the expected byte positions
1477        assert_eq!(offset_line0.unwrap().to_u32(), 0);
1478        assert_eq!(offset_line1.unwrap().to_u32(), 16); // After "//! Build with:\n"
1479        assert_eq!(offset_line2.unwrap().to_u32(), 36); // After "//!   cargo build \\\n"
1480        assert_eq!(offset_line3.unwrap().to_u32(), 54); // After "//!     --release\n"
1481    }
1482
1483    /// Test with multiple consecutive backslash-ended lines
1484    #[test]
1485    fn source_content_line_starts_multiple_trailing_backslashes() {
1486        // Multiple lines ending with backslashes
1487        const CONTENT: &str = "line1 \\\nline2 \\\nline3 \\\nline4\n";
1488
1489        let content = SourceContent::new("text", "test.txt", CONTENT);
1490
1491        // Should have 5 lines (4 lines of content + 1 empty line after final newline)
1492        assert_eq!(content.line_count(), 5);
1493
1494        // Verify each line is correctly separated
1495        assert_eq!(
1496            content
1497                .byte_slice(content.line_range(LineIndex(0)).expect("invalid line"))
1498                .expect("invalid byte range"),
1499            "line1 \\\n".as_bytes()
1500        );
1501        assert_eq!(
1502            content
1503                .byte_slice(content.line_range(LineIndex(1)).expect("invalid line"))
1504                .expect("invalid byte range"),
1505            "line2 \\\n".as_bytes()
1506        );
1507        assert_eq!(
1508            content
1509                .byte_slice(content.line_range(LineIndex(2)).expect("invalid line"))
1510                .expect("invalid byte range"),
1511            "line3 \\\n".as_bytes()
1512        );
1513        assert_eq!(
1514            content
1515                .byte_slice(content.line_range(LineIndex(3)).expect("invalid line"))
1516                .expect("invalid byte range"),
1517            "line4\n".as_bytes()
1518        );
1519    }
1520}