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