Skip to main content

tla_checker/
source.rs

1use std::sync::Arc;
2
3use crate::span::Span;
4
5#[derive(Clone)]
6pub struct Source {
7    text: Arc<str>,
8    name: Arc<str>,
9    line_starts: Vec<u32>,
10}
11
12impl Source {
13    pub fn new(name: impl Into<Arc<str>>, text: impl Into<Arc<str>>) -> Self {
14        let text: Arc<str> = text.into();
15        let line_starts = compute_line_starts(&text);
16        Self {
17            text,
18            name: name.into(),
19            line_starts,
20        }
21    }
22
23    pub fn name(&self) -> &str {
24        &self.name
25    }
26
27    pub fn text(&self) -> &str {
28        &self.text
29    }
30
31    pub fn line_col(&self, offset: u32) -> (usize, usize) {
32        let offset = offset as usize;
33        let line = self
34            .line_starts
35            .partition_point(|&start| (start as usize) <= offset)
36            .saturating_sub(1);
37        let line_start = self.line_starts.get(line).copied().unwrap_or(0) as usize;
38        let col = offset.saturating_sub(line_start);
39        (line + 1, col + 1)
40    }
41
42    pub fn line_text(&self, line: usize) -> &str {
43        if line == 0 || line > self.line_starts.len() {
44            return "";
45        }
46        let start = self.line_starts[line - 1] as usize;
47        let end = self
48            .line_starts
49            .get(line)
50            .map(|&e| e as usize)
51            .unwrap_or(self.text.len());
52        let text = &self.text[start..end];
53        text.trim_end_matches('\n').trim_end_matches('\r')
54    }
55
56    pub fn line_count(&self) -> usize {
57        self.line_starts.len()
58    }
59
60    pub fn slice(&self, span: Span) -> &str {
61        let start = span.start as usize;
62        let end = (span.end as usize).min(self.text.len());
63        if start <= end && end <= self.text.len() {
64            &self.text[start..end]
65        } else {
66            ""
67        }
68    }
69}
70
71fn compute_line_starts(text: &str) -> Vec<u32> {
72    let mut starts = vec![0];
73    for (i, c) in text.char_indices() {
74        if c == '\n' {
75            starts.push((i + 1) as u32);
76        }
77    }
78    starts
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn line_col_single_line() {
87        let src = Source::new("test.tla", "x = 42");
88        assert_eq!(src.line_col(0), (1, 1));
89        assert_eq!(src.line_col(2), (1, 3));
90        assert_eq!(src.line_col(5), (1, 6));
91    }
92
93    #[test]
94    fn line_col_multi_line() {
95        let src = Source::new("test.tla", "line1\nline2\nline3");
96        assert_eq!(src.line_col(0), (1, 1));
97        assert_eq!(src.line_col(5), (1, 6));
98        assert_eq!(src.line_col(6), (2, 1));
99        assert_eq!(src.line_col(12), (3, 1));
100    }
101
102    #[test]
103    fn line_text_retrieval() {
104        let src = Source::new("test.tla", "first\nsecond\nthird");
105        assert_eq!(src.line_text(1), "first");
106        assert_eq!(src.line_text(2), "second");
107        assert_eq!(src.line_text(3), "third");
108    }
109
110    #[test]
111    fn slice_extraction() {
112        let src = Source::new("test.tla", "hello world");
113        assert_eq!(src.slice(Span::new(0, 5)), "hello");
114        assert_eq!(src.slice(Span::new(6, 11)), "world");
115    }
116}