Skip to main content

error_enum_core/
indexer.rs

1use alloc::{boxed::Box, rc::Rc, sync::Arc, vec::Vec};
2use stringzilla::sz::find_newline_utf8;
3
4/// A indexable string.
5pub trait Indexer {
6    /// Returns the line and column number of this `Position`.
7    fn line_col_at(&self, pos: usize) -> (usize, usize);
8
9    /// Returns the start and the end of the line that contains the position at `pos`.
10    fn line_span_at(&self, pos: usize) -> (usize, usize);
11
12    /// Returns the start and the end of the `(context_lines_before + n + context_lines_after)`
13    /// lines that contains the span from `start` to `end`.
14    ///
15    /// `context_lines_before` and `context_lines_after` specify how many lines before and after
16    /// the span to include.
17    ///
18    /// If there are not enough lines before or after, it will include as many as possible.
19    /// And if `context_lines_before` or `context_lines_after` is zero, no extra lines will be included.
20    fn span_with_context_lines(
21        &self,
22        start: usize,
23        end: usize,
24        context_lines_before: usize,
25        context_lines_after: usize,
26    ) -> (usize, usize);
27}
28
29macro_rules! impl_indexable {
30    ($T:ty) => {
31        impl<T: Indexer + ?Sized> Indexer for $T {
32            fn line_col_at(&self, pos: usize) -> (usize, usize) {
33                T::line_col_at(self, pos)
34            }
35
36            fn line_span_at(&self, pos: usize) -> (usize, usize) {
37                T::line_span_at(self, pos)
38            }
39
40            fn span_with_context_lines(
41                &self,
42                start: usize,
43                end: usize,
44                context_lines_before: usize,
45                context_lines_after: usize,
46            ) -> (usize, usize) {
47                T::span_with_context_lines(
48                    self,
49                    start,
50                    end,
51                    context_lines_before,
52                    context_lines_after,
53                )
54            }
55        }
56    };
57}
58
59impl_indexable!(&T);
60impl_indexable!(Box<T>);
61impl_indexable!(Rc<T>);
62impl_indexable!(Arc<T>);
63
64/// An [`Indexer`] that stores ending positions of every line (including trailing newlines).
65///
66/// The line and column numbers are zero-based.
67///
68/// And note that the `LineIndexer` works as if there is an implicit newline at the end of the text.
69#[derive(Debug, PartialEq, Eq)]
70#[repr(transparent)]
71pub struct LineIndexer([usize]);
72
73impl LineIndexer {
74    /// Create an [`LineIndexer`].
75    pub fn new(s: &str) -> Box<Self> {
76        let mut line_starts = Vec::new();
77        let mut cur = 0usize;
78        let mut slice = s.as_bytes();
79        while let Some(index) = find_newline_utf8(slice) {
80            line_starts.push(cur + index.end());
81            cur += index.end();
82            slice = &slice[index.end()..]
83        }
84        line_starts.push(s.len());
85        let line_starts = line_starts.into_boxed_slice();
86        unsafe { core::mem::transmute(line_starts) }
87    }
88    /// Create an [`LineIndexer`] from a boxed slice.
89    pub fn from_boxed_slice(slice: Box<[usize]>) -> Box<Self> {
90        debug_assert!(slice.is_sorted(), "line endings must be sorted");
91        unsafe { core::mem::transmute(slice) }
92    }
93    /// Convert the [`LineIndexer`] into a boxed slice.
94    pub fn into_boxed_slice(self: Box<Self>) -> Box<[usize]> {
95        unsafe { core::mem::transmute(self) }
96    }
97    /// Create an [`LineIndexer`] from a slice reference.
98    pub fn from_slice(slice: &[usize]) -> &Self {
99        debug_assert!(slice.is_sorted(), "line endings must be sorted");
100        unsafe { core::mem::transmute(slice) }
101    }
102    /// Convert the [`LineIndexer`] into a slice reference.
103    pub fn as_slice(&self) -> &[usize] {
104        unsafe { core::mem::transmute(self) }
105    }
106}
107
108impl LineIndexer {
109    /// Get the line number and the starting position of the line at `pos`.
110    fn line_start_at(&self, pos: usize) -> usize {
111        match self.0.binary_search(&pos) {
112            Ok(i) => self.0[i],
113            Err(0) => 0,
114            Err(i) => self.0[i.saturating_sub(1)],
115        }
116    }
117    /// Get the line number and the starting position of the line at `pos`.
118    fn line_and_start_at(&self, pos: usize) -> (usize, usize) {
119        match self.0.binary_search(&pos) {
120            Ok(i) => (i + 1, self.0[i]),
121            Err(0) => (0, 0),
122            Err(i) => (i, self.0[i.saturating_sub(1)]),
123        }
124    }
125    /// Get the line number at `pos`.
126    fn line_at(&self, pos: usize) -> usize {
127        match self.0.binary_search(&pos) {
128            Ok(i) => i + 1,
129            Err(i) => i,
130        }
131    }
132}
133
134impl Indexer for LineIndexer {
135    fn line_col_at(&self, pos: usize) -> (usize, usize) {
136        let (line, line_start) = self.line_and_start_at(pos);
137        debug_assert!(pos >= line_start);
138        (line, pos - line_start)
139    }
140
141    fn line_span_at(&self, pos: usize) -> (usize, usize) {
142        match self.0.binary_search(&pos) {
143            Ok(i) if i + 1 == self.0.len() => (self.0[i], self.0[i]),
144            Ok(i) => (self.0[i], self.0[i + 1]),
145            Err(0) => (0, self.0[0]),
146            Err(i) if i == self.0.len() => {
147                let j = i.saturating_sub(1);
148                (self.0[j], self.0[j])
149            }
150            Err(i) => (self.0[i.saturating_sub(1)], self.0[i]),
151        }
152    }
153
154    fn span_with_context_lines(
155        &self,
156        start: usize,
157        end: usize,
158        context_lines_before: usize,
159        context_lines_after: usize,
160    ) -> (usize, usize) {
161        let start = if context_lines_before == 0 {
162            self.line_start_at(start)
163        } else {
164            self.line_at(start)
165                .saturating_sub(context_lines_before)
166                .checked_sub(1)
167                .map_or_else(|| 0, |i| self.0[i])
168        };
169        let end = if context_lines_after == 0 {
170            self.line_span_at(end).1
171        } else {
172            self.0[self
173                .line_at(end)
174                .saturating_add(context_lines_after)
175                .min(self.0.len() - 1)]
176        };
177        (start, end)
178    }
179}
180
181impl AsRef<[usize]> for LineIndexer {
182    fn as_ref(&self) -> &[usize] {
183        &self.0
184    }
185}