Skip to main content

m2_syn/
span.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::hash::{Hash, Hasher};
3use std::ops::Range;
4use std::sync::{
5    Arc, OnceLock,
6    atomic::{AtomicU16, Ordering},
7};
8
9/// A boundary in a source buffer, as a byte offset.
10///
11/// Boundaries sit between bytes rather than on them, so a span's `end` is the
12/// offset past its last byte and `end - start` is its length. One offset is
13/// enough because a span's only job is to index a buffer: line and column are
14/// facts about a particular buffer's contents, recovered from it when something
15/// needs to *display* a location, and so they belong with the buffer rather
16/// than with the span. This is what `rustc` does too — its `Span` stores `lo`
17/// and `hi` byte positions and nothing else, and `SourceMap` binary-searches a
18/// line table on demand.
19pub type Offset = usize;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22/// A human-facing position in a source snapshot.
23///
24/// Columns count Unicode scalar values rather
25/// than bytes or terminal cells, so the first character on every line is at
26/// column one regardless of its UTF-8 width.
27pub struct LineColumn {
28    /// The one-based line number.
29    pub line: usize,
30    /// The one-based Unicode-scalar column number.
31    pub column: usize,
32}
33
34impl Display for LineColumn {
35    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
36        write!(formatter, "{}:{}", self.line, self.column)
37    }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41/// The human-facing opening and closing positions of a source span.
42///
43/// Like the underlying byte range, this range is half-open: `end` is the
44/// position immediately after the final character.
45pub struct LineColumnRange {
46    /// The position at which the span opens.
47    pub start: LineColumn,
48    /// The position immediately after the span closes.
49    pub end: LineColumn,
50}
51
52impl Display for LineColumnRange {
53    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
54        if self.start == self.end {
55            Display::fmt(&self.start, formatter)
56        } else {
57            write!(formatter, "{}-{}", self.start, self.end)
58        }
59    }
60}
61
62/// One text change: the text a buffer gains at a point, or the stretch it
63/// loses.
64///
65/// An edit acts on *both* halves of the indexing relation — on source text
66/// through [`Self::apply`] and on a [`Span`] through [`Span::shifted`] — and
67/// those two actions are designed to agree.
68///
69/// An insertion carries the text it inserts rather than a pair of boundaries,
70/// because the boundaries are recoverable (`at` and `at + text.len()`) while the
71/// text is not, and without the text an edit cannot act on a buffer at all.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum TextEdit {
74    /// Inserts `text` at one byte boundary.
75    Insertion {
76        /// The insertion boundary.
77        at: Offset,
78        /// The inserted text.
79        text: String,
80    },
81    /// Deletes one half-open byte range.
82    Deletion {
83        /// The first deleted byte.
84        start: Offset,
85        /// The boundary after the last deleted byte.
86        end: Offset,
87    },
88}
89
90impl TextEdit {
91    /// Creates an insertion of `text` landing at `at`.
92    pub fn insertion(at: Offset, text: impl Into<String>) -> Self {
93        Self::Insertion {
94            at,
95            text: text.into(),
96        }
97    }
98
99    /// Creates a deletion of the text occupying `start..end`.
100    ///
101    /// # Panics
102    ///
103    /// Panics when `start` is greater than `end`.
104    pub fn deletion(start: Offset, end: Offset) -> Self {
105        assert!(start <= end, "deletion start must not exceed its end");
106        Self::Deletion { start, end }
107    }
108
109    /// Returns `offset` in the buffer this edit produces.
110    ///
111    /// For an insertion of width `d` at `s` this is `offset + (offset >= s) * d`
112    /// — everything at or after the insertion point moves by the width, and
113    /// nothing else moves at all. A deletion is the mirror image, with the text
114    /// it covered collapsing onto its start.
115    ///
116    /// The rule is monotone in `offset`, so applying it to both ends of a range
117    /// can never invert them, and only the edit's *width* enters the arithmetic.
118    /// Reaching the same result by summing two absolute offsets would
119    /// manufacture an overflow the buffer never causes.
120    fn shift(&self, offset: Offset) -> Offset {
121        match self {
122            Self::Insertion { at, text } => {
123                if offset >= *at {
124                    offset + text.len()
125                } else {
126                    offset
127                }
128            }
129            Self::Deletion { start, end } => {
130                if offset <= *start {
131                    offset
132                } else if offset <= *end {
133                    // Everything the deletion covered now sits at its start.
134                    *start
135                } else {
136                    offset - (end - start)
137                }
138            }
139        }
140    }
141
142    /// Returns `source` with this edit applied, or `None` when the edit falls
143    /// outside the buffer or across a character boundary.
144    pub fn apply(&self, source: &str) -> Option<String> {
145        let (cut, resumes) = match self {
146            Self::Insertion { at, .. } => (*at, *at),
147            Self::Deletion { start, end } => (*start, *end),
148        };
149        let mut edited = String::from(source.get(..cut)?);
150        if let Self::Insertion { text, .. } = self {
151            edited.push_str(text);
152        }
153        edited.push_str(source.get(resumes..)?);
154        Some(edited)
155    }
156}
157
158/// The NEXT FILE ID static value.
159static NEXT_FILE_ID: AtomicU16 = AtomicU16::new(1);
160
161/// The source site containing syntax.
162///
163/// `FileId` is the ordinary source-file identity. Keeping the identity behind
164/// an enum lets the syntax model add distinct embedded or macro hygiene sites
165/// without changing every span and token field.
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub enum File {
168    /// An ordinary source file registered under a compact process-local id.
169    FileId(u16),
170    /// Syntax constructed without a corresponding on-disk source file.
171    Synthetic(u16),
172}
173
174impl File {
175    /// Allocates a compact process-local file label.
176    ///
177    /// The counter may recycle after all `u16` values have been used. Source
178    /// snapshot identity is carried by the shared [`Source`] allocation, so a
179    /// recycled display label cannot make unrelated spans equal or joinable.
180    pub fn fresh() -> Self {
181        let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
182        Self::FileId(id)
183    }
184
185    /// Creates an identity from an application-defined integer.
186    pub const fn from_raw(raw: u16) -> Self {
187        Self::FileId(raw)
188    }
189
190    /// Reports whether this identity labels syntax constructed outside an
191    /// input file, whose text records no layout an author wrote.
192    pub const fn is_synthetic(self) -> bool {
193        matches!(self, Self::Synthetic(_))
194    }
195
196    /// Returns the application-defined integer stored by this identity.
197    pub const fn into_raw(self) -> u16 {
198        match self {
199            Self::FileId(id) | Self::Synthetic(id) => id,
200        }
201    }
202}
203
204/// Compatibility name for the source identity stored by [`Span`].
205pub type SourceId = File;
206
207/// One immutable source snapshot shared by every span into it.
208pub struct Source {
209    /// The file.
210    file: File,
211    /// The text.
212    text: Arc<str>,
213    /// The line starts.
214    line_starts: OnceLock<Box<[Offset]>>,
215}
216
217impl Source {
218    /// Creates an ordinary source file with a fresh compact identity.
219    pub fn new(text: impl Into<Arc<str>>) -> Arc<Self> {
220        Self::with_file(File::fresh(), text)
221    }
222
223    /// Creates source text for syntax constructed outside an input file.
224    pub fn synthetic(text: impl Into<Arc<str>>) -> Arc<Self> {
225        let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
226        Self::with_file(File::Synthetic(id), text)
227    }
228
229    /// Creates a source snapshot with an explicitly selected source kind.
230    pub fn with_file(file: File, text: impl Into<Arc<str>>) -> Arc<Self> {
231        let text = text.into();
232        Arc::new(Self {
233            file,
234            text,
235            line_starts: OnceLock::new(),
236        })
237    }
238
239    /// Returns this source's semantic identity.
240    pub fn file(&self) -> File {
241        self.file
242    }
243
244    /// Returns the complete immutable source snapshot.
245    pub fn text(&self) -> &str {
246        &self.text
247    }
248
249    /// Returns the byte range of the layout that closes this snapshot.
250    ///
251    /// The range is empty when the text ends with a significant byte. Parsers
252    /// give the end-of-file marker exactly this range, so the bytes that follow
253    /// the last token survive a parse and emit round trip.
254    ///
255    /// ```
256    /// use m2_syn::Source;
257    ///
258    /// let source = Source::new("x = 1\n\n");
259    /// assert_eq!(source.trailing_whitespace(), 5..7);
260    /// assert_eq!(&source.text()[source.trailing_whitespace()], "\n\n");
261    /// ```
262    pub fn trailing_whitespace(&self) -> Range<Offset> {
263        let significant = self
264            .text
265            .trim_end_matches(|character: char| character.is_ascii_whitespace());
266        significant.len()..self.text.len()
267    }
268
269    /// Returns the byte range of the layout that opens this snapshot.
270    ///
271    /// The range is empty when the text starts with a significant byte, and
272    /// also when the text holds no significant byte at all: that layout closes
273    /// the snapshot, so [`Source::trailing_whitespace`] owns it instead.
274    ///
275    /// ```
276    /// use m2_syn::Source;
277    ///
278    /// let source = Source::new("\n  x = 1\n");
279    /// assert_eq!(source.leading_whitespace(), 0..3);
280    /// assert_eq!(Source::new("   \n").leading_whitespace(), 0..0);
281    /// ```
282    pub fn leading_whitespace(&self) -> Range<Offset> {
283        let layout = self.text.len()
284            - self
285                .text
286                .trim_start_matches(|character: char| character.is_ascii_whitespace())
287                .len();
288        let layout = if layout == self.text.len() { 0 } else { layout };
289        0..layout
290    }
291
292    /// Converts a byte boundary into a one-based line and character column.
293    ///
294    /// Returns `None` when `offset` lies outside this snapshot or inside a
295    /// multi-byte UTF-8 character.
296    pub fn line_column(&self, offset: Offset) -> Option<LineColumn> {
297        self.text.get(..offset)?;
298        let line_starts = self.line_starts.get_or_init(|| {
299            std::iter::once(0)
300                .chain(
301                    self.text
302                        .bytes()
303                        .enumerate()
304                        .filter_map(|(offset, byte)| (byte == b'\n').then_some(offset + 1)),
305                )
306                .collect::<Vec<_>>()
307                .into_boxed_slice()
308        });
309        let line_index = line_starts
310            .partition_point(|line_start| *line_start <= offset)
311            .checked_sub(1)?;
312        let line_start = line_starts[line_index];
313        let column = self.text.get(line_start..offset)?.chars().count() + 1;
314        Some(LineColumn {
315            line: line_index + 1,
316            column,
317        })
318    }
319}
320
321impl Debug for Source {
322    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
323        formatter
324            .debug_struct("Source")
325            .field("file", &self.file)
326            .field("bytes", &self.text.len())
327            .finish_non_exhaustive()
328    }
329}
330
331/// A shared source snapshot and optional byte range within it.
332#[derive(Clone)]
333pub struct Span {
334    source: Arc<Source>,
335    range: Option<Range<Offset>>,
336}
337
338impl Span {
339    /// Builds a span in `source` with an optional byte range.
340    pub fn new(source: Arc<Source>, range: Option<Range<Offset>>) -> Self {
341        Self {
342            source,
343            range: range.filter(|range| range.start <= range.end),
344        }
345    }
346
347    /// Builds a span covering `range` in `source`.
348    pub fn ranged(source: Arc<Source>, range: Range<Offset>) -> Self {
349        Self::new(source, Some(range))
350    }
351
352    /// Builds a span whose source is known but whose byte range is not.
353    pub fn unlocated(source: Arc<Source>) -> Self {
354        Self {
355            source,
356            range: None,
357        }
358    }
359
360    /// Creates a span covering one complete synthetic source buffer.
361    pub fn for_text(text: impl Into<Arc<str>>) -> Self {
362        let source = Source::synthetic(text);
363        Self::ranged(Arc::clone(&source), 0..source.text().len())
364    }
365
366    /// Returns the source file identity.
367    pub fn file(&self) -> File {
368        self.source.file()
369    }
370
371    /// Borrows the shared source snapshot.
372    pub fn source(&self) -> &Arc<Source> {
373        &self.source
374    }
375
376    /// Returns the source identity.
377    pub fn source_id(&self) -> File {
378        self.file()
379    }
380
381    /// Returns a copy of the indexed byte range.
382    pub fn range(&self) -> Option<Range<Offset>> {
383        self.range.clone()
384    }
385
386    /// Changes the source snapshot while preserving this span's range.
387    pub fn with_source(mut self, source: Arc<Source>) -> Self {
388        self.source = source;
389        self
390    }
391
392    /// Ensures that this span resolves to exactly `text`.
393    ///
394    /// Parsed syntax retains its original source allocation. Programmatically
395    /// constructed syntax receives a compact synthetic source allocation
396    /// rather than storing a second copy of its spelling in every token.
397    pub fn with_text(self, text: impl Into<Arc<str>>) -> Self {
398        let text = text.into();
399        if self.text() == Some(text.as_ref()) {
400            return self;
401        }
402        let source = Source::with_file(self.file(), text);
403        Self::ranged(Arc::clone(&source), 0..source.text().len())
404    }
405
406    /// Returns the exact source text covered by this span.
407    pub fn text(&self) -> Option<&str> {
408        self.source.text().get(self.range.as_ref()?.clone())
409    }
410    /// Creates a zero-width span at `offset` with this span's source identity.
411    pub fn at(&self, offset: Offset) -> Self {
412        if self.range.is_none() {
413            return Self::unlocated(Arc::clone(&self.source));
414        }
415        Self {
416            source: Arc::clone(&self.source),
417            range: Some(offset..offset),
418        }
419    }
420
421    /// Returns a subspan at byte offsets relative to this span's opening.
422    ///
423    /// The returned span shares the same source identity. Invalid or unavailable
424    /// ranges become unlocated spans in the same file.
425    pub fn subspan(&self, start: Offset, end: Offset) -> Self {
426        let Some(outer) = &self.range else {
427            return Self::unlocated(Arc::clone(&self.source));
428        };
429        let Some(start) = outer.start.checked_add(start) else {
430            return Self::unlocated(Arc::clone(&self.source));
431        };
432        let Some(end) = outer.start.checked_add(end) else {
433            return Self::unlocated(Arc::clone(&self.source));
434        };
435        if start > end || end > outer.end {
436            return Self::unlocated(Arc::clone(&self.source));
437        }
438        Self {
439            source: Arc::clone(&self.source),
440            range: Some(start..end),
441        }
442    }
443
444    /// The boundary this span opens at, or `None` when it is unlocated.
445    pub fn start(&self) -> Option<Offset> {
446        self.bounds().map(|(start, _)| start)
447    }
448
449    /// The boundary this span closes at, past its last byte.
450    pub fn end(&self) -> Option<Offset> {
451        self.bounds().map(|(_, end)| end)
452    }
453
454    /// The boundaries this span lies between.
455    pub fn bounds(&self) -> Option<(Offset, Offset)> {
456        self.range.as_ref().map(|range| (range.start, range.end))
457    }
458
459    /// Returns this span as one-based line and character-column positions.
460    ///
461    /// Returns `None` for an unlocated span, a range outside its source
462    /// snapshot, or a boundary inside a multi-byte UTF-8 character.
463    pub fn line_column_range(&self) -> Option<LineColumnRange> {
464        let (start, end) = self.bounds()?;
465        Some(LineColumnRange {
466            start: self.source.line_column(start)?,
467            end: self.source.line_column(end)?,
468        })
469    }
470
471    /// How many bytes this span covers, or `None` when it is unlocated.
472    pub fn len(&self) -> Option<usize> {
473        self.bounds().map(|(start, end)| end - start)
474    }
475
476    /// Reports whether this span covers no bytes, as the virtual marker tokens
477    /// do. Unlocated syntax covers no bytes either.
478    pub fn is_empty(&self) -> bool {
479        self.len().unwrap_or(0) == 0
480    }
481
482    /// Reports whether both spans identify the same source site.
483    pub fn shares_source(&self, other: &Self) -> bool {
484        Arc::ptr_eq(&self.source, &other.source)
485    }
486
487    /// Returns whether `offset` lies inside this span, excluding its closing
488    /// boundary.
489    ///
490    /// Boundaries sit between bytes, so the closing one belongs to whatever
491    /// follows: in `a+b` the boundary after `a` is contained by `+`, not by `a`.
492    /// This is the predicate to use when exactly one span must own a position. A
493    /// zero-width span contains nothing, having no interior.
494    pub fn contains(&self, offset: Offset) -> bool {
495        self.bounds()
496            .is_some_and(|(start, end)| start <= offset && offset < end)
497    }
498
499    /// Returns whether `offset` lies inside this span or on either boundary.
500    ///
501    /// Adjacent spans both touch the boundary between them, so this answers
502    /// "which syntax is the cursor at" rather than "which syntax owns this
503    /// position" — a cursor just past an identifier still touches it, and a
504    /// zero-width marker is touched at its own position.
505    pub fn touches(&self, offset: Offset) -> bool {
506        self.bounds()
507            .is_some_and(|(start, end)| start <= offset && offset <= end)
508    }
509
510    /// Returns this span's position in the buffer produced by `edit`.
511    ///
512    /// Both boundaries move under the same rule, so nothing here decides which
513    /// side of an insertion a boundary belongs to. Because that rule is monotone
514    /// the pair can never invert, and a zero-width span needs no special case:
515    /// its two equal boundaries stay equal.
516    ///
517    /// Text inserted at a span's *closing* boundary therefore extends the span,
518    /// which is what an unclosed group wants — it keeps reaching whatever now
519    /// follows it. A span wholly removed by a deletion collapses to a zero-width
520    /// span at the deletion point rather than losing its location,
521    /// because it still records where the syntax used to be.
522    pub fn shifted(&self, edit: &TextEdit) -> Self {
523        match self.bounds() {
524            None => Self::unlocated(Arc::clone(&self.source)),
525            Some((start, end)) => Self {
526                source: Arc::clone(&self.source),
527                range: Some(edit.shift(start)..edit.shift(end)),
528            },
529        }
530    }
531
532    /// Returns this span with the same opening boundary and source identity but
533    /// a new byte length.
534    pub fn resized(&self, len: usize) -> Self {
535        let Some(start) = self.start() else {
536            return Self::unlocated(Arc::clone(&self.source));
537        };
538        Self {
539            source: Arc::clone(&self.source),
540            range: Some(start..start.saturating_add(len)),
541        }
542    }
543}
544
545impl Span {
546    /// The smallest span covering both, or `None` when they belong to
547    /// different files.
548    pub fn join(&self, other: Self) -> Option<Self> {
549        if !self.shares_source(&other) {
550            return None;
551        }
552        match (self.bounds(), other.bounds()) {
553            (None, _) => Some(other),
554            (_, None) => Some(self.clone()),
555            (Some((start, end)), Some((start2, end2))) => {
556                let start = start.min(start2);
557                let end = end.max(end2);
558                Some(Self {
559                    source: Arc::clone(&self.source),
560                    range: Some(start..end),
561                })
562            }
563        }
564    }
565
566    /// The overlap of both, or `None` when the files differ or the ranges do
567    /// not overlap.
568    pub fn meet(&self, other: Self) -> Option<Self> {
569        if !self.shares_source(&other) {
570            return None;
571        }
572        match (self.bounds(), other.bounds()) {
573            (Some((start, end)), Some((start2, end2))) => {
574                let start = start.max(start2);
575                let end = end.min(end2);
576                if start < end {
577                    Some(Self {
578                        source: Arc::clone(&self.source),
579                        range: Some(start..end),
580                    })
581                } else {
582                    None
583                }
584            }
585            _ => None,
586        }
587    }
588
589    /// Joins every span in an iterator, returning `None` for an empty iterator
590    /// or when any two spans belong to different files.
591    pub fn join_all<I, S>(spans: I) -> Option<Self>
592    where
593        I: IntoIterator<Item = S>,
594        S: Into<Option<Self>>,
595    {
596        let mut spans = spans.into_iter().filter_map(Into::into);
597        let first = spans.next()?;
598        spans.try_fold(first, |left, right| left.join(right))
599    }
600}
601
602impl PartialEq for Span {
603    fn eq(&self, other: &Self) -> bool {
604        self.shares_source(other) && self.range == other.range
605    }
606}
607
608impl Eq for Span {}
609
610impl Hash for Span {
611    fn hash<H: Hasher>(&self, state: &mut H) {
612        Arc::as_ptr(&self.source).hash(state);
613        self.range.hash(state);
614    }
615}
616
617impl Debug for Span {
618    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
619        let mut span = formatter.debug_struct("Span");
620        span.field("file", &self.file())
621            .field("range", &self.line_column_range())
622            .field("bytes", &self.range);
623        if let Some(text) = self.text() {
624            span.field("text", &text);
625        }
626        span.finish()
627    }
628}
629
630impl Display for Span {
631    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
632        match self.line_column_range() {
633            Some(range) => Display::fmt(&range, formatter),
634            None => formatter.write_str("unlocated"),
635        }
636    }
637}
638
639/// A syntax value that reports its source extent.
640pub trait Spanned {
641    /// Returns the smallest span covering this value.
642    ///
643    /// # Panics
644    ///
645    /// Panics when the value is absent or assembled from different source
646    /// snapshots. Use [`Self::try_span`] when either case is expected.
647    fn span(&self) -> Span {
648        self.try_span()
649            .expect("syntax must have one shared source snapshot")
650    }
651
652    /// Returns the smallest span covering this value, when its constituents
653    /// share one source snapshot.
654    fn try_span(&self) -> Option<Span> {
655        Some(self.span())
656    }
657
658    /// Returns this value's span when one source snapshot covers it.
659    ///
660    /// # Panics
661    ///
662    /// Panics for absent syntax or syntax assembled from different sources.
663    fn expect_span(&self) -> Span {
664        self.span()
665    }
666}
667
668impl Spanned for Span {
669    fn span(&self) -> Span {
670        self.clone()
671    }
672}
673
674impl<T: Spanned + ?Sized> Spanned for &T {
675    fn span(&self) -> Span {
676        (*self).span()
677    }
678
679    fn try_span(&self) -> Option<Span> {
680        (*self).try_span()
681    }
682}
683
684impl<T: Spanned + ?Sized> Spanned for Box<T> {
685    fn span(&self) -> Span {
686        self.as_ref().span()
687    }
688
689    fn try_span(&self) -> Option<Span> {
690        self.as_ref().try_span()
691    }
692}
693
694impl<T: Spanned> Spanned for Option<T> {
695    fn try_span(&self) -> Option<Span> {
696        self.as_ref().and_then(Spanned::try_span)
697    }
698}
699
700impl<T: Spanned> Spanned for Vec<T> {
701    fn try_span(&self) -> Option<Span> {
702        Span::join_all(self.iter().map(Spanned::try_span))
703    }
704}
705
706impl<A: Spanned, B: Spanned> Spanned for (A, B) {
707    fn try_span(&self) -> Option<Span> {
708        self.0.try_span()?.join(self.1.try_span()?)
709    }
710}
711
712impl<T: Spanned> Spanned for &[T] {
713    fn try_span(&self) -> Option<Span> {
714        Span::join_all(self.iter().map(Spanned::try_span))
715    }
716}
717
718#[cfg(test)]
719mod source_tests {
720    use std::sync::Arc;
721
722    use super::*;
723
724    #[test]
725    fn spans_resolve_text_from_one_shared_source() {
726        let source = Source::new("quotient ring");
727        let quotient = Span::ranged(Arc::clone(&source), 0..8);
728        let ring = Span::ranged(Arc::clone(&source), 9..13);
729
730        assert_eq!(quotient.text(), Some("quotient"));
731        assert_eq!(ring.text(), Some("ring"));
732        assert!(Arc::ptr_eq(quotient.source(), ring.source()));
733    }
734
735    #[test]
736    fn joining_different_sources_returns_none() {
737        let left = Span::for_text("x");
738        let right = Span::for_text("y");
739
740        assert_eq!(left.join(right), None);
741    }
742
743    #[test]
744    fn an_unlocated_span_still_knows_its_source() {
745        let source = Source::new("x + y");
746        let span = Span::unlocated(Arc::clone(&source));
747
748        assert_eq!(span.file(), source.file());
749        assert_eq!(span.range(), None);
750        assert_eq!(span.text(), None);
751    }
752
753    #[test]
754    fn source_snapshots_with_the_same_file_do_not_alias() {
755        let file = File::from_raw(7);
756        let before = Span::ranged(Source::with_file(file, "x"), 0..1);
757        let after = Span::ranged(Source::with_file(file, "y"), 0..1);
758
759        assert_ne!(before, after);
760        assert_eq!(before.join(after), None);
761    }
762
763    #[test]
764    fn shifting_preserves_source_identity() {
765        let source = Source::new("ring");
766        let span = Span::ranged(Arc::clone(&source), 0..4);
767        let shifted = span.shifted(&TextEdit::insertion(0, "graded "));
768
769        assert!(Arc::ptr_eq(&source, shifted.source()));
770        assert_eq!(shifted.range(), Some(7..11));
771    }
772
773    #[test]
774    fn line_columns_are_one_based_and_count_characters() {
775        let source = Source::new("aα\nβc");
776
777        assert_eq!(
778            source.line_column(0),
779            Some(LineColumn { line: 1, column: 1 }),
780        );
781        assert_eq!(
782            source.line_column("aα\n".len()),
783            Some(LineColumn { line: 2, column: 1 }),
784        );
785        assert_eq!(
786            source.line_column("aα\nβ".len()),
787            Some(LineColumn { line: 2, column: 2 }),
788        );
789        assert_eq!(source.line_column(2), None);
790    }
791
792    #[test]
793    fn spans_format_as_half_open_line_column_ranges() {
794        let source = Source::new("one\ntwo");
795        let range = Span::ranged(Arc::clone(&source), 2..6);
796        let point = range.at(4);
797
798        assert_eq!(range.to_string(), "1:3-2:3");
799        assert_eq!(point.to_string(), "2:1");
800    }
801}