use std::io::{Read, Write};
use std::time::Instant;
use anyhow::Result;
use serde::Deserialize;
use crate::atomic::{AtomicWriteOptions, atomic_write};
use crate::cli::FuzzyMode;
use crate::cli::{EditLoopArgs, GlobalArgs};
use crate::commands::resolve_backup;
use crate::fuzzy;
use crate::ndjson_types::{EditLoopPairResult, EditLoopSummary};
use crate::output::NdjsonWriter;
use crate::path_safety::validate_path;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct EditPair {
old: String,
new: String,
}
#[tracing::instrument(skip_all, fields(command = "edit-loop"))]
pub fn cmd_edit_loop(
args: &EditLoopArgs,
global: &GlobalArgs,
stdin: impl Read,
writer: &mut NdjsonWriter<impl Write>,
defaults: &crate::config::DefaultsSection,
fuzzy_cfg: &crate::config::FuzzySection,
) -> Result<()> {
let start = Instant::now();
let workspace = global.resolve_workspace()?;
let target = validate_path(&args.path, &workspace)?;
let resolved_backup = resolve_backup(&args.backup_opts, defaults);
let (fuzzy_mode, fuzzy_threshold) =
crate::config::resolve_fuzzy(FuzzyMode::Auto, None, fuzzy_cfg)?;
if !target.exists() {
return Err(crate::error::AtomwriteError::NotFound {
path: target.clone(),
}
.into());
}
let max_size = global.effective_max_filesize();
let mut content = crate::file_io::read_file_string(&target, max_size)?;
let mut buf = String::new();
stdin.take(max_size).read_to_string(&mut buf)?;
if buf.trim().is_empty() {
return Err(crate::error::AtomwriteError::InvalidInput {
reason: "stdin is empty; expected JSON array or NDJSON pairs".to_string(),
}
.into());
}
let trimmed = buf.trim_start();
let pairs: Vec<EditPair> = if trimmed.starts_with('[') {
serde_json::from_str::<Vec<EditPair>>(trimmed).map_err(|e| {
crate::error::AtomwriteError::InvalidInput {
reason: format!("failed to parse JSON array of edit pairs: {e}"),
}
})?
} else {
buf.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| {
serde_json::from_str::<EditPair>(l).map_err(|e| {
crate::error::AtomwriteError::InvalidInput {
reason: format!("failed to parse NDJSON pair: {l}: {e}"),
}
})
})
.collect::<std::result::Result<Vec<_>, _>>()?
};
let mut pair_results: Vec<EditLoopPairResult> = Vec::with_capacity(pairs.len());
let mut applied = 0usize;
let mut unmatched = 0usize;
for (i, pair) in pairs.iter().enumerate() {
match fuzzy::match_pair(
&content,
&pair.old,
&pair.new,
fuzzy_mode,
fuzzy_threshold,
) {
Ok((edited, _info)) => {
content = edited;
applied += 1;
pair_results.push(EditLoopPairResult {
index: i + 1,
matched: true,
old: pair.old.clone(),
new: pair.new.clone(),
});
}
Err(_) => {
unmatched += 1;
pair_results.push(EditLoopPairResult {
index: i + 1,
matched: false,
old: pair.old.clone(),
new: pair.new.clone(),
});
}
}
}
{
use crate::line_endings::{self, LineEnding};
let target_le = match args.line_ending {
LineEnding::Auto => line_endings::detect(content.as_bytes()),
other => other,
};
content = line_endings::normalize(&content, target_le);
}
let opts = AtomicWriteOptions {
backup: resolved_backup.backup,
syntax_check: args.syntax_check.is_some(),
retention: resolved_backup.retention,
preserve_timestamps: false,
backup_output_dir: None,
strategy: None,
strict_atomic: false,
wal_policy: crate::wal::WalPolicy::Auto,
keep_backup: resolved_backup.keep,
durability: crate::platform::Durability::Auto,
};
atomic_write(&target, content.as_bytes(), &opts, &workspace)?;
writer.write_event(&EditLoopSummary {
r#type: "result",
action: "edit_loop".to_string(),
path: target.display().to_string(),
pairs_total: pairs.len(),
pairs_applied: applied,
pairs_unmatched: unmatched,
elapsed_ms: start.elapsed().as_millis() as u64,
pair_results,
})?;
Ok(())
}