1use std::iter::Sum;
2use std::ops::{Add, Range, Sub};
3
4use salsa::Database;
5use serde::{Deserialize, Serialize};
6
7use crate::db::FilesGroup;
8use crate::ids::FileId;
9
10#[cfg(test)]
11#[path = "span_test.rs"]
12mod test;
13
14#[derive(
17 Copy,
18 Clone,
19 Default,
20 Debug,
21 PartialEq,
22 Eq,
23 PartialOrd,
24 Ord,
25 Hash,
26 Serialize,
27 Deserialize,
28 salsa::Update,
29)]
30pub struct TextWidth(u32);
31impl TextWidth {
32 pub const ZERO: Self = Self(0);
33
34 pub fn from_char(c: char) -> Self {
35 Self(c.len_utf8() as u32)
36 }
37 #[allow(clippy::should_implement_trait)]
38 pub fn from_str(s: &str) -> Self {
39 Self(s.len() as u32)
40 }
41 pub fn new_for_testing(value: u32) -> Self {
42 Self(value)
43 }
44 pub fn at(s: &str, index: usize) -> Self {
50 debug_assert!(
51 s.is_char_boundary(index),
52 "cannot create a TextWidth outside of a char boundary"
53 );
54 Self(index as u32)
55 }
56 pub fn as_u32(self) -> u32 {
57 self.0
58 }
59 pub fn as_offset(self) -> TextOffset {
60 TextOffset(self)
61 }
62}
63impl Add for TextWidth {
64 type Output = Self;
65
66 fn add(self, rhs: Self) -> Self::Output {
67 Self(self.0 + rhs.0)
68 }
69}
70impl Sub for TextWidth {
71 type Output = Self;
72
73 fn sub(self, rhs: Self) -> Self::Output {
74 Self(self.0 - rhs.0)
75 }
76}
77impl Sum for TextWidth {
78 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
79 Self(iter.map(|x| x.0).sum())
80 }
81}
82
83#[derive(
85 Copy,
86 Clone,
87 Debug,
88 Default,
89 PartialEq,
90 Eq,
91 PartialOrd,
92 Ord,
93 Hash,
94 Serialize,
95 Deserialize,
96 salsa::Update,
97)]
98pub struct TextOffset(TextWidth);
99impl TextOffset {
100 pub const START: Self = Self(TextWidth::ZERO);
101
102 #[allow(clippy::should_implement_trait)]
104 pub fn from_str(content: &str) -> Self {
105 Self(TextWidth::from_str(content))
106 }
107 pub fn add_width(self, width: TextWidth) -> Self {
108 TextOffset(self.0 + width)
109 }
110 pub fn sub_width(self, width: TextWidth) -> Self {
111 TextOffset(self.0 - width)
112 }
113 pub fn take_from(self, content: &str) -> &str {
114 &content[(self.0.0 as usize)..]
115 }
116 pub fn as_u32(self) -> u32 {
117 self.0.as_u32()
118 }
119}
120impl Sub for TextOffset {
121 type Output = TextWidth;
122
123 fn sub(self, rhs: Self) -> Self::Output {
124 TextWidth(self.0.0 - rhs.0.0)
125 }
126}
127
128#[derive(
130 Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
131)]
132pub struct TextSpan {
133 pub start: TextOffset,
134 pub end: TextOffset,
135}
136impl TextSpan {
137 pub fn new(start: TextOffset, end: TextOffset) -> Self {
139 Self { start, end }
140 }
141 pub fn new_with_width(start: TextOffset, width: TextWidth) -> Self {
143 Self::new(start, start.add_width(width))
144 }
145 pub fn cursor(offset: TextOffset) -> Self {
147 Self::new(offset, offset)
148 }
149 #[allow(clippy::should_implement_trait)]
151 pub fn from_str(content: &str) -> Self {
152 Self::new(TextOffset::START, TextOffset::from_str(content))
153 }
154 pub fn width(self) -> TextWidth {
155 self.end - self.start
156 }
157 pub fn contains(self, other: Self) -> bool {
158 self.start <= other.start && self.end >= other.end
159 }
160 pub fn take(self, content: &str) -> &str {
161 &content[(self.start.0.0 as usize)..(self.end.0.0 as usize)]
162 }
163 pub fn n_chars(self, content: &str) -> usize {
164 self.take(content).chars().count()
165 }
166 pub fn after(self) -> Self {
168 Self::cursor(self.end)
169 }
170 pub fn start_only(self) -> Self {
172 Self::cursor(self.start)
173 }
174
175 pub fn to_str_range(&self) -> Range<usize> {
177 self.start.0.0 as usize..self.end.0.0 as usize
178 }
179
180 pub fn position_in_file<'db>(
182 self,
183 db: &'db dyn Database,
184 file: FileId<'db>,
185 ) -> Option<TextPositionSpan> {
186 let start = self.start.position_in_file(db, file)?;
187 let end = self.end.position_in_file(db, file)?;
188 Some(TextPositionSpan { start, end })
189 }
190}
191
192#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
194pub struct TextPosition {
195 pub line: usize,
197 pub col: usize,
199}
200
201impl TextOffset {
202 fn get_line_number(self, db: &dyn Database, file: FileId<'_>) -> Option<usize> {
203 let summary = db.file_summary(file)?;
204 assert!(
205 self <= summary.last_offset,
206 "TextOffset out of range. {:?} > {:?}.",
207 self.0,
208 summary.last_offset.0
209 );
210 Some(summary.line_offsets.binary_search(&self).unwrap_or_else(|x| x - 1))
211 }
212
213 pub fn position_in_file(self, db: &dyn Database, file: FileId<'_>) -> Option<TextPosition> {
215 let summary = db.file_summary(file)?;
216 let line_number = self.get_line_number(db, file)?;
217 let line_offset = summary.line_offsets[line_number];
218 let content = db.file_content(file)?;
219 let col = TextSpan::new(line_offset, self).n_chars(content.as_ref());
220 Some(TextPosition { line: line_number, col })
221 }
222}
223
224impl TextPosition {
225 pub fn offset_in_file(self, db: &dyn Database, file: FileId<'_>) -> Option<TextOffset> {
232 let file_summary = db.file_summary(file)?;
233 let content = db.file_content(file)?;
234
235 let mut offset =
237 file_summary.line_offsets.get(self.line).copied().unwrap_or(file_summary.last_offset);
238
239 offset = offset.add_width(
241 offset
242 .take_from(content.as_ref())
243 .chars()
244 .take_while(|c| *c != '\n')
245 .take(self.col)
246 .map(TextWidth::from_char)
247 .sum(),
248 );
249
250 Some(offset)
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct FileSummary {
257 pub line_offsets: Vec<TextOffset>,
259 pub last_offset: TextOffset,
261}
262impl FileSummary {
263 pub fn line_count(&self) -> usize {
265 self.line_offsets.len()
266 }
267}
268
269#[derive(Copy, Clone, Debug, PartialEq, Eq)]
271pub struct TextPositionSpan {
272 pub start: TextPosition,
273 pub end: TextPosition,
274}
275
276impl TextPositionSpan {
277 pub fn offset_in_file<'db>(
279 Self { start, end }: Self,
280 db: &'db dyn Database,
281 file: FileId<'db>,
282 ) -> Option<TextSpan> {
283 Some(TextSpan::new(start.offset_in_file(db, file)?, end.offset_in_file(db, file)?))
284 }
285}