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 = tab_size * depth;
 266            let mut new_val = to_pretty_json(&new_value, tab_size, 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
 631/// Infers the indentation size used in JSON text by analyzing the tree structure.
 632/// Returns the detected indent size, or a default of 2 if no indentation is found.
 633pub fn infer_json_indent_size(text: &str) -> usize {
 634    const MAX_INDENT_SIZE: usize = 64;
 635
 636    let mut parser = tree_sitter::Parser::new();
 637    parser
 638        .set_language(&tree_sitter_json::LANGUAGE.into())
 639        .unwrap();
 640
 641    let Some(syntax_tree) = parser.parse(text, None) else {
 642        return 4;
 643    };
 644
 645    let mut cursor = syntax_tree.walk();
 646    let mut indent_counts = [0u32; MAX_INDENT_SIZE];
 647
 648    // Traverse the tree to find indentation patterns
 649    fn visit_node(
 650        cursor: &mut tree_sitter::TreeCursor,
 651        indent_counts: &mut [u32; MAX_INDENT_SIZE],
 652        depth: usize,
 653    ) {
 654        if depth >= 3 {
 655            return;
 656        }
 657        let node = cursor.node();
 658        let node_kind = node.kind();
 659
 660        // For objects and arrays, check the indentation of their first content child
 661        if matches!(node_kind, "object" | "array") {
 662            let container_column = node.start_position().column;
 663            let container_row = node.start_position().row;
 664
 665            if cursor.goto_first_child() {
 666                // Skip the opening bracket
 667                loop {
 668                    let child = cursor.node();
 669                    let child_kind = child.kind();
 670
 671                    // Look for the first actual content (pair for objects, value for arrays)
 672                    if (node_kind == "object" && child_kind == "pair")
 673                        || (node_kind == "array"
 674                            && !matches!(child_kind, "[" | "]" | "," | "comment"))
 675                    {
 676                        let child_column = child.start_position().column;
 677                        let child_row = child.start_position().row;
 678
 679                        // Only count if the child is on a different line
 680                        if child_row > container_row && child_column > container_column {
 681                            let indent = child_column - container_column;
 682                            if indent > 0 && indent < MAX_INDENT_SIZE {
 683                                indent_counts[indent] += 1;
 684                            }
 685                        }
 686                        break;
 687                    }
 688
 689                    if !cursor.goto_next_sibling() {
 690                        break;
 691                    }
 692                }
 693                cursor.goto_parent();
 694            }
 695        }
 696
 697        // Recurse to children
 698        if cursor.goto_first_child() {
 699            loop {
 700                visit_node(cursor, indent_counts, depth + 1);
 701                if !cursor.goto_next_sibling() {
 702                    break;
 703                }
 704            }
 705            cursor.goto_parent();
 706        }
 707    }
 708
 709    visit_node(&mut cursor, &mut indent_counts, 0);
 710
 711    // Find the indent size with the highest count
 712    let mut max_count = 0;
 713    let mut max_indent = 4;
 714
 715    for (indent, &count) in indent_counts.iter().enumerate() {
 716        if count > max_count {
 717            max_count = count;
 718            max_indent = indent;
 719        }
 720    }
 721
 722    if max_count == 0 { 2 } else { max_indent }
 723}
 724
 725pub fn to_pretty_json(
 726    value: &impl Serialize,
 727    indent_size: usize,
 728    indent_prefix_len: usize,
 729) -> String {
 730    let mut output = Vec::new();
 731    let indent = " ".repeat(indent_size);
 732    let mut ser = serde_json::Serializer::with_formatter(
 733        &mut output,
 734        serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()),
 735    );
 736
 737    value.serialize(&mut ser).unwrap();
 738    let text = String::from_utf8(output).unwrap();
 739
 740    let mut adjusted_text = String::new();
 741    for (i, line) in text.split('\n').enumerate() {
 742        if i > 0 {
 743            adjusted_text.extend(std::iter::repeat(' ').take(indent_prefix_len));
 744        }
 745        adjusted_text.push_str(line);
 746        adjusted_text.push('\n');
 747    }
 748    adjusted_text.pop();
 749    adjusted_text
 750}
 751
 752pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
 753    let mut deserializer = serde_json_lenient::Deserializer::from_str(content);
 754    Ok(serde_path_to_error::deserialize(&mut deserializer)?)
 755}
 756
 757#[cfg(test)]
 758mod tests {
 759    use super::*;
 760    use serde_json::{Value, json};
 761    use unindent::Unindent;
 762
 763    #[test]
 764    fn object_replace() {
 765        #[track_caller]
 766        fn check_object_replace(
 767            input: String,
 768            key_path: &[&str],
 769            value: Option<Value>,
 770            expected: String,
 771        ) {
 772            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
 773            let mut result_str = input;
 774            result_str.replace_range(result.0, &result.1);
 775            pretty_assertions::assert_eq!(expected, result_str);
 776        }
 777        check_object_replace(
 778            r#"{
 779                "a": 1,
 780                "b": 2
 781            }"#
 782            .unindent(),
 783            &["b"],
 784            Some(json!(3)),
 785            r#"{
 786                "a": 1,
 787                "b": 3
 788            }"#
 789            .unindent(),
 790        );
 791        check_object_replace(
 792            r#"{
 793                "a": 1,
 794                "b": 2
 795            }"#
 796            .unindent(),
 797            &["b"],
 798            None,
 799            r#"{
 800                "a": 1
 801            }"#
 802            .unindent(),
 803        );
 804        check_object_replace(
 805            r#"{
 806                "a": 1,
 807                "b": 2
 808            }"#
 809            .unindent(),
 810            &["c"],
 811            Some(json!(3)),
 812            r#"{
 813                "c": 3,
 814                "a": 1,
 815                "b": 2
 816            }"#
 817            .unindent(),
 818        );
 819        check_object_replace(
 820            r#"{
 821                "a": 1,
 822                "b": {
 823                    "c": 2,
 824                    "d": 3,
 825                }
 826            }"#
 827            .unindent(),
 828            &["b", "c"],
 829            Some(json!([1, 2, 3])),
 830            r#"{
 831                "a": 1,
 832                "b": {
 833                    "c": [
 834                        1,
 835                        2,
 836                        3
 837                    ],
 838                    "d": 3,
 839                }
 840            }"#
 841            .unindent(),
 842        );
 843
 844        check_object_replace(
 845            r#"{
 846                "name": "old_name",
 847                "id": 123
 848            }"#
 849            .unindent(),
 850            &["name"],
 851            Some(json!("new_name")),
 852            r#"{
 853                "name": "new_name",
 854                "id": 123
 855            }"#
 856            .unindent(),
 857        );
 858
 859        check_object_replace(
 860            r#"{
 861                "enabled": false,
 862                "count": 5
 863            }"#
 864            .unindent(),
 865            &["enabled"],
 866            Some(json!(true)),
 867            r#"{
 868                "enabled": true,
 869                "count": 5
 870            }"#
 871            .unindent(),
 872        );
 873
 874        check_object_replace(
 875            r#"{
 876                "value": null,
 877                "other": "test"
 878            }"#
 879            .unindent(),
 880            &["value"],
 881            Some(json!(42)),
 882            r#"{
 883                "value": 42,
 884                "other": "test"
 885            }"#
 886            .unindent(),
 887        );
 888
 889        check_object_replace(
 890            r#"{
 891                "config": {
 892                    "old": true
 893                },
 894                "name": "test"
 895            }"#
 896            .unindent(),
 897            &["config"],
 898            Some(json!({"new": false, "count": 3})),
 899            r#"{
 900                "config": {
 901                    "new": false,
 902                    "count": 3
 903                },
 904                "name": "test"
 905            }"#
 906            .unindent(),
 907        );
 908
 909        check_object_replace(
 910            r#"{
 911                // This is a comment
 912                "a": 1,
 913                "b": 2 // Another comment
 914            }"#
 915            .unindent(),
 916            &["b"],
 917            Some(json!({"foo": "bar"})),
 918            r#"{
 919                // This is a comment
 920                "a": 1,
 921                "b": {
 922                    "foo": "bar"
 923                } // Another comment
 924            }"#
 925            .unindent(),
 926        );
 927
 928        check_object_replace(
 929            r#"{}"#.to_string(),
 930            &["new_key"],
 931            Some(json!("value")),
 932            r#"{
 933                "new_key": "value"
 934            }
 935            "#
 936            .unindent(),
 937        );
 938
 939        check_object_replace(
 940            r#"{
 941                "only_key": 123
 942            }"#
 943            .unindent(),
 944            &["only_key"],
 945            None,
 946            "{\n    \n}".to_string(),
 947        );
 948
 949        check_object_replace(
 950            r#"{
 951                "level1": {
 952                    "level2": {
 953                        "level3": {
 954                            "target": "old"
 955                        }
 956                    }
 957                }
 958            }"#
 959            .unindent(),
 960            &["level1", "level2", "level3", "target"],
 961            Some(json!("new")),
 962            r#"{
 963                "level1": {
 964                    "level2": {
 965                        "level3": {
 966                            "target": "new"
 967                        }
 968                    }
 969                }
 970            }"#
 971            .unindent(),
 972        );
 973
 974        check_object_replace(
 975            r#"{
 976                "parent": {}
 977            }"#
 978            .unindent(),
 979            &["parent", "child"],
 980            Some(json!("value")),
 981            r#"{
 982                "parent": {
 983                    "child": "value"
 984                }
 985            }"#
 986            .unindent(),
 987        );
 988
 989        check_object_replace(
 990            r#"{
 991                "a": 1,
 992                "b": 2,
 993            }"#
 994            .unindent(),
 995            &["b"],
 996            Some(json!(3)),
 997            r#"{
 998                "a": 1,
 999                "b": 3,
1000            }"#
1001            .unindent(),
1002        );
1003
1004        check_object_replace(
1005            r#"{
1006                "items": [1, 2, 3],
1007                "count": 3
1008            }"#
1009            .unindent(),
1010            &["items", "1"],
1011            Some(json!(5)),
1012            r#"{
1013                "items": {
1014                    "1": 5
1015                },
1016                "count": 3
1017            }"#
1018            .unindent(),
1019        );
1020
1021        check_object_replace(
1022            r#"{
1023                "items": [1, 2, 3],
1024                "count": 3
1025            }"#
1026            .unindent(),
1027            &["items", "1"],
1028            None,
1029            r#"{
1030                "items": {
1031                    "1": null
1032                },
1033                "count": 3
1034            }"#
1035            .unindent(),
1036        );
1037
1038        check_object_replace(
1039            r#"{
1040                "items": [1, 2, 3],
1041                "count": 3
1042            }"#
1043            .unindent(),
1044            &["items"],
1045            Some(json!(["a", "b", "c", "d"])),
1046            r#"{
1047                "items": [
1048                    "a",
1049                    "b",
1050                    "c",
1051                    "d"
1052                ],
1053                "count": 3
1054            }"#
1055            .unindent(),
1056        );
1057
1058        check_object_replace(
1059            r#"{
1060                "0": "zero",
1061                "1": "one"
1062            }"#
1063            .unindent(),
1064            &["1"],
1065            Some(json!("ONE")),
1066            r#"{
1067                "0": "zero",
1068                "1": "ONE"
1069            }"#
1070            .unindent(),
1071        );
1072        // Test with comments between object members
1073        check_object_replace(
1074            r#"{
1075                "a": 1,
1076                // Comment between members
1077                "b": 2,
1078                /* Block comment */
1079                "c": 3
1080            }"#
1081            .unindent(),
1082            &["b"],
1083            Some(json!({"nested": true})),
1084            r#"{
1085                "a": 1,
1086                // Comment between members
1087                "b": {
1088                    "nested": true
1089                },
1090                /* Block comment */
1091                "c": 3
1092            }"#
1093            .unindent(),
1094        );
1095
1096        // Test with trailing comments on replaced value
1097        check_object_replace(
1098            r#"{
1099                "a": 1, // keep this comment
1100                "b": 2  // this should stay
1101            }"#
1102            .unindent(),
1103            &["a"],
1104            Some(json!("changed")),
1105            r#"{
1106                "a": "changed", // keep this comment
1107                "b": 2  // this should stay
1108            }"#
1109            .unindent(),
1110        );
1111
1112        // Test with deep indentation
1113        check_object_replace(
1114            r#"{
1115                        "deeply": {
1116                                "nested": {
1117                                        "value": "old"
1118                                }
1119                        }
1120                }"#
1121            .unindent(),
1122            &["deeply", "nested", "value"],
1123            Some(json!("new")),
1124            r#"{
1125                        "deeply": {
1126                                "nested": {
1127                                        "value": "new"
1128                                }
1129                        }
1130                }"#
1131            .unindent(),
1132        );
1133
1134        // Test removing value with comment preservation
1135        check_object_replace(
1136            r#"{
1137                // Header comment
1138                "a": 1,
1139                // This comment belongs to b
1140                "b": 2,
1141                // This comment belongs to c
1142                "c": 3
1143            }"#
1144            .unindent(),
1145            &["b"],
1146            None,
1147            r#"{
1148                // Header comment
1149                "a": 1,
1150                // This comment belongs to b
1151                // This comment belongs to c
1152                "c": 3
1153            }"#
1154            .unindent(),
1155        );
1156
1157        // Test with multiline block comments
1158        check_object_replace(
1159            r#"{
1160                /*
1161                 * This is a multiline
1162                 * block comment
1163                 */
1164                "value": "old",
1165                /* Another block */ "other": 123
1166            }"#
1167            .unindent(),
1168            &["value"],
1169            Some(json!("new")),
1170            r#"{
1171                /*
1172                 * This is a multiline
1173                 * block comment
1174                 */
1175                "value": "new",
1176                /* Another block */ "other": 123
1177            }"#
1178            .unindent(),
1179        );
1180
1181        check_object_replace(
1182            r#"{
1183                // This object is empty
1184            }"#
1185            .unindent(),
1186            &["key"],
1187            Some(json!("value")),
1188            r#"{
1189                // This object is empty
1190                "key": "value"
1191            }
1192            "#
1193            .unindent(),
1194        );
1195
1196        // Test replacing in object with only comments
1197        check_object_replace(
1198            r#"{
1199                // Comment 1
1200                // Comment 2
1201            }"#
1202            .unindent(),
1203            &["new"],
1204            Some(json!(42)),
1205            r#"{
1206                // Comment 1
1207                // Comment 2
1208                "new": 42
1209            }
1210            "#
1211            .unindent(),
1212        );
1213
1214        // Test with inconsistent spacing
1215        check_object_replace(
1216            r#"{
1217              "a":1,
1218                    "b"  :  2  ,
1219                "c":   3
1220            }"#
1221            .unindent(),
1222            &["b"],
1223            Some(json!("spaced")),
1224            r#"{
1225              "a":1,
1226                    "b"  :  "spaced"  ,
1227                "c":   3
1228            }"#
1229            .unindent(),
1230        );
1231    }
1232
1233    #[test]
1234    fn object_replace_array() {
1235        // Tests replacing values within arrays that are nested inside objects.
1236        // Uses "#N" syntax in key paths to indicate array indices.
1237        #[track_caller]
1238        fn check_object_replace_array(
1239            input: String,
1240            key_path: &[&str],
1241            value: Option<Value>,
1242            expected: String,
1243        ) {
1244            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
1245            let mut result_str = input;
1246            result_str.replace_range(result.0, &result.1);
1247            pretty_assertions::assert_eq!(expected, result_str);
1248        }
1249
1250        // Basic array element replacement
1251        check_object_replace_array(
1252            r#"{
1253                "a": [1, 3],
1254            }"#
1255            .unindent(),
1256            &["a", "#1"],
1257            Some(json!(2)),
1258            r#"{
1259                "a": [1, 2],
1260            }"#
1261            .unindent(),
1262        );
1263
1264        // Replace first element
1265        check_object_replace_array(
1266            r#"{
1267                "items": [1, 2, 3]
1268            }"#
1269            .unindent(),
1270            &["items", "#0"],
1271            Some(json!(10)),
1272            r#"{
1273                "items": [10, 2, 3]
1274            }"#
1275            .unindent(),
1276        );
1277
1278        // Replace last element
1279        check_object_replace_array(
1280            r#"{
1281                "items": [1, 2, 3]
1282            }"#
1283            .unindent(),
1284            &["items", "#2"],
1285            Some(json!(30)),
1286            r#"{
1287                "items": [1, 2, 30]
1288            }"#
1289            .unindent(),
1290        );
1291
1292        // Replace string in array
1293        check_object_replace_array(
1294            r#"{
1295                "names": ["alice", "bob", "charlie"]
1296            }"#
1297            .unindent(),
1298            &["names", "#1"],
1299            Some(json!("robert")),
1300            r#"{
1301                "names": ["alice", "robert", "charlie"]
1302            }"#
1303            .unindent(),
1304        );
1305
1306        // Replace boolean
1307        check_object_replace_array(
1308            r#"{
1309                "flags": [true, false, true]
1310            }"#
1311            .unindent(),
1312            &["flags", "#0"],
1313            Some(json!(false)),
1314            r#"{
1315                "flags": [false, false, true]
1316            }"#
1317            .unindent(),
1318        );
1319
1320        // Replace null with value
1321        check_object_replace_array(
1322            r#"{
1323                "values": [null, 2, null]
1324            }"#
1325            .unindent(),
1326            &["values", "#0"],
1327            Some(json!(1)),
1328            r#"{
1329                "values": [1, 2, null]
1330            }"#
1331            .unindent(),
1332        );
1333
1334        // Replace value with null
1335        check_object_replace_array(
1336            r#"{
1337                "data": [1, 2, 3]
1338            }"#
1339            .unindent(),
1340            &["data", "#1"],
1341            Some(json!(null)),
1342            r#"{
1343                "data": [1, null, 3]
1344            }"#
1345            .unindent(),
1346        );
1347
1348        // Replace simple value with object
1349        check_object_replace_array(
1350            r#"{
1351                "list": [1, 2, 3]
1352            }"#
1353            .unindent(),
1354            &["list", "#1"],
1355            Some(json!({"value": 2, "label": "two"})),
1356            r#"{
1357                "list": [1, { "value": 2, "label": "two" }, 3]
1358            }"#
1359            .unindent(),
1360        );
1361
1362        // Replace simple value with nested array
1363        check_object_replace_array(
1364            r#"{
1365                "matrix": [1, 2, 3]
1366            }"#
1367            .unindent(),
1368            &["matrix", "#1"],
1369            Some(json!([20, 21, 22])),
1370            r#"{
1371                "matrix": [1, [ 20, 21, 22 ], 3]
1372            }"#
1373            .unindent(),
1374        );
1375
1376        // Replace object in array
1377        check_object_replace_array(
1378            r#"{
1379                "users": [
1380                    {"name": "alice"},
1381                    {"name": "bob"},
1382                    {"name": "charlie"}
1383                ]
1384            }"#
1385            .unindent(),
1386            &["users", "#1"],
1387            Some(json!({"name": "robert", "age": 30})),
1388            r#"{
1389                "users": [
1390                    {"name": "alice"},
1391                    { "name": "robert", "age": 30 },
1392                    {"name": "charlie"}
1393                ]
1394            }"#
1395            .unindent(),
1396        );
1397
1398        // Replace property within object in array
1399        check_object_replace_array(
1400            r#"{
1401                "users": [
1402                    {"name": "alice", "age": 25},
1403                    {"name": "bob", "age": 30},
1404                    {"name": "charlie", "age": 35}
1405                ]
1406            }"#
1407            .unindent(),
1408            &["users", "#1", "age"],
1409            Some(json!(31)),
1410            r#"{
1411                "users": [
1412                    {"name": "alice", "age": 25},
1413                    {"name": "bob", "age": 31},
1414                    {"name": "charlie", "age": 35}
1415                ]
1416            }"#
1417            .unindent(),
1418        );
1419
1420        // Add new property to object in array
1421        check_object_replace_array(
1422            r#"{
1423                "items": [
1424                    {"id": 1},
1425                    {"id": 2},
1426                    {"id": 3}
1427                ]
1428            }"#
1429            .unindent(),
1430            &["items", "#1", "name"],
1431            Some(json!("Item Two")),
1432            r#"{
1433                "items": [
1434                    {"id": 1},
1435                    {"name": "Item Two", "id": 2},
1436                    {"id": 3}
1437                ]
1438            }"#
1439            .unindent(),
1440        );
1441
1442        // Remove property from object in array
1443        check_object_replace_array(
1444            r#"{
1445                "items": [
1446                    {"id": 1, "name": "one"},
1447                    {"id": 2, "name": "two"},
1448                    {"id": 3, "name": "three"}
1449                ]
1450            }"#
1451            .unindent(),
1452            &["items", "#1", "name"],
1453            None,
1454            r#"{
1455                "items": [
1456                    {"id": 1, "name": "one"},
1457                    {"id": 2},
1458                    {"id": 3, "name": "three"}
1459                ]
1460            }"#
1461            .unindent(),
1462        );
1463
1464        // Deeply nested: array in object in array
1465        check_object_replace_array(
1466            r#"{
1467                "data": [
1468                    {
1469                        "values": [1, 2, 3]
1470                    },
1471                    {
1472                        "values": [4, 5, 6]
1473                    }
1474                ]
1475            }"#
1476            .unindent(),
1477            &["data", "#0", "values", "#1"],
1478            Some(json!(20)),
1479            r#"{
1480                "data": [
1481                    {
1482                        "values": [1, 20, 3]
1483                    },
1484                    {
1485                        "values": [4, 5, 6]
1486                    }
1487                ]
1488            }"#
1489            .unindent(),
1490        );
1491
1492        // Multiple levels of nesting
1493        check_object_replace_array(
1494            r#"{
1495                "root": {
1496                    "level1": [
1497                        {
1498                            "level2": {
1499                                "level3": [10, 20, 30]
1500                            }
1501                        }
1502                    ]
1503                }
1504            }"#
1505            .unindent(),
1506            &["root", "level1", "#0", "level2", "level3", "#2"],
1507            Some(json!(300)),
1508            r#"{
1509                "root": {
1510                    "level1": [
1511                        {
1512                            "level2": {
1513                                "level3": [10, 20, 300]
1514                            }
1515                        }
1516                    ]
1517                }
1518            }"#
1519            .unindent(),
1520        );
1521
1522        // Array with mixed types
1523        check_object_replace_array(
1524            r#"{
1525                "mixed": [1, "two", true, null, {"five": 5}]
1526            }"#
1527            .unindent(),
1528            &["mixed", "#3"],
1529            Some(json!({"four": 4})),
1530            r#"{
1531                "mixed": [1, "two", true, { "four": 4 }, {"five": 5}]
1532            }"#
1533            .unindent(),
1534        );
1535
1536        // Replace with complex object
1537        check_object_replace_array(
1538            r#"{
1539                "config": [
1540                    "simple",
1541                    "values"
1542                ]
1543            }"#
1544            .unindent(),
1545            &["config", "#0"],
1546            Some(json!({
1547                "type": "complex",
1548                "settings": {
1549                    "enabled": true,
1550                    "level": 5
1551                }
1552            })),
1553            r#"{
1554                "config": [
1555                    {
1556                        "type": "complex",
1557                        "settings": {
1558                            "enabled": true,
1559                            "level": 5
1560                        }
1561                    },
1562                    "values"
1563                ]
1564            }"#
1565            .unindent(),
1566        );
1567
1568        // Array with trailing comma
1569        check_object_replace_array(
1570            r#"{
1571                "items": [
1572                    1,
1573                    2,
1574                    3,
1575                ]
1576            }"#
1577            .unindent(),
1578            &["items", "#1"],
1579            Some(json!(20)),
1580            r#"{
1581                "items": [
1582                    1,
1583                    20,
1584                    3,
1585                ]
1586            }"#
1587            .unindent(),
1588        );
1589
1590        // Array with comments
1591        check_object_replace_array(
1592            r#"{
1593                "items": [
1594                    1, // first item
1595                    2, // second item
1596                    3  // third item
1597                ]
1598            }"#
1599            .unindent(),
1600            &["items", "#1"],
1601            Some(json!(20)),
1602            r#"{
1603                "items": [
1604                    1, // first item
1605                    20, // second item
1606                    3  // third item
1607                ]
1608            }"#
1609            .unindent(),
1610        );
1611
1612        // Multiple arrays in object
1613        check_object_replace_array(
1614            r#"{
1615                "first": [1, 2, 3],
1616                "second": [4, 5, 6],
1617                "third": [7, 8, 9]
1618            }"#
1619            .unindent(),
1620            &["second", "#1"],
1621            Some(json!(50)),
1622            r#"{
1623                "first": [1, 2, 3],
1624                "second": [4, 50, 6],
1625                "third": [7, 8, 9]
1626            }"#
1627            .unindent(),
1628        );
1629
1630        // Empty array - add first element
1631        check_object_replace_array(
1632            r#"{
1633                "empty": []
1634            }"#
1635            .unindent(),
1636            &["empty", "#0"],
1637            Some(json!("first")),
1638            r#"{
1639                "empty": ["first"]
1640            }"#
1641            .unindent(),
1642        );
1643
1644        // Array of arrays
1645        check_object_replace_array(
1646            r#"{
1647                "matrix": [
1648                    [1, 2],
1649                    [3, 4],
1650                    [5, 6]
1651                ]
1652            }"#
1653            .unindent(),
1654            &["matrix", "#1", "#0"],
1655            Some(json!(30)),
1656            r#"{
1657                "matrix": [
1658                    [1, 2],
1659                    [30, 4],
1660                    [5, 6]
1661                ]
1662            }"#
1663            .unindent(),
1664        );
1665
1666        // Replace nested object property in array element
1667        check_object_replace_array(
1668            r#"{
1669                "users": [
1670                    {
1671                        "name": "alice",
1672                        "address": {
1673                            "city": "NYC",
1674                            "zip": "10001"
1675                        }
1676                    }
1677                ]
1678            }"#
1679            .unindent(),
1680            &["users", "#0", "address", "city"],
1681            Some(json!("Boston")),
1682            r#"{
1683                "users": [
1684                    {
1685                        "name": "alice",
1686                        "address": {
1687                            "city": "Boston",
1688                            "zip": "10001"
1689                        }
1690                    }
1691                ]
1692            }"#
1693            .unindent(),
1694        );
1695
1696        // Add element past end of array
1697        check_object_replace_array(
1698            r#"{
1699                "items": [1, 2]
1700            }"#
1701            .unindent(),
1702            &["items", "#5"],
1703            Some(json!(6)),
1704            r#"{
1705                "items": [1, 2, 6]
1706            }"#
1707            .unindent(),
1708        );
1709
1710        // Complex nested structure
1711        check_object_replace_array(
1712            r#"{
1713                "app": {
1714                    "modules": [
1715                        {
1716                            "name": "auth",
1717                            "routes": [
1718                                {"path": "/login", "method": "POST"},
1719                                {"path": "/logout", "method": "POST"}
1720                            ]
1721                        },
1722                        {
1723                            "name": "api",
1724                            "routes": [
1725                                {"path": "/users", "method": "GET"},
1726                                {"path": "/users", "method": "POST"}
1727                            ]
1728                        }
1729                    ]
1730                }
1731            }"#
1732            .unindent(),
1733            &["app", "modules", "#1", "routes", "#0", "method"],
1734            Some(json!("PUT")),
1735            r#"{
1736                "app": {
1737                    "modules": [
1738                        {
1739                            "name": "auth",
1740                            "routes": [
1741                                {"path": "/login", "method": "POST"},
1742                                {"path": "/logout", "method": "POST"}
1743                            ]
1744                        },
1745                        {
1746                            "name": "api",
1747                            "routes": [
1748                                {"path": "/users", "method": "PUT"},
1749                                {"path": "/users", "method": "POST"}
1750                            ]
1751                        }
1752                    ]
1753                }
1754            }"#
1755            .unindent(),
1756        );
1757
1758        // Escaped strings in array
1759        check_object_replace_array(
1760            r#"{
1761                "messages": ["hello", "world"]
1762            }"#
1763            .unindent(),
1764            &["messages", "#0"],
1765            Some(json!("hello \"quoted\" world")),
1766            r#"{
1767                "messages": ["hello \"quoted\" world", "world"]
1768            }"#
1769            .unindent(),
1770        );
1771
1772        // Block comments
1773        check_object_replace_array(
1774            r#"{
1775                "data": [
1776                    /* first */ 1,
1777                    /* second */ 2,
1778                    /* third */ 3
1779                ]
1780            }"#
1781            .unindent(),
1782            &["data", "#1"],
1783            Some(json!(20)),
1784            r#"{
1785                "data": [
1786                    /* first */ 1,
1787                    /* second */ 20,
1788                    /* third */ 3
1789                ]
1790            }"#
1791            .unindent(),
1792        );
1793
1794        // Inline array
1795        check_object_replace_array(
1796            r#"{"items": [1, 2, 3], "count": 3}"#.to_string(),
1797            &["items", "#1"],
1798            Some(json!(20)),
1799            r#"{"items": [1, 20, 3], "count": 3}"#.to_string(),
1800        );
1801
1802        // Single element array
1803        check_object_replace_array(
1804            r#"{
1805                "single": [42]
1806            }"#
1807            .unindent(),
1808            &["single", "#0"],
1809            Some(json!(100)),
1810            r#"{
1811                "single": [100]
1812            }"#
1813            .unindent(),
1814        );
1815
1816        // Inconsistent formatting
1817        check_object_replace_array(
1818            r#"{
1819                "messy": [1,
1820                    2,
1821                        3,
1822                4]
1823            }"#
1824            .unindent(),
1825            &["messy", "#2"],
1826            Some(json!(30)),
1827            r#"{
1828                "messy": [1,
1829                    2,
1830                        30,
1831                4]
1832            }"#
1833            .unindent(),
1834        );
1835
1836        // Creates array if has numbered key
1837        check_object_replace_array(
1838            r#"{
1839                "array": {"foo": "bar"}
1840            }"#
1841            .unindent(),
1842            &["array", "#3"],
1843            Some(json!(4)),
1844            r#"{
1845                "array": [
1846                    4
1847                ]
1848            }"#
1849            .unindent(),
1850        );
1851
1852        // Replace non-array element within array with array
1853        check_object_replace_array(
1854            r#"{
1855                "matrix": [
1856                    [1, 2],
1857                    [3, 4],
1858                    [5, 6]
1859                ]
1860            }"#
1861            .unindent(),
1862            &["matrix", "#1", "#0"],
1863            Some(json!(["foo", "bar"])),
1864            r#"{
1865                "matrix": [
1866                    [1, 2],
1867                    [[ "foo", "bar" ], 4],
1868                    [5, 6]
1869                ]
1870            }"#
1871            .unindent(),
1872        );
1873        // Replace non-array element within array with array
1874        check_object_replace_array(
1875            r#"{
1876                "matrix": [
1877                    [1, 2],
1878                    [3, 4],
1879                    [5, 6]
1880                ]
1881            }"#
1882            .unindent(),
1883            &["matrix", "#1", "#0", "#3"],
1884            Some(json!(["foo", "bar"])),
1885            r#"{
1886                "matrix": [
1887                    [1, 2],
1888                    [[ [ "foo", "bar" ] ], 4],
1889                    [5, 6]
1890                ]
1891            }"#
1892            .unindent(),
1893        );
1894
1895        // Create array in key that doesn't exist
1896        check_object_replace_array(
1897            r#"{
1898                "foo": {}
1899            }"#
1900            .unindent(),
1901            &["foo", "bar", "#0"],
1902            Some(json!({"is_object": true})),
1903            r#"{
1904                "foo": {
1905                    "bar": [
1906                        {
1907                            "is_object": true
1908                        }
1909                    ]
1910                }
1911            }"#
1912            .unindent(),
1913        );
1914    }
1915
1916    #[test]
1917    fn array_replace() {
1918        #[track_caller]
1919        fn check_array_replace(
1920            input: impl ToString,
1921            index: usize,
1922            key_path: &[&str],
1923            value: Option<Value>,
1924            expected: impl ToString,
1925        ) {
1926            let input = input.to_string();
1927            let result = replace_top_level_array_value_in_json_text(
1928                &input,
1929                key_path,
1930                value.as_ref(),
1931                None,
1932                index,
1933                4,
1934            );
1935            let mut result_str = input;
1936            result_str.replace_range(result.0, &result.1);
1937            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1938        }
1939
1940        check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1941        check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1942        check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1943        check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1944        check_array_replace(
1945            r#"[
1946                1,
1947                2,
1948                3,
1949            ]"#
1950            .unindent(),
1951            1,
1952            &[],
1953            Some(json!({"foo": "bar", "baz": "qux"})),
1954            r#"[
1955                1,
1956                {
1957                    "foo": "bar",
1958                    "baz": "qux"
1959                },
1960                3,
1961            ]"#
1962            .unindent(),
1963        );
1964        check_array_replace(
1965            r#"[1, 3, 3,]"#,
1966            1,
1967            &[],
1968            Some(json!({"foo": "bar", "baz": "qux"})),
1969            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1970        );
1971
1972        check_array_replace(
1973            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1974            1,
1975            &["baz"],
1976            Some(json!({"qux": "quz"})),
1977            r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1978        );
1979
1980        check_array_replace(
1981            r#"[
1982                1,
1983                {
1984                    "foo": "bar",
1985                    "baz": "qux"
1986                },
1987                3
1988            ]"#,
1989            1,
1990            &["baz"],
1991            Some(json!({"qux": "quz"})),
1992            r#"[
1993                1,
1994                {
1995                    "foo": "bar",
1996                    "baz": {
1997                        "qux": "quz"
1998                    }
1999                },
2000                3
2001            ]"#,
2002        );
2003
2004        check_array_replace(
2005            r#"[
2006                1,
2007                {
2008                    "foo": "bar",
2009                    "baz": {
2010                        "qux": "quz"
2011                    }
2012                },
2013                3
2014            ]"#,
2015            1,
2016            &["baz"],
2017            Some(json!("qux")),
2018            r#"[
2019                1,
2020                {
2021                    "foo": "bar",
2022                    "baz": "qux"
2023                },
2024                3
2025            ]"#,
2026        );
2027
2028        check_array_replace(
2029            r#"[
2030                1,
2031                {
2032                    "foo": "bar",
2033                    // some comment to keep
2034                    "baz": {
2035                        // some comment to remove
2036                        "qux": "quz"
2037                    }
2038                    // some other comment to keep
2039                },
2040                3
2041            ]"#,
2042            1,
2043            &["baz"],
2044            Some(json!("qux")),
2045            r#"[
2046                1,
2047                {
2048                    "foo": "bar",
2049                    // some comment to keep
2050                    "baz": "qux"
2051                    // some other comment to keep
2052                },
2053                3
2054            ]"#,
2055        );
2056
2057        // Test with comments between array elements
2058        check_array_replace(
2059            r#"[
2060                1,
2061                // This is element 2
2062                2,
2063                /* Block comment */ 3,
2064                4 // Trailing comment
2065            ]"#,
2066            2,
2067            &[],
2068            Some(json!("replaced")),
2069            r#"[
2070                1,
2071                // This is element 2
2072                2,
2073                /* Block comment */ "replaced",
2074                4 // Trailing comment
2075            ]"#,
2076        );
2077
2078        // Test empty array with comments
2079        check_array_replace(
2080            r#"[
2081                // Empty array with comment
2082            ]"#
2083            .unindent(),
2084            0,
2085            &[],
2086            Some(json!("first")),
2087            r#"[
2088                // Empty array with comment
2089                "first"
2090            ]"#
2091            .unindent(),
2092        );
2093        check_array_replace(
2094            r#"[]"#.unindent(),
2095            0,
2096            &[],
2097            Some(json!("first")),
2098            r#"["first"]"#.unindent(),
2099        );
2100
2101        // Test array with leading comments
2102        check_array_replace(
2103            r#"[
2104                // Leading comment
2105                // Another leading comment
2106                1,
2107                2
2108            ]"#,
2109            0,
2110            &[],
2111            Some(json!({"new": "object"})),
2112            r#"[
2113                // Leading comment
2114                // Another leading comment
2115                {
2116                    "new": "object"
2117                },
2118                2
2119            ]"#,
2120        );
2121
2122        // Test with deep indentation
2123        check_array_replace(
2124            r#"[
2125                        1,
2126                        2,
2127                        3
2128                    ]"#,
2129            1,
2130            &[],
2131            Some(json!("deep")),
2132            r#"[
2133                        1,
2134                        "deep",
2135                        3
2136                    ]"#,
2137        );
2138
2139        // Test with mixed spacing
2140        check_array_replace(
2141            r#"[1,2,   3,    4]"#,
2142            2,
2143            &[],
2144            Some(json!("spaced")),
2145            r#"[1,2,   "spaced",    4]"#,
2146        );
2147
2148        // Test replacing nested array element
2149        check_array_replace(
2150            r#"[
2151                [1, 2, 3],
2152                [4, 5, 6],
2153                [7, 8, 9]
2154            ]"#,
2155            1,
2156            &[],
2157            Some(json!(["a", "b", "c", "d"])),
2158            r#"[
2159                [1, 2, 3],
2160                [
2161                    "a",
2162                    "b",
2163                    "c",
2164                    "d"
2165                ],
2166                [7, 8, 9]
2167            ]"#,
2168        );
2169
2170        // Test with multiline block comments
2171        check_array_replace(
2172            r#"[
2173                /*
2174                 * This is a
2175                 * multiline comment
2176                 */
2177                "first",
2178                "second"
2179            ]"#,
2180            0,
2181            &[],
2182            Some(json!("updated")),
2183            r#"[
2184                /*
2185                 * This is a
2186                 * multiline comment
2187                 */
2188                "updated",
2189                "second"
2190            ]"#,
2191        );
2192
2193        // Test replacing with null
2194        check_array_replace(
2195            r#"[true, false, true]"#,
2196            1,
2197            &[],
2198            Some(json!(null)),
2199            r#"[true, null, true]"#,
2200        );
2201
2202        // Test single element array
2203        check_array_replace(
2204            r#"[42]"#,
2205            0,
2206            &[],
2207            Some(json!({"answer": 42})),
2208            r#"[{ "answer": 42 }]"#,
2209        );
2210
2211        // Test array with only comments
2212        check_array_replace(
2213            r#"[
2214                // Comment 1
2215                // Comment 2
2216                // Comment 3
2217            ]"#
2218            .unindent(),
2219            10,
2220            &[],
2221            Some(json!(123)),
2222            r#"[
2223                // Comment 1
2224                // Comment 2
2225                // Comment 3
2226                123
2227            ]"#
2228            .unindent(),
2229        );
2230
2231        check_array_replace(
2232            r#"[
2233                {
2234                    "key": "value"
2235                },
2236                {
2237                    "key": "value2"
2238                }
2239            ]"#
2240            .unindent(),
2241            0,
2242            &[],
2243            None,
2244            r#"[
2245                {
2246                    "key": "value2"
2247                }
2248            ]"#
2249            .unindent(),
2250        );
2251
2252        check_array_replace(
2253            r#"[
2254                {
2255                    "key": "value"
2256                },
2257                {
2258                    "key": "value2"
2259                },
2260                {
2261                    "key": "value3"
2262                },
2263            ]"#
2264            .unindent(),
2265            1,
2266            &[],
2267            None,
2268            r#"[
2269                {
2270                    "key": "value"
2271                },
2272                {
2273                    "key": "value3"
2274                },
2275            ]"#
2276            .unindent(),
2277        );
2278
2279        check_array_replace(
2280            r#""#,
2281            2,
2282            &[],
2283            Some(json!(42)),
2284            r#"[
2285                42
2286            ]"#
2287            .unindent(),
2288        );
2289
2290        check_array_replace(
2291            r#""#,
2292            2,
2293            &["foo", "bar"],
2294            Some(json!(42)),
2295            r#"[
2296                {
2297                    "foo": {
2298                        "bar": 42
2299                    }
2300                }
2301            ]"#
2302            .unindent(),
2303        );
2304    }
2305
2306    #[test]
2307    fn array_append() {
2308        #[track_caller]
2309        fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
2310            let input = input.to_string();
2311            let result = append_top_level_array_value_in_json_text(&input, &value, 4);
2312            let mut result_str = input;
2313            result_str.replace_range(result.0, &result.1);
2314            pretty_assertions::assert_eq!(expected.to_string(), result_str);
2315        }
2316        check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
2317        check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
2318        check_array_append(r#"[1, 3, 3   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2319        check_array_append(r#"[1, 3, 3,   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2320        check_array_append(
2321            r#"[
2322                1,
2323                2,
2324                3
2325            ]"#
2326            .unindent(),
2327            json!(4),
2328            r#"[
2329                1,
2330                2,
2331                3,
2332                4
2333            ]"#
2334            .unindent(),
2335        );
2336        check_array_append(
2337            r#"[
2338                1,
2339                2,
2340                3,
2341            ]"#
2342            .unindent(),
2343            json!(4),
2344            r#"[
2345                1,
2346                2,
2347                3,
2348                4
2349            ]"#
2350            .unindent(),
2351        );
2352        check_array_append(
2353            r#"[
2354                1,
2355                2,
2356                3,
2357            ]"#
2358            .unindent(),
2359            json!({"foo": "bar", "baz": "qux"}),
2360            r#"[
2361                1,
2362                2,
2363                3,
2364                {
2365                    "foo": "bar",
2366                    "baz": "qux"
2367                }
2368            ]"#
2369            .unindent(),
2370        );
2371        check_array_append(
2372            r#"[ 1, 2, 3, ]"#.unindent(),
2373            json!({"foo": "bar", "baz": "qux"}),
2374            r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
2375        );
2376        check_array_append(
2377            r#"[]"#,
2378            json!({"foo": "bar"}),
2379            r#"[
2380                {
2381                    "foo": "bar"
2382                }
2383            ]"#
2384            .unindent(),
2385        );
2386
2387        // Test with comments between array elements
2388        check_array_append(
2389            r#"[
2390                1,
2391                // Comment between elements
2392                2,
2393                /* Block comment */ 3
2394            ]"#
2395            .unindent(),
2396            json!(4),
2397            r#"[
2398                1,
2399                // Comment between elements
2400                2,
2401                /* Block comment */ 3,
2402                4
2403            ]"#
2404            .unindent(),
2405        );
2406
2407        // Test with trailing comment on last element
2408        check_array_append(
2409            r#"[
2410                1,
2411                2,
2412                3 // Trailing comment
2413            ]"#
2414            .unindent(),
2415            json!("new"),
2416            r#"[
2417                1,
2418                2,
2419                3 // Trailing comment
2420            ,
2421                "new"
2422            ]"#
2423            .unindent(),
2424        );
2425
2426        // Test empty array with comments
2427        check_array_append(
2428            r#"[
2429                // Empty array with comment
2430            ]"#
2431            .unindent(),
2432            json!("first"),
2433            r#"[
2434                // Empty array with comment
2435                "first"
2436            ]"#
2437            .unindent(),
2438        );
2439
2440        // Test with multiline block comment at end
2441        check_array_append(
2442            r#"[
2443                1,
2444                2
2445                /*
2446                 * This is a
2447                 * multiline comment
2448                 */
2449            ]"#
2450            .unindent(),
2451            json!(3),
2452            r#"[
2453                1,
2454                2
2455                /*
2456                 * This is a
2457                 * multiline comment
2458                 */
2459            ,
2460                3
2461            ]"#
2462            .unindent(),
2463        );
2464
2465        // Test with deep indentation
2466        check_array_append(
2467            r#"[
2468                1,
2469                    2,
2470                        3
2471            ]"#
2472            .unindent(),
2473            json!("deep"),
2474            r#"[
2475                1,
2476                    2,
2477                        3,
2478                        "deep"
2479            ]"#
2480            .unindent(),
2481        );
2482
2483        // Test with no spacing
2484        check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
2485
2486        // Test appending complex nested structure
2487        check_array_append(
2488            r#"[
2489                {"a": 1},
2490                {"b": 2}
2491            ]"#
2492            .unindent(),
2493            json!({"c": {"nested": [1, 2, 3]}}),
2494            r#"[
2495                {"a": 1},
2496                {"b": 2},
2497                {
2498                    "c": {
2499                        "nested": [
2500                            1,
2501                            2,
2502                            3
2503                        ]
2504                    }
2505                }
2506            ]"#
2507            .unindent(),
2508        );
2509
2510        // Test array ending with comment after bracket
2511        check_array_append(
2512            r#"[
2513                1,
2514                2,
2515                3
2516            ] // Comment after array"#
2517                .unindent(),
2518            json!(4),
2519            r#"[
2520                1,
2521                2,
2522                3,
2523                4
2524            ] // Comment after array"#
2525                .unindent(),
2526        );
2527
2528        // Test with inconsistent element formatting
2529        check_array_append(
2530            r#"[1,
2531               2,
2532                    3,
2533            ]"#
2534            .unindent(),
2535            json!(4),
2536            r#"[1,
2537               2,
2538                    3,
2539                    4
2540            ]"#
2541            .unindent(),
2542        );
2543
2544        // Test appending to single-line array with trailing comma
2545        check_array_append(
2546            r#"[1, 2, 3,]"#,
2547            json!({"key": "value"}),
2548            r#"[1, 2, 3, { "key": "value" }]"#,
2549        );
2550
2551        // Test appending null value
2552        check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
2553
2554        // Test appending to array with only comments
2555        check_array_append(
2556            r#"[
2557                // Just comments here
2558                // More comments
2559            ]"#
2560            .unindent(),
2561            json!(42),
2562            r#"[
2563                // Just comments here
2564                // More comments
2565                42
2566            ]"#
2567            .unindent(),
2568        );
2569
2570        check_array_append(
2571            r#""#,
2572            json!(42),
2573            r#"[
2574                42
2575            ]"#
2576            .unindent(),
2577        )
2578    }
2579
2580    #[test]
2581    fn test_infer_json_indent_size() {
2582        let json_2_spaces = r#"{
2583  "key1": "value1",
2584  "nested": {
2585    "key2": "value2",
2586    "array": [
2587      1,
2588      2,
2589      3
2590    ]
2591  }
2592}"#;
2593        assert_eq!(infer_json_indent_size(json_2_spaces), 2);
2594
2595        let json_4_spaces = r#"{
2596    "key1": "value1",
2597    "nested": {
2598        "key2": "value2",
2599        "array": [
2600            1,
2601            2,
2602            3
2603        ]
2604    }
2605}"#;
2606        assert_eq!(infer_json_indent_size(json_4_spaces), 4);
2607
2608        let json_8_spaces = r#"{
2609        "key1": "value1",
2610        "nested": {
2611                "key2": "value2"
2612        }
2613}"#;
2614        assert_eq!(infer_json_indent_size(json_8_spaces), 8);
2615
2616        let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#;
2617        assert_eq!(infer_json_indent_size(json_single_line), 2);
2618
2619        let json_empty = r#"{}"#;
2620        assert_eq!(infer_json_indent_size(json_empty), 2);
2621
2622        let json_array = r#"[
2623  {
2624    "id": 1,
2625    "name": "first"
2626  },
2627  {
2628    "id": 2,
2629    "name": "second"
2630  }
2631]"#;
2632        assert_eq!(infer_json_indent_size(json_array), 2);
2633
2634        let json_mixed = r#"{
2635  "a": {
2636    "b": {
2637        "c": "value"
2638    }
2639  },
2640  "d": "value2"
2641}"#;
2642        assert_eq!(infer_json_indent_size(json_mixed), 2);
2643    }
2644}