use std::collections::HashMap;
use std::path::PathBuf;
use crate::storage::UniqueProjectId;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum EditChange {
ReplaceText {
start: usize,
end: usize,
new_text: String,
},
RenameSymbol {
old_name: String,
new_name: String,
},
ExtractFunction {
start: usize,
end: usize,
function_name: String,
},
InlineVariable {
variable_name: String,
},
}
#[derive(Debug, Clone)]
pub struct EditRequest {
pub project_id: UniqueProjectId,
pub file_path: PathBuf,
pub changes: Vec<EditChange>,
pub preview_only: bool,
}
#[derive(Debug, Clone)]
pub struct EditPreview {
pub diff: String,
pub impact: ImpactAnalysis,
pub files_affected: Vec<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct EditResult {
pub success: bool,
pub changes_applied: usize,
pub files_modified: Vec<PathBuf>,
pub error: Option<String>,
pub original_contents: Option<HashMap<String, String>>,
pub modified_contents: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct ImpactAnalysis {
pub affected_nodes: Vec<String>,
pub affected_files: Vec<PathBuf>,
pub breaking_changes: Vec<String>,
pub risk_level: RiskLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RiskLevel {
Low,
Medium,
High,
}
#[derive(Debug, Clone)]
pub enum EditCommand {
Edit {
project_id: UniqueProjectId,
file_path: PathBuf,
changes: Vec<EditChange>,
timestamp: chrono::DateTime<chrono::Utc>,
original_content: Option<String>,
modified_content: Option<String>,
},
Rename {
project_id: UniqueProjectId,
old_name: String,
new_name: String,
timestamp: chrono::DateTime<chrono::Utc>,
original_contents: HashMap<String, String>,
modified_contents: HashMap<String, String>,
},
RollbackPoint {
name: String,
timestamp: chrono::DateTime<chrono::Utc>,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub enum EditType {
Insert,
Delete,
Replace,
Move,
Rename,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct ResolvedEditChange {
pub file_path: PathBuf,
pub original_content: String,
pub new_content: String,
pub language: Option<String>,
pub edit_type: EditType,
}
impl ResolvedEditChange {
pub fn new(file_path: PathBuf, original_content: String, new_content: String) -> Self {
let edit_type = if original_content.is_empty() {
EditType::Insert
} else if new_content.is_empty() {
EditType::Delete
} else {
EditType::Replace
};
Self {
file_path,
original_content,
new_content,
language: None,
edit_type,
}
}
pub fn insert(file_path: PathBuf, content: String) -> Self {
Self {
file_path,
original_content: String::new(),
new_content: content,
language: None,
edit_type: EditType::Insert,
}
}
pub fn delete(file_path: PathBuf, content: String) -> Self {
Self {
file_path,
original_content: content,
new_content: String::new(),
language: None,
edit_type: EditType::Delete,
}
}
pub fn replace(file_path: PathBuf, original: String, new: String) -> Self {
Self {
file_path,
original_content: original,
new_content: new,
language: None,
edit_type: EditType::Replace,
}
}
pub fn with_language(mut self, language: String) -> Self {
self.language = Some(language);
self
}
pub fn with_edit_type(mut self, edit_type: EditType) -> Self {
self.edit_type = edit_type;
self
}
pub fn extension(&self) -> Option<&str> {
self.file_path.extension().and_then(|ext| ext.to_str())
}
pub fn infer_language(&self) -> &str {
if let Some(ref lang) = self.language {
return lang;
}
let ext = self.extension().map(|e| e.to_ascii_lowercase());
match ext.as_deref() {
Some("py") => "python",
Some("js") | Some("jsx") | Some("mjs") | Some("cjs") => "javascript",
Some("ts") | Some("tsx") | Some("mts") | Some("cts") => "typescript",
Some("go") => "go",
Some("rs") => "rust",
Some("java") => "java",
Some("cpp") | Some("cc") | Some("cxx") | Some("hpp") | Some("h") => "cpp",
Some("cs") => "csharp",
Some("rb") => "ruby",
Some("php") => "php",
Some("lua") => "lua",
Some("scala") | Some("sc") => "scala",
Some("c") => "c",
Some("sh") | Some("bash") => "bash",
Some("json") => "json",
_ => "unknown",
}
}
}
#[cfg(test)]
mod tests_resolved {
use super::*;
#[test]
fn test_resolved_edit_change_new_insert() {
let change = ResolvedEditChange::new(
PathBuf::from("test.py"),
String::new(),
"print('hello')".to_string(),
);
assert_eq!(change.edit_type, EditType::Insert);
assert!(change.original_content.is_empty());
assert_eq!(change.new_content, "print('hello')");
}
#[test]
fn test_resolved_edit_change_new_delete() {
let change = ResolvedEditChange::new(
PathBuf::from("test.py"),
"print('hello')".to_string(),
String::new(),
);
assert_eq!(change.edit_type, EditType::Delete);
assert_eq!(change.original_content, "print('hello')");
assert!(change.new_content.is_empty());
}
#[test]
fn test_resolved_edit_change_new_replace() {
let change = ResolvedEditChange::new(
PathBuf::from("test.py"),
"print('hello')".to_string(),
"print('world')".to_string(),
);
assert_eq!(change.edit_type, EditType::Replace);
}
#[test]
fn test_resolved_edit_change_insert() {
let change = ResolvedEditChange::insert(PathBuf::from("test.py"), "x = 1".to_string());
assert_eq!(change.edit_type, EditType::Insert);
assert!(change.original_content.is_empty());
assert_eq!(change.new_content, "x = 1");
}
#[test]
fn test_resolved_edit_change_delete() {
let change = ResolvedEditChange::delete(PathBuf::from("test.py"), "x = 1".to_string());
assert_eq!(change.edit_type, EditType::Delete);
assert_eq!(change.original_content, "x = 1");
assert!(change.new_content.is_empty());
}
#[test]
fn test_resolved_edit_change_replace() {
let change = ResolvedEditChange::replace(
PathBuf::from("test.py"),
"x = 1".to_string(),
"x = 2".to_string(),
);
assert_eq!(change.edit_type, EditType::Replace);
assert_eq!(change.original_content, "x = 1");
assert_eq!(change.new_content, "x = 2");
}
#[test]
fn test_resolved_with_language() {
let change = ResolvedEditChange::insert(PathBuf::from("test.txt"), "content".to_string())
.with_language("python".to_string());
assert_eq!(change.language, Some("python".to_string()));
assert_eq!(change.infer_language(), "python");
}
#[test]
fn test_resolved_infer_language() {
let cases = [
("test.py", "python"),
("test.js", "javascript"),
("test.mjs", "javascript"),
("test.MJS", "javascript"),
("test.cjs", "javascript"),
("test.ts", "typescript"),
("test.mts", "typescript"),
("test.MTS", "typescript"),
("test.cts", "typescript"),
("test.go", "go"),
("test.rs", "rust"),
("test.java", "java"),
("test.cpp", "cpp"),
("test.rb", "ruby"),
("test.php", "php"),
("test.lua", "lua"),
("test.scala", "scala"),
("test.c", "c"),
("test.sh", "bash"),
("test.json", "json"),
];
for (file, expected_lang) in cases {
let change = ResolvedEditChange::insert(PathBuf::from(file), "content".to_string());
assert_eq!(
change.infer_language(),
expected_lang,
"Failed for {}",
file
);
}
}
#[test]
fn test_resolved_extension() {
let change = ResolvedEditChange::insert(PathBuf::from("test.py"), "content".to_string());
assert_eq!(change.extension(), Some("py"));
}
#[test]
fn test_edit_type_equality() {
assert_eq!(EditType::Insert, EditType::Insert);
assert_ne!(EditType::Insert, EditType::Delete);
}
}