cairo_lang_filesystem/
span.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
use std::iter::Sum;
use std::ops::{Add, Range, Sub};

use serde::{Deserialize, Serialize};

use crate::db::FilesGroup;
use crate::ids::FileId;

#[cfg(test)]
#[path = "span_test.rs"]
mod test;

/// Byte length of an utf8 string.
// This wrapper type is used to avoid confusion with non-utf8 sizes.
#[derive(
    Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct TextWidth(u32);
impl TextWidth {
    pub fn from_char(c: char) -> Self {
        Self(c.len_utf8() as u32)
    }
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(s: &str) -> Self {
        Self(s.len() as u32)
    }
    pub fn new_for_testing(value: u32) -> Self {
        Self(value)
    }
    pub fn as_u32(self) -> u32 {
        self.0
    }
}
impl Add for TextWidth {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0 + rhs.0)
    }
}
impl Sub for TextWidth {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0 - rhs.0)
    }
}
impl Sum for TextWidth {
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
        Self(iter.map(|x| x.0).sum())
    }
}

/// Byte offset inside a utf8 string.
#[derive(
    Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct TextOffset(TextWidth);
impl TextOffset {
    pub fn add_width(self, width: TextWidth) -> Self {
        TextOffset(self.0 + width)
    }
    pub fn sub_width(self, width: TextWidth) -> Self {
        TextOffset(self.0 - width)
    }
    pub fn take_from(self, content: &str) -> &str {
        &content[(self.0.0 as usize)..]
    }
    pub fn as_u32(self) -> u32 {
        self.0.as_u32()
    }
}
impl Sub for TextOffset {
    type Output = TextWidth;

    fn sub(self, rhs: Self) -> Self::Output {
        TextWidth(self.0.0 - rhs.0.0)
    }
}

/// A range of text offsets that form a span (like text selection).
#[derive(
    Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct TextSpan {
    pub start: TextOffset,
    pub end: TextOffset,
}
impl TextSpan {
    pub fn width(self) -> TextWidth {
        self.end - self.start
    }
    pub fn contains(self, other: Self) -> bool {
        self.start <= other.start && self.end >= other.end
    }
    pub fn take(self, content: &str) -> &str {
        &content[(self.start.0.0 as usize)..(self.end.0.0 as usize)]
    }
    pub fn n_chars(self, content: &str) -> usize {
        self.take(content).chars().count()
    }
    /// Get the span of width 0, located right after this span.
    pub fn after(self) -> Self {
        Self { start: self.end, end: self.end }
    }
    /// Get the span of width 0, located right at the beginning of this span.
    pub fn start_only(self) -> Self {
        Self { start: self.start, end: self.start }
    }

    /// Returns self.start..self.end as [`Range<usize>`]
    pub fn to_str_range(&self) -> Range<usize> {
        self.start.0.0 as usize..self.end.0.0 as usize
    }

    /// Convert this span to a [`TextPositionSpan`] in the file.
    pub fn position_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextPositionSpan> {
        let start = self.start.position_in_file(db, file)?;
        let end = self.end.position_in_file(db, file)?;
        Some(TextPositionSpan { start, end })
    }
}

/// Human-readable position inside a file, in lines and characters.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct TextPosition {
    /// Line index, 0 based.
    pub line: usize,
    /// Character index inside the line, 0 based.
    pub col: usize,
}

impl TextOffset {
    fn get_line_number(self, db: &dyn FilesGroup, file: FileId) -> Option<usize> {
        let summary = db.file_summary(file)?;
        assert!(
            self <= summary.last_offset,
            "TextOffset out of range. {:?} > {:?}.",
            self.0,
            summary.last_offset.0
        );
        Some(summary.line_offsets.binary_search(&self).unwrap_or_else(|x| x - 1))
    }

    /// Convert this offset to an equivalent [`TextPosition`] in the file.
    pub fn position_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextPosition> {
        let summary = db.file_summary(file)?;
        let line_number = self.get_line_number(db, file)?;
        let line_offset = summary.line_offsets[line_number];
        let content = db.file_content(file)?;
        let col = TextSpan { start: line_offset, end: self }.n_chars(&content);
        Some(TextPosition { line: line_number, col })
    }
}

impl TextPosition {
    /// Convert this position to an equivalent [`TextOffset`] in the file.
    ///
    /// If `line` or `col` are out of range, the offset will be clamped to the end of file, or end
    /// of line respectively.
    ///
    /// Returns `None` if file is not found in `db`.
    pub fn offset_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextOffset> {
        let file_summary = db.file_summary(file)?;
        let content = db.file_content(file)?;

        // Get the offset of the first character in line, or clamp to the last offset in the file.
        let mut offset =
            file_summary.line_offsets.get(self.line).copied().unwrap_or(file_summary.last_offset);

        // Add the column offset, or clamp to the last character in line.
        offset = offset.add_width(
            offset
                .take_from(&content)
                .chars()
                .take_while(|c| *c != '\n')
                .take(self.col)
                .map(TextWidth::from_char)
                .sum(),
        );

        Some(offset)
    }
}

/// A set of offset-related information about a file.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileSummary {
    /// Starting offsets of all lines in this file.
    pub line_offsets: Vec<TextOffset>,
    /// Offset of the last character in the file.
    pub last_offset: TextOffset,
}
impl FileSummary {
    /// Gets the number of lines
    pub fn line_count(&self) -> usize {
        self.line_offsets.len()
    }
}

/// A range of text positions that form a span (like text selection).
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct TextPositionSpan {
    pub start: TextPosition,
    pub end: TextPosition,
}

impl TextPositionSpan {
    /// Convert this span to a [`TextSpan`] in the file.
    pub fn offset_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextSpan> {
        let start = self.start.offset_in_file(db, file)?;
        let end = self.end.offset_in_file(db, file)?;
        Some(TextSpan { start, end })
    }
}