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
9pub type Offset = usize;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct LineColumn {
28 pub line: usize,
30 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)]
41pub struct LineColumnRange {
46 pub start: LineColumn,
48 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#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum TextEdit {
74 Insertion {
76 at: Offset,
78 text: String,
80 },
81 Deletion {
83 start: Offset,
85 end: Offset,
87 },
88}
89
90impl TextEdit {
91 pub fn insertion(at: Offset, text: impl Into<String>) -> Self {
93 Self::Insertion {
94 at,
95 text: text.into(),
96 }
97 }
98
99 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 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 *start
135 } else {
136 offset - (end - start)
137 }
138 }
139 }
140 }
141
142 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
158static NEXT_FILE_ID: AtomicU16 = AtomicU16::new(1);
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub enum File {
168 FileId(u16),
170 Synthetic(u16),
172}
173
174impl File {
175 pub fn fresh() -> Self {
181 let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
182 Self::FileId(id)
183 }
184
185 pub const fn from_raw(raw: u16) -> Self {
187 Self::FileId(raw)
188 }
189
190 pub const fn into_raw(self) -> u16 {
192 match self {
193 Self::FileId(id) | Self::Synthetic(id) => id,
194 }
195 }
196}
197
198pub type SourceId = File;
200
201pub struct Source {
203 file: File,
205 text: Arc<str>,
207 line_starts: OnceLock<Box<[Offset]>>,
209}
210
211impl Source {
212 pub fn new(text: impl Into<Arc<str>>) -> Arc<Self> {
214 Self::with_file(File::fresh(), text)
215 }
216
217 pub fn synthetic(text: impl Into<Arc<str>>) -> Arc<Self> {
219 let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
220 Self::with_file(File::Synthetic(id), text)
221 }
222
223 pub fn with_file(file: File, text: impl Into<Arc<str>>) -> Arc<Self> {
225 let text = text.into();
226 Arc::new(Self {
227 file,
228 text,
229 line_starts: OnceLock::new(),
230 })
231 }
232
233 pub fn file(&self) -> File {
235 self.file
236 }
237
238 pub fn text(&self) -> &str {
240 &self.text
241 }
242
243 pub fn line_column(&self, offset: Offset) -> Option<LineColumn> {
248 self.text.get(..offset)?;
249 let line_starts = self.line_starts.get_or_init(|| {
250 std::iter::once(0)
251 .chain(
252 self.text
253 .bytes()
254 .enumerate()
255 .filter_map(|(offset, byte)| (byte == b'\n').then_some(offset + 1)),
256 )
257 .collect::<Vec<_>>()
258 .into_boxed_slice()
259 });
260 let line_index = line_starts
261 .partition_point(|line_start| *line_start <= offset)
262 .checked_sub(1)?;
263 let line_start = line_starts[line_index];
264 let column = self.text.get(line_start..offset)?.chars().count() + 1;
265 Some(LineColumn {
266 line: line_index + 1,
267 column,
268 })
269 }
270}
271
272impl Debug for Source {
273 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
274 formatter
275 .debug_struct("Source")
276 .field("file", &self.file)
277 .field("bytes", &self.text.len())
278 .finish_non_exhaustive()
279 }
280}
281
282#[derive(Clone)]
284pub struct Span {
285 source: Arc<Source>,
287 range: Option<Range<Offset>>,
289}
290
291impl Span {
292 pub fn new(source: Arc<Source>, range: Option<Range<Offset>>) -> Self {
294 Self {
295 source,
296 range: range.filter(|range| range.start <= range.end),
297 }
298 }
299
300 pub fn ranged(source: Arc<Source>, range: Range<Offset>) -> Self {
302 Self::new(source, Some(range))
303 }
304
305 pub fn unlocated(source: Arc<Source>) -> Self {
307 Self {
308 source,
309 range: None,
310 }
311 }
312
313 pub fn for_text(text: impl Into<Arc<str>>) -> Self {
315 let source = Source::synthetic(text);
316 Self::ranged(Arc::clone(&source), 0..source.text().len())
317 }
318
319 pub fn file(&self) -> File {
321 self.source.file()
322 }
323
324 pub fn source(&self) -> &Arc<Source> {
326 &self.source
327 }
328
329 pub fn source_id(&self) -> File {
331 self.file()
332 }
333
334 pub fn range(&self) -> Option<Range<Offset>> {
336 self.range.clone()
337 }
338
339 pub fn with_source(mut self, source: Arc<Source>) -> Self {
341 self.source = source;
342 self
343 }
344
345 pub fn with_text(self, text: impl Into<Arc<str>>) -> Self {
351 let text = text.into();
352 if self.text() == Some(text.as_ref()) {
353 return self;
354 }
355 let source = Source::with_file(self.file(), text);
356 Self::ranged(Arc::clone(&source), 0..source.text().len())
357 }
358
359 pub fn text(&self) -> Option<&str> {
361 self.source.text().get(self.range.as_ref()?.clone())
362 }
363 pub fn at(&self, offset: Offset) -> Self {
365 if self.range.is_none() {
366 return Self::unlocated(Arc::clone(&self.source));
367 }
368 Self {
369 source: Arc::clone(&self.source),
370 range: Some(offset..offset),
371 }
372 }
373
374 pub fn subspan(&self, start: Offset, end: Offset) -> Self {
379 let Some(outer) = &self.range else {
380 return Self::unlocated(Arc::clone(&self.source));
381 };
382 let Some(start) = outer.start.checked_add(start) else {
383 return Self::unlocated(Arc::clone(&self.source));
384 };
385 let Some(end) = outer.start.checked_add(end) else {
386 return Self::unlocated(Arc::clone(&self.source));
387 };
388 if start > end || end > outer.end {
389 return Self::unlocated(Arc::clone(&self.source));
390 }
391 Self {
392 source: Arc::clone(&self.source),
393 range: Some(start..end),
394 }
395 }
396
397 pub fn start(&self) -> Option<Offset> {
399 self.bounds().map(|(start, _)| start)
400 }
401
402 pub fn end(&self) -> Option<Offset> {
404 self.bounds().map(|(_, end)| end)
405 }
406
407 pub fn bounds(&self) -> Option<(Offset, Offset)> {
409 self.range.as_ref().map(|range| (range.start, range.end))
410 }
411
412 pub fn line_column_range(&self) -> Option<LineColumnRange> {
417 let (start, end) = self.bounds()?;
418 Some(LineColumnRange {
419 start: self.source.line_column(start)?,
420 end: self.source.line_column(end)?,
421 })
422 }
423
424 pub fn len(&self) -> Option<usize> {
426 self.bounds().map(|(start, end)| end - start)
427 }
428
429 pub fn is_empty(&self) -> bool {
432 self.len().unwrap_or(0) == 0
433 }
434
435 pub fn shares_source(&self, other: &Self) -> bool {
437 Arc::ptr_eq(&self.source, &other.source)
438 }
439
440 pub fn contains(&self, offset: Offset) -> bool {
448 self.bounds()
449 .is_some_and(|(start, end)| start <= offset && offset < end)
450 }
451
452 pub fn touches(&self, offset: Offset) -> bool {
459 self.bounds()
460 .is_some_and(|(start, end)| start <= offset && offset <= end)
461 }
462
463 pub fn shifted(&self, edit: &TextEdit) -> Self {
476 match self.bounds() {
477 None => Self::unlocated(Arc::clone(&self.source)),
478 Some((start, end)) => Self {
479 source: Arc::clone(&self.source),
480 range: Some(edit.shift(start)..edit.shift(end)),
481 },
482 }
483 }
484
485 pub fn resized(&self, len: usize) -> Self {
488 let Some(start) = self.start() else {
489 return Self::unlocated(Arc::clone(&self.source));
490 };
491 Self {
492 source: Arc::clone(&self.source),
493 range: Some(start..start.saturating_add(len)),
494 }
495 }
496}
497
498impl Span {
499 pub fn join(&self, other: Self) -> Option<Self> {
502 if !self.shares_source(&other) {
503 return None;
504 }
505 match (self.bounds(), other.bounds()) {
506 (None, _) => Some(other),
507 (_, None) => Some(self.clone()),
508 (Some((start, end)), Some((start2, end2))) => {
509 let start = start.min(start2);
510 let end = end.max(end2);
511 Some(Self {
512 source: Arc::clone(&self.source),
513 range: Some(start..end),
514 })
515 }
516 }
517 }
518
519 pub fn meet(&self, other: Self) -> Option<Self> {
522 if !self.shares_source(&other) {
523 return None;
524 }
525 match (self.bounds(), other.bounds()) {
526 (Some((start, end)), Some((start2, end2))) => {
527 let start = start.max(start2);
528 let end = end.min(end2);
529 if start < end {
530 Some(Self {
531 source: Arc::clone(&self.source),
532 range: Some(start..end),
533 })
534 } else {
535 None
536 }
537 }
538 _ => None,
539 }
540 }
541
542 pub fn join_all<I, S>(spans: I) -> Option<Self>
545 where
546 I: IntoIterator<Item = S>,
547 S: Into<Option<Self>>,
548 {
549 let mut spans = spans.into_iter().filter_map(Into::into);
550 let first = spans.next()?;
551 spans.try_fold(first, |left, right| left.join(right))
552 }
553}
554
555impl PartialEq for Span {
556 fn eq(&self, other: &Self) -> bool {
557 self.shares_source(other) && self.range == other.range
558 }
559}
560
561impl Eq for Span {}
562
563impl Hash for Span {
564 fn hash<H: Hasher>(&self, state: &mut H) {
565 Arc::as_ptr(&self.source).hash(state);
566 self.range.hash(state);
567 }
568}
569
570impl Debug for Span {
571 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
572 let mut span = formatter.debug_struct("Span");
573 span.field("file", &self.file())
574 .field("range", &self.line_column_range())
575 .field("bytes", &self.range);
576 if let Some(text) = self.text() {
577 span.field("text", &text);
578 }
579 span.finish()
580 }
581}
582
583impl Display for Span {
584 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
585 match self.line_column_range() {
586 Some(range) => Display::fmt(&range, formatter),
587 None => formatter.write_str("unlocated"),
588 }
589 }
590}
591
592pub trait Spanned {
594 fn span(&self) -> Span {
601 self.try_span()
602 .expect("syntax must have one shared source snapshot")
603 }
604
605 fn try_span(&self) -> Option<Span> {
608 Some(self.span())
609 }
610
611 fn expect_span(&self) -> Span {
617 self.span()
618 }
619}
620
621impl Spanned for Span {
622 fn span(&self) -> Span {
623 self.clone()
624 }
625}
626
627impl<T: Spanned + ?Sized> Spanned for &T {
628 fn span(&self) -> Span {
629 (*self).span()
630 }
631
632 fn try_span(&self) -> Option<Span> {
633 (*self).try_span()
634 }
635}
636
637impl<T: Spanned + ?Sized> Spanned for Box<T> {
638 fn span(&self) -> Span {
639 self.as_ref().span()
640 }
641
642 fn try_span(&self) -> Option<Span> {
643 self.as_ref().try_span()
644 }
645}
646
647impl<T: Spanned> Spanned for Option<T> {
648 fn try_span(&self) -> Option<Span> {
649 self.as_ref().and_then(Spanned::try_span)
650 }
651}
652
653impl<T: Spanned> Spanned for Vec<T> {
654 fn try_span(&self) -> Option<Span> {
655 Span::join_all(self.iter().map(Spanned::try_span))
656 }
657}
658
659impl<A: Spanned, B: Spanned> Spanned for (A, B) {
660 fn try_span(&self) -> Option<Span> {
661 self.0.try_span()?.join(self.1.try_span()?)
662 }
663}
664
665impl<T: Spanned> Spanned for &[T] {
666 fn try_span(&self) -> Option<Span> {
667 Span::join_all(self.iter().map(Spanned::try_span))
668 }
669}
670
671#[cfg(test)]
672mod source_tests {
673 use std::sync::Arc;
674
675 use super::*;
676
677 #[test]
678 fn spans_resolve_text_from_one_shared_source() {
679 let source = Source::new("quotient ring");
680 let quotient = Span::ranged(Arc::clone(&source), 0..8);
681 let ring = Span::ranged(Arc::clone(&source), 9..13);
682
683 assert_eq!(quotient.text(), Some("quotient"));
684 assert_eq!(ring.text(), Some("ring"));
685 assert!(Arc::ptr_eq(quotient.source(), ring.source()));
686 }
687
688 #[test]
689 fn joining_different_sources_returns_none() {
690 let left = Span::for_text("x");
691 let right = Span::for_text("y");
692
693 assert_eq!(left.join(right), None);
694 }
695
696 #[test]
697 fn an_unlocated_span_still_knows_its_source() {
698 let source = Source::new("x + y");
699 let span = Span::unlocated(Arc::clone(&source));
700
701 assert_eq!(span.file(), source.file());
702 assert_eq!(span.range(), None);
703 assert_eq!(span.text(), None);
704 }
705
706 #[test]
707 fn source_snapshots_with_the_same_file_do_not_alias() {
708 let file = File::from_raw(7);
709 let before = Span::ranged(Source::with_file(file, "x"), 0..1);
710 let after = Span::ranged(Source::with_file(file, "y"), 0..1);
711
712 assert_ne!(before, after);
713 assert_eq!(before.join(after), None);
714 }
715
716 #[test]
717 fn shifting_preserves_source_identity() {
718 let source = Source::new("ring");
719 let span = Span::ranged(Arc::clone(&source), 0..4);
720 let shifted = span.shifted(&TextEdit::insertion(0, "graded "));
721
722 assert!(Arc::ptr_eq(&source, shifted.source()));
723 assert_eq!(shifted.range(), Some(7..11));
724 }
725
726 #[test]
727 fn line_columns_are_one_based_and_count_characters() {
728 let source = Source::new("aα\nβc");
729
730 assert_eq!(
731 source.line_column(0),
732 Some(LineColumn { line: 1, column: 1 }),
733 );
734 assert_eq!(
735 source.line_column("aα\n".len()),
736 Some(LineColumn { line: 2, column: 1 }),
737 );
738 assert_eq!(
739 source.line_column("aα\nβ".len()),
740 Some(LineColumn { line: 2, column: 2 }),
741 );
742 assert_eq!(source.line_column(2), None);
743 }
744
745 #[test]
746 fn spans_format_as_half_open_line_column_ranges() {
747 let source = Source::new("one\ntwo");
748 let range = Span::ranged(Arc::clone(&source), 2..6);
749 let point = range.at(4);
750
751 assert_eq!(range.to_string(), "1:3-2:3");
752 assert_eq!(point.to_string(), "2:1");
753 }
754}