1use std::fmt;
4
5pub use text_size::TextRange;
7pub use text_size::TextSize;
8
9#[derive(Copy, Clone, Eq, PartialEq, Hash, Default)]
13pub struct LineCol {
14 pub line: u32,
16 pub col: u32,
18}
19
20impl LineCol {
21 #[inline]
23 pub const fn new(line: u32, col: u32) -> Self {
24 Self { line, col }
25 }
26
27 #[inline]
29 pub const fn from_one_indexed(line: u32, col: u32) -> Self {
30 Self {
31 line: line.saturating_sub(1),
32 col: col.saturating_sub(1),
33 }
34 }
35
36 #[inline]
38 pub const fn line_one_indexed(self) -> u32 {
39 self.line + 1
40 }
41
42 #[inline]
44 pub const fn col_one_indexed(self) -> u32 {
45 self.col + 1
46 }
47}
48
49impl fmt::Debug for LineCol {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "{}:{}", self.line_one_indexed(), self.col_one_indexed())
52 }
53}
54
55impl fmt::Display for LineCol {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{}:{}", self.line_one_indexed(), self.col_one_indexed())
58 }
59}
60
61#[derive(Clone, Debug)]
63pub struct LineIndex {
64 line_starts: Vec<TextSize>,
66}
67
68impl LineIndex {
69 pub fn new(text: &str) -> Self {
71 let mut line_starts = vec![TextSize::from(0)];
72
73 for (offset, c) in text.char_indices() {
74 if c == '\n' {
75 line_starts.push(TextSize::from((offset + 1) as u32));
76 }
77 }
78
79 Self { line_starts }
80 }
81
82 pub fn line_col(&self, offset: TextSize) -> LineCol {
84 let line = self
85 .line_starts
86 .partition_point(|&start| start <= offset)
87 .saturating_sub(1);
88
89 let line_start = self.line_starts[line];
90 let col = offset - line_start;
91
92 LineCol {
93 line: line as u32,
94 col: col.into(),
95 }
96 }
97
98 pub fn offset(&self, line_col: LineCol) -> Option<TextSize> {
100 let line_start = self.line_starts.get(line_col.line as usize)?;
101 Some(*line_start + TextSize::from(line_col.col))
102 }
103
104 pub fn len(&self) -> usize {
106 self.line_starts.len()
107 }
108
109 pub fn is_empty(&self) -> bool {
111 self.line_starts.is_empty()
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_line_col_display() {
121 let pos = LineCol::new(0, 0);
122 assert_eq!(format!("{}", pos), "1:1");
123
124 let pos = LineCol::new(5, 10);
125 assert_eq!(format!("{}", pos), "6:11");
126 }
127
128 #[test]
129 fn test_line_col_from_one_indexed() {
130 let pos = LineCol::from_one_indexed(1, 1);
131 assert_eq!(pos.line, 0);
132 assert_eq!(pos.col, 0);
133 }
134
135 #[test]
136 fn test_line_index_single_line() {
137 let index = LineIndex::new("hello world");
138
139 assert_eq!(index.line_col(TextSize::from(0)), LineCol::new(0, 0));
140 assert_eq!(index.line_col(TextSize::from(5)), LineCol::new(0, 5));
141 }
142
143 #[test]
144 fn test_line_index_multi_line() {
145 let index = LineIndex::new("hello\nworld\n!");
146
147 assert_eq!(index.line_col(TextSize::from(0)), LineCol::new(0, 0));
148 assert_eq!(index.line_col(TextSize::from(5)), LineCol::new(0, 5));
149 assert_eq!(index.line_col(TextSize::from(6)), LineCol::new(1, 0));
150 assert_eq!(index.line_col(TextSize::from(11)), LineCol::new(1, 5));
151 assert_eq!(index.line_col(TextSize::from(12)), LineCol::new(2, 0));
152 }
153
154 #[test]
155 fn test_line_index_offset() {
156 let index = LineIndex::new("hello\nworld");
157
158 assert_eq!(index.offset(LineCol::new(0, 0)), Some(TextSize::from(0)));
159 assert_eq!(index.offset(LineCol::new(1, 0)), Some(TextSize::from(6)));
160 assert_eq!(index.offset(LineCol::new(1, 3)), Some(TextSize::from(9)));
161 }
162}