use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Location {
pub file: PathBuf,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Mapping {
pub rust_file: PathBuf,
pub rust_line: usize,
pub rust_column: usize,
pub wj_file: PathBuf,
pub wj_line: usize,
pub wj_column: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMap {
#[serde(rename = "mappings")]
mappings_vec: Vec<Mapping>,
version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
workspace_root: Option<PathBuf>,
#[serde(skip)]
lookup_index: HashMap<(PathBuf, usize), usize>, }
impl SourceMap {
pub fn new() -> Self {
Self {
mappings_vec: Vec::new(),
version: 1,
workspace_root: None,
lookup_index: HashMap::new(),
}
}
pub fn set_workspace_root(&mut self, root: impl Into<PathBuf>) {
self.workspace_root = Some(root.into());
}
fn to_relative_path(&self, path: &Path) -> PathBuf {
if let Some(ref root) = self.workspace_root {
if let Ok(relative) = path.strip_prefix(root) {
return relative.to_path_buf();
}
}
path.to_path_buf()
}
fn rebuild_index(&mut self) {
self.lookup_index.clear();
for (idx, mapping) in self.mappings_vec.iter().enumerate() {
self.lookup_index
.insert((mapping.rust_file.clone(), mapping.rust_line), idx);
}
}
pub fn add_mapping(
&mut self,
rust_file: impl Into<PathBuf>,
rust_line: usize,
rust_column: usize,
wj_file: impl Into<PathBuf>,
wj_line: usize,
wj_column: usize,
) {
let rust_file_abs = rust_file.into();
let wj_file_abs = wj_file.into();
let rust_file_rel = self.to_relative_path(&rust_file_abs);
let wj_file_rel = self.to_relative_path(&wj_file_abs);
let mapping = Mapping {
rust_file: rust_file_rel.clone(),
rust_line,
rust_column,
wj_file: wj_file_rel,
wj_line,
wj_column,
};
let key = (rust_file_rel, rust_line);
if let Some(&idx) = self.lookup_index.get(&key) {
self.mappings_vec[idx] = mapping;
} else {
let idx = self.mappings_vec.len();
self.mappings_vec.push(mapping);
self.lookup_index.insert(key, idx);
}
}
pub fn lookup(&self, rust_file: &Path, rust_line: usize) -> Option<&Mapping> {
let key = (rust_file.to_path_buf(), rust_line);
self.lookup_index
.get(&key)
.and_then(|&idx| self.mappings_vec.get(idx))
}
pub fn lookup_fuzzy(&self, rust_file: &Path, rust_line: usize) -> Option<&Mapping> {
if let Some(mapping) = self.lookup(rust_file, rust_line) {
return Some(mapping);
}
for offset in 1..=5 {
if rust_line > offset {
if let Some(mapping) = self.lookup(rust_file, rust_line - offset) {
return Some(mapping);
}
}
if let Some(mapping) = self.lookup(rust_file, rust_line + offset) {
return Some(mapping);
}
}
None
}
pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<(), std::io::Error> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, Box<dyn std::error::Error>> {
let json = std::fs::read_to_string(path)?;
let mut source_map: Self = serde_json::from_str(&json)?;
source_map.rebuild_index();
Ok(source_map)
}
pub fn len(&self) -> usize {
self.mappings_vec.len()
}
pub fn is_empty(&self) -> bool {
self.mappings_vec.is_empty()
}
pub fn mappings_for_wj_file(&self, wj_file: &Path) -> Vec<&Mapping> {
self.mappings_vec
.iter()
.filter(|m| m.wj_file == wj_file)
.collect()
}
pub fn mappings_for_rust_file(&self, rust_file: &Path) -> Vec<&Mapping> {
self.mappings_vec
.iter()
.filter(|m| m.rust_file == rust_file)
.collect()
}
pub fn get_location(&self, rust_line: usize) -> Option<Location> {
self.mappings_vec
.iter()
.find(|m| m.rust_line == rust_line)
.map(|m| Location {
file: m.wj_file.clone(),
line: m.wj_line,
column: m.wj_column,
})
}
pub fn map_rust_to_windjammer(&self, rust_location: &Location) -> Option<Location> {
if let Some(mapping) = self.lookup(&rust_location.file, rust_location.line) {
return Some(Location {
file: mapping.wj_file.clone(),
line: mapping.wj_line,
column: mapping.wj_column,
});
}
if let Some(mapping) = self.lookup_fuzzy(&rust_location.file, rust_location.line) {
return Some(Location {
file: mapping.wj_file.clone(),
line: mapping.wj_line,
column: mapping.wj_column,
});
}
None
}
}
impl Default for SourceMap {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_map_creation() {
let source_map = SourceMap::new();
assert_eq!(source_map.len(), 0);
assert!(source_map.is_empty());
}
#[test]
fn test_add_and_lookup_mapping() {
let mut source_map = SourceMap::new();
source_map.add_mapping("output/main.rs", 10, 5, "src/main.wj", 5, 1);
let mapping = source_map.lookup(Path::new("output/main.rs"), 10);
assert!(mapping.is_some());
let mapping = mapping.unwrap();
assert_eq!(mapping.rust_line, 10);
assert_eq!(mapping.rust_column, 5);
assert_eq!(mapping.wj_line, 5);
assert_eq!(mapping.wj_column, 1);
assert_eq!(mapping.wj_file, PathBuf::from("src/main.wj"));
}
#[test]
fn test_fuzzy_lookup() {
let mut source_map = SourceMap::new();
source_map.add_mapping("output/main.rs", 10, 5, "src/main.wj", 5, 1);
assert!(source_map
.lookup_fuzzy(Path::new("output/main.rs"), 10)
.is_some());
assert!(source_map
.lookup_fuzzy(Path::new("output/main.rs"), 11)
.is_some());
assert!(source_map
.lookup_fuzzy(Path::new("output/main.rs"), 9)
.is_some());
assert!(source_map
.lookup_fuzzy(Path::new("output/main.rs"), 12)
.is_some());
assert!(source_map
.lookup_fuzzy(Path::new("output/main.rs"), 8)
.is_some());
assert!(source_map
.lookup_fuzzy(Path::new("output/main.rs"), 20)
.is_none());
}
#[test]
fn test_save_and_load() {
let mut source_map = SourceMap::new();
source_map.add_mapping("output/main.rs", 10, 5, "src/main.wj", 5, 1);
source_map.add_mapping("output/lib.rs", 20, 10, "src/lib.wj", 15, 3);
let temp_file = std::env::temp_dir().join("test_source_map.json");
source_map.save_to_file(&temp_file).unwrap();
let loaded = SourceMap::load_from_file(&temp_file).unwrap();
assert_eq!(loaded.len(), 2);
assert!(loaded.lookup(Path::new("output/main.rs"), 10).is_some());
assert!(loaded.lookup(Path::new("output/lib.rs"), 20).is_some());
std::fs::remove_file(temp_file).ok();
}
#[test]
fn test_mappings_for_file() {
let mut source_map = SourceMap::new();
source_map.add_mapping("output/main.rs", 10, 5, "src/main.wj", 5, 1);
source_map.add_mapping("output/main.rs", 20, 10, "src/main.wj", 15, 3);
source_map.add_mapping("output/lib.rs", 5, 1, "src/lib.wj", 3, 1);
let main_mappings = source_map.mappings_for_rust_file(Path::new("output/main.rs"));
assert_eq!(main_mappings.len(), 2);
let wj_main_mappings = source_map.mappings_for_wj_file(Path::new("src/main.wj"));
assert_eq!(wj_main_mappings.len(), 2);
let lib_mappings = source_map.mappings_for_rust_file(Path::new("output/lib.rs"));
assert_eq!(lib_mappings.len(), 1);
}
}