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