miden_core/debuginfo/
source_file.rs

1use alloc::{boxed::Box, sync::Arc, vec::Vec};
2use core::{fmt, num::NonZeroU32, ops::Range};
3
4use super::{FileLineCol, SourceId, SourceSpan};
5
6// SOURCE FILE
7// ================================================================================================
8
9/// A [SourceFile] represents a single file stored in a [super::SourceManager]
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct SourceFile {
12    /// The unique identifier allocated for this [SourceFile] by its owning [super::SourceManager]
13    id: SourceId,
14    /// The file content
15    content: SourceContent,
16}
17
18#[cfg(feature = "diagnostics")]
19impl miette::SourceCode for SourceFile {
20    fn read_span<'a>(
21        &'a self,
22        span: &miette::SourceSpan,
23        context_lines_before: usize,
24        context_lines_after: usize,
25    ) -> Result<alloc::boxed::Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
26        let mut start =
27            u32::try_from(span.offset()).map_err(|_| miette::MietteError::OutOfBounds)?;
28        let len = u32::try_from(span.len()).map_err(|_| miette::MietteError::OutOfBounds)?;
29        let mut end = start.checked_add(len).ok_or(miette::MietteError::OutOfBounds)?;
30        if context_lines_before > 0 {
31            let line_index = self.content.line_index(start.into());
32            let start_line_index = line_index.saturating_sub(context_lines_before as u32);
33            start = self.content.line_start(start_line_index).map(|idx| idx.to_u32()).unwrap_or(0);
34        }
35        if context_lines_after > 0 {
36            let line_index = self.content.line_index(end.into());
37            let end_line_index = line_index
38                .checked_add(context_lines_after as u32)
39                .ok_or(miette::MietteError::OutOfBounds)?;
40            end = self
41                .content
42                .line_range(end_line_index)
43                .map(|range| range.end.to_u32())
44                .unwrap_or_else(|| self.content.source_range().end.to_u32());
45        }
46        Ok(Box::new(ScopedSourceFileRef {
47            file: self,
48            span: miette::SourceSpan::new((start as usize).into(), end.abs_diff(start) as usize),
49        }))
50    }
51}
52
53impl SourceFile {
54    /// Create a new [SourceFile] from its raw components
55    pub fn new(id: SourceId, path: impl Into<Arc<str>>, content: impl Into<Box<str>>) -> Self {
56        let path = path.into();
57        let content = SourceContent::new(path, content.into());
58        Self { id, content }
59    }
60
61    pub(super) fn from_raw_parts(id: SourceId, content: SourceContent) -> Self {
62        Self { id, content }
63    }
64
65    /// Get the [SourceId] associated with this file
66    pub const fn id(&self) -> SourceId {
67        self.id
68    }
69
70    /// Get the name of this source file
71    pub fn name(&self) -> Arc<str> {
72        self.content.name()
73    }
74
75    /// Get the path of this source file as a [std::path::Path]
76    #[cfg(feature = "std")]
77    #[inline]
78    pub fn path(&self) -> &std::path::Path {
79        self.content.path()
80    }
81
82    /// Returns a reference to the underlying [SourceContent]
83    pub fn content(&self) -> &SourceContent {
84        &self.content
85    }
86
87    /// Returns the number of lines in this file
88    pub fn line_count(&self) -> usize {
89        self.content.last_line_index().to_usize() + 1
90    }
91
92    /// Returns the number of bytes in this file
93    pub fn len(&self) -> usize {
94        self.content.len()
95    }
96
97    /// Returns true if this file is empty
98    pub fn is_empty(&self) -> bool {
99        self.content.is_empty()
100    }
101
102    /// Get the underlying content of this file
103    #[inline(always)]
104    pub fn as_str(&self) -> &str {
105        self.content.as_str()
106    }
107
108    /// Get the underlying content of this file as a byte slice
109    #[inline(always)]
110    pub fn as_bytes(&self) -> &[u8] {
111        self.content.as_bytes()
112    }
113
114    /// Returns a [SourceSpan] covering the entirety of this file
115    #[inline]
116    pub fn source_span(&self) -> SourceSpan {
117        let range = self.content.source_range();
118        SourceSpan::new(self.id, range.start.0..range.end.0)
119    }
120
121    /// Returns a subset of the underlying content as a string slice.
122    ///
123    /// The bounds of the given span are character indices, _not_ byte indices.
124    ///
125    /// Returns `None` if the given span is out of bounds
126    #[inline(always)]
127    pub fn source_slice(&self, span: impl Into<Range<usize>>) -> Option<&str> {
128        self.content.source_slice(span)
129    }
130
131    /// Returns a [SourceFileRef] corresponding to the bytes contained in the specified span.
132    pub fn slice(self: &Arc<Self>, span: impl Into<Range<u32>>) -> SourceFileRef {
133        SourceFileRef::new(Arc::clone(self), span)
134    }
135
136    /// Get a [SourceSpan] which points to the first byte of the character at `column` on `line`
137    ///
138    /// Returns `None` if the given line/column is out of bounds for this file.
139    pub fn line_column_to_span(&self, line: u32, column: u32) -> Option<SourceSpan> {
140        let line_index = LineIndex::from(line.saturating_sub(1));
141        let column_index = ColumnIndex::from(column.saturating_sub(1));
142        let offset = self.content.line_column_to_offset(line_index, column_index)?;
143        Some(SourceSpan::at(self.id, offset.0))
144    }
145
146    /// Get a [FileLineCol] equivalent to the start of the given [SourceSpan]
147    pub fn location(&self, span: SourceSpan) -> FileLineCol {
148        assert_eq!(span.source_id(), self.id, "mismatched source ids");
149
150        self.content
151            .location(ByteIndex(span.into_range().start))
152            .expect("invalid source span: starting byte is out of bounds")
153    }
154}
155
156impl AsRef<str> for SourceFile {
157    #[inline(always)]
158    fn as_ref(&self) -> &str {
159        self.as_str()
160    }
161}
162
163impl AsRef<[u8]> for SourceFile {
164    #[inline(always)]
165    fn as_ref(&self) -> &[u8] {
166        self.as_bytes()
167    }
168}
169
170#[cfg(feature = "std")]
171impl AsRef<std::path::Path> for SourceFile {
172    #[inline(always)]
173    fn as_ref(&self) -> &std::path::Path {
174        self.path()
175    }
176}
177
178// SOURCE FILE REF
179// ================================================================================================
180
181/// A reference to a specific spanned region of a [SourceFile], that provides access to the actual
182/// [SourceFile], but scoped to the span it was created with.
183///
184/// This is useful in error types that implement [miette::Diagnostic], as it contains all of the
185/// data necessary to render the source code being referenced, without a [super::SourceManager] on
186/// hand.
187#[derive(Debug, Clone)]
188pub struct SourceFileRef {
189    file: Arc<SourceFile>,
190    span: SourceSpan,
191}
192
193impl SourceFileRef {
194    /// Create a [SourceFileRef] from a [SourceFile] and desired span (in bytes)
195    ///
196    /// The given span will be constrained to the bytes of `file`, so a span that reaches out of
197    /// bounds will have its end bound set to the last byte of the file.
198    pub fn new(file: Arc<SourceFile>, span: impl Into<Range<u32>>) -> Self {
199        let span = span.into();
200        let end = core::cmp::min(span.end, file.len() as u32);
201        let span = SourceSpan::new(file.id(), span.start..end);
202        Self { file, span }
203    }
204
205    /// Returns a ref-counted handle to the underlying [SourceFile]
206    pub fn source_file(&self) -> Arc<SourceFile> {
207        self.file.clone()
208    }
209
210    /// Returns the name of the file this [SourceFileRef] is selecting, as a [std::path::Path]
211    #[cfg(feature = "std")]
212    pub fn path(&self) -> &std::path::Path {
213        self.file.path()
214    }
215
216    /// Returns the name of the file this [SourceFileRef] is selecting
217    pub fn name(&self) -> &str {
218        self.file.content.path.as_ref()
219    }
220
221    /// Returns the [SourceSpan] selected by this [SourceFileRef]
222    pub const fn span(&self) -> SourceSpan {
223        self.span
224    }
225
226    /// Returns the underlying `str` selected by this [SourceFileRef]
227    pub fn as_str(&self) -> &str {
228        self.file.source_slice(self.span).unwrap()
229    }
230
231    /// Returns the underlying bytes selected by this [SourceFileRef]
232    #[inline]
233    pub fn as_bytes(&self) -> &[u8] {
234        self.as_str().as_bytes()
235    }
236
237    /// Returns the number of bytes represented by the subset of the underlying file that is covered
238    /// by this [SourceFileRef]
239    pub fn len(&self) -> usize {
240        self.span.len()
241    }
242
243    /// Returns true if this selection is empty
244    pub fn is_empty(&self) -> bool {
245        self.len() == 0
246    }
247}
248
249impl Eq for SourceFileRef {}
250
251impl PartialEq for SourceFileRef {
252    fn eq(&self, other: &Self) -> bool {
253        self.as_str() == other.as_str()
254    }
255}
256
257impl Ord for SourceFileRef {
258    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
259        self.as_str().cmp(other.as_str())
260    }
261}
262
263impl PartialOrd for SourceFileRef {
264    #[inline(always)]
265    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
266        Some(self.cmp(other))
267    }
268}
269
270impl core::hash::Hash for SourceFileRef {
271    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
272        self.span.hash(state);
273        self.as_str().hash(state);
274    }
275}
276
277impl AsRef<str> for SourceFileRef {
278    #[inline(always)]
279    fn as_ref(&self) -> &str {
280        self.as_str()
281    }
282}
283
284impl AsRef<[u8]> for SourceFileRef {
285    #[inline(always)]
286    fn as_ref(&self) -> &[u8] {
287        self.as_bytes()
288    }
289}
290
291#[cfg(feature = "diagnostics")]
292impl From<&SourceFileRef> for miette::SourceSpan {
293    fn from(source: &SourceFileRef) -> Self {
294        source.span.into()
295    }
296}
297
298/// Used to implement [miette::SpanContents] for [SourceFile] and [SourceFileRef]
299#[cfg(feature = "diagnostics")]
300struct ScopedSourceFileRef<'a> {
301    file: &'a SourceFile,
302    span: miette::SourceSpan,
303}
304
305#[cfg(feature = "diagnostics")]
306impl<'a> miette::SpanContents<'a> for ScopedSourceFileRef<'a> {
307    #[inline]
308    fn data(&self) -> &'a [u8] {
309        let start = self.span.offset();
310        let end = start + self.span.len();
311        &self.file.as_bytes()[start..end]
312    }
313
314    #[inline]
315    fn span(&self) -> &miette::SourceSpan {
316        &self.span
317    }
318
319    fn line(&self) -> usize {
320        let offset = self.span.offset() as u32;
321        self.file.content.line_index(offset.into()).to_usize()
322    }
323
324    fn column(&self) -> usize {
325        let start = self.span.offset() as u32;
326        let end = start + self.span.len() as u32;
327        let span = SourceSpan::new(self.file.id(), start..end);
328        let loc = self.file.location(span);
329        loc.column.saturating_sub(1) as usize
330    }
331
332    #[inline]
333    fn line_count(&self) -> usize {
334        self.file.line_count()
335    }
336
337    #[inline]
338    fn name(&self) -> Option<&str> {
339        Some(self.file.content.path.as_ref())
340    }
341
342    #[inline]
343    fn language(&self) -> Option<&str> {
344        None
345    }
346}
347
348#[cfg(feature = "diagnostics")]
349impl miette::SourceCode for SourceFileRef {
350    #[inline]
351    fn read_span<'a>(
352        &'a self,
353        span: &miette::SourceSpan,
354        context_lines_before: usize,
355        context_lines_after: usize,
356    ) -> Result<alloc::boxed::Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
357        self.file.read_span(span, context_lines_before, context_lines_after)
358    }
359}
360
361// SOURCE CONTENT
362// ================================================================================================
363
364/// Represents key information about a source file and its content:
365///
366/// * The path to the file (or its name, in the case of virtual files)
367/// * The content of the file
368/// * The byte offsets of every line in the file, for use in looking up line/column information
369#[derive(Clone)]
370pub struct SourceContent {
371    /// The path (or name) of this file
372    path: Arc<str>,
373    /// The underlying content of this file
374    content: Box<str>,
375    /// The byte offsets for each line in this file
376    line_starts: Box<[ByteIndex]>,
377}
378
379impl fmt::Debug for SourceContent {
380    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
381        f.debug_struct("SourceContent")
382            .field("path", &self.path)
383            .field("size_in_bytes", &self.content.len())
384            .field("line_count", &self.line_starts.len())
385            .field("content", &self.content)
386            .finish()
387    }
388}
389
390impl Eq for SourceContent {}
391
392impl PartialEq for SourceContent {
393    #[inline]
394    fn eq(&self, other: &Self) -> bool {
395        self.path == other.path && self.content == other.content
396    }
397}
398
399impl Ord for SourceContent {
400    #[inline]
401    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
402        self.path.cmp(&other.path).then_with(|| self.content.cmp(&other.content))
403    }
404}
405
406impl PartialOrd for SourceContent {
407    #[inline]
408    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
409        Some(self.cmp(other))
410    }
411}
412
413impl core::hash::Hash for SourceContent {
414    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
415        self.path.hash(state);
416        self.content.hash(state);
417    }
418}
419
420impl SourceContent {
421    /// Create a new [SourceContent] from the (possibly virtual) file path, and its content as a
422    /// UTF-8 string.
423    ///
424    /// When created, the line starts for this file will be computed, which requires scanning the
425    /// file content once.
426    pub fn new(path: Arc<str>, content: Box<str>) -> Self {
427        let bytes = content.as_bytes();
428
429        assert!(
430            bytes.len() < u32::MAX as usize,
431            "unsupported source file: current maximum supported length in bytes is 2^32"
432        );
433
434        let line_starts = core::iter::once(ByteIndex(0))
435            .chain(memchr::memchr_iter(b'\n', content.as_bytes()).filter_map(|mut offset| {
436                // Determine if the newline has any preceding escapes
437                let mut preceding_escapes = 0;
438                let line_start = offset + 1;
439                while let Some(prev_offset) = offset.checked_sub(1) {
440                    if bytes[prev_offset] == b'\\' {
441                        offset = prev_offset;
442                        preceding_escapes += 1;
443                        continue;
444                    }
445                    break;
446                }
447
448                // If the newline is escaped, do not count it as a new line
449                let is_escaped = preceding_escapes > 0 && preceding_escapes % 2 != 0;
450                if is_escaped {
451                    None
452                } else {
453                    Some(ByteIndex(line_start as u32))
454                }
455            }))
456            .collect::<Vec<_>>()
457            .into_boxed_slice();
458
459        Self { path, content, line_starts }
460    }
461
462    /// Get the name of this source file
463    pub fn name(&self) -> Arc<str> {
464        self.path.clone()
465    }
466
467    /// Get the name of this source file as a [std::path::Path]
468    #[cfg(feature = "std")]
469    #[inline]
470    pub fn path(&self) -> &std::path::Path {
471        std::path::Path::new(self.path.as_ref())
472    }
473
474    /// Returns the underlying content as a string slice
475    #[inline(always)]
476    pub fn as_str(&self) -> &str {
477        self.content.as_ref()
478    }
479
480    /// Returns the underlying content as a byte slice
481    #[inline(always)]
482    pub fn as_bytes(&self) -> &[u8] {
483        self.content.as_bytes()
484    }
485
486    /// Returns the size in bytes of the underlying content
487    #[inline(always)]
488    pub fn len(&self) -> usize {
489        self.content.len()
490    }
491
492    /// Returns true if the underlying content is empty
493    #[inline(always)]
494    pub fn is_empty(&self) -> bool {
495        self.content.is_empty()
496    }
497
498    /// Returns the range of valid byte indices for this file
499    #[inline]
500    pub fn source_range(&self) -> Range<ByteIndex> {
501        ByteIndex(0)..ByteIndex(self.content.len() as u32)
502    }
503
504    /// Returns a subset of the underlying content as a string slice.
505    ///
506    /// The bounds of the given span are character indices, _not_ byte indices.
507    ///
508    /// Returns `None` if the given span is out of bounds
509    #[inline(always)]
510    pub fn source_slice(&self, span: impl Into<Range<usize>>) -> Option<&str> {
511        self.as_str().get(span.into())
512    }
513
514    /// Returns the byte index at which the line corresponding to `line_index` starts
515    ///
516    /// Returns `None` if the given index is out of bounds
517    pub fn line_start(&self, line_index: LineIndex) -> Option<ByteIndex> {
518        self.line_starts.get(line_index.to_usize()).copied()
519    }
520
521    /// Returns the index of the last line in this file
522    #[inline]
523    pub fn last_line_index(&self) -> LineIndex {
524        LineIndex(self.line_starts.len() as u32)
525    }
526
527    /// Get the range of byte indices covered by the given line
528    pub fn line_range(&self, line_index: LineIndex) -> Option<Range<ByteIndex>> {
529        let line_start = self.line_start(line_index)?;
530        match self.line_start(line_index + 1) {
531            Some(line_end) => Some(line_start..line_end),
532            None => Some(line_start..ByteIndex(self.content.len() as u32)),
533        }
534    }
535
536    /// Get the index of the line to which `byte_index` belongs
537    pub fn line_index(&self, byte_index: ByteIndex) -> LineIndex {
538        match self.line_starts.binary_search(&byte_index) {
539            Ok(line) => LineIndex(line as u32),
540            Err(next_line) => LineIndex(next_line as u32 - 1),
541        }
542    }
543
544    /// Get the [ByteIndex] corresponding to the given line and column indices.
545    ///
546    /// Returns `None` if the line or column indices are out of bounds.
547    pub fn line_column_to_offset(
548        &self,
549        line_index: LineIndex,
550        column_index: ColumnIndex,
551    ) -> Option<ByteIndex> {
552        let column_index = column_index.to_usize();
553        let line_span = self.line_range(line_index)?;
554        let line_src = self
555            .content
556            .get(line_span.start.to_usize()..line_span.end.to_usize())
557            .expect("invalid line boundaries: invalid utf-8");
558        if line_src.len() < column_index {
559            return None;
560        }
561        let (pre, _) = line_src.split_at(column_index);
562        let start = line_span.start;
563        Some(start + ByteOffset::from_str_len(pre))
564    }
565
566    /// Get a [FileLineCol] corresponding to the line/column in this file at which `byte_index`
567    /// occurs
568    pub fn location(&self, byte_index: ByteIndex) -> Option<FileLineCol> {
569        let line_index = self.line_index(byte_index);
570        let line_start_index = self.line_start(line_index)?;
571        let line_src = self.content.get(line_start_index.to_usize()..byte_index.to_usize())?;
572        let column_index = ColumnIndex::from(line_src.chars().count() as u32);
573        Some(FileLineCol {
574            path: self.path.clone(),
575            line: line_index.number().get(),
576            column: column_index.number().get(),
577        })
578    }
579}
580
581// SOURCE CONTENT INDICES
582// ================================================================================================
583
584/// An index representing the offset in bytes from the start of a source file
585#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
586pub struct ByteIndex(u32);
587impl ByteIndex {
588    /// Create a [ByteIndex] from a raw `u32` index
589    pub const fn new(index: u32) -> Self {
590        Self(index)
591    }
592
593    /// Get the raw index as a usize
594    #[inline(always)]
595    pub const fn to_usize(self) -> usize {
596        self.0 as usize
597    }
598
599    /// Get the raw index as a u32
600    #[inline(always)]
601    pub const fn to_u32(self) -> u32 {
602        self.0
603    }
604}
605impl core::ops::Add<ByteOffset> for ByteIndex {
606    type Output = ByteIndex;
607
608    fn add(self, rhs: ByteOffset) -> Self {
609        Self((self.0 as i64 + rhs.0) as u32)
610    }
611}
612impl core::ops::Add<u32> for ByteIndex {
613    type Output = ByteIndex;
614
615    fn add(self, rhs: u32) -> Self {
616        Self(self.0 + rhs)
617    }
618}
619impl core::ops::AddAssign<ByteOffset> for ByteIndex {
620    fn add_assign(&mut self, rhs: ByteOffset) {
621        *self = *self + rhs;
622    }
623}
624impl core::ops::AddAssign<u32> for ByteIndex {
625    fn add_assign(&mut self, rhs: u32) {
626        self.0 += rhs;
627    }
628}
629impl core::ops::Sub<ByteOffset> for ByteIndex {
630    type Output = ByteIndex;
631
632    fn sub(self, rhs: ByteOffset) -> Self {
633        Self((self.0 as i64 - rhs.0) as u32)
634    }
635}
636impl core::ops::Sub<u32> for ByteIndex {
637    type Output = ByteIndex;
638
639    fn sub(self, rhs: u32) -> Self {
640        Self(self.0 - rhs)
641    }
642}
643impl core::ops::SubAssign<ByteOffset> for ByteIndex {
644    fn sub_assign(&mut self, rhs: ByteOffset) {
645        *self = *self - rhs;
646    }
647}
648impl core::ops::SubAssign<u32> for ByteIndex {
649    fn sub_assign(&mut self, rhs: u32) {
650        self.0 -= rhs;
651    }
652}
653impl From<u32> for ByteIndex {
654    fn from(index: u32) -> Self {
655        Self(index)
656    }
657}
658impl From<ByteIndex> for u32 {
659    fn from(index: ByteIndex) -> Self {
660        index.0
661    }
662}
663
664/// An offset in bytes relative to some [ByteIndex]
665#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
666pub struct ByteOffset(i64);
667impl ByteOffset {
668    /// Compute the offset in bytes represented by the given `char`
669    pub fn from_char_len(c: char) -> ByteOffset {
670        Self(c.len_utf8() as i64)
671    }
672
673    /// Compute the offset in bytes represented by the given `str`
674    pub fn from_str_len(s: &str) -> ByteOffset {
675        Self(s.len() as i64)
676    }
677}
678impl core::ops::Add for ByteOffset {
679    type Output = ByteOffset;
680
681    fn add(self, rhs: Self) -> Self {
682        Self(self.0 + rhs.0)
683    }
684}
685impl core::ops::AddAssign for ByteOffset {
686    fn add_assign(&mut self, rhs: Self) {
687        self.0 += rhs.0;
688    }
689}
690impl core::ops::Sub for ByteOffset {
691    type Output = ByteOffset;
692
693    fn sub(self, rhs: Self) -> Self {
694        Self(self.0 - rhs.0)
695    }
696}
697impl core::ops::SubAssign for ByteOffset {
698    fn sub_assign(&mut self, rhs: Self) {
699        self.0 -= rhs.0;
700    }
701}
702
703/// A zero-indexed line number
704#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
705pub struct LineIndex(u32);
706impl LineIndex {
707    /// Get a one-indexed number for display
708    pub const fn number(self) -> NonZeroU32 {
709        unsafe { NonZeroU32::new_unchecked(self.0 + 1) }
710    }
711
712    /// Get the raw index as a usize
713    #[inline(always)]
714    pub const fn to_usize(self) -> usize {
715        self.0 as usize
716    }
717
718    /// Add `offset` to this index, returning `None` on overflow
719    pub fn checked_add(self, offset: u32) -> Option<Self> {
720        self.0.checked_add(offset).map(Self)
721    }
722
723    /// Subtract `offset` from this index, returning `None` on underflow
724    pub fn checked_sub(self, offset: u32) -> Option<Self> {
725        self.0.checked_sub(offset).map(Self)
726    }
727
728    /// Add `offset` to this index, saturating to `u32::MAX` on overflow
729    pub const fn saturating_add(self, offset: u32) -> Self {
730        Self(self.0.saturating_add(offset))
731    }
732
733    /// Subtract `offset` from this index, saturating to `0` on overflow
734    pub const fn saturating_sub(self, offset: u32) -> Self {
735        Self(self.0.saturating_sub(offset))
736    }
737}
738impl From<u32> for LineIndex {
739    fn from(index: u32) -> Self {
740        Self(index)
741    }
742}
743impl core::ops::Add<u32> for LineIndex {
744    type Output = LineIndex;
745
746    fn add(self, rhs: u32) -> Self {
747        Self(self.0 + rhs)
748    }
749}
750
751/// A zero-indexed column number
752#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
753pub struct ColumnIndex(u32);
754impl ColumnIndex {
755    /// Get a one-indexed number for display
756    pub const fn number(self) -> NonZeroU32 {
757        unsafe { NonZeroU32::new_unchecked(self.0 + 1) }
758    }
759
760    /// Get the raw index as a usize
761    #[inline(always)]
762    pub const fn to_usize(self) -> usize {
763        self.0 as usize
764    }
765}
766impl From<u32> for ColumnIndex {
767    fn from(index: u32) -> Self {
768        Self(index)
769    }
770}