shader_sense/
position.rs

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