1use anyhow::Result;
   2use serde::{Serialize, de::DeserializeOwned};
   3use serde_json::Value;
   4use std::{ops::Range, sync::LazyLock};
   5use tree_sitter::{Query, StreamingIterator as _};
   6use util::RangeExt;
   7
   8pub fn update_value_in_json_text<'a>(
   9    text: &mut String,
  10    key_path: &mut Vec<&'a str>,
  11    tab_size: usize,
  12    old_value: &'a Value,
  13    new_value: &'a Value,
  14    edits: &mut Vec<(Range<usize>, String)>,
  15) {
  16    // If the old and new values are both objects, then compare them key by key,
  17    // preserving the comments and formatting of the unchanged parts. Otherwise,
  18    // replace the old value with the new value.
  19    if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
  20        for (key, old_sub_value) in old_object.iter() {
  21            key_path.push(key);
  22            if let Some(new_sub_value) = new_object.get(key) {
  23                // Key exists in both old and new, recursively update
  24                update_value_in_json_text(
  25                    text,
  26                    key_path,
  27                    tab_size,
  28                    old_sub_value,
  29                    new_sub_value,
  30                    edits,
  31                );
  32            } else {
  33                // Key was removed from new object, remove the entire key-value pair
  34                let (range, replacement) =
  35                    replace_value_in_json_text(text, key_path, 0, None, None);
  36                text.replace_range(range.clone(), &replacement);
  37                edits.push((range, replacement));
  38            }
  39            key_path.pop();
  40        }
  41        for (key, new_sub_value) in new_object.iter() {
  42            key_path.push(key);
  43            if !old_object.contains_key(key) {
  44                update_value_in_json_text(
  45                    text,
  46                    key_path,
  47                    tab_size,
  48                    &Value::Null,
  49                    new_sub_value,
  50                    edits,
  51                );
  52            }
  53            key_path.pop();
  54        }
  55    } else if old_value != new_value {
  56        let mut new_value = new_value.clone();
  57        if let Some(new_object) = new_value.as_object_mut() {
  58            new_object.retain(|_, v| !v.is_null());
  59        }
  60        let (range, replacement) =
  61            replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None);
  62        text.replace_range(range.clone(), &replacement);
  63        edits.push((range, replacement));
  64    }
  65}
  66
  67/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`.
  68pub fn replace_value_in_json_text<T: AsRef<str>>(
  69    text: &str,
  70    key_path: &[T],
  71    tab_size: usize,
  72    new_value: Option<&Value>,
  73    replace_key: Option<&str>,
  74) -> (Range<usize>, String) {
  75    static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
  76        Query::new(
  77            &tree_sitter_json::LANGUAGE.into(),
  78            "(pair key: (string) @key value: (_) @value)",
  79        )
  80        .expect("Failed to create PAIR_QUERY")
  81    });
  82
  83    let mut parser = tree_sitter::Parser::new();
  84    parser
  85        .set_language(&tree_sitter_json::LANGUAGE.into())
  86        .unwrap();
  87    let syntax_tree = parser.parse(text, None).unwrap();
  88
  89    let mut cursor = tree_sitter::QueryCursor::new();
  90
  91    let mut depth = 0;
  92    let mut last_value_range = 0..0;
  93    let mut first_key_start = None;
  94    let mut existing_value_range = 0..text.len();
  95
  96    let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
  97    while let Some(mat) = matches.next() {
  98        if mat.captures.len() != 2 {
  99            continue;
 100        }
 101
 102        let key_range = mat.captures[0].node.byte_range();
 103        let value_range = mat.captures[1].node.byte_range();
 104
 105        // Don't enter sub objects until we find an exact
 106        // match for the current keypath
 107        if last_value_range.contains_inclusive(&value_range) {
 108            continue;
 109        }
 110
 111        last_value_range = value_range.clone();
 112
 113        if key_range.start > existing_value_range.end {
 114            break;
 115        }
 116
 117        first_key_start.get_or_insert(key_range.start);
 118
 119        let found_key = text
 120            .get(key_range.clone())
 121            .zip(key_path.get(depth))
 122            .and_then(|(key_text, key_path_value)| {
 123                serde_json::to_string(key_path_value.as_ref())
 124                    .ok()
 125                    .map(|key_path| depth < key_path.len() && key_text == key_path)
 126            })
 127            .unwrap_or(false);
 128
 129        if found_key {
 130            existing_value_range = value_range;
 131            // Reset last value range when increasing in depth
 132            last_value_range = existing_value_range.start..existing_value_range.start;
 133            depth += 1;
 134
 135            if depth == key_path.len() {
 136                break;
 137            }
 138
 139            if let Some(array_replacement) = handle_possible_array_value(
 140                &mat.captures[0].node,
 141                &mat.captures[1].node,
 142                text,
 143                &key_path[depth..],
 144                new_value,
 145                replace_key,
 146                tab_size,
 147            ) {
 148                return array_replacement;
 149            }
 150
 151            first_key_start = None;
 152        }
 153    }
 154
 155    // We found the exact key we want
 156    if depth == key_path.len() {
 157        if let Some(new_value) = new_value {
 158            let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
 159            if let Some(replace_key) = replace_key.and_then(|str| serde_json::to_string(str).ok()) {
 160                let new_key = format!("{}: ", replace_key);
 161                if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
 162                    if let Some(prev_key_start) = text[..key_start].rfind('"') {
 163                        existing_value_range.start = prev_key_start;
 164                    } else {
 165                        existing_value_range.start = key_start;
 166                    }
 167                }
 168                (existing_value_range, new_key + &new_val)
 169            } else {
 170                (existing_value_range, new_val)
 171            }
 172        } else {
 173            let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
 174            let mut removal_end = existing_value_range.end;
 175
 176            // Find the actual key position by looking for the key in the pair
 177            // We need to extend the range to include the key, not just the value
 178            if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
 179                if let Some(prev_key_start) = text[..key_start].rfind('"') {
 180                    removal_start = prev_key_start;
 181                } else {
 182                    removal_start = key_start;
 183                }
 184            }
 185
 186            let mut removed_comma = false;
 187            // Look backward for a preceding comma first
 188            let preceding_text = text.get(0..removal_start).unwrap_or("");
 189            if let Some(comma_pos) = preceding_text.rfind(',') {
 190                // Check if there are only whitespace characters between the comma and our key
 191                let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
 192                if between_comma_and_key.trim().is_empty() {
 193                    removal_start = comma_pos;
 194                    removed_comma = true;
 195                }
 196            }
 197            if let Some(remaining_text) = text.get(existing_value_range.end..)
 198                && !removed_comma
 199            {
 200                let mut chars = remaining_text.char_indices();
 201                while let Some((offset, ch)) = chars.next() {
 202                    if ch == ',' {
 203                        removal_end = existing_value_range.end + offset + 1;
 204                        // Also consume whitespace after the comma
 205                        for (_, next_ch) in chars.by_ref() {
 206                            if next_ch.is_whitespace() {
 207                                removal_end += next_ch.len_utf8();
 208                            } else {
 209                                break;
 210                            }
 211                        }
 212                        break;
 213                    } else if !ch.is_whitespace() {
 214                        break;
 215                    }
 216                }
 217            }
 218            (removal_start..removal_end, String::new())
 219        }
 220    } else {
 221        if let Some(first_key_start) = first_key_start {
 222            // We have key paths, construct the sub objects
 223            let new_key = key_path[depth].as_ref();
 224            // We don't have the key, construct the nested objects
 225            let new_value = construct_json_value(&key_path[(depth + 1)..], new_value);
 226
 227            let mut row = 0;
 228            let mut column = 0;
 229            for (ix, char) in text.char_indices() {
 230                if ix == first_key_start {
 231                    break;
 232                }
 233                if char == '\n' {
 234                    row += 1;
 235                    column = 0;
 236                } else {
 237                    column += char.len_utf8();
 238                }
 239            }
 240
 241            if row > 0 {
 242                // depth is 0 based, but division needs to be 1 based.
 243                let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
 244                let space = ' ';
 245                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
 246                (first_key_start..first_key_start, content)
 247            } else {
 248                let new_val = serde_json::to_string(&new_value).unwrap();
 249                let mut content = format!(r#""{new_key}": {new_val},"#);
 250                content.push(' ');
 251                (first_key_start..first_key_start, content)
 252            }
 253        } else {
 254            // We don't have the key, construct the nested objects
 255            let new_value = construct_json_value(&key_path[depth..], new_value);
 256            let indent_prefix_len = tab_size * depth;
 257            let mut new_val = to_pretty_json(&new_value, tab_size, indent_prefix_len);
 258            if depth == 0 {
 259                new_val.push('\n');
 260            }
 261            // best effort to keep comments with best effort indentation
 262            let mut replace_text = &text[existing_value_range.clone()];
 263            while let Some(comment_start) = replace_text.rfind("//") {
 264                if let Some(comment_end) = replace_text[comment_start..].find('\n') {
 265                    let mut comment_with_indent_start = replace_text[..comment_start]
 266                        .rfind('\n')
 267                        .unwrap_or(comment_start);
 268                    if !replace_text[comment_with_indent_start..comment_start]
 269                        .trim()
 270                        .is_empty()
 271                    {
 272                        comment_with_indent_start = comment_start;
 273                    }
 274                    new_val.insert_str(
 275                        1,
 276                        &replace_text[comment_with_indent_start..comment_start + comment_end],
 277                    );
 278                }
 279                replace_text = &replace_text[..comment_start];
 280            }
 281
 282            (existing_value_range, new_val)
 283        }
 284    }
 285}
 286
 287fn construct_json_value(
 288    key_path: &[impl AsRef<str>],
 289    new_value: Option<&serde_json::Value>,
 290) -> serde_json::Value {
 291    let mut new_value =
 292        serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
 293    for key in key_path.iter().rev() {
 294        if parse_index_key(key.as_ref()).is_some() {
 295            new_value = serde_json::json!([new_value]);
 296        } else {
 297            new_value = serde_json::json!({ key.as_ref().to_string(): new_value });
 298        }
 299    }
 300    return new_value;
 301}
 302
 303fn parse_index_key(index_key: &str) -> Option<usize> {
 304    index_key.strip_prefix('#')?.parse().ok()
 305}
 306
 307fn handle_possible_array_value(
 308    key_node: &tree_sitter::Node,
 309    value_node: &tree_sitter::Node,
 310    text: &str,
 311    remaining_key_path: &[impl AsRef<str>],
 312    new_value: Option<&Value>,
 313    replace_key: Option<&str>,
 314    tab_size: usize,
 315) -> Option<(Range<usize>, String)> {
 316    if remaining_key_path.is_empty() {
 317        return None;
 318    }
 319    let key_path = remaining_key_path;
 320    let index = parse_index_key(key_path[0].as_ref())?;
 321
 322    let value_is_array = value_node.kind() == TS_ARRAY_KIND;
 323
 324    let array_str = if value_is_array {
 325        &text[value_node.byte_range()]
 326    } else {
 327        ""
 328    };
 329
 330    let (mut replace_range, mut replace_value) = replace_top_level_array_value_in_json_text(
 331        array_str,
 332        &key_path[1..],
 333        new_value,
 334        replace_key,
 335        index,
 336        tab_size,
 337    );
 338
 339    if value_is_array {
 340        replace_range.start += value_node.start_byte();
 341        replace_range.end += value_node.start_byte();
 342    } else {
 343        // replace the full value if it wasn't an array
 344        replace_range = value_node.byte_range();
 345    }
 346    let non_whitespace_char_count = replace_value.len()
 347        - replace_value
 348            .chars()
 349            .filter(char::is_ascii_whitespace)
 350            .count();
 351    let needs_indent = replace_value.ends_with('\n')
 352        || (replace_value
 353            .chars()
 354            .zip(replace_value.chars().skip(1))
 355            .any(|(c, next_c)| c == '\n' && !next_c.is_ascii_whitespace()));
 356    let contains_comment = (replace_value.contains("//") && replace_value.contains('\n'))
 357        || (replace_value.contains("/*") && replace_value.contains("*/"));
 358    if needs_indent {
 359        let indent_width = key_node.start_position().column;
 360        let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
 361        replace_value = replace_value.replace('\n', &increased_indent);
 362    } else if non_whitespace_char_count < 32 && !contains_comment {
 363        // remove indentation
 364        while let Some(idx) = replace_value.find("\n ") {
 365            replace_value.remove(idx);
 366        }
 367        while let Some(idx) = replace_value.find("  ") {
 368            replace_value.remove(idx);
 369        }
 370    }
 371    return Some((replace_range, replace_value));
 372}
 373
 374const TS_DOCUMENT_KIND: &str = "document";
 375const TS_ARRAY_KIND: &str = "array";
 376const TS_COMMENT_KIND: &str = "comment";
 377
 378pub fn replace_top_level_array_value_in_json_text(
 379    text: &str,
 380    key_path: &[impl AsRef<str>],
 381    new_value: Option<&Value>,
 382    replace_key: Option<&str>,
 383    array_index: usize,
 384    tab_size: usize,
 385) -> (Range<usize>, String) {
 386    let mut parser = tree_sitter::Parser::new();
 387    parser
 388        .set_language(&tree_sitter_json::LANGUAGE.into())
 389        .unwrap();
 390
 391    let syntax_tree = parser.parse(text, None).unwrap();
 392
 393    let mut cursor = syntax_tree.walk();
 394
 395    if cursor.node().kind() == TS_DOCUMENT_KIND {
 396        cursor.goto_first_child();
 397    }
 398
 399    while cursor.node().kind() != TS_ARRAY_KIND {
 400        if !cursor.goto_next_sibling() {
 401            let json_value = construct_json_value(key_path, new_value);
 402            let json_value = serde_json::json!([json_value]);
 403            return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
 404        }
 405    }
 406
 407    // false if no children
 408    //
 409    cursor.goto_first_child();
 410    debug_assert_eq!(cursor.node().kind(), "[");
 411
 412    let mut index = 0;
 413
 414    while index <= array_index {
 415        let node = cursor.node();
 416        if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",")
 417            && !node.is_extra()
 418            && !node.is_missing()
 419        {
 420            if index == array_index {
 421                break;
 422            }
 423            index += 1;
 424        }
 425        if !cursor.goto_next_sibling() {
 426            if let Some(new_value) = new_value {
 427                return append_top_level_array_value_in_json_text(text, new_value, tab_size);
 428            } else {
 429                return (0..0, String::new());
 430            }
 431        }
 432    }
 433
 434    let range = cursor.node().range();
 435    let indent_width = range.start_point.column;
 436    let offset = range.start_byte;
 437    let text_range = range.start_byte..range.end_byte;
 438    let value_str = &text[text_range.clone()];
 439    let needs_indent = range.start_point.row > 0;
 440
 441    if new_value.is_none() && key_path.is_empty() {
 442        let mut remove_range = text_range;
 443        if index == 0 {
 444            while cursor.goto_next_sibling()
 445                && (cursor.node().is_extra() || cursor.node().is_missing())
 446            {}
 447            if cursor.node().kind() == "," {
 448                remove_range.end = cursor.node().range().end_byte;
 449            }
 450            if let Some(next_newline) = &text[remove_range.end + 1..].find('\n')
 451                && text[remove_range.end + 1..remove_range.end + next_newline]
 452                    .chars()
 453                    .all(|c| c.is_ascii_whitespace())
 454            {
 455                remove_range.end = remove_range.end + next_newline;
 456            }
 457        } else {
 458            while cursor.goto_previous_sibling()
 459                && (cursor.node().is_extra() || cursor.node().is_missing())
 460            {}
 461            if cursor.node().kind() == "," {
 462                remove_range.start = cursor.node().range().start_byte;
 463            }
 464        }
 465        (remove_range, String::new())
 466    } else {
 467        if let Some(array_replacement) = handle_possible_array_value(
 468            &cursor.node(),
 469            &cursor.node(),
 470            text,
 471            key_path,
 472            new_value,
 473            replace_key,
 474            tab_size,
 475        ) {
 476            return array_replacement;
 477        }
 478        let (mut replace_range, mut replace_value) =
 479            replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
 480
 481        replace_range.start += offset;
 482        replace_range.end += offset;
 483
 484        if needs_indent {
 485            let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
 486            replace_value = replace_value.replace('\n', &increased_indent);
 487        } else {
 488            while let Some(idx) = replace_value.find("\n ") {
 489                replace_value.remove(idx + 1);
 490            }
 491            while let Some(idx) = replace_value.find("\n") {
 492                replace_value.replace_range(idx..idx + 1, " ");
 493            }
 494        }
 495
 496        (replace_range, replace_value)
 497    }
 498}
 499
 500pub fn append_top_level_array_value_in_json_text(
 501    text: &str,
 502    new_value: &Value,
 503    tab_size: usize,
 504) -> (Range<usize>, String) {
 505    let mut parser = tree_sitter::Parser::new();
 506    parser
 507        .set_language(&tree_sitter_json::LANGUAGE.into())
 508        .unwrap();
 509    let syntax_tree = parser.parse(text, None).unwrap();
 510
 511    let mut cursor = syntax_tree.walk();
 512
 513    if cursor.node().kind() == TS_DOCUMENT_KIND {
 514        cursor.goto_first_child();
 515    }
 516
 517    while cursor.node().kind() != TS_ARRAY_KIND {
 518        if !cursor.goto_next_sibling() {
 519            let json_value = serde_json::json!([new_value]);
 520            return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
 521        }
 522    }
 523
 524    let went_to_last_child = cursor.goto_last_child();
 525    debug_assert!(
 526        went_to_last_child && cursor.node().kind() == "]",
 527        "Malformed JSON syntax tree, expected `]` at end of array"
 528    );
 529    let close_bracket_start = cursor.node().start_byte();
 530    while cursor.goto_previous_sibling()
 531        && (cursor.node().is_extra() || cursor.node().is_missing())
 532        && !cursor.node().is_error()
 533    {}
 534
 535    let mut comma_range = None;
 536    let mut prev_item_range = None;
 537
 538    if cursor.node().kind() == "," || is_error_of_kind(&mut cursor, ",") {
 539        comma_range = Some(cursor.node().byte_range());
 540        while cursor.goto_previous_sibling()
 541            && (cursor.node().is_extra() || cursor.node().is_missing())
 542        {}
 543
 544        debug_assert_ne!(cursor.node().kind(), "[");
 545        prev_item_range = Some(cursor.node().range());
 546    } else {
 547        while (cursor.node().is_extra() || cursor.node().is_missing())
 548            && cursor.goto_previous_sibling()
 549        {}
 550        if cursor.node().kind() != "[" {
 551            prev_item_range = Some(cursor.node().range());
 552        }
 553    }
 554
 555    let (mut replace_range, mut replace_value) =
 556        replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None);
 557
 558    replace_range.start = close_bracket_start;
 559    replace_range.end = close_bracket_start;
 560
 561    let space = ' ';
 562    if let Some(prev_item_range) = prev_item_range {
 563        let needs_newline = prev_item_range.start_point.row > 0;
 564        let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
 565            prev_item_range.start_point.column,
 566            |idx| {
 567                prev_item_range.start_point.column
 568                    - text[idx + 1..prev_item_range.start_byte].trim_start().len()
 569            },
 570        );
 571
 572        let prev_item_end = comma_range
 573            .as_ref()
 574            .map_or(prev_item_range.end_byte, |range| range.end);
 575        if text[prev_item_end..replace_range.start].trim().is_empty() {
 576            replace_range.start = prev_item_end;
 577        }
 578
 579        if needs_newline {
 580            let increased_indent = format!("\n{space:width$}", width = indent_width);
 581            replace_value = replace_value.replace('\n', &increased_indent);
 582            replace_value.push('\n');
 583            replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
 584        } else {
 585            while let Some(idx) = replace_value.find("\n ") {
 586                replace_value.remove(idx + 1);
 587            }
 588            while let Some(idx) = replace_value.find('\n') {
 589                replace_value.replace_range(idx..idx + 1, " ");
 590            }
 591            replace_value.insert(0, ' ');
 592        }
 593
 594        if comma_range.is_none() {
 595            replace_value.insert(0, ',');
 596        }
 597    } else if replace_value.contains('\n') || text.contains('\n') {
 598        if let Some(prev_newline) = text[..replace_range.start].rfind('\n')
 599            && text[prev_newline..replace_range.start].trim().is_empty()
 600        {
 601            replace_range.start = prev_newline;
 602        }
 603        let indent = format!("\n{space:width$}", width = tab_size);
 604        replace_value = replace_value.replace('\n', &indent);
 605        replace_value.insert_str(0, &indent);
 606        replace_value.push('\n');
 607    }
 608    return (replace_range, replace_value);
 609
 610    fn is_error_of_kind(cursor: &mut tree_sitter::TreeCursor<'_>, kind: &str) -> bool {
 611        if cursor.node().kind() != "ERROR" {
 612            return false;
 613        }
 614
 615        let descendant_index = cursor.descendant_index();
 616        let res = cursor.goto_first_child() && cursor.node().kind() == kind;
 617        cursor.goto_descendant(descendant_index);
 618        res
 619    }
 620}
 621
 622/// Infers the indentation size used in JSON text by analyzing the tree structure.
 623/// Returns the detected indent size, or a default of 2 if no indentation is found.
 624pub fn infer_json_indent_size(text: &str) -> usize {
 625    const MAX_INDENT_SIZE: usize = 64;
 626
 627    let mut parser = tree_sitter::Parser::new();
 628    parser
 629        .set_language(&tree_sitter_json::LANGUAGE.into())
 630        .unwrap();
 631
 632    let Some(syntax_tree) = parser.parse(text, None) else {
 633        return 4;
 634    };
 635
 636    let mut cursor = syntax_tree.walk();
 637    let mut indent_counts = [0u32; MAX_INDENT_SIZE];
 638
 639    // Traverse the tree to find indentation patterns
 640    fn visit_node(
 641        cursor: &mut tree_sitter::TreeCursor,
 642        indent_counts: &mut [u32; MAX_INDENT_SIZE],
 643        depth: usize,
 644    ) {
 645        if depth >= 3 {
 646            return;
 647        }
 648        let node = cursor.node();
 649        let node_kind = node.kind();
 650
 651        // For objects and arrays, check the indentation of their first content child
 652        if matches!(node_kind, "object" | "array") {
 653            let container_column = node.start_position().column;
 654            let container_row = node.start_position().row;
 655
 656            if cursor.goto_first_child() {
 657                // Skip the opening bracket
 658                loop {
 659                    let child = cursor.node();
 660                    let child_kind = child.kind();
 661
 662                    // Look for the first actual content (pair for objects, value for arrays)
 663                    if (node_kind == "object" && child_kind == "pair")
 664                        || (node_kind == "array"
 665                            && !matches!(child_kind, "[" | "]" | "," | "comment"))
 666                    {
 667                        let child_column = child.start_position().column;
 668                        let child_row = child.start_position().row;
 669
 670                        // Only count if the child is on a different line
 671                        if child_row > container_row && child_column > container_column {
 672                            let indent = child_column - container_column;
 673                            if indent > 0 && indent < MAX_INDENT_SIZE {
 674                                indent_counts[indent] += 1;
 675                            }
 676                        }
 677                        break;
 678                    }
 679
 680                    if !cursor.goto_next_sibling() {
 681                        break;
 682                    }
 683                }
 684                cursor.goto_parent();
 685            }
 686        }
 687
 688        // Recurse to children
 689        if cursor.goto_first_child() {
 690            loop {
 691                visit_node(cursor, indent_counts, depth + 1);
 692                if !cursor.goto_next_sibling() {
 693                    break;
 694                }
 695            }
 696            cursor.goto_parent();
 697        }
 698    }
 699
 700    visit_node(&mut cursor, &mut indent_counts, 0);
 701
 702    // Find the indent size with the highest count
 703    let mut max_count = 0;
 704    let mut max_indent = 4;
 705
 706    for (indent, &count) in indent_counts.iter().enumerate() {
 707        if count > max_count {
 708            max_count = count;
 709            max_indent = indent;
 710        }
 711    }
 712
 713    if max_count == 0 { 2 } else { max_indent }
 714}
 715
 716pub fn to_pretty_json(
 717    value: &impl Serialize,
 718    indent_size: usize,
 719    indent_prefix_len: usize,
 720) -> String {
 721    let mut output = Vec::new();
 722    let indent = " ".repeat(indent_size);
 723    let mut ser = serde_json::Serializer::with_formatter(
 724        &mut output,
 725        serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()),
 726    );
 727
 728    value.serialize(&mut ser).unwrap();
 729    let text = String::from_utf8(output).unwrap();
 730
 731    let mut adjusted_text = String::new();
 732    for (i, line) in text.split('\n').enumerate() {
 733        if i > 0 {
 734            adjusted_text.extend(std::iter::repeat(' ').take(indent_prefix_len));
 735        }
 736        adjusted_text.push_str(line);
 737        adjusted_text.push('\n');
 738    }
 739    adjusted_text.pop();
 740    adjusted_text
 741}
 742
 743pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
 744    let mut deserializer = serde_json_lenient::Deserializer::from_str(content);
 745    Ok(serde_path_to_error::deserialize(&mut deserializer)?)
 746}
 747
 748#[cfg(test)]
 749mod tests {
 750    use super::*;
 751    use serde_json::{Value, json};
 752    use unindent::Unindent;
 753
 754    #[test]
 755    fn object_replace() {
 756        #[track_caller]
 757        fn check_object_replace(
 758            input: String,
 759            key_path: &[&str],
 760            value: Option<Value>,
 761            expected: String,
 762        ) {
 763            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
 764            let mut result_str = input;
 765            result_str.replace_range(result.0, &result.1);
 766            pretty_assertions::assert_eq!(expected, result_str);
 767        }
 768        check_object_replace(
 769            r#"{
 770                "a": 1,
 771                "b": 2
 772            }"#
 773            .unindent(),
 774            &["b"],
 775            Some(json!(3)),
 776            r#"{
 777                "a": 1,
 778                "b": 3
 779            }"#
 780            .unindent(),
 781        );
 782        check_object_replace(
 783            r#"{
 784                "a": 1,
 785                "b": 2
 786            }"#
 787            .unindent(),
 788            &["b"],
 789            None,
 790            r#"{
 791                "a": 1
 792            }"#
 793            .unindent(),
 794        );
 795        check_object_replace(
 796            r#"{
 797                "a": 1,
 798                "b": 2
 799            }"#
 800            .unindent(),
 801            &["c"],
 802            Some(json!(3)),
 803            r#"{
 804                "c": 3,
 805                "a": 1,
 806                "b": 2
 807            }"#
 808            .unindent(),
 809        );
 810        check_object_replace(
 811            r#"{
 812                "a": 1,
 813                "b": {
 814                    "c": 2,
 815                    "d": 3,
 816                }
 817            }"#
 818            .unindent(),
 819            &["b", "c"],
 820            Some(json!([1, 2, 3])),
 821            r#"{
 822                "a": 1,
 823                "b": {
 824                    "c": [
 825                        1,
 826                        2,
 827                        3
 828                    ],
 829                    "d": 3,
 830                }
 831            }"#
 832            .unindent(),
 833        );
 834
 835        check_object_replace(
 836            r#"{
 837                "name": "old_name",
 838                "id": 123
 839            }"#
 840            .unindent(),
 841            &["name"],
 842            Some(json!("new_name")),
 843            r#"{
 844                "name": "new_name",
 845                "id": 123
 846            }"#
 847            .unindent(),
 848        );
 849
 850        check_object_replace(
 851            r#"{
 852                "enabled": false,
 853                "count": 5
 854            }"#
 855            .unindent(),
 856            &["enabled"],
 857            Some(json!(true)),
 858            r#"{
 859                "enabled": true,
 860                "count": 5
 861            }"#
 862            .unindent(),
 863        );
 864
 865        check_object_replace(
 866            r#"{
 867                "value": null,
 868                "other": "test"
 869            }"#
 870            .unindent(),
 871            &["value"],
 872            Some(json!(42)),
 873            r#"{
 874                "value": 42,
 875                "other": "test"
 876            }"#
 877            .unindent(),
 878        );
 879
 880        check_object_replace(
 881            r#"{
 882                "config": {
 883                    "old": true
 884                },
 885                "name": "test"
 886            }"#
 887            .unindent(),
 888            &["config"],
 889            Some(json!({"new": false, "count": 3})),
 890            r#"{
 891                "config": {
 892                    "new": false,
 893                    "count": 3
 894                },
 895                "name": "test"
 896            }"#
 897            .unindent(),
 898        );
 899
 900        check_object_replace(
 901            r#"{
 902                // This is a comment
 903                "a": 1,
 904                "b": 2 // Another comment
 905            }"#
 906            .unindent(),
 907            &["b"],
 908            Some(json!({"foo": "bar"})),
 909            r#"{
 910                // This is a comment
 911                "a": 1,
 912                "b": {
 913                    "foo": "bar"
 914                } // Another comment
 915            }"#
 916            .unindent(),
 917        );
 918
 919        check_object_replace(
 920            r#"{}"#.to_string(),
 921            &["new_key"],
 922            Some(json!("value")),
 923            r#"{
 924                "new_key": "value"
 925            }
 926            "#
 927            .unindent(),
 928        );
 929
 930        check_object_replace(
 931            r#"{
 932                "only_key": 123
 933            }"#
 934            .unindent(),
 935            &["only_key"],
 936            None,
 937            "{\n    \n}".to_string(),
 938        );
 939
 940        check_object_replace(
 941            r#"{
 942                "level1": {
 943                    "level2": {
 944                        "level3": {
 945                            "target": "old"
 946                        }
 947                    }
 948                }
 949            }"#
 950            .unindent(),
 951            &["level1", "level2", "level3", "target"],
 952            Some(json!("new")),
 953            r#"{
 954                "level1": {
 955                    "level2": {
 956                        "level3": {
 957                            "target": "new"
 958                        }
 959                    }
 960                }
 961            }"#
 962            .unindent(),
 963        );
 964
 965        check_object_replace(
 966            r#"{
 967                "parent": {}
 968            }"#
 969            .unindent(),
 970            &["parent", "child"],
 971            Some(json!("value")),
 972            r#"{
 973                "parent": {
 974                    "child": "value"
 975                }
 976            }"#
 977            .unindent(),
 978        );
 979
 980        check_object_replace(
 981            r#"{
 982                "a": 1,
 983                "b": 2,
 984            }"#
 985            .unindent(),
 986            &["b"],
 987            Some(json!(3)),
 988            r#"{
 989                "a": 1,
 990                "b": 3,
 991            }"#
 992            .unindent(),
 993        );
 994
 995        check_object_replace(
 996            r#"{
 997                "items": [1, 2, 3],
 998                "count": 3
 999            }"#
1000            .unindent(),
1001            &["items", "1"],
1002            Some(json!(5)),
1003            r#"{
1004                "items": {
1005                    "1": 5
1006                },
1007                "count": 3
1008            }"#
1009            .unindent(),
1010        );
1011
1012        check_object_replace(
1013            r#"{
1014                "items": [1, 2, 3],
1015                "count": 3
1016            }"#
1017            .unindent(),
1018            &["items", "1"],
1019            None,
1020            r#"{
1021                "items": {
1022                    "1": null
1023                },
1024                "count": 3
1025            }"#
1026            .unindent(),
1027        );
1028
1029        check_object_replace(
1030            r#"{
1031                "items": [1, 2, 3],
1032                "count": 3
1033            }"#
1034            .unindent(),
1035            &["items"],
1036            Some(json!(["a", "b", "c", "d"])),
1037            r#"{
1038                "items": [
1039                    "a",
1040                    "b",
1041                    "c",
1042                    "d"
1043                ],
1044                "count": 3
1045            }"#
1046            .unindent(),
1047        );
1048
1049        check_object_replace(
1050            r#"{
1051                "0": "zero",
1052                "1": "one"
1053            }"#
1054            .unindent(),
1055            &["1"],
1056            Some(json!("ONE")),
1057            r#"{
1058                "0": "zero",
1059                "1": "ONE"
1060            }"#
1061            .unindent(),
1062        );
1063        // Test with comments between object members
1064        check_object_replace(
1065            r#"{
1066                "a": 1,
1067                // Comment between members
1068                "b": 2,
1069                /* Block comment */
1070                "c": 3
1071            }"#
1072            .unindent(),
1073            &["b"],
1074            Some(json!({"nested": true})),
1075            r#"{
1076                "a": 1,
1077                // Comment between members
1078                "b": {
1079                    "nested": true
1080                },
1081                /* Block comment */
1082                "c": 3
1083            }"#
1084            .unindent(),
1085        );
1086
1087        // Test with trailing comments on replaced value
1088        check_object_replace(
1089            r#"{
1090                "a": 1, // keep this comment
1091                "b": 2  // this should stay
1092            }"#
1093            .unindent(),
1094            &["a"],
1095            Some(json!("changed")),
1096            r#"{
1097                "a": "changed", // keep this comment
1098                "b": 2  // this should stay
1099            }"#
1100            .unindent(),
1101        );
1102
1103        // Test with deep indentation
1104        check_object_replace(
1105            r#"{
1106                        "deeply": {
1107                                "nested": {
1108                                        "value": "old"
1109                                }
1110                        }
1111                }"#
1112            .unindent(),
1113            &["deeply", "nested", "value"],
1114            Some(json!("new")),
1115            r#"{
1116                        "deeply": {
1117                                "nested": {
1118                                        "value": "new"
1119                                }
1120                        }
1121                }"#
1122            .unindent(),
1123        );
1124
1125        // Test removing value with comment preservation
1126        check_object_replace(
1127            r#"{
1128                // Header comment
1129                "a": 1,
1130                // This comment belongs to b
1131                "b": 2,
1132                // This comment belongs to c
1133                "c": 3
1134            }"#
1135            .unindent(),
1136            &["b"],
1137            None,
1138            r#"{
1139                // Header comment
1140                "a": 1,
1141                // This comment belongs to b
1142                // This comment belongs to c
1143                "c": 3
1144            }"#
1145            .unindent(),
1146        );
1147
1148        // Test with multiline block comments
1149        check_object_replace(
1150            r#"{
1151                /*
1152                 * This is a multiline
1153                 * block comment
1154                 */
1155                "value": "old",
1156                /* Another block */ "other": 123
1157            }"#
1158            .unindent(),
1159            &["value"],
1160            Some(json!("new")),
1161            r#"{
1162                /*
1163                 * This is a multiline
1164                 * block comment
1165                 */
1166                "value": "new",
1167                /* Another block */ "other": 123
1168            }"#
1169            .unindent(),
1170        );
1171
1172        check_object_replace(
1173            r#"{
1174                // This object is empty
1175            }"#
1176            .unindent(),
1177            &["key"],
1178            Some(json!("value")),
1179            r#"{
1180                // This object is empty
1181                "key": "value"
1182            }
1183            "#
1184            .unindent(),
1185        );
1186
1187        // Test replacing in object with only comments
1188        check_object_replace(
1189            r#"{
1190                // Comment 1
1191                // Comment 2
1192            }"#
1193            .unindent(),
1194            &["new"],
1195            Some(json!(42)),
1196            r#"{
1197                // Comment 1
1198                // Comment 2
1199                "new": 42
1200            }
1201            "#
1202            .unindent(),
1203        );
1204
1205        // Test with inconsistent spacing
1206        check_object_replace(
1207            r#"{
1208              "a":1,
1209                    "b"  :  2  ,
1210                "c":   3
1211            }"#
1212            .unindent(),
1213            &["b"],
1214            Some(json!("spaced")),
1215            r#"{
1216              "a":1,
1217                    "b"  :  "spaced"  ,
1218                "c":   3
1219            }"#
1220            .unindent(),
1221        );
1222    }
1223
1224    #[test]
1225    fn object_replace_array() {
1226        // Tests replacing values within arrays that are nested inside objects.
1227        // Uses "#N" syntax in key paths to indicate array indices.
1228        #[track_caller]
1229        fn check_object_replace_array(
1230            input: String,
1231            key_path: &[&str],
1232            value: Option<Value>,
1233            expected: String,
1234        ) {
1235            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
1236            let mut result_str = input;
1237            result_str.replace_range(result.0, &result.1);
1238            pretty_assertions::assert_eq!(expected, result_str);
1239        }
1240
1241        // Basic array element replacement
1242        check_object_replace_array(
1243            r#"{
1244                "a": [1, 3],
1245            }"#
1246            .unindent(),
1247            &["a", "#1"],
1248            Some(json!(2)),
1249            r#"{
1250                "a": [1, 2],
1251            }"#
1252            .unindent(),
1253        );
1254
1255        // Replace first element
1256        check_object_replace_array(
1257            r#"{
1258                "items": [1, 2, 3]
1259            }"#
1260            .unindent(),
1261            &["items", "#0"],
1262            Some(json!(10)),
1263            r#"{
1264                "items": [10, 2, 3]
1265            }"#
1266            .unindent(),
1267        );
1268
1269        // Replace last element
1270        check_object_replace_array(
1271            r#"{
1272                "items": [1, 2, 3]
1273            }"#
1274            .unindent(),
1275            &["items", "#2"],
1276            Some(json!(30)),
1277            r#"{
1278                "items": [1, 2, 30]
1279            }"#
1280            .unindent(),
1281        );
1282
1283        // Replace string in array
1284        check_object_replace_array(
1285            r#"{
1286                "names": ["alice", "bob", "charlie"]
1287            }"#
1288            .unindent(),
1289            &["names", "#1"],
1290            Some(json!("robert")),
1291            r#"{
1292                "names": ["alice", "robert", "charlie"]
1293            }"#
1294            .unindent(),
1295        );
1296
1297        // Replace boolean
1298        check_object_replace_array(
1299            r#"{
1300                "flags": [true, false, true]
1301            }"#
1302            .unindent(),
1303            &["flags", "#0"],
1304            Some(json!(false)),
1305            r#"{
1306                "flags": [false, false, true]
1307            }"#
1308            .unindent(),
1309        );
1310
1311        // Replace null with value
1312        check_object_replace_array(
1313            r#"{
1314                "values": [null, 2, null]
1315            }"#
1316            .unindent(),
1317            &["values", "#0"],
1318            Some(json!(1)),
1319            r#"{
1320                "values": [1, 2, null]
1321            }"#
1322            .unindent(),
1323        );
1324
1325        // Replace value with null
1326        check_object_replace_array(
1327            r#"{
1328                "data": [1, 2, 3]
1329            }"#
1330            .unindent(),
1331            &["data", "#1"],
1332            Some(json!(null)),
1333            r#"{
1334                "data": [1, null, 3]
1335            }"#
1336            .unindent(),
1337        );
1338
1339        // Replace simple value with object
1340        check_object_replace_array(
1341            r#"{
1342                "list": [1, 2, 3]
1343            }"#
1344            .unindent(),
1345            &["list", "#1"],
1346            Some(json!({"value": 2, "label": "two"})),
1347            r#"{
1348                "list": [1, { "value": 2, "label": "two" }, 3]
1349            }"#
1350            .unindent(),
1351        );
1352
1353        // Replace simple value with nested array
1354        check_object_replace_array(
1355            r#"{
1356                "matrix": [1, 2, 3]
1357            }"#
1358            .unindent(),
1359            &["matrix", "#1"],
1360            Some(json!([20, 21, 22])),
1361            r#"{
1362                "matrix": [1, [ 20, 21, 22 ], 3]
1363            }"#
1364            .unindent(),
1365        );
1366
1367        // Replace object in array
1368        check_object_replace_array(
1369            r#"{
1370                "users": [
1371                    {"name": "alice"},
1372                    {"name": "bob"},
1373                    {"name": "charlie"}
1374                ]
1375            }"#
1376            .unindent(),
1377            &["users", "#1"],
1378            Some(json!({"name": "robert", "age": 30})),
1379            r#"{
1380                "users": [
1381                    {"name": "alice"},
1382                    { "name": "robert", "age": 30 },
1383                    {"name": "charlie"}
1384                ]
1385            }"#
1386            .unindent(),
1387        );
1388
1389        // Replace property within object in array
1390        check_object_replace_array(
1391            r#"{
1392                "users": [
1393                    {"name": "alice", "age": 25},
1394                    {"name": "bob", "age": 30},
1395                    {"name": "charlie", "age": 35}
1396                ]
1397            }"#
1398            .unindent(),
1399            &["users", "#1", "age"],
1400            Some(json!(31)),
1401            r#"{
1402                "users": [
1403                    {"name": "alice", "age": 25},
1404                    {"name": "bob", "age": 31},
1405                    {"name": "charlie", "age": 35}
1406                ]
1407            }"#
1408            .unindent(),
1409        );
1410
1411        // Add new property to object in array
1412        check_object_replace_array(
1413            r#"{
1414                "items": [
1415                    {"id": 1},
1416                    {"id": 2},
1417                    {"id": 3}
1418                ]
1419            }"#
1420            .unindent(),
1421            &["items", "#1", "name"],
1422            Some(json!("Item Two")),
1423            r#"{
1424                "items": [
1425                    {"id": 1},
1426                    {"name": "Item Two", "id": 2},
1427                    {"id": 3}
1428                ]
1429            }"#
1430            .unindent(),
1431        );
1432
1433        // Remove property from object in array
1434        check_object_replace_array(
1435            r#"{
1436                "items": [
1437                    {"id": 1, "name": "one"},
1438                    {"id": 2, "name": "two"},
1439                    {"id": 3, "name": "three"}
1440                ]
1441            }"#
1442            .unindent(),
1443            &["items", "#1", "name"],
1444            None,
1445            r#"{
1446                "items": [
1447                    {"id": 1, "name": "one"},
1448                    {"id": 2},
1449                    {"id": 3, "name": "three"}
1450                ]
1451            }"#
1452            .unindent(),
1453        );
1454
1455        // Deeply nested: array in object in array
1456        check_object_replace_array(
1457            r#"{
1458                "data": [
1459                    {
1460                        "values": [1, 2, 3]
1461                    },
1462                    {
1463                        "values": [4, 5, 6]
1464                    }
1465                ]
1466            }"#
1467            .unindent(),
1468            &["data", "#0", "values", "#1"],
1469            Some(json!(20)),
1470            r#"{
1471                "data": [
1472                    {
1473                        "values": [1, 20, 3]
1474                    },
1475                    {
1476                        "values": [4, 5, 6]
1477                    }
1478                ]
1479            }"#
1480            .unindent(),
1481        );
1482
1483        // Multiple levels of nesting
1484        check_object_replace_array(
1485            r#"{
1486                "root": {
1487                    "level1": [
1488                        {
1489                            "level2": {
1490                                "level3": [10, 20, 30]
1491                            }
1492                        }
1493                    ]
1494                }
1495            }"#
1496            .unindent(),
1497            &["root", "level1", "#0", "level2", "level3", "#2"],
1498            Some(json!(300)),
1499            r#"{
1500                "root": {
1501                    "level1": [
1502                        {
1503                            "level2": {
1504                                "level3": [10, 20, 300]
1505                            }
1506                        }
1507                    ]
1508                }
1509            }"#
1510            .unindent(),
1511        );
1512
1513        // Array with mixed types
1514        check_object_replace_array(
1515            r#"{
1516                "mixed": [1, "two", true, null, {"five": 5}]
1517            }"#
1518            .unindent(),
1519            &["mixed", "#3"],
1520            Some(json!({"four": 4})),
1521            r#"{
1522                "mixed": [1, "two", true, { "four": 4 }, {"five": 5}]
1523            }"#
1524            .unindent(),
1525        );
1526
1527        // Replace with complex object
1528        check_object_replace_array(
1529            r#"{
1530                "config": [
1531                    "simple",
1532                    "values"
1533                ]
1534            }"#
1535            .unindent(),
1536            &["config", "#0"],
1537            Some(json!({
1538                "type": "complex",
1539                "settings": {
1540                    "enabled": true,
1541                    "level": 5
1542                }
1543            })),
1544            r#"{
1545                "config": [
1546                    {
1547                        "type": "complex",
1548                        "settings": {
1549                            "enabled": true,
1550                            "level": 5
1551                        }
1552                    },
1553                    "values"
1554                ]
1555            }"#
1556            .unindent(),
1557        );
1558
1559        // Array with trailing comma
1560        check_object_replace_array(
1561            r#"{
1562                "items": [
1563                    1,
1564                    2,
1565                    3,
1566                ]
1567            }"#
1568            .unindent(),
1569            &["items", "#1"],
1570            Some(json!(20)),
1571            r#"{
1572                "items": [
1573                    1,
1574                    20,
1575                    3,
1576                ]
1577            }"#
1578            .unindent(),
1579        );
1580
1581        // Array with comments
1582        check_object_replace_array(
1583            r#"{
1584                "items": [
1585                    1, // first item
1586                    2, // second item
1587                    3  // third item
1588                ]
1589            }"#
1590            .unindent(),
1591            &["items", "#1"],
1592            Some(json!(20)),
1593            r#"{
1594                "items": [
1595                    1, // first item
1596                    20, // second item
1597                    3  // third item
1598                ]
1599            }"#
1600            .unindent(),
1601        );
1602
1603        // Multiple arrays in object
1604        check_object_replace_array(
1605            r#"{
1606                "first": [1, 2, 3],
1607                "second": [4, 5, 6],
1608                "third": [7, 8, 9]
1609            }"#
1610            .unindent(),
1611            &["second", "#1"],
1612            Some(json!(50)),
1613            r#"{
1614                "first": [1, 2, 3],
1615                "second": [4, 50, 6],
1616                "third": [7, 8, 9]
1617            }"#
1618            .unindent(),
1619        );
1620
1621        // Empty array - add first element
1622        check_object_replace_array(
1623            r#"{
1624                "empty": []
1625            }"#
1626            .unindent(),
1627            &["empty", "#0"],
1628            Some(json!("first")),
1629            r#"{
1630                "empty": ["first"]
1631            }"#
1632            .unindent(),
1633        );
1634
1635        // Array of arrays
1636        check_object_replace_array(
1637            r#"{
1638                "matrix": [
1639                    [1, 2],
1640                    [3, 4],
1641                    [5, 6]
1642                ]
1643            }"#
1644            .unindent(),
1645            &["matrix", "#1", "#0"],
1646            Some(json!(30)),
1647            r#"{
1648                "matrix": [
1649                    [1, 2],
1650                    [30, 4],
1651                    [5, 6]
1652                ]
1653            }"#
1654            .unindent(),
1655        );
1656
1657        // Replace nested object property in array element
1658        check_object_replace_array(
1659            r#"{
1660                "users": [
1661                    {
1662                        "name": "alice",
1663                        "address": {
1664                            "city": "NYC",
1665                            "zip": "10001"
1666                        }
1667                    }
1668                ]
1669            }"#
1670            .unindent(),
1671            &["users", "#0", "address", "city"],
1672            Some(json!("Boston")),
1673            r#"{
1674                "users": [
1675                    {
1676                        "name": "alice",
1677                        "address": {
1678                            "city": "Boston",
1679                            "zip": "10001"
1680                        }
1681                    }
1682                ]
1683            }"#
1684            .unindent(),
1685        );
1686
1687        // Add element past end of array
1688        check_object_replace_array(
1689            r#"{
1690                "items": [1, 2]
1691            }"#
1692            .unindent(),
1693            &["items", "#5"],
1694            Some(json!(6)),
1695            r#"{
1696                "items": [1, 2, 6]
1697            }"#
1698            .unindent(),
1699        );
1700
1701        // Complex nested structure
1702        check_object_replace_array(
1703            r#"{
1704                "app": {
1705                    "modules": [
1706                        {
1707                            "name": "auth",
1708                            "routes": [
1709                                {"path": "/login", "method": "POST"},
1710                                {"path": "/logout", "method": "POST"}
1711                            ]
1712                        },
1713                        {
1714                            "name": "api",
1715                            "routes": [
1716                                {"path": "/users", "method": "GET"},
1717                                {"path": "/users", "method": "POST"}
1718                            ]
1719                        }
1720                    ]
1721                }
1722            }"#
1723            .unindent(),
1724            &["app", "modules", "#1", "routes", "#0", "method"],
1725            Some(json!("PUT")),
1726            r#"{
1727                "app": {
1728                    "modules": [
1729                        {
1730                            "name": "auth",
1731                            "routes": [
1732                                {"path": "/login", "method": "POST"},
1733                                {"path": "/logout", "method": "POST"}
1734                            ]
1735                        },
1736                        {
1737                            "name": "api",
1738                            "routes": [
1739                                {"path": "/users", "method": "PUT"},
1740                                {"path": "/users", "method": "POST"}
1741                            ]
1742                        }
1743                    ]
1744                }
1745            }"#
1746            .unindent(),
1747        );
1748
1749        // Escaped strings in array
1750        check_object_replace_array(
1751            r#"{
1752                "messages": ["hello", "world"]
1753            }"#
1754            .unindent(),
1755            &["messages", "#0"],
1756            Some(json!("hello \"quoted\" world")),
1757            r#"{
1758                "messages": ["hello \"quoted\" world", "world"]
1759            }"#
1760            .unindent(),
1761        );
1762
1763        // Block comments
1764        check_object_replace_array(
1765            r#"{
1766                "data": [
1767                    /* first */ 1,
1768                    /* second */ 2,
1769                    /* third */ 3
1770                ]
1771            }"#
1772            .unindent(),
1773            &["data", "#1"],
1774            Some(json!(20)),
1775            r#"{
1776                "data": [
1777                    /* first */ 1,
1778                    /* second */ 20,
1779                    /* third */ 3
1780                ]
1781            }"#
1782            .unindent(),
1783        );
1784
1785        // Inline array
1786        check_object_replace_array(
1787            r#"{"items": [1, 2, 3], "count": 3}"#.to_string(),
1788            &["items", "#1"],
1789            Some(json!(20)),
1790            r#"{"items": [1, 20, 3], "count": 3}"#.to_string(),
1791        );
1792
1793        // Single element array
1794        check_object_replace_array(
1795            r#"{
1796                "single": [42]
1797            }"#
1798            .unindent(),
1799            &["single", "#0"],
1800            Some(json!(100)),
1801            r#"{
1802                "single": [100]
1803            }"#
1804            .unindent(),
1805        );
1806
1807        // Inconsistent formatting
1808        check_object_replace_array(
1809            r#"{
1810                "messy": [1,
1811                    2,
1812                        3,
1813                4]
1814            }"#
1815            .unindent(),
1816            &["messy", "#2"],
1817            Some(json!(30)),
1818            r#"{
1819                "messy": [1,
1820                    2,
1821                        30,
1822                4]
1823            }"#
1824            .unindent(),
1825        );
1826
1827        // Creates array if has numbered key
1828        check_object_replace_array(
1829            r#"{
1830                "array": {"foo": "bar"}
1831            }"#
1832            .unindent(),
1833            &["array", "#3"],
1834            Some(json!(4)),
1835            r#"{
1836                "array": [
1837                    4
1838                ]
1839            }"#
1840            .unindent(),
1841        );
1842
1843        // Replace non-array element within array with array
1844        check_object_replace_array(
1845            r#"{
1846                "matrix": [
1847                    [1, 2],
1848                    [3, 4],
1849                    [5, 6]
1850                ]
1851            }"#
1852            .unindent(),
1853            &["matrix", "#1", "#0"],
1854            Some(json!(["foo", "bar"])),
1855            r#"{
1856                "matrix": [
1857                    [1, 2],
1858                    [[ "foo", "bar" ], 4],
1859                    [5, 6]
1860                ]
1861            }"#
1862            .unindent(),
1863        );
1864        // Replace non-array element within array with array
1865        check_object_replace_array(
1866            r#"{
1867                "matrix": [
1868                    [1, 2],
1869                    [3, 4],
1870                    [5, 6]
1871                ]
1872            }"#
1873            .unindent(),
1874            &["matrix", "#1", "#0", "#3"],
1875            Some(json!(["foo", "bar"])),
1876            r#"{
1877                "matrix": [
1878                    [1, 2],
1879                    [[ [ "foo", "bar" ] ], 4],
1880                    [5, 6]
1881                ]
1882            }"#
1883            .unindent(),
1884        );
1885
1886        // Create array in key that doesn't exist
1887        check_object_replace_array(
1888            r#"{
1889                "foo": {}
1890            }"#
1891            .unindent(),
1892            &["foo", "bar", "#0"],
1893            Some(json!({"is_object": true})),
1894            r#"{
1895                "foo": {
1896                    "bar": [
1897                        {
1898                            "is_object": true
1899                        }
1900                    ]
1901                }
1902            }"#
1903            .unindent(),
1904        );
1905    }
1906
1907    #[test]
1908    fn array_replace() {
1909        #[track_caller]
1910        fn check_array_replace(
1911            input: impl ToString,
1912            index: usize,
1913            key_path: &[&str],
1914            value: Option<Value>,
1915            expected: impl ToString,
1916        ) {
1917            let input = input.to_string();
1918            let result = replace_top_level_array_value_in_json_text(
1919                &input,
1920                key_path,
1921                value.as_ref(),
1922                None,
1923                index,
1924                4,
1925            );
1926            let mut result_str = input;
1927            result_str.replace_range(result.0, &result.1);
1928            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1929        }
1930
1931        check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1932        check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1933        check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1934        check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1935        check_array_replace(
1936            r#"[
1937                1,
1938                2,
1939                3,
1940            ]"#
1941            .unindent(),
1942            1,
1943            &[],
1944            Some(json!({"foo": "bar", "baz": "qux"})),
1945            r#"[
1946                1,
1947                {
1948                    "foo": "bar",
1949                    "baz": "qux"
1950                },
1951                3,
1952            ]"#
1953            .unindent(),
1954        );
1955        check_array_replace(
1956            r#"[1, 3, 3,]"#,
1957            1,
1958            &[],
1959            Some(json!({"foo": "bar", "baz": "qux"})),
1960            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1961        );
1962
1963        check_array_replace(
1964            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1965            1,
1966            &["baz"],
1967            Some(json!({"qux": "quz"})),
1968            r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1969        );
1970
1971        check_array_replace(
1972            r#"[
1973                1,
1974                {
1975                    "foo": "bar",
1976                    "baz": "qux"
1977                },
1978                3
1979            ]"#,
1980            1,
1981            &["baz"],
1982            Some(json!({"qux": "quz"})),
1983            r#"[
1984                1,
1985                {
1986                    "foo": "bar",
1987                    "baz": {
1988                        "qux": "quz"
1989                    }
1990                },
1991                3
1992            ]"#,
1993        );
1994
1995        check_array_replace(
1996            r#"[
1997                1,
1998                {
1999                    "foo": "bar",
2000                    "baz": {
2001                        "qux": "quz"
2002                    }
2003                },
2004                3
2005            ]"#,
2006            1,
2007            &["baz"],
2008            Some(json!("qux")),
2009            r#"[
2010                1,
2011                {
2012                    "foo": "bar",
2013                    "baz": "qux"
2014                },
2015                3
2016            ]"#,
2017        );
2018
2019        check_array_replace(
2020            r#"[
2021                1,
2022                {
2023                    "foo": "bar",
2024                    // some comment to keep
2025                    "baz": {
2026                        // some comment to remove
2027                        "qux": "quz"
2028                    }
2029                    // some other comment to keep
2030                },
2031                3
2032            ]"#,
2033            1,
2034            &["baz"],
2035            Some(json!("qux")),
2036            r#"[
2037                1,
2038                {
2039                    "foo": "bar",
2040                    // some comment to keep
2041                    "baz": "qux"
2042                    // some other comment to keep
2043                },
2044                3
2045            ]"#,
2046        );
2047
2048        // Test with comments between array elements
2049        check_array_replace(
2050            r#"[
2051                1,
2052                // This is element 2
2053                2,
2054                /* Block comment */ 3,
2055                4 // Trailing comment
2056            ]"#,
2057            2,
2058            &[],
2059            Some(json!("replaced")),
2060            r#"[
2061                1,
2062                // This is element 2
2063                2,
2064                /* Block comment */ "replaced",
2065                4 // Trailing comment
2066            ]"#,
2067        );
2068
2069        // Test empty array with comments
2070        check_array_replace(
2071            r#"[
2072                // Empty array with comment
2073            ]"#
2074            .unindent(),
2075            0,
2076            &[],
2077            Some(json!("first")),
2078            r#"[
2079                // Empty array with comment
2080                "first"
2081            ]"#
2082            .unindent(),
2083        );
2084        check_array_replace(
2085            r#"[]"#.unindent(),
2086            0,
2087            &[],
2088            Some(json!("first")),
2089            r#"["first"]"#.unindent(),
2090        );
2091
2092        // Test array with leading comments
2093        check_array_replace(
2094            r#"[
2095                // Leading comment
2096                // Another leading comment
2097                1,
2098                2
2099            ]"#,
2100            0,
2101            &[],
2102            Some(json!({"new": "object"})),
2103            r#"[
2104                // Leading comment
2105                // Another leading comment
2106                {
2107                    "new": "object"
2108                },
2109                2
2110            ]"#,
2111        );
2112
2113        // Test with deep indentation
2114        check_array_replace(
2115            r#"[
2116                        1,
2117                        2,
2118                        3
2119                    ]"#,
2120            1,
2121            &[],
2122            Some(json!("deep")),
2123            r#"[
2124                        1,
2125                        "deep",
2126                        3
2127                    ]"#,
2128        );
2129
2130        // Test with mixed spacing
2131        check_array_replace(
2132            r#"[1,2,   3,    4]"#,
2133            2,
2134            &[],
2135            Some(json!("spaced")),
2136            r#"[1,2,   "spaced",    4]"#,
2137        );
2138
2139        // Test replacing nested array element
2140        check_array_replace(
2141            r#"[
2142                [1, 2, 3],
2143                [4, 5, 6],
2144                [7, 8, 9]
2145            ]"#,
2146            1,
2147            &[],
2148            Some(json!(["a", "b", "c", "d"])),
2149            r#"[
2150                [1, 2, 3],
2151                [
2152                    "a",
2153                    "b",
2154                    "c",
2155                    "d"
2156                ],
2157                [7, 8, 9]
2158            ]"#,
2159        );
2160
2161        // Test with multiline block comments
2162        check_array_replace(
2163            r#"[
2164                /*
2165                 * This is a
2166                 * multiline comment
2167                 */
2168                "first",
2169                "second"
2170            ]"#,
2171            0,
2172            &[],
2173            Some(json!("updated")),
2174            r#"[
2175                /*
2176                 * This is a
2177                 * multiline comment
2178                 */
2179                "updated",
2180                "second"
2181            ]"#,
2182        );
2183
2184        // Test replacing with null
2185        check_array_replace(
2186            r#"[true, false, true]"#,
2187            1,
2188            &[],
2189            Some(json!(null)),
2190            r#"[true, null, true]"#,
2191        );
2192
2193        // Test single element array
2194        check_array_replace(
2195            r#"[42]"#,
2196            0,
2197            &[],
2198            Some(json!({"answer": 42})),
2199            r#"[{ "answer": 42 }]"#,
2200        );
2201
2202        // Test array with only comments
2203        check_array_replace(
2204            r#"[
2205                // Comment 1
2206                // Comment 2
2207                // Comment 3
2208            ]"#
2209            .unindent(),
2210            10,
2211            &[],
2212            Some(json!(123)),
2213            r#"[
2214                // Comment 1
2215                // Comment 2
2216                // Comment 3
2217                123
2218            ]"#
2219            .unindent(),
2220        );
2221
2222        check_array_replace(
2223            r#"[
2224                {
2225                    "key": "value"
2226                },
2227                {
2228                    "key": "value2"
2229                }
2230            ]"#
2231            .unindent(),
2232            0,
2233            &[],
2234            None,
2235            r#"[
2236                {
2237                    "key": "value2"
2238                }
2239            ]"#
2240            .unindent(),
2241        );
2242
2243        check_array_replace(
2244            r#"[
2245                {
2246                    "key": "value"
2247                },
2248                {
2249                    "key": "value2"
2250                },
2251                {
2252                    "key": "value3"
2253                },
2254            ]"#
2255            .unindent(),
2256            1,
2257            &[],
2258            None,
2259            r#"[
2260                {
2261                    "key": "value"
2262                },
2263                {
2264                    "key": "value3"
2265                },
2266            ]"#
2267            .unindent(),
2268        );
2269
2270        check_array_replace(
2271            r#""#,
2272            2,
2273            &[],
2274            Some(json!(42)),
2275            r#"[
2276                42
2277            ]"#
2278            .unindent(),
2279        );
2280
2281        check_array_replace(
2282            r#""#,
2283            2,
2284            &["foo", "bar"],
2285            Some(json!(42)),
2286            r#"[
2287                {
2288                    "foo": {
2289                        "bar": 42
2290                    }
2291                }
2292            ]"#
2293            .unindent(),
2294        );
2295    }
2296
2297    #[test]
2298    fn array_append() {
2299        #[track_caller]
2300        fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
2301            let input = input.to_string();
2302            let result = append_top_level_array_value_in_json_text(&input, &value, 4);
2303            let mut result_str = input;
2304            result_str.replace_range(result.0, &result.1);
2305            pretty_assertions::assert_eq!(expected.to_string(), result_str);
2306        }
2307        check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
2308        check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
2309        check_array_append(r#"[1, 3, 3   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2310        check_array_append(r#"[1, 3, 3,   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2311        check_array_append(
2312            r#"[
2313                1,
2314                2,
2315                3
2316            ]"#
2317            .unindent(),
2318            json!(4),
2319            r#"[
2320                1,
2321                2,
2322                3,
2323                4
2324            ]"#
2325            .unindent(),
2326        );
2327        check_array_append(
2328            r#"[
2329                1,
2330                2,
2331                3,
2332            ]"#
2333            .unindent(),
2334            json!(4),
2335            r#"[
2336                1,
2337                2,
2338                3,
2339                4
2340            ]"#
2341            .unindent(),
2342        );
2343        check_array_append(
2344            r#"[
2345                1,
2346                2,
2347                3,
2348            ]"#
2349            .unindent(),
2350            json!({"foo": "bar", "baz": "qux"}),
2351            r#"[
2352                1,
2353                2,
2354                3,
2355                {
2356                    "foo": "bar",
2357                    "baz": "qux"
2358                }
2359            ]"#
2360            .unindent(),
2361        );
2362        check_array_append(
2363            r#"[ 1, 2, 3, ]"#.unindent(),
2364            json!({"foo": "bar", "baz": "qux"}),
2365            r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
2366        );
2367        check_array_append(
2368            r#"[]"#,
2369            json!({"foo": "bar"}),
2370            r#"[
2371                {
2372                    "foo": "bar"
2373                }
2374            ]"#
2375            .unindent(),
2376        );
2377
2378        // Test with comments between array elements
2379        check_array_append(
2380            r#"[
2381                1,
2382                // Comment between elements
2383                2,
2384                /* Block comment */ 3
2385            ]"#
2386            .unindent(),
2387            json!(4),
2388            r#"[
2389                1,
2390                // Comment between elements
2391                2,
2392                /* Block comment */ 3,
2393                4
2394            ]"#
2395            .unindent(),
2396        );
2397
2398        // Test with trailing comment on last element
2399        check_array_append(
2400            r#"[
2401                1,
2402                2,
2403                3 // Trailing comment
2404            ]"#
2405            .unindent(),
2406            json!("new"),
2407            r#"[
2408                1,
2409                2,
2410                3 // Trailing comment
2411            ,
2412                "new"
2413            ]"#
2414            .unindent(),
2415        );
2416
2417        // Test empty array with comments
2418        check_array_append(
2419            r#"[
2420                // Empty array with comment
2421            ]"#
2422            .unindent(),
2423            json!("first"),
2424            r#"[
2425                // Empty array with comment
2426                "first"
2427            ]"#
2428            .unindent(),
2429        );
2430
2431        // Test with multiline block comment at end
2432        check_array_append(
2433            r#"[
2434                1,
2435                2
2436                /*
2437                 * This is a
2438                 * multiline comment
2439                 */
2440            ]"#
2441            .unindent(),
2442            json!(3),
2443            r#"[
2444                1,
2445                2
2446                /*
2447                 * This is a
2448                 * multiline comment
2449                 */
2450            ,
2451                3
2452            ]"#
2453            .unindent(),
2454        );
2455
2456        // Test with deep indentation
2457        check_array_append(
2458            r#"[
2459                1,
2460                    2,
2461                        3
2462            ]"#
2463            .unindent(),
2464            json!("deep"),
2465            r#"[
2466                1,
2467                    2,
2468                        3,
2469                        "deep"
2470            ]"#
2471            .unindent(),
2472        );
2473
2474        // Test with no spacing
2475        check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
2476
2477        // Test appending complex nested structure
2478        check_array_append(
2479            r#"[
2480                {"a": 1},
2481                {"b": 2}
2482            ]"#
2483            .unindent(),
2484            json!({"c": {"nested": [1, 2, 3]}}),
2485            r#"[
2486                {"a": 1},
2487                {"b": 2},
2488                {
2489                    "c": {
2490                        "nested": [
2491                            1,
2492                            2,
2493                            3
2494                        ]
2495                    }
2496                }
2497            ]"#
2498            .unindent(),
2499        );
2500
2501        // Test array ending with comment after bracket
2502        check_array_append(
2503            r#"[
2504                1,
2505                2,
2506                3
2507            ] // Comment after array"#
2508                .unindent(),
2509            json!(4),
2510            r#"[
2511                1,
2512                2,
2513                3,
2514                4
2515            ] // Comment after array"#
2516                .unindent(),
2517        );
2518
2519        // Test with inconsistent element formatting
2520        check_array_append(
2521            r#"[1,
2522               2,
2523                    3,
2524            ]"#
2525            .unindent(),
2526            json!(4),
2527            r#"[1,
2528               2,
2529                    3,
2530                    4
2531            ]"#
2532            .unindent(),
2533        );
2534
2535        // Test appending to single-line array with trailing comma
2536        check_array_append(
2537            r#"[1, 2, 3,]"#,
2538            json!({"key": "value"}),
2539            r#"[1, 2, 3, { "key": "value" }]"#,
2540        );
2541
2542        // Test appending null value
2543        check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
2544
2545        // Test appending to array with only comments
2546        check_array_append(
2547            r#"[
2548                // Just comments here
2549                // More comments
2550            ]"#
2551            .unindent(),
2552            json!(42),
2553            r#"[
2554                // Just comments here
2555                // More comments
2556                42
2557            ]"#
2558            .unindent(),
2559        );
2560
2561        check_array_append(
2562            r#""#,
2563            json!(42),
2564            r#"[
2565                42
2566            ]"#
2567            .unindent(),
2568        )
2569    }
2570
2571    #[test]
2572    fn test_infer_json_indent_size() {
2573        let json_2_spaces = r#"{
2574  "key1": "value1",
2575  "nested": {
2576    "key2": "value2",
2577    "array": [
2578      1,
2579      2,
2580      3
2581    ]
2582  }
2583}"#;
2584        assert_eq!(infer_json_indent_size(json_2_spaces), 2);
2585
2586        let json_4_spaces = r#"{
2587    "key1": "value1",
2588    "nested": {
2589        "key2": "value2",
2590        "array": [
2591            1,
2592            2,
2593            3
2594        ]
2595    }
2596}"#;
2597        assert_eq!(infer_json_indent_size(json_4_spaces), 4);
2598
2599        let json_8_spaces = r#"{
2600        "key1": "value1",
2601        "nested": {
2602                "key2": "value2"
2603        }
2604}"#;
2605        assert_eq!(infer_json_indent_size(json_8_spaces), 8);
2606
2607        let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#;
2608        assert_eq!(infer_json_indent_size(json_single_line), 2);
2609
2610        let json_empty = r#"{}"#;
2611        assert_eq!(infer_json_indent_size(json_empty), 2);
2612
2613        let json_array = r#"[
2614  {
2615    "id": 1,
2616    "name": "first"
2617  },
2618  {
2619    "id": 2,
2620    "name": "second"
2621  }
2622]"#;
2623        assert_eq!(infer_json_indent_size(json_array), 2);
2624
2625        let json_mixed = r#"{
2626  "a": {
2627    "b": {
2628        "c": "value"
2629    }
2630  },
2631  "d": "value2"
2632}"#;
2633        assert_eq!(infer_json_indent_size(json_mixed), 2);
2634    }
2635}