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#[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 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#[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 #[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#[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 #[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 pub fn after(self) -> Self {
133 Self { start: self.end, end: self.end }
134 }
135 pub fn start_only(self) -> Self {
137 Self { start: self.start, end: self.start }
138 }
139
140 pub fn to_str_range(&self) -> Range<usize> {
142 self.start.0.0 as usize..self.end.0.0 as usize
143 }
144
145 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
155pub struct TextPosition {
156 pub line: usize,
158 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 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 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 let mut offset =
198 file_summary.line_offsets.get(self.line).copied().unwrap_or(file_summary.last_offset);
199
200 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#[derive(Clone, Debug, PartialEq, Eq)]
217pub struct FileSummary {
218 pub line_offsets: Vec<TextOffset>,
220 pub last_offset: TextOffset,
222}
223impl FileSummary {
224 pub fn line_count(&self) -> usize {
226 self.line_offsets.len()
227 }
228}
229
230#[derive(Copy, Clone, Debug, PartialEq, Eq)]
232pub struct TextPositionSpan {
233 pub start: TextPosition,
234 pub end: TextPosition,
235}
236
237impl TextPositionSpan {
238 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}