cairo_lang_filesystem/
span.rs

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