use anyhow::{Result, Context, bail};
use clap::{Args, Subcommand};
use std::path::PathBuf;
use std::sync::Arc;
use qdrant_client::Qdrant;
use crate::config::AppConfig;
use crate::cli::commands::CliArgs; use crate::edit::engine::{self, EditTarget, EngineValidationSeverity, EngineEditOptions};
#[derive(Args, Debug, Clone)]
pub struct EditArgs {
#[command(subcommand)]
pub command: EditCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum EditCommand {
Apply(ApplyArgs),
Validate(ValidateArgs),
}
#[derive(Args, Debug, Clone)]
pub struct ApplyArgs {
#[arg(long, required = true)]
pub file: PathBuf,
#[arg(long)]
pub line_start: Option<usize>,
#[arg(long)]
pub line_end: Option<usize>,
#[arg(long)]
pub element: Option<String>,
#[arg(long)]
pub content_file: Option<PathBuf>,
#[arg(long)]
pub content: Option<String>,
#[arg(long, default_value_t = false)]
pub format: bool,
#[arg(long, default_value_t = false)]
pub update_references: bool,
}
#[derive(Args, Debug, Clone)]
pub struct ValidateArgs {
#[arg(long, required = true)]
pub file: PathBuf,
#[arg(long)]
pub line_start: Option<usize>,
#[arg(long)]
pub line_end: Option<usize>,
#[arg(long)]
pub element: Option<String>,
#[arg(long)]
pub content_file: Option<PathBuf>,
#[arg(long)]
pub content: Option<String>,
#[arg(long, default_value_t = false)]
pub format: bool,
#[arg(long, default_value_t = false)]
pub update_references: bool,
}
pub async fn handle_edit_command(
args: EditArgs,
_global_args: &CliArgs, _config: AppConfig, _client: Arc<Qdrant>, ) -> Result<()> {
match args.command {
EditCommand::Apply(apply_args) => handle_apply(apply_args).await,
EditCommand::Validate(validate_args) => handle_validate(validate_args).await,
}
}
async fn handle_apply(args: ApplyArgs) -> Result<()> {
println!("Applying edit to file: {:?}", args.file);
let target = determine_target(&args.line_start, &args.line_end, &args.element)?;
let new_content = get_content(&args.content_file, &args.content)?;
let options = EngineEditOptions {
format_code: args.format,
update_references: args.update_references,
};
engine::apply_edit(&args.file, &target, &new_content, Some(&options))
.with_context(|| format!("Failed to apply edit to file: {:?}", args.file))?;
println!("Edit applied successfully.");
Ok(())
}
async fn handle_validate(args: ValidateArgs) -> Result<()> {
println!("Validating edit for file: {:?}", args.file);
let target = determine_target(&args.line_start, &args.line_end, &args.element)?;
let new_content = get_content(&args.content_file, &args.content)?;
let options = EngineEditOptions {
format_code: args.format,
update_references: args.update_references,
};
match engine::validate_edit(&args.file, &target, &new_content, Some(&options)) {
Ok(issues) => {
if issues.is_empty() {
println!("Validation successful (basic checks passed).");
} else {
println!("Validation finished with the following issues:");
let mut has_errors = false;
for issue in issues {
let severity_str = match issue.severity {
EngineValidationSeverity::Error => { has_errors = true; "ERROR" },
EngineValidationSeverity::Warning => "WARNING",
EngineValidationSeverity::Info => "INFO",
};
if let Some(line_num) = issue.line_number {
println!("- {}: [Line {}] {}", severity_str, line_num, issue.message);
} else {
println!("- {}: {}", severity_str, issue.message);
}
}
if has_errors {
anyhow::bail!("Validation failed due to one or more errors.");
}
}
Ok(())
}
Err(e) => {
eprintln!("Validation engine error: {}", e);
anyhow::bail!("Validation failed due to an internal error: {}", e)
}
}
}
fn determine_target(line_start: &Option<usize>, line_end: &Option<usize>, element: &Option<String>) -> Result<EditTarget> {
match (line_start, line_end, element) {
(Some(start), Some(end), None) => {
if *start == 0 || *end == 0 {
anyhow::bail!("Line numbers must be 1-based.");
}
if *start > *end {
anyhow::bail!("Start line ({}) cannot be greater than end line ({}).", start, end);
}
Ok(EditTarget::LineRange { start: *start, end: *end })
}
(Some(start), None, None) => {
anyhow::bail!("Missing --line-end argument, required when using --line-start.");
}
(None, None, Some(el)) => {
if el.is_empty() {
bail!("Semantic element query (--element) cannot be empty.");
}
Ok(EditTarget::Semantic { element_query: el.clone() })
}
(None, Some(_), None) => {
anyhow::bail!("Missing --line-start argument, required when using --line-end.");
}
_ => {
anyhow::bail!("Invalid target specification. Provide either (--line-start and --line-end) or --element.")
}
}
}
fn get_content(content_file: &Option<PathBuf>, content: &Option<String>) -> Result<String> {
match (content_file, content) {
(Some(path), None) => std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read content file '{:?}': {}", path, e)),
(None, Some(text)) => Ok(text.clone()),
_ => {
anyhow::bail!("Invalid content specification. Use either --content-file or --content.")
}
}
}