Skip to main content

qanglang_core/frontend/
source.rs

1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone, Default, PartialEq)]
4pub struct SourceMap {
5    source: Vec<char>,
6    line_indices: Vec<usize>,
7    path: PathBuf,
8}
9
10impl SourceMap {
11    pub fn new(source: String, path: PathBuf) -> Self {
12        let (chars, line_indices) = Self::load_source(source);
13
14        Self {
15            path,
16            source: chars,
17            line_indices,
18        }
19    }
20
21    pub fn from_source(source: String) -> Self {
22        let (chars, line_indices) = Self::load_source(source);
23
24        Self {
25            path: PathBuf::new(),
26            source: chars,
27            line_indices,
28        }
29    }
30
31    pub fn from_path(path: &Path) -> std::io::Result<Self> {
32        let source = std::fs::read_to_string(path)?;
33        let (chars, line_indices) = Self::load_source(source);
34        Ok(Self {
35            path: path.to_path_buf(),
36            line_indices,
37            source: chars,
38        })
39    }
40
41    fn load_source(source: String) -> (Vec<char>, Vec<usize>) {
42        let chars: Vec<char> = source.chars().collect();
43        let mut line_indices = Vec::new();
44        let mut in_string = false;
45        let mut i = 0;
46
47        while i < chars.len() {
48            match chars[i] {
49                '"' if !in_string => {
50                    in_string = true;
51                }
52                '"' if in_string => {
53                    in_string = false;
54                }
55                '\\' if in_string => {
56                    // Skip escape sequence
57                    i += 1; // Skip the backslash
58                    if i < chars.len() {
59                        i += 1; // Skip the escaped character
60                        continue;
61                    }
62                }
63                '\n' if !in_string => {
64                    line_indices.push(i);
65                }
66                _ => {}
67            }
68            i += 1;
69        }
70
71        (chars, line_indices)
72    }
73
74    pub fn get_path(&self) -> &Path {
75        self.path.as_path()
76    }
77
78    pub fn get_source(&self) -> &Vec<char> {
79        &self.source
80    }
81
82    pub fn get_line(&self, line_number: u32) -> &[char] {
83        if line_number == 0 {
84            return &[];
85        }
86
87        let line_index = (line_number - 1) as usize;
88
89        let start = if line_number == 1 {
90            0
91        } else {
92            // Start after the previous newline
93            if let Some(&prev_newline_pos) = self.line_indices.get(line_index - 1) {
94                prev_newline_pos + 1
95            } else {
96                return &[];
97            }
98        };
99
100        let end = if let Some(&newline_pos) = self.line_indices.get(line_index) {
101            newline_pos // Stop at the newline (don't include it)
102        } else if line_index == self.line_indices.len() {
103            // Last line (no trailing newline)
104            self.source.len()
105        } else {
106            // Line doesn't exist
107            return &[];
108        };
109
110        if start <= end && end <= self.source.len() {
111            &self.source[start..end]
112        } else {
113            &[]
114        }
115    }
116
117    pub fn get_line_number(&self, position: usize) -> u32 {
118        if position >= self.source.len() {
119            return (self.line_indices.len() + 1) as u32;
120        }
121
122        // Binary search to find which line this position belongs to
123        match self.line_indices.binary_search(&position) {
124            Ok(index) => (index + 1) as u32,  // Exact match on newline
125            Err(index) => (index + 1) as u32, // Position is between line_indices[index-1] and line_indices[index]
126        }
127    }
128
129    pub fn get_column_number(&self, position: usize) -> u32 {
130        if position >= self.source.len() {
131            return 1;
132        }
133        let line_number = self.get_line_number(position);
134        let line_start = if line_number == 1 {
135            0
136        } else {
137            let line_index = (line_number - 2) as usize;
138            if let Some(&newline_pos) = self.line_indices.get(line_index) {
139                newline_pos + 1
140            } else {
141                0
142            }
143        };
144        (position - line_start + 1) as u32
145    }
146
147    /// Convert LSP position (0-based line/column) to byte offset in source
148    pub fn position_to_offset(&self, line: u32, column: u32) -> Option<usize> {
149        // LSP uses 0-based indexing
150        let target_line = line as usize;
151        let target_col = column as usize;
152
153        // Calculate the start of the target line
154        let line_start = if target_line == 0 {
155            0
156        } else {
157            // Get the position after the newline of the previous line
158            if let Some(&newline_pos) = self.line_indices.get(target_line - 1) {
159                newline_pos + 1
160            } else {
161                // Line doesn't exist
162                return None;
163            }
164        };
165
166        // Calculate the offset by adding the column to the line start
167        let offset = line_start + target_col;
168
169        // Ensure offset is within bounds
170        if offset <= self.source.len() {
171            Some(offset)
172        } else {
173            None
174        }
175    }
176}