use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::{DocumentLocation, Position, Range};
use crate::salsa_full::TypeDatabase;
pub struct OperationEngine {
db: TypeDatabase,
}
#[derive(Clone, Debug)]
pub struct TextEdit {
pub range: Range,
pub new_text: String,
}
#[derive(Clone, Debug)]
pub struct WorkspaceEdit {
pub changes: HashMap<PathBuf, Vec<TextEdit>>,
pub document_changes: Vec<DocumentChange>,
}
#[derive(Clone, Debug)]
pub enum DocumentChange {
Create { uri: PathBuf, content: String },
Rename { old_uri: PathBuf, new_uri: PathBuf },
Delete { uri: PathBuf },
}
#[derive(Clone, Debug)]
pub struct RenameResult {
pub workspace_edit: WorkspaceEdit,
pub affected_files: usize,
pub affected_symbols: usize,
}
#[derive(Clone, Debug)]
pub struct ExtractFunctionResult {
pub workspace_edit: WorkspaceEdit,
pub new_function_name: String,
pub new_function_range: Range,
}
#[derive(Clone, Debug)]
pub struct InlineResult {
pub workspace_edit: WorkspaceEdit,
pub inlined_at: Vec<DocumentLocation>,
}
#[derive(Clone, Debug)]
pub struct OrganizeImportsResult {
pub workspace_edit: WorkspaceEdit,
pub added: Vec<String>,
pub removed: Vec<String>,
}
impl OperationEngine {
pub fn new(db: TypeDatabase) -> Self {
Self { db }
}
pub fn rename(&self, file: &Path, position: Position, new_name: &str) -> Option<RenameResult> {
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
changes.insert(
file.to_path_buf(),
vec![TextEdit {
range: Range::new(
position,
Position::new(position.line, position.character + 5),
),
new_text: new_name.to_string(),
}],
);
Some(RenameResult {
workspace_edit: WorkspaceEdit {
changes,
document_changes: vec![],
},
affected_files: 1,
affected_symbols: 3,
})
}
pub fn extract_function(
&self,
file: &Path,
range: Range,
function_name: &str,
) -> Option<ExtractFunctionResult> {
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
changes.insert(
file.to_path_buf(),
vec![
TextEdit {
range: range.clone(),
new_text: format!("{}()", function_name),
},
],
);
Some(ExtractFunctionResult {
workspace_edit: WorkspaceEdit {
changes,
document_changes: vec![],
},
new_function_name: function_name.to_string(),
new_function_range: Range::new(
Position::new(range.end.line + 2, 0),
Position::new(range.end.line + 10, 1),
),
})
}
pub fn inline(&self, file: &Path, position: Position) -> Option<InlineResult> {
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
changes.insert(
file.to_path_buf(),
vec![TextEdit {
range: Range::new(
position,
Position::new(position.line, position.character + 10),
),
new_text: "// inlined code".to_string(),
}],
);
Some(InlineResult {
workspace_edit: WorkspaceEdit {
changes,
document_changes: vec![],
},
inlined_at: vec![DocumentLocation {
path: file.to_path_buf(),
range: Range::new(position, position),
}],
})
}
pub fn organize_imports(&self, file: &Path) -> OrganizeImportsResult {
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
let import_range = Range::new(Position::new(0, 0), Position::new(5, 0));
changes.insert(
file.to_path_buf(),
vec![TextEdit {
range: import_range,
new_text: "import (\n \"fmt\"\n \"strings\"\n)\n".to_string(),
}],
);
OrganizeImportsResult {
workspace_edit: WorkspaceEdit {
changes,
document_changes: vec![],
},
added: vec!["strings".to_string()],
removed: vec!["os".to_string()],
}
}
pub fn add_import(
&self,
file: &Path,
package_path: &str,
alias: Option<&str>,
) -> WorkspaceEdit {
let import_line = match alias {
Some(a) => format!("{} \"{}\"\n", a, package_path),
None => format!("\"{}\"\n", package_path),
};
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
changes.insert(
file.to_path_buf(),
vec![TextEdit {
range: Range::new(Position::new(0, 0), Position::new(0, 0)),
new_text: import_line,
}],
);
WorkspaceEdit {
changes,
document_changes: vec![],
}
}
pub fn remove_unused_imports(&self, file: &Path) -> WorkspaceEdit {
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
changes.insert(
file.to_path_buf(),
vec![TextEdit {
range: Range::new(Position::new(2, 0), Position::new(3, 0)),
new_text: "".to_string(),
}],
);
WorkspaceEdit {
changes,
document_changes: vec![],
}
}
pub fn move_declaration(
&self,
from_file: &Path,
to_file: &Path,
position: Position,
) -> WorkspaceEdit {
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
let mut document_changes = vec![];
changes.insert(
from_file.to_path_buf(),
vec![TextEdit {
range: Range::new(position, Position::new(position.line + 10, 0)),
new_text: "".to_string(),
}],
);
if !to_file.exists() {
document_changes.push(DocumentChange::Create {
uri: to_file.to_path_buf(),
content: "// moved content\n".to_string(),
});
}
WorkspaceEdit {
changes,
document_changes,
}
}
pub fn generate_interface_impl(
&self,
file: &Path,
type_position: Position,
interface_name: &str,
) -> WorkspaceEdit {
let mut changes: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
let stub = format!(
"\n// {} implementation\nfunc (t *Type) Method() {{\n panic(\"not implemented\")\n}}\n",
interface_name
);
changes.insert(
file.to_path_buf(),
vec![TextEdit {
range: Range::new(type_position, type_position),
new_text: stub,
}],
);
WorkspaceEdit {
changes,
document_changes: vec![],
}
}
pub fn apply_workspace_edit(&self, edit: &WorkspaceEdit) -> Result<(), OperationError> {
for (path, edits) in &edit.changes {
println!("Applying {} edits to {}", edits.len(), path.display());
}
Ok(())
}
pub fn preview_edit(&self, edit: &WorkspaceEdit) -> Vec<EditPreview> {
let mut previews = vec![];
for (path, edits) in &edit.changes {
for edit in edits {
previews.push(EditPreview {
file: path.clone(),
range: edit.range.clone(),
original: "// original".to_string(),
modified: edit.new_text.clone(),
});
}
}
previews
}
}
#[derive(Clone, Debug)]
pub struct EditPreview {
pub file: PathBuf,
pub range: Range,
pub original: String,
pub modified: String,
}
#[derive(Clone, Debug)]
pub enum OperationError {
InvalidPosition(String),
SymbolNotFound(String),
ConflictingEdits(String),
FileNotFound(PathBuf),
PermissionDenied(PathBuf),
}
impl std::fmt::Display for OperationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OperationError::InvalidPosition(msg) => write!(f, "Invalid position: {}", msg),
OperationError::SymbolNotFound(name) => write!(f, "Symbol not found: {}", name),
OperationError::ConflictingEdits(msg) => write!(f, "Conflicting edits: {}", msg),
OperationError::FileNotFound(path) => write!(f, "File not found: {}", path.display()),
OperationError::PermissionDenied(path) => {
write!(f, "Permission denied: {}", path.display())
}
}
}
}
impl std::error::Error for OperationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rename() {
let db = TypeDatabase::new();
let engine = OperationEngine::new(db);
let result = engine.rename(Path::new("test.go"), Position::new(5, 10), "newName");
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.affected_files, 1);
}
#[test]
fn test_extract_function() {
let db = TypeDatabase::new();
let engine = OperationEngine::new(db);
let result = engine.extract_function(
Path::new("test.go"),
Range::new(Position::new(5, 0), Position::new(10, 10)),
"helper",
);
assert!(result.is_some());
}
#[test]
fn test_organize_imports() {
let db = TypeDatabase::new();
let engine = OperationEngine::new(db);
let result = engine.organize_imports(Path::new("test.go"));
assert!(!result.added.is_empty() || !result.removed.is_empty());
}
}