Skip to main content

m2_syn/
span.rs

1use std::hash::{Hash, Hasher};
2use std::ops::Index;
3use std::sync::Arc;
4
5/// A boundary in a source buffer, as a byte offset.
6///
7/// Boundaries sit between bytes rather than on them, so a span's `end` is the
8/// offset past its last byte and `end - start` is its length. One offset is
9/// enough because a span's only job is to index a buffer: line and column are
10/// facts about a particular buffer's contents, recovered from it when something
11/// needs to *display* a location, and so they belong with the buffer rather
12/// than with the span. This is what `rustc` does too — its `Span` stores `lo`
13/// and `hi` byte positions and nothing else, and `SourceMap` binary-searches a
14/// line table on demand.
15pub type Offset = usize;
16
17/// One text change: the text a buffer gains at a point, or the stretch it
18/// loses.
19///
20/// An edit acts on *both* halves of the indexing relation — on source text
21/// through [`Self::apply`] and on a [`Span`] through [`Span::shifted`] — and
22/// those two actions are designed to agree.
23///
24/// An insertion carries the text it inserts rather than a pair of boundaries,
25/// because the boundaries are recoverable (`at` and `at + text.len()`) while the
26/// text is not, and without the text an edit cannot act on a buffer at all.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum TextEdit {
29    /// Inserts `text` at one byte boundary.
30    Insertion {
31        /// The insertion boundary.
32        at: Offset,
33        /// The inserted text.
34        text: String,
35    },
36    /// Deletes one half-open byte range.
37    Deletion {
38        /// The first deleted byte.
39        start: Offset,
40        /// The boundary after the last deleted byte.
41        end: Offset,
42    },
43}
44
45impl TextEdit {
46    /// Creates an insertion of `text` landing at `at`.
47    pub fn insertion(at: Offset, text: impl Into<String>) -> Self {
48        Self::Insertion {
49            at,
50            text: text.into(),
51        }
52    }
53
54    /// Creates a deletion of the text occupying `start..end`.
55    ///
56    /// # Panics
57    ///
58    /// Panics when `start` is greater than `end`.
59    pub fn deletion(start: Offset, end: Offset) -> Self {
60        assert!(start <= end, "deletion start must not exceed its end");
61        Self::Deletion { start, end }
62    }
63
64    /// Returns `offset` in the buffer this edit produces.
65    ///
66    /// For an insertion of width `d` at `s` this is `offset + (offset >= s) * d`
67    /// — everything at or after the insertion point moves by the width, and
68    /// nothing else moves at all. A deletion is the mirror image, with the text
69    /// it covered collapsing onto its start.
70    ///
71    /// The rule is monotone in `offset`, so applying it to both ends of a range
72    /// can never invert them, and only the edit's *width* enters the arithmetic.
73    /// Reaching the same result by summing two absolute offsets would
74    /// manufacture an overflow the buffer never causes.
75    fn shift(&self, offset: Offset) -> Offset {
76        match self {
77            Self::Insertion { at, text } => {
78                if offset >= *at {
79                    offset + text.len()
80                } else {
81                    offset
82                }
83            }
84            Self::Deletion { start, end } => {
85                if offset <= *start {
86                    offset
87                } else if offset <= *end {
88                    // Everything the deletion covered now sits at its start.
89                    *start
90                } else {
91                    offset - (end - start)
92                }
93            }
94        }
95    }
96
97    /// Returns `source` with this edit applied, or `None` when the edit falls
98    /// outside the buffer or across a character boundary.
99    pub fn apply(&self, source: &str) -> Option<String> {
100        let (cut, resumes) = match self {
101            Self::Insertion { at, .. } => (*at, *at),
102            Self::Deletion { start, end } => (*start, *end),
103        };
104        let mut edited = String::from(source.get(..cut)?);
105        if let Self::Insertion { text, .. } = self {
106            edited.push_str(text);
107        }
108        edited.push_str(source.get(resumes..)?);
109        Some(edited)
110    }
111}
112
113/// Where syntax originated: a stretch of a buffer, or nowhere at all.
114///
115/// A zero-width `Range` is `start == end`, which is what the virtual marker
116/// nodes use — end of input, the `Adj` between adjacent expressions,
117/// an omitted component in `(x,,y)`. Because boundaries sit between bytes,
118/// `x..x` already names a position exactly, so such a marker needs no
119/// representation of its own. An inverted pair can never be constructed:
120/// [`Self::new`] collapses it to `Detached` rather than fabricating a range.
121///
122#[derive(Debug, Clone)]
123struct SourceSlice {
124    text: Arc<str>,
125    origin: Offset,
126}
127
128impl PartialEq for SourceSlice {
129    fn eq(&self, other: &Self) -> bool {
130        self.origin == other.origin && Arc::ptr_eq(&self.text, &other.text)
131    }
132}
133
134impl Eq for SourceSlice {}
135
136impl Hash for SourceSlice {
137    fn hash<H: Hasher>(&self, state: &mut H) {
138        self.origin.hash(state);
139        std::ptr::hash(Arc::as_ptr(&self.text), state);
140    }
141}
142
143impl SourceSlice {
144    fn covers(&self, start: Offset, end: Offset) -> bool {
145        self.origin <= start && end <= self.origin.saturating_add(self.text.len())
146    }
147}
148
149fn common_source(
150    left: Option<&SourceSlice>,
151    right: Option<&SourceSlice>,
152    start: Offset,
153    end: Offset,
154) -> Option<SourceSlice> {
155    match (left, right) {
156        (Some(left), Some(right)) if left == right => Some(left.clone()),
157        (Some(source), None) | (None, Some(source)) if source.covers(start, end) => {
158            Some(source.clone())
159        }
160        _ => None,
161    }
162}
163
164/// Where syntax originated and, when available, the source buffer it indexes.
165///
166/// Source-derived tokens retain only this value. Their spelling is recovered
167/// by slicing the shared source buffer; it is never copied into token or syntax
168/// node storage. Spans constructed only from offsets remain useful for
169/// diagnostics and synthetic trees, but naturally cannot yield source text.
170#[derive(Debug, Clone, Default)]
171pub struct Span {
172    bounds: Option<(Offset, Offset)>,
173    source: Option<SourceSlice>,
174}
175
176impl PartialEq for Span {
177    fn eq(&self, other: &Self) -> bool {
178        self.bounds == other.bounds && self.source == other.source
179    }
180}
181
182impl Eq for Span {}
183
184impl Hash for Span {
185    fn hash<H: Hasher>(&self, state: &mut H) {
186        self.bounds.hash(state);
187        self.source.hash(state);
188    }
189}
190
191impl Span {
192    /// A detached span without source text.
193    #[allow(non_upper_case_globals)]
194    pub const Detached: Self = Self {
195        bounds: None,
196        source: None,
197    };
198
199    /// Creates a span with no source location.
200    pub fn detached() -> Self {
201        Self::Detached
202    }
203
204    /// Builds a span covering `start..end`, where `end` is the offset past the
205    /// last byte.
206    ///
207    /// An inverted pair collapses to `Detached` rather than fabricating a range.
208    pub fn new(start: Offset, end: Offset) -> Self {
209        if start <= end {
210            Self {
211                bounds: Some((start, end)),
212                source: None,
213            }
214        } else {
215            Self::Detached
216        }
217    }
218
219    /// A zero-width span at one boundary, for syntax that occupies no bytes.
220    pub fn from_offset(offset: Offset) -> Self {
221        Self {
222            bounds: Some((offset, offset)),
223            source: None,
224        }
225    }
226
227    /// Attaches the complete source fragment covered by this span.
228    ///
229    /// Existing source backing is retained. For an offset-only span, `text`
230    /// must have the same byte width as the span; its first byte is associated
231    /// with the span's opening offset. Detached syntax may also carry a source
232    /// fragment, which is useful for programmatically constructed syntax.
233    ///
234    /// # Panics
235    ///
236    /// Panics when an offset-only span and `text` have different byte widths.
237    pub fn with_text(self, text: impl Into<Arc<str>>) -> Self {
238        if self.source.is_some() {
239            return self;
240        }
241        let text = text.into();
242        let origin = match self.bounds {
243            Some((start, end)) => {
244                assert_eq!(
245                    end - start,
246                    text.len(),
247                    "source fragment width must equal its span width"
248                );
249                start
250            }
251            None => 0,
252        };
253        Self {
254            source: Some(SourceSlice { text, origin }),
255            ..self
256        }
257    }
258
259    /// Replaces this span's source fragment while retaining its opening
260    /// location and updating its width.
261    pub fn replaced_text(&self, text: impl Into<Arc<str>>) -> Self {
262        let text = text.into();
263        let bounds = self
264            .start()
265            .map(|start| (start, start.saturating_add(text.len())));
266        let origin = bounds.map_or(0, |(start, _)| start);
267        Self {
268            bounds,
269            source: Some(SourceSlice { text, origin }),
270        }
271    }
272
273    /// Creates a source-backed span covering an entire source buffer.
274    pub fn source(text: impl Into<Arc<str>>) -> Self {
275        let text = text.into();
276        let end = text.len();
277        Self {
278            bounds: Some((0, end)),
279            source: Some(SourceSlice { text, origin: 0 }),
280        }
281    }
282
283    /// Returns a subspan at byte offsets relative to this span's opening.
284    ///
285    /// The returned span shares the same source buffer. Invalid or detached
286    /// ranges become detached spans.
287    pub fn subspan(&self, start: Offset, end: Offset) -> Self {
288        let Some((outer_start, outer_end)) = self.bounds else {
289            return Self::Detached;
290        };
291        let Some(start) = outer_start.checked_add(start) else {
292            return Self::Detached;
293        };
294        let Some(end) = outer_start.checked_add(end) else {
295            return Self::Detached;
296        };
297        if start > end || end > outer_end {
298            return Self::Detached;
299        }
300        Self {
301            bounds: Some((start, end)),
302            source: self.source.clone(),
303        }
304    }
305
306    /// Returns the exact source text covered by this span when source backing
307    /// is available.
308    pub fn text(&self) -> Option<&str> {
309        let source = self.source.as_ref()?;
310        match self.bounds {
311            Some((start, end)) => source
312                .text
313                .get(start.checked_sub(source.origin)?..end.checked_sub(source.origin)?),
314            None => Some(&source.text),
315        }
316    }
317
318    /// Reports whether both spans slice the same shared source buffer.
319    pub fn shares_source(&self, other: &Self) -> bool {
320        match (&self.source, &other.source) {
321            (Some(left), Some(right)) => Arc::ptr_eq(&left.text, &right.text),
322            _ => false,
323        }
324    }
325
326    /// The boundary this span opens at, or `None` when it is detached.
327    pub fn start(&self) -> Option<Offset> {
328        self.bounds().map(|(start, _)| start)
329    }
330
331    /// The boundary this span closes at, past its last byte.
332    pub fn end(&self) -> Option<Offset> {
333        self.bounds().map(|(_, end)| end)
334    }
335
336    /// The boundaries this span lies between.
337    pub fn bounds(&self) -> Option<(Offset, Offset)> {
338        self.bounds
339    }
340
341    /// How many bytes this span covers, or `None` when it is detached.
342    pub fn len(&self) -> Option<usize> {
343        self.bounds().map(|(start, end)| end - start)
344    }
345
346    /// Reports whether this span covers no bytes, as the virtual marker tokens
347    /// do. Detached syntax covers no bytes either.
348    pub fn is_empty(&self) -> bool {
349        self.len().unwrap_or(0) == 0
350    }
351
352    /// Returns whether `offset` lies inside this span, excluding its closing
353    /// boundary.
354    ///
355    /// Boundaries sit between bytes, so the closing one belongs to whatever
356    /// follows: in `a+b` the boundary after `a` is contained by `+`, not by `a`.
357    /// This is the predicate to use when exactly one span must own a position. A
358    /// zero-width span contains nothing, having no interior.
359    pub fn contains(&self, offset: Offset) -> bool {
360        self.bounds()
361            .is_some_and(|(start, end)| start <= offset && offset < end)
362    }
363
364    /// Returns whether `offset` lies inside this span or on either boundary.
365    ///
366    /// Adjacent spans both touch the boundary between them, so this answers
367    /// "which syntax is the cursor at" rather than "which syntax owns this
368    /// position" — a cursor just past an identifier still touches it, and a
369    /// zero-width marker is touched at its own position.
370    pub fn touches(&self, offset: Offset) -> bool {
371        self.bounds()
372            .is_some_and(|(start, end)| start <= offset && offset <= end)
373    }
374
375    /// Returns this span's position in the buffer produced by `edit`.
376    ///
377    /// Both boundaries move under the same rule, so nothing here decides which
378    /// side of an insertion a boundary belongs to. Because that rule is monotone
379    /// the pair can never invert, and a zero-width span needs no special case:
380    /// its two equal boundaries stay equal.
381    ///
382    /// Text inserted at a span's *closing* boundary therefore extends the span,
383    /// which is what an unclosed group wants — it keeps reaching whatever now
384    /// follows it. A span wholly removed by a deletion collapses to a zero-width
385    /// span at the deletion point rather than becoming [`Self::Detached`],
386    /// because it still records where the syntax used to be.
387    pub fn shifted(&self, edit: &TextEdit) -> Self {
388        match self.bounds() {
389            None => Self::Detached,
390            Some((start, end)) => Self::new(edit.shift(start), edit.shift(end)),
391        }
392    }
393
394    /// The smallest span covering both, ignoring detached operands.
395    pub fn join(&self, other: Self) -> Self {
396        match (self.bounds(), other.bounds()) {
397            (None, _) => other,
398            (_, None) => self.clone(),
399            (Some((start, end)), Some((start2, end2))) => {
400                let start = start.min(start2);
401                let end = end.max(end2);
402                let bounds = Some((start, end));
403                let source = common_source(self.source.as_ref(), other.source.as_ref(), start, end);
404                Self { bounds, source }
405            }
406        }
407    }
408
409    /// The overlap of both, or `Detached` when they do not overlap.
410    pub fn meet(&self, other: Self) -> Self {
411        match (self.bounds(), other.bounds()) {
412            (Some((start, end)), Some((start2, end2))) => {
413                let start = start.max(start2);
414                let end = end.min(end2);
415                if start < end {
416                    let source =
417                        common_source(self.source.as_ref(), other.source.as_ref(), start, end);
418                    Self {
419                        bounds: Some((start, end)),
420                        source,
421                    }
422                } else {
423                    Self::Detached
424                }
425            }
426            _ => Self::Detached,
427        }
428    }
429
430    /// Joins every span in an iterator, ignoring detached spans.
431    pub fn join_all(spans: impl IntoIterator<Item = Span>) -> Self {
432        spans
433            .into_iter()
434            .reduce(|left, right| left.join(right))
435            .unwrap_or_default()
436    }
437}
438
439/// Indexes the buffer, panicking when the span does not name text in it.
440///
441/// This is legal despite `Index` and `str` both being foreign because [`Span`]
442/// is local, and it cannot overlap `str`'s own
443/// `impl<I: SliceIndex<str>> Index<I> for str` because `Span` does not implement
444/// `SliceIndex`.
445impl Index<Span> for str {
446    type Output = str;
447
448    fn index(&self, span: Span) -> &str {
449        span.bounds()
450            .and_then(|(start, end)| self.get(start..end))
451            .unwrap_or_else(|| panic!("{span:?} does not name text in this buffer"))
452    }
453}
454
455impl Index<&Span> for str {
456    type Output = str;
457
458    fn index(&self, span: &Span) -> &str {
459        span.bounds()
460            .and_then(|(start, end)| self.get(start..end))
461            .unwrap_or_else(|| panic!("{span:?} does not name text in this buffer"))
462    }
463}
464
465/// `String` needs its own implementation rather than reaching `str`'s through
466/// deref: it already has an `Index` impl of its own, so operator resolution stops
467/// there instead of falling through.
468impl Index<Span> for String {
469    type Output = str;
470
471    fn index(&self, span: Span) -> &str {
472        self.as_str().index(span)
473    }
474}
475
476impl Index<&Span> for String {
477    type Output = str;
478
479    fn index(&self, span: &Span) -> &str {
480        self.as_str().index(span)
481    }
482}
483
484/// A syntax value that reports its source extent.
485pub trait Spanned {
486    /// Returns the smallest span covering this value.
487    fn span(&self) -> Span;
488}
489
490impl Spanned for Span {
491    fn span(&self) -> Span {
492        self.clone()
493    }
494}
495
496impl<T: Spanned + ?Sized> Spanned for &T {
497    fn span(&self) -> Span {
498        (*self).span()
499    }
500}
501
502impl<T: Spanned + ?Sized> Spanned for Box<T> {
503    fn span(&self) -> Span {
504        self.as_ref().span()
505    }
506}
507
508impl<T: Spanned> Spanned for Option<T> {
509    fn span(&self) -> Span {
510        self.as_ref().map_or_else(Span::detached, Spanned::span)
511    }
512}
513
514impl<T: Spanned> Spanned for Vec<T> {
515    fn span(&self) -> Span {
516        Span::join_all(self.iter().map(Spanned::span))
517    }
518}
519
520impl<A: Spanned, B: Spanned> Spanned for (A, B) {
521    fn span(&self) -> Span {
522        self.0.span().join(self.1.span())
523    }
524}
525
526impl<T: Spanned> Spanned for &[T] {
527    fn span(&self) -> Span {
528        Span::join_all(self.iter().map(Spanned::span))
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    /// The span of `text` starting at `start`, as the lexer would emit it:
537    /// boundaries around the text, so `end` is one past its last byte.
538    fn span_of(start: Offset, text: &str) -> Span {
539        Span::new(start, start + text.len())
540    }
541
542    #[test]
543    fn boundaries_are_gaps_so_adjacent_spans_never_own_the_same_position() {
544        // `a+b`: the gap at byte 1 closes `a` and opens `+`.
545        let a = span_of(0, "a");
546        let plus = span_of(1, "+");
547
548        assert!(!a.contains(1));
549        assert!(plus.contains(1));
550        // Both touch it, which is what a cursor query wants.
551        assert!(a.touches(1));
552        assert!(plus.touches(1));
553    }
554
555    #[test]
556    fn a_marker_token_contains_nothing_but_touches_its_own_position() {
557        let eof = Span::from_offset(3);
558
559        assert!(eof.is_empty());
560        assert!(!eof.contains(3));
561        assert!(eof.touches(3));
562    }
563
564    /// An insertion of width `d` at `s` sends `x` to `x + (x >= s) * d`, and
565    /// nothing else about it varies — there is no side to choose.
566    #[test]
567    fn an_insertion_moves_exactly_the_offsets_at_or_after_it() {
568        let insert = TextEdit::insertion(3, "xy");
569
570        assert_eq!(insert.shift(2), 2);
571        // At the insertion point, so it travels — which is what a marker token
572        // at end of input wants, staying past everything typed at it.
573        assert_eq!(insert.shift(3), 5);
574        assert_eq!(insert.shift(4), 6);
575    }
576
577    /// Shifting is monotone, so the two ends of a span can never cross and a
578    /// zero-width span stays zero-width without needing a special case.
579    #[test]
580    fn shifting_never_inverts_a_span() {
581        let edits = [TextEdit::insertion(3, "xy"), TextEdit::deletion(2, 5)];
582
583        for edit in &edits {
584            for start in 0..8 {
585                for end in start..8 {
586                    let shifted = Span::new(start, end).shifted(edit);
587                    assert_ne!(shifted, Span::Detached, "{start}..{end} under {edit:?}");
588                }
589            }
590            assert!(Span::from_offset(3).shifted(edit).is_empty());
591        }
592    }
593
594    #[test]
595    fn insertion_at_a_span_boundary_moves_it_or_extends_the_span() {
596        let span = span_of(2, "xy");
597
598        // At byte 2, the span's own start: both ends clear the threshold, so the
599        // span moves whole and the new text lands before it.
600        assert_eq!(
601            span.shifted(&TextEdit::insertion(2, "AB")),
602            span_of(4, "xy")
603        );
604        // At byte 4, the span's own closing boundary: only the end clears it, so
605        // the span extends to keep reaching what now follows.
606        assert_eq!(span.shifted(&TextEdit::insertion(4, "AB")), Span::new(2, 6));
607    }
608
609    #[test]
610    fn insertion_strictly_inside_a_span_extends_it() {
611        let span = span_of(0, "abcd");
612
613        assert_eq!(
614            span.shifted(&TextEdit::insertion(2, "efg")),
615            span_of(0, "abcdefg")
616        );
617    }
618
619    #[test]
620    fn text_after_a_deletion_moves_back_and_covered_text_collapses() {
621        let deleted = TextEdit::deletion(1, 3);
622
623        // Wholly after: shifts back by the deleted width.
624        assert_eq!(span_of(4, "z").shifted(&deleted), span_of(2, "z"));
625        // Wholly before: untouched.
626        assert_eq!(span_of(0, "a").shifted(&deleted), span_of(0, "a"));
627        // Wholly covered: collapses to where it used to be, not detached.
628        let covered = span_of(1, "xy").shifted(&deleted);
629        assert_eq!(covered, Span::from_offset(1));
630        assert!(covered.is_empty());
631    }
632
633    #[test]
634    fn a_detached_span_is_unaffected_by_edits() {
635        assert_eq!(
636            Span::detached().shifted(&TextEdit::insertion(0, "abc")),
637            Span::detached()
638        );
639    }
640
641    /// Offsets near the top of the range are ordinary buffer positions, so
642    /// shifting must stay within the edit's own width rather than summing two
643    /// absolute offsets.
644    #[test]
645    fn shifting_late_offsets_does_not_overflow() {
646        let span = Span::new(usize::MAX - 4, usize::MAX - 3);
647        // Two bytes inserted far below the span.
648        let insert = TextEdit::insertion(1, "ab");
649
650        // The displacement is two bytes. Summing the absolute offsets instead
651        // would compute `(usize::MAX - 4) + 3`, overflowing.
652        assert_eq!(
653            span.shifted(&insert),
654            Span::new(usize::MAX - 2, usize::MAX - 1)
655        );
656    }
657
658    /// `shift` moves its argument whenever the edit has any width, so leaving
659    /// earlier text alone rests entirely on its guards.
660    #[test]
661    fn text_before_an_edit_never_moves() {
662        let edits = [TextEdit::insertion(8, "AB"), TextEdit::deletion(8, 10)];
663        let earlier = [
664            span_of(5, "ab"),
665            Span::new(0, 2),
666            // Ends just short of the edit. A span ending *exactly* at an
667            // insertion extends instead, which
668            // `insertion_at_a_span_boundary_moves_it_or_extends_the_span`
669            // covers.
670            Span::new(6, 7),
671            // A zero-width marker before the edit.
672            Span::from_offset(1),
673        ];
674
675        for edit in &edits {
676            for span in &earlier {
677                assert_eq!(span.shifted(edit), *span, "{span:?} moved under {edit:?}");
678            }
679        }
680    }
681
682    /// Inserting at a span's own start carries the whole span forward: both
683    /// endpoints move by the edit's width, so the span keeps its length and the
684    /// new text lands outside it.
685    #[test]
686    fn inserting_at_a_span_start_moves_both_endpoints() {
687        let span = Span::new(2, 6);
688        let shifted = span.shifted(&TextEdit::insertion(2, "xyz"));
689
690        assert_eq!(shifted, Span::new(5, 9));
691        assert_eq!(shifted.len(), span.len());
692    }
693
694    #[test]
695    fn an_inverted_pair_collapses_rather_than_becoming_a_range() {
696        assert_eq!(Span::new(5, 2), Span::Detached);
697    }
698
699    #[test]
700    #[should_panic(expected = "deletion start must not exceed its end")]
701    fn an_inverted_deletion_is_rejected_at_construction() {
702        TextEdit::deletion(3, 1);
703    }
704
705    #[test]
706    fn adjacent_spans_do_not_have_an_overlap() {
707        assert_eq!(Span::new(0, 1).meet(Span::new(1, 2)), Span::Detached);
708    }
709
710    #[test]
711    fn a_span_indexes_the_buffer_it_came_from() {
712        let source = "quotient ring";
713
714        assert_eq!(&source[span_of(9, "ring")], "ring");
715        // A marker token names the empty string at its own position.
716        assert_eq!(&source[Span::from_offset(9)], "");
717        assert_eq!(Span::new(0, source.len()), Span::new(0, 13));
718
719        // Spans that name no text in this buffer are refused rather than guessed.
720    }
721
722    #[test]
723    #[should_panic(expected = "does not name text in this buffer")]
724    fn indexing_with_a_detached_span_is_a_bug_rather_than_an_empty_slice() {
725        let _ = &"ring"[Span::detached()];
726    }
727
728    /// The law tying the two halves together: an edit acts on the buffer and on
729    /// the span, and a span disjoint from the edit still names the same text
730    /// afterwards. This is what `shifted` is *for*, so it is checked against
731    /// every edit shape rather than a single hand-picked case.
732    #[test]
733    fn an_edit_acts_on_buffer_and_span_alike() {
734        let source = "quotient ring R";
735        let ring = span_of(9, "ring");
736
737        let disjoint = [
738            // Before the span.
739            TextEdit::insertion(0, "the "),
740            TextEdit::deletion(0, 9),
741            // Exactly at its opening boundary: the span moves whole, so it
742            // still names the same text. An insertion at the *closing*
743            // boundary extends the span instead and is not disjoint.
744            TextEdit::insertion(9, "graded "),
745            // After the span.
746            TextEdit::insertion(15, " = QQ"),
747            TextEdit::deletion(13, 15),
748            // A multi-byte character, to catch byte/char confusion.
749            TextEdit::insertion(0, "ℤ "),
750        ];
751
752        for edit in &disjoint {
753            let edited = edit.apply(source).expect("edit lands inside the buffer");
754            assert_eq!(
755                &edited[ring.shifted(edit)],
756                &source[&ring],
757                "{edit:?} broke the span"
758            );
759        }
760    }
761
762    /// The general form of the same law: for an edit landing *inside* a span,
763    /// the shifted span names the edited text, so the edit acts on the slice too.
764    #[test]
765    fn an_edit_inside_a_span_acts_on_the_text_the_span_names() {
766        let source = "quotient ring";
767        let ring = span_of(9, "ring");
768        let edit = TextEdit::insertion(11, "AB");
769
770        let edited = edit.apply(source).expect("edit lands inside the buffer");
771        // The span still names its own text, with the insertion carried into it.
772        assert_eq!(&edited[ring.shifted(&edit)], "riABng");
773        // Which is exactly the edit acting on the original slice, rebased to it.
774        let inside = TextEdit::insertion(11 - ring.start().unwrap(), "AB");
775        assert_eq!(inside.apply(&source[ring]).unwrap(), "riABng");
776    }
777
778    /// Text typed at a span's closing boundary is absorbed by it. This is what
779    /// an unclosed group wants: its span keeps reaching the end of whatever has
780    /// been typed since, without anyone re-deriving it.
781    #[test]
782    fn a_span_absorbs_text_appended_at_its_closing_boundary() {
783        let source = "f(x";
784        // An unclosed group, running from its opening delimiter to end of input.
785        let group = Span::new(1, source.len());
786        assert_eq!(&source[&group], "(x");
787
788        let typed = TextEdit::insertion(3, ", y");
789        let edited = typed.apply(source).expect("edit lands inside the buffer");
790
791        assert_eq!(&edited[group.shifted(&typed)], "(x, y");
792        // Still opens at the delimiter and still reaches end of input.
793        assert_eq!(group.shifted(&typed), Span::new(1, edited.len()));
794    }
795}