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#[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 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#[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 #[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#[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 pub fn new(start: TextOffset, end: TextOffset) -> Self {
153 Self { start, end }
154 }
155 pub fn new_with_width(start: TextOffset, width: TextWidth) -> Self {
157 Self::new(start, start.add_width(width))
158 }
159 pub fn cursor(offset: TextOffset) -> Self {
161 Self::new(offset, offset)
162 }
163 #[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 pub fn after(self) -> Self {
182 Self::cursor(self.end)
183 }
184 pub fn start_only(self) -> Self {
186 Self::cursor(self.start)
187 }
188
189 pub fn to_str_range(&self) -> Range<usize> {
191 self.start.0.0 as usize..self.end.0.0 as usize
192 }
193
194 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#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
208pub struct TextPosition {
209 pub line: usize,
211 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 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 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 let mut offset =
251 file_summary.line_offsets.get(self.line).copied().unwrap_or(file_summary.last_offset);
252
253 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#[derive(Clone, Debug, PartialEq, Eq)]
270pub struct FileSummary {
271 pub line_offsets: Vec<TextOffset>,
273 pub last_offset: TextOffset,
275}
276impl FileSummary {
277 pub fn line_count(&self) -> usize {
279 self.line_offsets.len()
280 }
281}
282
283#[derive(Copy, Clone, Debug, PartialEq, Eq)]
285pub struct TextPositionSpan {
286 pub start: TextPosition,
287 pub end: TextPosition,
288}
289
290impl TextPositionSpan {
291 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}