use std::path::Path;
use crate::containment::PathGuard;
use crate::plan::Operation;
use super::{ApplyMode, ContentEditResult, EditResult, ReplaceOptions};
pub fn replace_text(
path: &Path,
from: &str,
to: &str,
opts: &ReplaceOptions,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
if from.is_empty() && !opts.regex {
anyhow::bail!("empty search pattern");
}
if opts.range.is_some() && !opts.whole_line {
anyhow::bail!("range requires whole_line to be true");
}
if opts.whole_line && opts.multiline {
anyhow::bail!("whole_line and multiline cannot be combined");
}
let range_str = opts.range.map(|(start, end)| {
if let Some(e) = end {
format!("{start}:{e}")
} else {
format!("{start}:")
}
});
let op = Operation::Replace {
glob: None,
path: Some(path.to_string_lossy().into()),
regex: opts.regex,
old: from.into(),
new_text: Some(to.into()),
nth: opts.nth,
insert_before: opts.insert_before.clone(),
insert_after: opts.insert_after.clone(),
case_insensitive: opts.case_insensitive,
multiline: opts.multiline,
if_exists: opts.if_exists,
whole_line: opts.whole_line,
range: range_str,
word_boundary: opts.word_boundary,
before_context: opts.before_context.clone(),
after_context: opts.after_context.clone(),
unique: opts.unique,
};
replace_write(op, path, mode, guard)
}
#[cfg(any(feature = "cli", feature = "files"))]
fn replace_write(
op: Operation,
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
let cwd = path.parent().unwrap_or_else(|| Path::new("."));
super::execute_as_edit_result(op, mode, cwd, guard, "replace", None)
}
#[cfg(not(any(feature = "cli", feature = "files")))]
fn replace_write(
_op: Operation,
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
use crate::ops;
use anyhow::{Context, bail};
if let Operation::Replace {
old,
new_text,
regex: regex_mode,
insert_before,
insert_after,
case_insensitive,
multiline,
if_exists,
whole_line,
range,
word_boundary,
nth,
unique,
..
} = _op
{
let path_str = path.to_string_lossy();
let original = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
let is_regex = regex_mode;
if old.is_empty() && !is_regex {
bail!("empty search pattern");
}
let compiled_re = ops::replace::compile_replace_regex(
&old,
is_regex,
case_insensitive,
multiline,
word_boundary,
)?;
let direct_to = if insert_before.is_none() && insert_after.is_none() {
new_text.clone()
} else {
None
};
let replacement = ops::replace::replacement_text(
&old,
&direct_to,
&insert_before,
&insert_after,
compiled_re.is_some(),
is_regex,
);
let parsed_range = range.as_deref().map(|r| {
let parts: Vec<&str> = r.splitn(2, ':').collect();
let start: usize = parts[0].parse().unwrap_or(1);
let end: Option<usize> = parts
.get(1)
.and_then(|s| if s.is_empty() { None } else { s.parse().ok() });
(start, end)
});
let (new_content, count) = if whole_line {
ops::replace::replace_whole_lines(
&original,
&old,
&replacement,
compiled_re.as_ref(),
nth,
parsed_range,
)
} else {
ops::replace::replace_content(&original, &old, &replacement, compiled_re.as_ref(), nth)
};
let new_content = new_content.into_owned();
if unique && count > 1 {
bail!(
"ambiguous match: pattern {:?} matches {} times; provide more context to disambiguate",
old,
count
);
}
if count == 0 && if_exists {
return Ok(super::build_edit_result(
&path_str,
original.clone(),
original,
false,
"replace",
None,
));
}
let policy = crate::write::WritePolicy::default();
let applied = super::write_if_apply(path, &new_content, mode, &policy, guard)?;
let mut result =
super::build_edit_result(&path_str, original, new_content, applied, "replace", None);
result.match_count = count;
Ok(result)
} else {
bail!("expected Replace operation")
}
}
pub fn replace_in_content(
content: &str,
from: &str,
to: &str,
opts: &ReplaceOptions,
) -> anyhow::Result<ContentEditResult> {
use crate::ops;
use anyhow::bail;
let is_regex = opts.regex;
if from.is_empty() && !is_regex {
bail!("empty search pattern");
}
if opts.range.is_some() && !opts.whole_line {
bail!("range requires whole_line to be true");
}
if opts.whole_line && opts.multiline {
bail!("whole_line and multiline cannot be combined");
}
let compiled_re = ops::replace::compile_replace_regex(
from,
is_regex,
opts.case_insensitive,
opts.multiline,
opts.word_boundary,
)?;
let direct_to = if opts.insert_before.is_none() && opts.insert_after.is_none() {
Some(to.to_string())
} else {
None
};
let replacement = ops::replace::replacement_text(
from,
&direct_to,
&opts.insert_before,
&opts.insert_after,
compiled_re.is_some(),
is_regex,
);
let parsed_range = opts.range;
let (new_content, count) = if opts.whole_line {
ops::replace::replace_whole_lines(
content,
from,
&replacement,
compiled_re.as_ref(),
opts.nth,
parsed_range,
)
} else {
ops::replace::replace_content(content, from, &replacement, compiled_re.as_ref(), opts.nth)
};
let new_content = new_content.into_owned();
if opts.unique && count > 1 {
anyhow::bail!(
"ambiguous match: pattern {:?} matches {} times; provide more context to disambiguate",
from,
count
);
}
if count == 0 && opts.fuzzy && !is_regex {
use crate::fallback;
match fallback::resolve_with_fallback(content, from, None, None) {
Ok(anchor) => {
let to_text = if let Some(ib) = &opts.insert_before {
format!("{}{}", ib, anchor.matched_text)
} else if let Some(ia) = &opts.insert_after {
format!("{}{}", anchor.matched_text, ia)
} else {
to.to_string()
};
let fuzzy_content = format!(
"{}{}{}",
&content[..anchor.start_offset],
to_text,
&content[anchor.start_offset + anchor.matched_text.len()..]
);
let diff = super::make_diff("<content>", content, &fuzzy_content);
return Ok(ContentEditResult {
original: content.to_string(),
new_content: fuzzy_content,
diff,
changed: true,
match_count: 1,
});
}
Err(edit_error) => {
if opts.if_exists {
return Ok(ContentEditResult {
original: content.to_string(),
new_content: content.to_string(),
diff: String::new(),
changed: false,
match_count: 0,
});
}
let similar = fallback::find_similar_targets(content, from, 3);
let mut msg = format!("no matches for {:?}", from);
if let Some(suggestion) = &edit_error.suggestion {
msg.push_str(&format!(" (suggestion: {})", suggestion));
}
if !similar.is_empty() {
msg.push_str(&format!(" (did you mean: {}?)", similar.join(", ")));
}
bail!("{msg}");
}
}
}
if count == 0 && opts.if_exists {
return Ok(ContentEditResult {
original: content.to_string(),
new_content: content.to_string(),
diff: String::new(),
changed: false,
match_count: 0,
});
}
let changed = content != new_content;
let diff = if changed {
super::make_diff("<content>", content, &new_content)
} else {
String::new()
};
Ok(ContentEditResult {
original: content.to_string(),
new_content,
diff,
changed,
match_count: count,
})
}