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