diff options
Diffstat (limited to 'src/syntax/lines.rs')
| -rw-r--r-- | src/syntax/lines.rs | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/src/syntax/lines.rs b/src/syntax/lines.rs new file mode 100644 index 00000000..86fc461b --- /dev/null +++ b/src/syntax/lines.rs @@ -0,0 +1,114 @@ +//! Conversion of byte positions to line/column locations. + +use std::fmt::{self, Debug, Display, Formatter}; + +use super::Pos; +use crate::parse::is_newline_char; + +/// Enables conversion of byte position to locations. +pub struct LineMap<'s> { + src: &'s str, + line_starts: Vec<Pos>, +} + +impl<'s> LineMap<'s> { + /// Create a new line map for a source string. + pub fn new(src: &'s str) -> Self { + let mut line_starts = vec![Pos::ZERO]; + let mut iter = src.char_indices().peekable(); + + while let Some((mut i, c)) = iter.next() { + if is_newline_char(c) { + i += c.len_utf8(); + if c == '\r' && matches!(iter.peek(), Some((_, '\n'))) { + i += '\n'.len_utf8(); + iter.next(); + } + + line_starts.push(Pos(i as u32)); + } + } + + Self { src, line_starts } + } + + /// Convert a byte position to a location. + /// + /// # Panics + /// This panics if the position is out of bounds. + pub fn location(&self, pos: Pos) -> Location { + let line_index = match self.line_starts.binary_search(&pos) { + Ok(i) => i, + Err(i) => i - 1, + }; + + let line_start = self.line_starts[line_index]; + let head = &self.src[line_start.to_usize() .. pos.to_usize()]; + let column_index = head.chars().count(); + + Location { + line: 1 + line_index as u32, + column: 1 + column_index as u32, + } + } +} + +/// One-indexed line-column position in source code. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +pub struct Location { + /// The one-indexed line. + pub line: u32, + /// The one-indexed column. + pub column: u32, +} + +impl Location { + /// Create a new location from line and column. + pub fn new(line: u32, column: u32) -> Self { + Self { line, column } + } +} + +impl Debug for Location { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for Location { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}:{}", self.line, self.column) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST: &str = "äbcde\nf💛g\r\nhi\rjkl"; + + #[test] + fn test_line_map_new() { + let map = LineMap::new(TEST); + assert_eq!(map.line_starts, vec![Pos(0), Pos(7), Pos(15), Pos(18)]); + } + + #[test] + fn test_line_map_location() { + let map = LineMap::new(TEST); + assert_eq!(map.location(Pos(0)), Location::new(1, 1)); + assert_eq!(map.location(Pos(2)), Location::new(1, 2)); + assert_eq!(map.location(Pos(6)), Location::new(1, 6)); + assert_eq!(map.location(Pos(7)), Location::new(2, 1)); + assert_eq!(map.location(Pos(8)), Location::new(2, 2)); + assert_eq!(map.location(Pos(12)), Location::new(2, 3)); + assert_eq!(map.location(Pos(21)), Location::new(4, 4)); + } + + #[test] + #[should_panic] + fn test_line_map_panics_out_of_bounds() { + LineMap::new(TEST).location(Pos(22)); + } +} |
