shader_sense/
position.rs

1use std::{cmp::Ordering, path::PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Default, Serialize, Deserialize, Clone)]
6pub struct ShaderPosition {
7    pub line: u32,
8    pub pos: u32,
9}
10impl Eq for ShaderPosition {}
11
12impl Ord for ShaderPosition {
13    fn cmp(&self, other: &Self) -> Ordering {
14        (&self.line, &self.pos).cmp(&(&other.line, &other.pos))
15    }
16}
17
18impl PartialOrd for ShaderPosition {
19    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
20        Some(self.cmp(other))
21    }
22}
23
24impl PartialEq for ShaderPosition {
25    fn eq(&self, other: &Self) -> bool {
26        (&self.line, &self.pos) == (&other.line, &other.pos)
27    }
28}
29
30impl ShaderPosition {
31    pub fn new(line: u32, pos: u32) -> Self {
32        Self { line, pos }
33    }
34    pub fn zero() -> Self {
35        Self { line: 0, pos: 0 }
36    }
37    pub fn into_file(self, file_path: PathBuf) -> ShaderFilePosition {
38        ShaderFilePosition::from(file_path, self)
39    }
40    pub fn clone_into_file(&self, file_path: PathBuf) -> ShaderFilePosition {
41        ShaderFilePosition::from(file_path, self.clone())
42    }
43    pub fn from_byte_offset(content: &str, byte_offset: usize) -> std::io::Result<ShaderPosition> {
44        // https://en.wikipedia.org/wiki/UTF-8
45        if byte_offset == 0 {
46            Ok(ShaderPosition::zero())
47        } else if content.len() == 0 {
48            Err(std::io::Error::new(
49                std::io::ErrorKind::InvalidInput,
50                "Content is empty.",
51            ))
52        } else if byte_offset > content.len() {
53            Err(std::io::Error::new(
54                std::io::ErrorKind::InvalidInput,
55                "byte_offset is out of bounds.",
56            ))
57        } else {
58            // lines iterator does the same, but skip the last empty line by relying on split_inclusive.
59            // We need it so use split instead to keep it.
60            // We only care about line start, so \r being there or not on Windows should not be an issue.
61            let line = content[..byte_offset].split('\n').count() - 1;
62            let line_start = content[..byte_offset]
63                .split('\n')
64                .rev()
65                .next()
66                .expect("No last line available.");
67            let pos_in_byte =
68                content[byte_offset..].as_ptr() as usize - line_start.as_ptr() as usize;
69            if line_start.is_char_boundary(pos_in_byte) {
70                Ok(ShaderPosition::new(
71                    line as u32,
72                    line_start[..pos_in_byte].chars().count() as u32,
73                ))
74            } else {
75                Err(std::io::Error::new(
76                    std::io::ErrorKind::InvalidData,
77                    "Pos in line is not at UTF8 char boundary.",
78                ))
79            }
80        }
81    }
82    pub fn to_byte_offset(&self, content: &str) -> std::io::Result<usize> {
83        // https://en.wikipedia.org/wiki/UTF-8
84        match content.lines().nth(self.line as usize) {
85            Some(line) => {
86                // This pointer operation is safe to operate because lines iterator should start at char boundary.
87                let line_byte_offset = line.as_ptr() as usize - content.as_ptr() as usize;
88                assert!(
89                    content.is_char_boundary(line_byte_offset),
90                    "Start of line is not char boundary."
91                );
92                // We have line offset, find pos offset.
93                match content[line_byte_offset..]
94                    .char_indices()
95                    .nth(self.pos as usize)
96                {
97                    Some((byte_offset, _)) => {
98                        let global_offset = line_byte_offset + byte_offset;
99                        if content.len() <= global_offset {
100                            Err(std::io::Error::new(
101                                std::io::ErrorKind::InvalidData,
102                                "Byte offset is not in content range.",
103                            ))
104                        } else if !content.is_char_boundary(global_offset) {
105                            Err(std::io::Error::new(
106                                std::io::ErrorKind::InvalidData,
107                                "Position is not at UTF8 char boundary.",
108                            ))
109                        } else {
110                            Ok(global_offset)
111                        }
112                    }
113                    None => {
114                        if self.pos as usize == line.chars().count() {
115                            assert!(content.is_char_boundary(line_byte_offset + line.len()));
116                            Ok(line_byte_offset + line.len())
117                        } else {
118                            Err(std::io::Error::new(
119                                std::io::ErrorKind::InvalidInput,
120                                format!("Position is not in range of line"),
121                            ))
122                        }
123                    }
124                }
125            }
126            // Last line in line iterator is skipped if its empty.
127            None => Ok(content.len()), // Line is out of bounds, assume its at the end.
128        }
129    }
130}
131
132#[derive(Debug, Default, Serialize, Deserialize, Clone)]
133pub struct ShaderFilePosition {
134    pub file_path: PathBuf,
135    pub position: ShaderPosition,
136}
137impl Eq for ShaderFilePosition {}
138
139impl Ord for ShaderFilePosition {
140    fn cmp(&self, other: &Self) -> Ordering {
141        assert!(
142            self.file_path == other.file_path,
143            "Cannot compare file from different path"
144        );
145        (&self.file_path, &self.position.line, &self.position.pos).cmp(&(
146            &other.file_path,
147            &other.position.line,
148            &other.position.pos,
149        ))
150    }
151}
152
153impl PartialOrd for ShaderFilePosition {
154    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155        Some(self.cmp(other))
156    }
157}
158
159impl PartialEq for ShaderFilePosition {
160    fn eq(&self, other: &Self) -> bool {
161        (&self.file_path, &self.position.line, &self.position.pos)
162            == (&other.file_path, &other.position.line, &other.position.pos)
163    }
164}
165
166impl ShaderFilePosition {
167    pub fn from(file_path: PathBuf, position: ShaderPosition) -> Self {
168        Self {
169            file_path,
170            position,
171        }
172    }
173    pub fn new(file_path: PathBuf, line: u32, pos: u32) -> Self {
174        Self {
175            file_path,
176            position: ShaderPosition::new(line, pos),
177        }
178    }
179    pub fn zero(file_path: PathBuf) -> Self {
180        Self {
181            file_path,
182            position: ShaderPosition::zero(),
183        }
184    }
185    pub fn pos(&self) -> u32 {
186        self.position.pos
187    }
188    pub fn line(&self) -> u32 {
189        self.position.line
190    }
191}
192
193#[derive(Debug, Default, Clone, PartialEq, Eq)]
194pub struct ShaderRange {
195    pub start: ShaderPosition,
196    pub end: ShaderPosition,
197}
198
199impl ShaderRange {
200    pub fn new(start: ShaderPosition, end: ShaderPosition) -> Self {
201        Self { start, end }
202    }
203    pub fn zero() -> Self {
204        Self::new(ShaderPosition::zero(), ShaderPosition::zero())
205    }
206    pub fn into_file(self, file_path: PathBuf) -> ShaderFileRange {
207        ShaderFileRange::from(file_path, self)
208    }
209    pub fn clone_into_file(&self, file_path: PathBuf) -> ShaderFileRange {
210        ShaderFileRange::from(file_path, self.clone())
211    }
212    pub fn whole(content: &str) -> Self {
213        let line_count = content.lines().count() as u32;
214        let char_count = match content.lines().last() {
215            Some(last_line) => (last_line.char_indices().count()) as u32, // Last line
216            None => (content.char_indices().count()) as u32, // No last line, means no line, pick string length
217        };
218        Self {
219            start: ShaderPosition::new(0, 0),
220            end: ShaderPosition::new(line_count, char_count),
221        }
222    }
223    pub fn contain_bounds(&self, range: &ShaderRange) -> bool {
224        if range.start.line > self.start.line && range.end.line < self.end.line {
225            true
226        } else if range.start.line == self.start.line && range.end.line == self.end.line {
227            range.start.pos >= self.start.pos && range.end.pos <= self.end.pos
228        } else if range.start.line == self.start.line && range.end.line < self.end.line {
229            range.start.pos >= self.start.pos
230        } else if range.end.line == self.end.line && range.start.line > self.start.line {
231            range.end.pos <= self.end.pos
232        } else {
233            false
234        }
235    }
236    pub fn contain(&self, position: &ShaderPosition) -> bool {
237        // Check line & position bounds.
238        if position.line > self.start.line && position.line < self.end.line {
239            true
240        } else if position.line == self.start.line && position.line == self.end.line {
241            position.pos >= self.start.pos && position.pos <= self.end.pos
242        } else if position.line == self.start.line && position.line < self.end.line {
243            position.pos >= self.start.pos
244        } else if position.line == self.end.line && position.line > self.start.line {
245            position.pos <= self.end.pos
246        } else {
247            false
248        }
249    }
250    pub fn join(mut lhs: ShaderRange, rhs: ShaderRange) -> ShaderRange {
251        lhs.start.line = std::cmp::min(lhs.start.line, rhs.start.line);
252        lhs.start.pos = std::cmp::min(lhs.start.pos, rhs.start.pos);
253        lhs.end.line = std::cmp::max(lhs.end.line, rhs.end.line);
254        lhs.end.pos = std::cmp::max(lhs.end.pos, rhs.end.pos);
255        lhs
256    }
257}
258
259#[derive(Debug, Default, Clone, PartialEq, Eq)]
260pub struct ShaderFileRange {
261    pub file_path: PathBuf,
262    pub range: ShaderRange,
263}
264
265impl ShaderFileRange {
266    pub fn from(file_path: PathBuf, range: ShaderRange) -> Self {
267        Self { file_path, range }
268    }
269    pub fn new(file_path: PathBuf, start: ShaderPosition, end: ShaderPosition) -> Self {
270        Self {
271            file_path,
272            range: ShaderRange::new(start, end),
273        }
274    }
275    pub fn zero(file_path: PathBuf) -> Self {
276        Self::new(file_path, ShaderPosition::zero(), ShaderPosition::zero())
277    }
278    pub fn whole(file_path: PathBuf, content: &str) -> Self {
279        Self::from(file_path, ShaderRange::whole(content))
280    }
281    pub fn start(&self) -> &ShaderPosition {
282        &self.range.start
283    }
284    pub fn end(&self) -> &ShaderPosition {
285        &self.range.end
286    }
287    pub fn start_as_file_position(&self) -> ShaderFilePosition {
288        ShaderFilePosition::from(self.file_path.clone(), self.range.start.clone())
289    }
290    pub fn end_as_file_position(&self) -> ShaderFilePosition {
291        ShaderFilePosition::from(self.file_path.clone(), self.range.end.clone())
292    }
293    pub fn contain_bounds(&self, range: &ShaderFileRange) -> bool {
294        if self.file_path.as_os_str() == range.file_path.as_os_str() {
295            debug_assert!(
296                range.file_path == self.file_path,
297                "Raw string identical but not components"
298            );
299            self.range.contain_bounds(&range.range)
300        } else {
301            debug_assert!(
302                range.file_path != self.file_path,
303                "Raw string different but not components"
304            );
305            false
306        }
307    }
308    pub fn contain(&self, position: &ShaderFilePosition) -> bool {
309        // Check same file. Comparing components is hitting perf, so just compare raw path, which should already be canonical.
310        if position.file_path.as_os_str() == self.file_path.as_os_str() {
311            debug_assert!(
312                position.file_path == self.file_path,
313                "Raw string identical but not components"
314            );
315            self.range.contain(&position.position)
316        } else {
317            debug_assert!(
318                position.file_path != self.file_path,
319                "Raw string different but not components"
320            );
321            false
322        }
323    }
324    pub fn join(mut lhs: ShaderFileRange, rhs: ShaderFileRange) -> ShaderFileRange {
325        lhs.range = ShaderRange::join(lhs.range, rhs.range);
326        lhs
327    }
328}