summaryrefslogtreecommitdiff
path: root/src/syntax/lines.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-09-30 17:25:09 +0200
committerLaurenz <laurmaedje@gmail.com>2020-09-30 17:25:09 +0200
commit7cc279f7ae122f4c40592004dde89792c636b3c8 (patch)
treea71d3567950c147d41bfa649ca6cd76edb47cc4f /src/syntax/lines.rs
parent3c3730425f0a9a4241c4f57cb7f4d00b71db201e (diff)
Replace line/column with byte positions 🔢
Diffstat (limited to 'src/syntax/lines.rs')
-rw-r--r--src/syntax/lines.rs114
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));
+ }
+}