cairo_lang_filesystem/
span.rs

1use std::iter::Sum;
2use std::ops::{Add, Range, Sub};
3
4use salsa::Database;
5use serde::{Deserialize, Serialize};
6
7use crate::db::FilesGroup;
8use crate::ids::FileId;
9
10#[cfg(test)]
11#[path = "span_test.rs"]
12mod test;
13
14/// Byte length of an utf8 string.
15// This wrapper type is used to avoid confusion with non-utf8 sizes.
16#[derive(
17    Copy,
18    Clone,
19    Default,
20    Debug,
21    PartialEq,
22    Eq,
23    PartialOrd,
24    Ord,
25    Hash,
26    Serialize,
27    Deserialize,
28    salsa::Update,
29)]
30pub struct TextWidth(u32);
31impl TextWidth {
32    pub const ZERO: Self = Self(0);
33
34    pub fn from_char(c: char) -> Self {
35        Self(c.len_utf8() as u32)
36    }
37    #[allow(clippy::should_implement_trait)]
38    pub fn from_str(s: &str) -> Self {
39        Self(s.len() as u32)
40    }
41    pub fn new_for_testing(value: u32) -> Self {
42        Self(value)
43    }
44    /// Creates a `TextWidth` at the given index of a string.
45    ///
46    /// The index is required to be a char boundary.
47    /// This function runs a debug assertion to verify this,
48    /// while retains performance on release builds.
49    pub fn at(s: &str, index: usize) -> Self {
50        debug_assert!(
51            s.is_char_boundary(index),
52            "cannot create a TextWidth outside of a char boundary"
53        );
54        Self(index as u32)
55    }
56    pub fn as_u32(self) -> u32 {
57        self.0
58    }
59    pub fn as_offset(self) -> TextOffset {
60        TextOffset(self)
61    }
62}
63impl Add for TextWidth {
64    type Output = Self;
65
66    fn add(self, rhs: Self) -> Self::Output {
67        Self(self.0 + rhs.0)
68    }
69}
70impl Sub for TextWidth {
71    type Output = Self;
72
73    fn sub(self, rhs: Self) -> Self::Output {
74        Self(self.0 - rhs.0)
75    }
76}
77impl Sum for TextWidth {
78    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
79        Self(iter.map(|x| x.0).sum())
80    }
81}
82
83/// Byte offset inside a utf8 string.
84#[derive(
85    Copy,
86    Clone,
87    Debug,
88    Default,
89    PartialEq,
90    Eq,
91    PartialOrd,
92    Ord,
93    Hash,
94    Serialize,
95    Deserialize,
96    salsa::Update,
97)]
98pub struct TextOffset(TextWidth);
99impl TextOffset {
100    pub const START: Self = Self(TextWidth::ZERO);
101
102    /// Creates a `TextOffset` at the end of a given string.
103    #[allow(clippy::should_implement_trait)]
104    pub fn from_str(content: &str) -> Self {
105        Self(TextWidth::from_str(content))
106    }
107    pub fn add_width(self, width: TextWidth) -> Self {
108        TextOffset(self.0 + width)
109    }
110    pub fn sub_width(self, width: TextWidth) -> Self {
111        TextOffset(self.0 - width)
112    }
113    pub fn take_from(self, content: &str) -> &str {
114        &content[(self.0.0 as usize)..]
115    }
116    pub fn as_u32(self) -> u32 {
117        self.0.as_u32()
118    }
119}
120impl Sub for TextOffset {
121    type Output = TextWidth;
122
123    fn sub(self, rhs: Self) -> Self::Output {
124        TextWidth(self.0.0 - rhs.0.0)
125    }
126}
127
128/// A range of text offsets that form a span (like text selection).
129#[derive(
130    Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
131)]
132pub struct TextSpan {
133    pub start: TextOffset,
134    pub end: TextOffset,
135}
136impl TextSpan {
137    /// Creates a `TextSpan` from a start and end offset.
138    pub fn new(start: TextOffset, end: TextOffset) -> Self {
139        Self { start, end }
140    }
141    /// Creates a `TextSpan` from a start offset and a width.
142    pub fn new_with_width(start: TextOffset, width: TextWidth) -> Self {
143        Self::new(start, start.add_width(width))
144    }
145    /// Creates a `TextSpan` of width 0, located at the given offset.
146    pub fn cursor(offset: TextOffset) -> Self {
147        Self::new(offset, offset)
148    }
149    /// Creates a `TextSpan` for the entirety of a given string.
150    #[allow(clippy::should_implement_trait)]
151    pub fn from_str(content: &str) -> Self {
152        Self::new(TextOffset::START, TextOffset::from_str(content))
153    }
154    pub fn width(self) -> TextWidth {
155        self.end - self.start
156    }
157    pub fn contains(self, other: Self) -> bool {
158        self.start <= other.start && self.end >= other.end
159    }
160    pub fn take(self, content: &str) -> &str {
161        &content[(self.start.0.0 as usize)..(self.end.0.0 as usize)]
162    }
163    pub fn n_chars(self, content: &str) -> usize {
164        self.take(content).chars().count()
165    }
166    /// Get the span of width 0, located right after this span.
167    pub fn after(self) -> Self {
168        Self::cursor(self.end)
169    }
170    /// Get the span of width 0, located right at the beginning of this span.
171    pub fn start_only(self) -> Self {
172        Self::cursor(self.start)
173    }
174
175    /// Returns self.start..self.end as [`Range<usize>`]
176    pub fn to_str_range(&self) -> Range<usize> {
177        self.start.0.0 as usize..self.end.0.0 as usize
178    }
179
180    /// Convert this span to a [`TextPositionSpan`] in the file.
181    pub fn position_in_file<'db>(
182        self,
183        db: &'db dyn Database,
184        file: FileId<'db>,
185    ) -> Option<TextPositionSpan> {
186        let start = self.start.position_in_file(db, file)?;
187        let end = self.end.position_in_file(db, file)?;
188        Some(TextPositionSpan { start, end })
189    }
190}
191
192/// Human-readable position inside a file, in lines and characters.
193#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
194pub struct TextPosition {
195    /// Line index, 0 based.
196    pub line: usize,
197    /// Character index inside the line, 0 based.
198    pub col: usize,
199}
200
201impl TextOffset {
202    fn get_line_number(self, db: &dyn Database, file: FileId<'_>) -> Option<usize> {
203        let summary = db.file_summary(file)?;
204        assert!(
205            self <= summary.last_offset,
206            "TextOffset out of range. {:?} > {:?}.",
207            self.0,
208            summary.last_offset.0
209        );
210        Some(summary.line_offsets.binary_search(&self).unwrap_or_else(|x| x - 1))
211    }
212
213    /// Convert this offset to an equivalent [`TextPosition`] in the file.
214    pub fn position_in_file(self, db: &dyn Database, file: FileId<'_>) -> Option<TextPosition> {
215        let summary = db.file_summary(file)?;
216        let line_number = self.get_line_number(db, file)?;
217        let line_offset = summary.line_offsets[line_number];
218        let content = db.file_content(file)?;
219        let col = TextSpan::new(line_offset, self).n_chars(content.as_ref());
220        Some(TextPosition { line: line_number, col })
221    }
222}
223
224impl TextPosition {
225    /// Convert this position to an equivalent [`TextOffset`] in the file.
226    ///
227    /// If `line` or `col` are out of range, the offset will be clamped to the end of file, or end
228    /// of line respectively.
229    ///
230    /// Returns `None` if file is not found in `db`.
231    pub fn offset_in_file(self, db: &dyn Database, file: FileId<'_>) -> Option<TextOffset> {
232        let file_summary = db.file_summary(file)?;
233        let content = db.file_content(file)?;
234
235        // Get the offset of the first character in line, or clamp to the last offset in the file.
236        let mut offset =
237            file_summary.line_offsets.get(self.line).copied().unwrap_or(file_summary.last_offset);
238
239        // Add the column offset, or clamp to the last character in line.
240        offset = offset.add_width(
241            offset
242                .take_from(content.as_ref())
243                .chars()
244                .take_while(|c| *c != '\n')
245                .take(self.col)
246                .map(TextWidth::from_char)
247                .sum(),
248        );
249
250        Some(offset)
251    }
252}
253
254/// A set of offset-related information about a file.
255#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct FileSummary {
257    /// Starting offsets of all lines in this file.
258    pub line_offsets: Vec<TextOffset>,
259    /// Offset of the last character in the file.
260    pub last_offset: TextOffset,
261}
262impl FileSummary {
263    /// Gets the number of lines
264    pub fn line_count(&self) -> usize {
265        self.line_offsets.len()
266    }
267}
268
269/// A range of text positions that form a span (like text selection).
270#[derive(Copy, Clone, Debug, PartialEq, Eq)]
271pub struct TextPositionSpan {
272    pub start: TextPosition,
273    pub end: TextPosition,
274}
275
276impl TextPositionSpan {
277    /// Convert this span to a [`TextSpan`] in the file.
278    pub fn offset_in_file<'db>(
279        Self { start, end }: Self,
280        db: &'db dyn Database,
281        file: FileId<'db>,
282    ) -> Option<TextSpan> {
283        Some(TextSpan::new(start.offset_in_file(db, file)?, end.offset_in_file(db, file)?))
284    }
285}