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}