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 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#[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#[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 pub fn after(self) -> Self {
104 Self { start: self.end, end: self.end }
105 }
106 pub fn start_only(self) -> Self {
108 Self { start: self.start, end: self.start }
109 }
110
111 pub fn to_str_range(&self) -> Range<usize> {
113 self.start.0.0 as usize..self.end.0.0 as usize
114 }
115
116 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
126pub struct TextPosition {
127 pub line: usize,
129 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 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 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 let mut offset =
169 file_summary.line_offsets.get(self.line).copied().unwrap_or(file_summary.last_offset);
170
171 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#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct FileSummary {
189 pub line_offsets: Vec<TextOffset>,
191 pub last_offset: TextOffset,
193}
194impl FileSummary {
195 pub fn line_count(&self) -> usize {
197 self.line_offsets.len()
198 }
199}
200
201#[derive(Copy, Clone, Debug, PartialEq, Eq)]
203pub struct TextPositionSpan {
204 pub start: TextPosition,
205 pub end: TextPosition,
206}
207
208impl TextPositionSpan {
209 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}