use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub line: usize,
pub column: usize,
pub source_file: Option<String>,
pub name: Option<String>,
}
pub struct SourceMap {
source_file: String,
lines: Vec<String>,
byte_to_line: Vec<usize>,
line_to_byte: Vec<usize>,
}
impl SourceMap {
pub fn new(source_file: &str, source: &str) -> Self {
let lines: Vec<String> = source.lines().map(String::from).collect();
let mut byte_to_line = Vec::new();
let mut line_to_byte = Vec::new();
let mut byte_offset = 0;
for (line_idx, line) in lines.iter().enumerate() {
line_to_byte.push(byte_offset);
for _ in 0..line.len() {
byte_to_line.push(line_idx);
}
byte_offset += line.len();
byte_to_line.push(line_idx);
byte_offset += 1;
}
Self {
source_file: source_file.to_string(),
lines,
byte_to_line,
line_to_byte,
}
}
pub fn get_location(&self, byte_offset: usize) -> Option<SourceLocation> {
if byte_offset >= self.byte_to_line.len() {
return None;
}
let line_idx = self.byte_to_line[byte_offset];
let line_start = self.line_to_byte.get(line_idx).copied().unwrap_or(0);
let column = byte_offset - line_start + 1;
Some(SourceLocation {
line: line_idx + 1,
column,
source_file: Some(self.source_file.clone()),
name: None,
})
}
pub fn get_line(&self, line: usize) -> Option<&str> {
self.lines.get(line - 1).map(|s| s.as_str())
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn source_file(&self) -> &str {
&self.source_file
}
pub fn search(&self, query: &str) -> Vec<SourceLocation> {
let mut results = Vec::new();
if query.is_empty() {
return results;
}
let advance = query.chars().next().map(|c| c.len_utf8()).unwrap_or(1);
for (line_idx, line) in self.lines.iter().enumerate() {
let mut col = 0;
while col <= line.len() {
let rest = line.get(col..).unwrap_or("");
let Some(pos) = rest.find(query) else {
break;
};
results.push(SourceLocation {
line: line_idx + 1,
column: col + pos + 1,
source_file: Some(self.source_file.clone()),
name: None,
});
col += pos + advance;
while col < line.len() && !line.is_char_boundary(col) {
col += 1;
}
}
}
results
}
}
pub struct BidirectionalSourceMap {
original: SourceMap,
generated: SourceMap,
original_to_generated: HashMap<usize, usize>,
generated_to_original: HashMap<usize, usize>,
}
impl BidirectionalSourceMap {
pub fn new(original: SourceMap, generated: SourceMap) -> Self {
Self {
original,
generated,
original_to_generated: HashMap::new(),
generated_to_original: HashMap::new(),
}
}
pub fn add_mapping(&mut self, original_line: usize, generated_line: usize) {
self.original_to_generated
.insert(original_line, generated_line);
self.generated_to_original
.insert(generated_line, original_line);
}
pub fn original_to_generated_location(
&self,
location: &SourceLocation,
) -> Option<SourceLocation> {
let generated_line = self.original_to_generated.get(&(location.line - 1))?;
Some(SourceLocation {
line: generated_line + 1,
column: location.column,
source_file: Some(self.generated.source_file().to_string()),
name: location.name.clone(),
})
}
pub fn generated_to_original_location(
&self,
location: &SourceLocation,
) -> Option<SourceLocation> {
let original_line = self.generated_to_original.get(&(location.line - 1))?;
Some(SourceLocation {
line: original_line + 1,
column: location.column,
source_file: Some(self.original.source_file().to_string()),
name: location.name.clone(),
})
}
}
pub struct SourceMapBuilder {
source_file: String,
lines: Vec<String>,
names: HashMap<String, usize>,
}
impl SourceMapBuilder {
pub fn new(source_file: &str, source: &str) -> Self {
Self {
source_file: source_file.to_string(),
lines: source.lines().map(String::from).collect(),
names: HashMap::new(),
}
}
pub fn add_name(&mut self, name: &str) -> usize {
let index = self.names.len();
self.names.insert(name.to_string(), index);
index
}
pub fn build(self) -> SourceMap {
SourceMap::new(&self.source_file, &self.lines.join("\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_map_creation() {
let map = SourceMap::new("test.rs", "line 1\nline 2\nline 3");
assert_eq!(map.line_count(), 3);
assert_eq!(map.source_file(), "test.rs");
}
#[test]
fn test_get_location() {
let map = SourceMap::new("test.txt", "hello\nworld");
let loc = map.get_location(0).unwrap();
assert_eq!(loc.line, 1);
assert_eq!(loc.column, 1);
}
#[test]
fn test_get_line() {
let map = SourceMap::new("test.rs", "line 1\nline 2\nline 3");
assert_eq!(map.get_line(1), Some("line 1"));
assert_eq!(map.get_line(2), Some("line 2"));
assert_eq!(map.get_line(3), Some("line 3"));
}
#[test]
fn test_search() {
let map = SourceMap::new("test.rs", "hello world\nfoo bar\nhello baz");
let results = map.search("hello");
assert_eq!(results.len(), 2);
assert_eq!(results[0].line, 1);
assert_eq!(results[1].line, 3);
}
}