settings_json.rs

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