1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
11pub struct TextRange {
12 start: u32,
13 end: u32,
14}
15
16impl TextRange {
17 #[must_use]
18 pub const fn new(start: u32, end: u32) -> Self {
19 debug_assert!(start <= end);
20 Self { start, end }
21 }
22
23 #[must_use]
26 pub const fn empty(offset: u32) -> Self {
27 Self {
28 start: offset,
29 end: offset,
30 }
31 }
32
33 #[must_use]
34 pub const fn start(self) -> u32 {
35 self.start
36 }
37
38 #[must_use]
39 pub const fn end(self) -> u32 {
40 self.end
41 }
42
43 #[must_use]
44 pub const fn len(self) -> u32 {
45 self.end - self.start
46 }
47
48 #[must_use]
49 pub const fn is_empty(self) -> bool {
50 self.start == self.end
51 }
52
53 #[must_use]
55 pub fn cover(self, other: Self) -> Self {
56 Self {
57 start: self.start.min(other.start),
58 end: self.end.max(other.end),
59 }
60 }
61
62 #[must_use]
64 pub fn slice(self, text: &str) -> &str {
65 &text[self.start as usize..self.end as usize]
66 }
67}
68
69impl fmt::Display for TextRange {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "{}..{}", self.start, self.end)
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub struct LineCol {
78 pub line: u32,
79 pub col: u32,
81}
82
83impl fmt::Display for LineCol {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "{}:{}", self.line, self.col)
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct LineIndex {
92 line_starts: Vec<u32>,
94}
95
96impl LineIndex {
97 #[must_use]
98 pub fn new(text: &str) -> Self {
99 let mut line_starts = vec![0];
100 for (offset, byte) in text.bytes().enumerate() {
101 if byte == b'\n' {
102 line_starts.push(offset as u32 + 1);
103 }
104 }
105 Self { line_starts }
106 }
107
108 #[must_use]
110 pub fn line_col(&self, offset: u32) -> LineCol {
111 let line = self.line_starts.partition_point(|&start| start <= offset);
114 let line_start = self.line_starts[line - 1];
115 LineCol {
116 line: line as u32,
117 col: offset - line_start + 1,
118 }
119 }
120
121 #[must_use]
123 pub fn line_count(&self) -> usize {
124 self.line_starts.len()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn range_basics() {
134 let range = TextRange::new(2, 5);
135 assert_eq!(range.len(), 3);
136 assert!(!range.is_empty());
137 assert_eq!(range.slice("abcdefg"), "cde");
138 assert!(TextRange::empty(4).is_empty());
139 assert_eq!(TextRange::new(1, 3).cover(TextRange::new(7, 9)).len(), 8);
140 }
141
142 #[test]
143 fn line_col_resolves_positions() {
144 let index = LineIndex::new("ab\ncd\n\nef");
145 assert_eq!(index.line_col(0), LineCol { line: 1, col: 1 });
146 assert_eq!(index.line_col(1), LineCol { line: 1, col: 2 });
147 assert_eq!(index.line_col(2), LineCol { line: 1, col: 3 });
149 assert_eq!(index.line_col(3), LineCol { line: 2, col: 1 });
150 assert_eq!(index.line_col(6), LineCol { line: 3, col: 1 });
151 assert_eq!(index.line_col(7), LineCol { line: 4, col: 1 });
152 assert_eq!(index.line_count(), 4);
153 }
154
155 #[test]
156 fn line_col_counts_utf8_bytes() {
157 let index = LineIndex::new("é!");
159 assert_eq!(index.line_col(2), LineCol { line: 1, col: 3 });
160 }
161}