cairo_lang_filesystem/
span.rs

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