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