use crate::{CommentKind, Document, EditError, Expr, Step, Value, eval};
pub fn apply_comment_mutation(
doc: &mut dyn Document,
expr: &Expr,
) -> Result<Vec<String>, EditError> {
let mut warnings = Vec::new();
apply_inner(doc, expr, &mut warnings)?;
Ok(warnings)
}
fn apply_inner(
doc: &mut dyn Document,
expr: &Expr,
warnings: &mut Vec<String>,
) -> Result<(), EditError> {
match expr {
Expr::Pipe(a, b) => {
apply_inner(doc, a, warnings)?;
apply_inner(doc, b, warnings)
}
Expr::UpdateAssign(lhs, rhs) | Expr::AddAssign(lhs, rhs) if is_comments_stream(lhs) => {
let append = matches!(expr, Expr::AddAssign(..));
bulk_edit(doc, rhs, append, warnings)
}
Expr::Assign(lhs, rhs) => {
let (prefix, kind) = comment_target(lhs)?;
let text = eval_text(rhs, &doc.to_value())?;
warnings.extend(doc.set_comment(prefix, kind, &text)?);
Ok(())
}
Expr::UpdateAssign(lhs, rhs) => {
let (prefix, kind) = comment_target(lhs)?;
let current = current_comment(doc, prefix, kind).unwrap_or_default();
let text = eval_text(rhs, &Value::Str(current))?;
warnings.extend(doc.set_comment(prefix, kind, &text)?);
Ok(())
}
Expr::AddAssign(lhs, rhs) => {
let (prefix, kind) = comment_target(lhs)?;
let mut text = current_comment(doc, prefix, kind).unwrap_or_default();
text.push_str(&eval_text(rhs, &doc.to_value())?);
warnings.extend(doc.set_comment(prefix, kind, &text)?);
Ok(())
}
Expr::Call(name, args) if name == "del" => {
if args.len() != 1 {
return Err(EditError::new("del(...) takes one argument"));
}
if is_comments_stream(&args[0]) {
return bulk_delete(doc);
}
let steps = args[0]
.as_path()
.ok_or_else(|| EditError::new("del(...) takes a path"))?;
let (prefix, kind) = split_comment(steps)?;
doc.delete_comment(prefix, kind)
}
_ => Err(EditError::new(
"unsupported comment edit - use `.path.# = ...`, `|=`, `+=`, `del(.path.#)`, \
or the bulk `comments |= ...` / `del(comments)`",
)),
}
}
fn is_comments_stream(expr: &Expr) -> bool {
matches!(expr, Expr::Call(name, args) if name == "comments" && args.is_empty())
}
fn bulk_edit(
doc: &mut dyn Document,
rhs: &Expr,
append: bool,
warnings: &mut Vec<String>,
) -> Result<(), EditError> {
let targets = doc
.to_commented()
.ok_or_else(|| EditError::new("this format has no comments"))?
.comment_targets();
for (steps, kind, current) in targets {
let text = if append {
let mut t = current;
t.push_str(&eval_text(rhs, &doc.to_value())?);
t
} else {
eval_text(rhs, &Value::Str(current))?
};
warnings.extend(doc.set_comment(&steps, kind, &text)?);
}
Ok(())
}
fn bulk_delete(doc: &mut dyn Document) -> Result<(), EditError> {
let targets = doc
.to_commented()
.ok_or_else(|| EditError::new("this format has no comments"))?
.comment_targets();
for (steps, kind, _) in targets {
doc.delete_comment(&steps, kind)?;
}
Ok(())
}
fn comment_target(lhs: &Expr) -> Result<(&[Step], CommentKind), EditError> {
let steps = lhs
.as_path()
.ok_or_else(|| EditError::new("left side of a comment assignment must be a path"))?;
split_comment(steps)
}
fn split_comment(steps: &[Step]) -> Result<(&[Step], CommentKind), EditError> {
match steps.split_last() {
Some((Step::Comment(kind), prefix)) => Ok((prefix, *kind)),
_ => Err(EditError::new("expected a comment path ending in `#`")),
}
}
fn eval_text(rhs: &Expr, input: &Value) -> Result<String, EditError> {
let v = eval(rhs, input)
.map_err(|e| EditError::new(e.to_string()))?
.into_iter()
.next()
.ok_or_else(|| EditError::new("the comment text expression produced no value"))?;
match v {
Value::Array(_) | Value::Object(_) => {
Err(EditError::new("a comment is text, not a container"))
}
scalar => Ok(scalar.to_raw_string()),
}
}
pub fn place_line_comment(
source: &str,
target_line: usize,
is_head: bool,
indent: &str,
delim: &str,
is_comment_line: &dyn Fn(&str) -> bool,
wrapped: Option<&[String]>,
) -> String {
let mut lines: Vec<String> = source.split_inclusive('\n').map(str::to_string).collect();
if target_line >= lines.len() {
return source.to_string();
}
let bare = |s: &str| s.trim_end_matches(['\n', '\r']).to_string();
let block: Vec<String> = wrapped
.into_iter()
.flatten()
.map(|l| format!("{indent}{delim}{l}\n"))
.collect();
if is_head {
let mut start = target_line;
while start > 0 && is_comment_line(&bare(&lines[start - 1])) {
start -= 1;
}
lines.splice(start..target_line, block);
} else {
let mut end = target_line + 1;
while end < lines.len() && is_comment_line(&bare(&lines[end])) {
end += 1;
}
lines.splice(target_line + 1..end, block);
}
lines.concat()
}
pub fn line_index(source: &str, at: usize) -> usize {
source[..at.min(source.len())].matches('\n').count()
}
fn current_comment(doc: &dyn Document, prefix: &[Step], kind: CommentKind) -> Option<String> {
let mut path = prefix.to_vec();
path.push(Step::Comment(kind));
let commented = doc.to_commented()?;
match commented.resolve_comment(&path).into_iter().next() {
Some(Value::Str(s)) => Some(s),
_ => None,
}
}