settings_json.rs

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