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