settings_json.rs

   1use anyhow::Result;
   2use gpui::App;
   3use serde::{Serialize, de::DeserializeOwned};
   4use serde_json::Value;
   5use std::{ops::Range, sync::LazyLock};
   6use tree_sitter::{Query, StreamingIterator as _};
   7use util::RangeExt;
   8
   9/// Parameters that are used when generating some JSON schemas at runtime.
  10pub struct SettingsJsonSchemaParams<'a> {
  11    pub language_names: &'a [String],
  12    pub font_names: &'a [String],
  13}
  14
  15/// Value registered which specifies JSON schemas that are generated at runtime.
  16pub struct ParameterizedJsonSchema {
  17    pub add_and_get_ref:
  18        fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema,
  19}
  20
  21inventory::collect!(ParameterizedJsonSchema);
  22
  23pub fn update_value_in_json_text<'a>(
  24    text: &mut String,
  25    key_path: &mut Vec<&'a str>,
  26    tab_size: usize,
  27    old_value: &'a Value,
  28    new_value: &'a Value,
  29    preserved_keys: &[&str],
  30    edits: &mut Vec<(Range<usize>, String)>,
  31) {
  32    // If the old and new values are both objects, then compare them key by key,
  33    // preserving the comments and formatting of the unchanged parts. Otherwise,
  34    // replace the old value with the new value.
  35    if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
  36        for (key, old_sub_value) in old_object.iter() {
  37            key_path.push(key);
  38            if let Some(new_sub_value) = new_object.get(key) {
  39                // Key exists in both old and new, recursively update
  40                update_value_in_json_text(
  41                    text,
  42                    key_path,
  43                    tab_size,
  44                    old_sub_value,
  45                    new_sub_value,
  46                    preserved_keys,
  47                    edits,
  48                );
  49            } else {
  50                // Key was removed from new object, remove the entire key-value pair
  51                let (range, replacement) =
  52                    replace_value_in_json_text(text, key_path, 0, None, None);
  53                text.replace_range(range.clone(), &replacement);
  54                edits.push((range, replacement));
  55            }
  56            key_path.pop();
  57        }
  58        for (key, new_sub_value) in new_object.iter() {
  59            key_path.push(key);
  60            if !old_object.contains_key(key) {
  61                update_value_in_json_text(
  62                    text,
  63                    key_path,
  64                    tab_size,
  65                    &Value::Null,
  66                    new_sub_value,
  67                    preserved_keys,
  68                    edits,
  69                );
  70            }
  71            key_path.pop();
  72        }
  73    } else if key_path
  74        .last()
  75        .map_or(false, |key| preserved_keys.contains(key))
  76        || old_value != new_value
  77    {
  78        let mut new_value = new_value.clone();
  79        if let Some(new_object) = new_value.as_object_mut() {
  80            new_object.retain(|_, v| !v.is_null());
  81        }
  82        let (range, replacement) =
  83            replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None);
  84        text.replace_range(range.clone(), &replacement);
  85        edits.push((range, replacement));
  86    }
  87}
  88
  89/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`.
  90fn replace_value_in_json_text(
  91    text: &str,
  92    key_path: &[&str],
  93    tab_size: usize,
  94    new_value: Option<&Value>,
  95    replace_key: Option<&str>,
  96) -> (Range<usize>, String) {
  97    static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
  98        Query::new(
  99            &tree_sitter_json::LANGUAGE.into(),
 100            "(pair key: (string) @key value: (_) @value)",
 101        )
 102        .expect("Failed to create PAIR_QUERY")
 103    });
 104
 105    let mut parser = tree_sitter::Parser::new();
 106    parser
 107        .set_language(&tree_sitter_json::LANGUAGE.into())
 108        .unwrap();
 109    let syntax_tree = parser.parse(text, None).unwrap();
 110
 111    let mut cursor = tree_sitter::QueryCursor::new();
 112
 113    let mut depth = 0;
 114    let mut last_value_range = 0..0;
 115    let mut first_key_start = None;
 116    let mut existing_value_range = 0..text.len();
 117
 118    let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
 119    while let Some(mat) = matches.next() {
 120        if mat.captures.len() != 2 {
 121            continue;
 122        }
 123
 124        let key_range = mat.captures[0].node.byte_range();
 125        let value_range = mat.captures[1].node.byte_range();
 126
 127        // Don't enter sub objects until we find an exact
 128        // match for the current keypath
 129        if last_value_range.contains_inclusive(&value_range) {
 130            continue;
 131        }
 132
 133        last_value_range = value_range.clone();
 134
 135        if key_range.start > existing_value_range.end {
 136            break;
 137        }
 138
 139        first_key_start.get_or_insert(key_range.start);
 140
 141        let found_key = text
 142            .get(key_range.clone())
 143            .map(|key_text| {
 144                depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth])
 145            })
 146            .unwrap_or(false);
 147
 148        if found_key {
 149            existing_value_range = value_range;
 150            // Reset last value range when increasing in depth
 151            last_value_range = existing_value_range.start..existing_value_range.start;
 152            depth += 1;
 153
 154            if depth == key_path.len() {
 155                break;
 156            }
 157
 158            first_key_start = None;
 159        }
 160    }
 161
 162    // We found the exact key we want
 163    if depth == key_path.len() {
 164        if let Some(new_value) = new_value {
 165            let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
 166            if let Some(replace_key) = replace_key {
 167                let new_key = format!("\"{}\": ", replace_key);
 168                if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
 169                    if let Some(prev_key_start) = text[..key_start].rfind('"') {
 170                        existing_value_range.start = prev_key_start;
 171                    } else {
 172                        existing_value_range.start = key_start;
 173                    }
 174                }
 175                (existing_value_range, new_key + &new_val)
 176            } else {
 177                (existing_value_range, new_val)
 178            }
 179        } else {
 180            let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
 181            let mut removal_end = existing_value_range.end;
 182
 183            // Find the actual key position by looking for the key in the pair
 184            // We need to extend the range to include the key, not just the value
 185            if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
 186                if let Some(prev_key_start) = text[..key_start].rfind('"') {
 187                    removal_start = prev_key_start;
 188                } else {
 189                    removal_start = key_start;
 190                }
 191            }
 192
 193            // Look backward for a preceding comma first
 194            let preceding_text = text.get(0..removal_start).unwrap_or("");
 195            if let Some(comma_pos) = preceding_text.rfind(',') {
 196                // Check if there are only whitespace characters between the comma and our key
 197                let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
 198                if between_comma_and_key.trim().is_empty() {
 199                    removal_start = comma_pos;
 200                }
 201            }
 202
 203            if let Some(remaining_text) = text.get(existing_value_range.end..) {
 204                let mut chars = remaining_text.char_indices();
 205                while let Some((offset, ch)) = chars.next() {
 206                    if ch == ',' {
 207                        removal_end = existing_value_range.end + offset + 1;
 208                        // Also consume whitespace after the comma
 209                        while let Some((_, next_ch)) = chars.next() {
 210                            if next_ch.is_whitespace() {
 211                                removal_end += next_ch.len_utf8();
 212                            } else {
 213                                break;
 214                            }
 215                        }
 216                        break;
 217                    } else if !ch.is_whitespace() {
 218                        break;
 219                    }
 220                }
 221            }
 222            (removal_start..removal_end, String::new())
 223        }
 224    } else {
 225        // We have key paths, construct the sub objects
 226        let new_key = key_path[depth];
 227
 228        // We don't have the key, construct the nested objects
 229        let mut new_value =
 230            serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
 231        for key in key_path[(depth + 1)..].iter().rev() {
 232            new_value = serde_json::json!({ key.to_string(): new_value });
 233        }
 234
 235        if let Some(first_key_start) = first_key_start {
 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            new_value = serde_json::json!({ new_key.to_string(): new_value });
 264            let indent_prefix_len = 4 * depth;
 265            let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
 266            if depth == 0 {
 267                new_val.push('\n');
 268            }
 269            // best effort to keep comments with best effort indentation
 270            let mut replace_text = &text[existing_value_range.clone()];
 271            while let Some(comment_start) = replace_text.rfind("//") {
 272                if let Some(comment_end) = replace_text[comment_start..].find('\n') {
 273                    let mut comment_with_indent_start = replace_text[..comment_start]
 274                        .rfind('\n')
 275                        .unwrap_or(comment_start);
 276                    if !replace_text[comment_with_indent_start..comment_start]
 277                        .trim()
 278                        .is_empty()
 279                    {
 280                        comment_with_indent_start = comment_start;
 281                    }
 282                    new_val.insert_str(
 283                        1,
 284                        &replace_text[comment_with_indent_start..comment_start + comment_end],
 285                    );
 286                }
 287                replace_text = &replace_text[..comment_start];
 288            }
 289
 290            (existing_value_range, new_val)
 291        }
 292    }
 293}
 294
 295const TS_DOCUMENT_KIND: &'static str = "document";
 296const TS_ARRAY_KIND: &'static str = "array";
 297const TS_COMMENT_KIND: &'static str = "comment";
 298
 299pub fn replace_top_level_array_value_in_json_text(
 300    text: &str,
 301    key_path: &[&str],
 302    new_value: Option<&Value>,
 303    replace_key: Option<&str>,
 304    array_index: usize,
 305    tab_size: usize,
 306) -> Result<(Range<usize>, String)> {
 307    let mut parser = tree_sitter::Parser::new();
 308    parser
 309        .set_language(&tree_sitter_json::LANGUAGE.into())
 310        .unwrap();
 311    let syntax_tree = parser.parse(text, None).unwrap();
 312
 313    let mut cursor = syntax_tree.walk();
 314
 315    if cursor.node().kind() == TS_DOCUMENT_KIND {
 316        anyhow::ensure!(
 317            cursor.goto_first_child(),
 318            "Document empty - No top level array"
 319        );
 320    }
 321
 322    while cursor.node().kind() != TS_ARRAY_KIND {
 323        anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
 324    }
 325
 326    // false if no children
 327    //
 328    cursor.goto_first_child();
 329    debug_assert_eq!(cursor.node().kind(), "[");
 330
 331    let mut index = 0;
 332
 333    while index <= array_index {
 334        let node = cursor.node();
 335        if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",")
 336            && !node.is_extra()
 337            && !node.is_missing()
 338        {
 339            if index == array_index {
 340                break;
 341            }
 342            index += 1;
 343        }
 344        if !cursor.goto_next_sibling() {
 345            if let Some(new_value) = new_value {
 346                return append_top_level_array_value_in_json_text(text, new_value, tab_size);
 347            } else {
 348                return Ok((0..0, String::new()));
 349            }
 350        }
 351    }
 352
 353    let range = cursor.node().range();
 354    let indent_width = range.start_point.column;
 355    let offset = range.start_byte;
 356    let value_str = &text[range.start_byte..range.end_byte];
 357    let needs_indent = range.start_point.row > 0;
 358
 359    let (mut replace_range, mut replace_value) =
 360        replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
 361
 362    replace_range.start += offset;
 363    replace_range.end += offset;
 364
 365    if needs_indent {
 366        let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
 367        replace_value = replace_value.replace('\n', &increased_indent);
 368        // replace_value.push('\n');
 369    } else {
 370        while let Some(idx) = replace_value.find("\n ") {
 371            replace_value.remove(idx + 1);
 372        }
 373        while let Some(idx) = replace_value.find("\n") {
 374            replace_value.replace_range(idx..idx + 1, " ");
 375        }
 376    }
 377
 378    return Ok((replace_range, replace_value));
 379}
 380
 381pub fn append_top_level_array_value_in_json_text(
 382    text: &str,
 383    new_value: &Value,
 384    tab_size: usize,
 385) -> Result<(Range<usize>, String)> {
 386    let mut parser = tree_sitter::Parser::new();
 387    parser
 388        .set_language(&tree_sitter_json::LANGUAGE.into())
 389        .unwrap();
 390    let syntax_tree = parser.parse(text, None).unwrap();
 391
 392    let mut cursor = syntax_tree.walk();
 393
 394    if cursor.node().kind() == TS_DOCUMENT_KIND {
 395        anyhow::ensure!(
 396            cursor.goto_first_child(),
 397            "Document empty - No top level array"
 398        );
 399    }
 400
 401    while cursor.node().kind() != TS_ARRAY_KIND {
 402        anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
 403    }
 404
 405    anyhow::ensure!(
 406        cursor.goto_last_child(),
 407        "Malformed JSON syntax tree, expected `]` at end of array"
 408    );
 409    debug_assert_eq!(cursor.node().kind(), "]");
 410    let close_bracket_start = cursor.node().start_byte();
 411    cursor.goto_previous_sibling();
 412    while (cursor.node().is_extra() || cursor.node().is_missing()) && cursor.goto_previous_sibling()
 413    {
 414    }
 415
 416    let mut comma_range = None;
 417    let mut prev_item_range = None;
 418
 419    if cursor.node().kind() == "," {
 420        comma_range = Some(cursor.node().byte_range());
 421        while cursor.goto_previous_sibling() && cursor.node().is_extra() {}
 422
 423        debug_assert_ne!(cursor.node().kind(), "[");
 424        prev_item_range = Some(cursor.node().range());
 425    } else {
 426        while (cursor.node().is_extra() || cursor.node().is_missing())
 427            && cursor.goto_previous_sibling()
 428        {}
 429        if cursor.node().kind() != "[" {
 430            prev_item_range = Some(cursor.node().range());
 431        }
 432    }
 433
 434    let (mut replace_range, mut replace_value) =
 435        replace_value_in_json_text("", &[], tab_size, Some(new_value), None);
 436
 437    replace_range.start = close_bracket_start;
 438    replace_range.end = close_bracket_start;
 439
 440    let space = ' ';
 441    if let Some(prev_item_range) = prev_item_range {
 442        let needs_newline = prev_item_range.start_point.row > 0;
 443        let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
 444            prev_item_range.start_point.column,
 445            |idx| {
 446                prev_item_range.start_point.column
 447                    - text[idx + 1..prev_item_range.start_byte].trim_start().len()
 448            },
 449        );
 450
 451        let prev_item_end = comma_range
 452            .as_ref()
 453            .map_or(prev_item_range.end_byte, |range| range.end);
 454        if text[prev_item_end..replace_range.start].trim().is_empty() {
 455            replace_range.start = prev_item_end;
 456        }
 457
 458        if needs_newline {
 459            let increased_indent = format!("\n{space:width$}", width = indent_width);
 460            replace_value = replace_value.replace('\n', &increased_indent);
 461            replace_value.push('\n');
 462            replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
 463        } else {
 464            while let Some(idx) = replace_value.find("\n ") {
 465                replace_value.remove(idx + 1);
 466            }
 467            while let Some(idx) = replace_value.find('\n') {
 468                replace_value.replace_range(idx..idx + 1, " ");
 469            }
 470            replace_value.insert(0, ' ');
 471        }
 472
 473        if comma_range.is_none() {
 474            replace_value.insert(0, ',');
 475        }
 476    } else {
 477        if let Some(prev_newline) = text[..replace_range.start].rfind('\n') {
 478            if text[prev_newline..replace_range.start].trim().is_empty() {
 479                replace_range.start = prev_newline;
 480            }
 481        }
 482        let indent = format!("\n{space:width$}", width = tab_size);
 483        replace_value = replace_value.replace('\n', &indent);
 484        replace_value.insert_str(0, &indent);
 485        replace_value.push('\n');
 486    }
 487    return Ok((replace_range, replace_value));
 488}
 489
 490pub fn to_pretty_json(
 491    value: &impl Serialize,
 492    indent_size: usize,
 493    indent_prefix_len: usize,
 494) -> String {
 495    const SPACES: [u8; 32] = [b' '; 32];
 496
 497    debug_assert!(indent_size <= SPACES.len());
 498    debug_assert!(indent_prefix_len <= SPACES.len());
 499
 500    let mut output = Vec::new();
 501    let mut ser = serde_json::Serializer::with_formatter(
 502        &mut output,
 503        serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
 504    );
 505
 506    value.serialize(&mut ser).unwrap();
 507    let text = String::from_utf8(output).unwrap();
 508
 509    let mut adjusted_text = String::new();
 510    for (i, line) in text.split('\n').enumerate() {
 511        if i > 0 {
 512            adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
 513        }
 514        adjusted_text.push_str(line);
 515        adjusted_text.push('\n');
 516    }
 517    adjusted_text.pop();
 518    adjusted_text
 519}
 520
 521pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
 522    Ok(serde_json_lenient::from_str(content)?)
 523}
 524
 525#[cfg(test)]
 526mod tests {
 527    use super::*;
 528    use serde_json::{Value, json};
 529    use unindent::Unindent;
 530
 531    #[test]
 532    fn object_replace() {
 533        #[track_caller]
 534        fn check_object_replace(
 535            input: String,
 536            key_path: &[&str],
 537            value: Option<Value>,
 538            expected: String,
 539        ) {
 540            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
 541            let mut result_str = input.to_string();
 542            result_str.replace_range(result.0, &result.1);
 543            pretty_assertions::assert_eq!(expected, result_str);
 544        }
 545        check_object_replace(
 546            r#"{
 547                "a": 1,
 548                "b": 2
 549            }"#
 550            .unindent(),
 551            &["b"],
 552            Some(json!(3)),
 553            r#"{
 554                "a": 1,
 555                "b": 3
 556            }"#
 557            .unindent(),
 558        );
 559        check_object_replace(
 560            r#"{
 561                "a": 1,
 562                "b": 2
 563            }"#
 564            .unindent(),
 565            &["b"],
 566            None,
 567            r#"{
 568                "a": 1
 569            }"#
 570            .unindent(),
 571        );
 572        check_object_replace(
 573            r#"{
 574                "a": 1,
 575                "b": 2
 576            }"#
 577            .unindent(),
 578            &["c"],
 579            Some(json!(3)),
 580            r#"{
 581                "c": 3,
 582                "a": 1,
 583                "b": 2
 584            }"#
 585            .unindent(),
 586        );
 587        check_object_replace(
 588            r#"{
 589                "a": 1,
 590                "b": {
 591                    "c": 2,
 592                    "d": 3,
 593                }
 594            }"#
 595            .unindent(),
 596            &["b", "c"],
 597            Some(json!([1, 2, 3])),
 598            r#"{
 599                "a": 1,
 600                "b": {
 601                    "c": [
 602                        1,
 603                        2,
 604                        3
 605                    ],
 606                    "d": 3,
 607                }
 608            }"#
 609            .unindent(),
 610        );
 611
 612        check_object_replace(
 613            r#"{
 614                "name": "old_name",
 615                "id": 123
 616            }"#
 617            .unindent(),
 618            &["name"],
 619            Some(json!("new_name")),
 620            r#"{
 621                "name": "new_name",
 622                "id": 123
 623            }"#
 624            .unindent(),
 625        );
 626
 627        check_object_replace(
 628            r#"{
 629                "enabled": false,
 630                "count": 5
 631            }"#
 632            .unindent(),
 633            &["enabled"],
 634            Some(json!(true)),
 635            r#"{
 636                "enabled": true,
 637                "count": 5
 638            }"#
 639            .unindent(),
 640        );
 641
 642        check_object_replace(
 643            r#"{
 644                "value": null,
 645                "other": "test"
 646            }"#
 647            .unindent(),
 648            &["value"],
 649            Some(json!(42)),
 650            r#"{
 651                "value": 42,
 652                "other": "test"
 653            }"#
 654            .unindent(),
 655        );
 656
 657        check_object_replace(
 658            r#"{
 659                "config": {
 660                    "old": true
 661                },
 662                "name": "test"
 663            }"#
 664            .unindent(),
 665            &["config"],
 666            Some(json!({"new": false, "count": 3})),
 667            r#"{
 668                "config": {
 669                    "new": false,
 670                    "count": 3
 671                },
 672                "name": "test"
 673            }"#
 674            .unindent(),
 675        );
 676
 677        check_object_replace(
 678            r#"{
 679                // This is a comment
 680                "a": 1,
 681                "b": 2 // Another comment
 682            }"#
 683            .unindent(),
 684            &["b"],
 685            Some(json!({"foo": "bar"})),
 686            r#"{
 687                // This is a comment
 688                "a": 1,
 689                "b": {
 690                    "foo": "bar"
 691                } // Another comment
 692            }"#
 693            .unindent(),
 694        );
 695
 696        check_object_replace(
 697            r#"{}"#.to_string(),
 698            &["new_key"],
 699            Some(json!("value")),
 700            r#"{
 701                "new_key": "value"
 702            }
 703            "#
 704            .unindent(),
 705        );
 706
 707        check_object_replace(
 708            r#"{
 709                "only_key": 123
 710            }"#
 711            .unindent(),
 712            &["only_key"],
 713            None,
 714            "{\n    \n}".to_string(),
 715        );
 716
 717        check_object_replace(
 718            r#"{
 719                "level1": {
 720                    "level2": {
 721                        "level3": {
 722                            "target": "old"
 723                        }
 724                    }
 725                }
 726            }"#
 727            .unindent(),
 728            &["level1", "level2", "level3", "target"],
 729            Some(json!("new")),
 730            r#"{
 731                "level1": {
 732                    "level2": {
 733                        "level3": {
 734                            "target": "new"
 735                        }
 736                    }
 737                }
 738            }"#
 739            .unindent(),
 740        );
 741
 742        check_object_replace(
 743            r#"{
 744                "parent": {}
 745            }"#
 746            .unindent(),
 747            &["parent", "child"],
 748            Some(json!("value")),
 749            r#"{
 750                "parent": {
 751                    "child": "value"
 752                }
 753            }"#
 754            .unindent(),
 755        );
 756
 757        check_object_replace(
 758            r#"{
 759                "a": 1,
 760                "b": 2,
 761            }"#
 762            .unindent(),
 763            &["b"],
 764            Some(json!(3)),
 765            r#"{
 766                "a": 1,
 767                "b": 3,
 768            }"#
 769            .unindent(),
 770        );
 771
 772        check_object_replace(
 773            r#"{
 774                "items": [1, 2, 3],
 775                "count": 3
 776            }"#
 777            .unindent(),
 778            &["items", "1"],
 779            Some(json!(5)),
 780            r#"{
 781                "items": {
 782                    "1": 5
 783                },
 784                "count": 3
 785            }"#
 786            .unindent(),
 787        );
 788
 789        check_object_replace(
 790            r#"{
 791                "items": [1, 2, 3],
 792                "count": 3
 793            }"#
 794            .unindent(),
 795            &["items", "1"],
 796            None,
 797            r#"{
 798                "items": {
 799                    "1": null
 800                },
 801                "count": 3
 802            }"#
 803            .unindent(),
 804        );
 805
 806        check_object_replace(
 807            r#"{
 808                "items": [1, 2, 3],
 809                "count": 3
 810            }"#
 811            .unindent(),
 812            &["items"],
 813            Some(json!(["a", "b", "c", "d"])),
 814            r#"{
 815                "items": [
 816                    "a",
 817                    "b",
 818                    "c",
 819                    "d"
 820                ],
 821                "count": 3
 822            }"#
 823            .unindent(),
 824        );
 825
 826        check_object_replace(
 827            r#"{
 828                "0": "zero",
 829                "1": "one"
 830            }"#
 831            .unindent(),
 832            &["1"],
 833            Some(json!("ONE")),
 834            r#"{
 835                "0": "zero",
 836                "1": "ONE"
 837            }"#
 838            .unindent(),
 839        );
 840        // Test with comments between object members
 841        check_object_replace(
 842            r#"{
 843                "a": 1,
 844                // Comment between members
 845                "b": 2,
 846                /* Block comment */
 847                "c": 3
 848            }"#
 849            .unindent(),
 850            &["b"],
 851            Some(json!({"nested": true})),
 852            r#"{
 853                "a": 1,
 854                // Comment between members
 855                "b": {
 856                    "nested": true
 857                },
 858                /* Block comment */
 859                "c": 3
 860            }"#
 861            .unindent(),
 862        );
 863
 864        // Test with trailing comments on replaced value
 865        check_object_replace(
 866            r#"{
 867                "a": 1, // keep this comment
 868                "b": 2  // this should stay
 869            }"#
 870            .unindent(),
 871            &["a"],
 872            Some(json!("changed")),
 873            r#"{
 874                "a": "changed", // keep this comment
 875                "b": 2  // this should stay
 876            }"#
 877            .unindent(),
 878        );
 879
 880        // Test with deep indentation
 881        check_object_replace(
 882            r#"{
 883                        "deeply": {
 884                                "nested": {
 885                                        "value": "old"
 886                                }
 887                        }
 888                }"#
 889            .unindent(),
 890            &["deeply", "nested", "value"],
 891            Some(json!("new")),
 892            r#"{
 893                        "deeply": {
 894                                "nested": {
 895                                        "value": "new"
 896                                }
 897                        }
 898                }"#
 899            .unindent(),
 900        );
 901
 902        // Test removing value with comment preservation
 903        check_object_replace(
 904            r#"{
 905                // Header comment
 906                "a": 1,
 907                // This comment belongs to b
 908                "b": 2,
 909                // This comment belongs to c
 910                "c": 3
 911            }"#
 912            .unindent(),
 913            &["b"],
 914            None,
 915            r#"{
 916                // Header comment
 917                "a": 1,
 918                // This comment belongs to b
 919                // This comment belongs to c
 920                "c": 3
 921            }"#
 922            .unindent(),
 923        );
 924
 925        // Test with multiline block comments
 926        check_object_replace(
 927            r#"{
 928                /*
 929                 * This is a multiline
 930                 * block comment
 931                 */
 932                "value": "old",
 933                /* Another block */ "other": 123
 934            }"#
 935            .unindent(),
 936            &["value"],
 937            Some(json!("new")),
 938            r#"{
 939                /*
 940                 * This is a multiline
 941                 * block comment
 942                 */
 943                "value": "new",
 944                /* Another block */ "other": 123
 945            }"#
 946            .unindent(),
 947        );
 948
 949        check_object_replace(
 950            r#"{
 951                // This object is empty
 952            }"#
 953            .unindent(),
 954            &["key"],
 955            Some(json!("value")),
 956            r#"{
 957                // This object is empty
 958                "key": "value"
 959            }
 960            "#
 961            .unindent(),
 962        );
 963
 964        // Test replacing in object with only comments
 965        check_object_replace(
 966            r#"{
 967                // Comment 1
 968                // Comment 2
 969            }"#
 970            .unindent(),
 971            &["new"],
 972            Some(json!(42)),
 973            r#"{
 974                // Comment 1
 975                // Comment 2
 976                "new": 42
 977            }
 978            "#
 979            .unindent(),
 980        );
 981
 982        // Test with inconsistent spacing
 983        check_object_replace(
 984            r#"{
 985              "a":1,
 986                    "b"  :  2  ,
 987                "c":   3
 988            }"#
 989            .unindent(),
 990            &["b"],
 991            Some(json!("spaced")),
 992            r#"{
 993              "a":1,
 994                    "b"  :  "spaced"  ,
 995                "c":   3
 996            }"#
 997            .unindent(),
 998        );
 999    }
1000
1001    #[test]
1002    fn array_replace() {
1003        #[track_caller]
1004        fn check_array_replace(
1005            input: impl ToString,
1006            index: usize,
1007            key_path: &[&str],
1008            value: Value,
1009            expected: impl ToString,
1010        ) {
1011            let input = input.to_string();
1012            let result = replace_top_level_array_value_in_json_text(
1013                &input,
1014                key_path,
1015                Some(&value),
1016                None,
1017                index,
1018                4,
1019            )
1020            .expect("replace succeeded");
1021            let mut result_str = input;
1022            result_str.replace_range(result.0, &result.1);
1023            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1024        }
1025
1026        check_array_replace(r#"[1, 3, 3]"#, 1, &[], json!(2), r#"[1, 2, 3]"#);
1027        check_array_replace(r#"[1, 3, 3]"#, 2, &[], json!(2), r#"[1, 3, 2]"#);
1028        check_array_replace(r#"[1, 3, 3,]"#, 3, &[], json!(2), r#"[1, 3, 3, 2]"#);
1029        check_array_replace(r#"[1, 3, 3,]"#, 100, &[], json!(2), r#"[1, 3, 3, 2]"#);
1030        check_array_replace(
1031            r#"[
1032                1,
1033                2,
1034                3,
1035            ]"#
1036            .unindent(),
1037            1,
1038            &[],
1039            json!({"foo": "bar", "baz": "qux"}),
1040            r#"[
1041                1,
1042                {
1043                    "foo": "bar",
1044                    "baz": "qux"
1045                },
1046                3,
1047            ]"#
1048            .unindent(),
1049        );
1050        check_array_replace(
1051            r#"[1, 3, 3,]"#,
1052            1,
1053            &[],
1054            json!({"foo": "bar", "baz": "qux"}),
1055            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1056        );
1057
1058        check_array_replace(
1059            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1060            1,
1061            &["baz"],
1062            json!({"qux": "quz"}),
1063            r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1064        );
1065
1066        check_array_replace(
1067            r#"[
1068                1,
1069                {
1070                    "foo": "bar",
1071                    "baz": "qux"
1072                },
1073                3
1074            ]"#,
1075            1,
1076            &["baz"],
1077            json!({"qux": "quz"}),
1078            r#"[
1079                1,
1080                {
1081                    "foo": "bar",
1082                    "baz": {
1083                        "qux": "quz"
1084                    }
1085                },
1086                3
1087            ]"#,
1088        );
1089
1090        check_array_replace(
1091            r#"[
1092                1,
1093                {
1094                    "foo": "bar",
1095                    "baz": {
1096                        "qux": "quz"
1097                    }
1098                },
1099                3
1100            ]"#,
1101            1,
1102            &["baz"],
1103            json!("qux"),
1104            r#"[
1105                1,
1106                {
1107                    "foo": "bar",
1108                    "baz": "qux"
1109                },
1110                3
1111            ]"#,
1112        );
1113
1114        check_array_replace(
1115            r#"[
1116                1,
1117                {
1118                    "foo": "bar",
1119                    // some comment to keep
1120                    "baz": {
1121                        // some comment to remove
1122                        "qux": "quz"
1123                    }
1124                    // some other comment to keep
1125                },
1126                3
1127            ]"#,
1128            1,
1129            &["baz"],
1130            json!("qux"),
1131            r#"[
1132                1,
1133                {
1134                    "foo": "bar",
1135                    // some comment to keep
1136                    "baz": "qux"
1137                    // some other comment to keep
1138                },
1139                3
1140            ]"#,
1141        );
1142
1143        // Test with comments between array elements
1144        check_array_replace(
1145            r#"[
1146                1,
1147                // This is element 2
1148                2,
1149                /* Block comment */ 3,
1150                4 // Trailing comment
1151            ]"#,
1152            2,
1153            &[],
1154            json!("replaced"),
1155            r#"[
1156                1,
1157                // This is element 2
1158                2,
1159                /* Block comment */ "replaced",
1160                4 // Trailing comment
1161            ]"#,
1162        );
1163
1164        // Test empty array with comments
1165        check_array_replace(
1166            r#"[
1167                // Empty array with comment
1168            ]"#
1169            .unindent(),
1170            0,
1171            &[],
1172            json!("first"),
1173            r#"[
1174                // Empty array with comment
1175                "first"
1176            ]"#
1177            .unindent(),
1178        );
1179        check_array_replace(
1180            r#"[]"#.unindent(),
1181            0,
1182            &[],
1183            json!("first"),
1184            r#"[
1185                "first"
1186            ]"#
1187            .unindent(),
1188        );
1189
1190        // Test array with leading comments
1191        check_array_replace(
1192            r#"[
1193                // Leading comment
1194                // Another leading comment
1195                1,
1196                2
1197            ]"#,
1198            0,
1199            &[],
1200            json!({"new": "object"}),
1201            r#"[
1202                // Leading comment
1203                // Another leading comment
1204                {
1205                    "new": "object"
1206                },
1207                2
1208            ]"#,
1209        );
1210
1211        // Test with deep indentation
1212        check_array_replace(
1213            r#"[
1214                        1,
1215                        2,
1216                        3
1217                    ]"#,
1218            1,
1219            &[],
1220            json!("deep"),
1221            r#"[
1222                        1,
1223                        "deep",
1224                        3
1225                    ]"#,
1226        );
1227
1228        // Test with mixed spacing
1229        check_array_replace(
1230            r#"[1,2,   3,    4]"#,
1231            2,
1232            &[],
1233            json!("spaced"),
1234            r#"[1,2,   "spaced",    4]"#,
1235        );
1236
1237        // Test replacing nested array element
1238        check_array_replace(
1239            r#"[
1240                [1, 2, 3],
1241                [4, 5, 6],
1242                [7, 8, 9]
1243            ]"#,
1244            1,
1245            &[],
1246            json!(["a", "b", "c", "d"]),
1247            r#"[
1248                [1, 2, 3],
1249                [
1250                    "a",
1251                    "b",
1252                    "c",
1253                    "d"
1254                ],
1255                [7, 8, 9]
1256            ]"#,
1257        );
1258
1259        // Test with multiline block comments
1260        check_array_replace(
1261            r#"[
1262                /*
1263                 * This is a
1264                 * multiline comment
1265                 */
1266                "first",
1267                "second"
1268            ]"#,
1269            0,
1270            &[],
1271            json!("updated"),
1272            r#"[
1273                /*
1274                 * This is a
1275                 * multiline comment
1276                 */
1277                "updated",
1278                "second"
1279            ]"#,
1280        );
1281
1282        // Test replacing with null
1283        check_array_replace(
1284            r#"[true, false, true]"#,
1285            1,
1286            &[],
1287            json!(null),
1288            r#"[true, null, true]"#,
1289        );
1290
1291        // Test single element array
1292        check_array_replace(
1293            r#"[42]"#,
1294            0,
1295            &[],
1296            json!({"answer": 42}),
1297            r#"[{ "answer": 42 }]"#,
1298        );
1299
1300        // Test array with only comments
1301        check_array_replace(
1302            r#"[
1303                // Comment 1
1304                // Comment 2
1305                // Comment 3
1306            ]"#
1307            .unindent(),
1308            10,
1309            &[],
1310            json!(123),
1311            r#"[
1312                // Comment 1
1313                // Comment 2
1314                // Comment 3
1315                123
1316            ]"#
1317            .unindent(),
1318        );
1319    }
1320
1321    #[test]
1322    fn array_append() {
1323        #[track_caller]
1324        fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
1325            let input = input.to_string();
1326            let result = append_top_level_array_value_in_json_text(&input, &value, 4)
1327                .expect("append succeeded");
1328            let mut result_str = input;
1329            result_str.replace_range(result.0, &result.1);
1330            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1331        }
1332        check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
1333        check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
1334        check_array_append(r#"[1, 3, 3   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
1335        check_array_append(r#"[1, 3, 3,   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
1336        check_array_append(
1337            r#"[
1338                1,
1339                2,
1340                3
1341            ]"#
1342            .unindent(),
1343            json!(4),
1344            r#"[
1345                1,
1346                2,
1347                3,
1348                4
1349            ]"#
1350            .unindent(),
1351        );
1352        check_array_append(
1353            r#"[
1354                1,
1355                2,
1356                3,
1357            ]"#
1358            .unindent(),
1359            json!(4),
1360            r#"[
1361                1,
1362                2,
1363                3,
1364                4
1365            ]"#
1366            .unindent(),
1367        );
1368        check_array_append(
1369            r#"[
1370                1,
1371                2,
1372                3,
1373            ]"#
1374            .unindent(),
1375            json!({"foo": "bar", "baz": "qux"}),
1376            r#"[
1377                1,
1378                2,
1379                3,
1380                {
1381                    "foo": "bar",
1382                    "baz": "qux"
1383                }
1384            ]"#
1385            .unindent(),
1386        );
1387        check_array_append(
1388            r#"[ 1, 2, 3, ]"#.unindent(),
1389            json!({"foo": "bar", "baz": "qux"}),
1390            r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
1391        );
1392        check_array_append(
1393            r#"[]"#,
1394            json!({"foo": "bar"}),
1395            r#"[
1396                {
1397                    "foo": "bar"
1398                }
1399            ]"#
1400            .unindent(),
1401        );
1402
1403        // Test with comments between array elements
1404        check_array_append(
1405            r#"[
1406                1,
1407                // Comment between elements
1408                2,
1409                /* Block comment */ 3
1410            ]"#
1411            .unindent(),
1412            json!(4),
1413            r#"[
1414                1,
1415                // Comment between elements
1416                2,
1417                /* Block comment */ 3,
1418                4
1419            ]"#
1420            .unindent(),
1421        );
1422
1423        // Test with trailing comment on last element
1424        check_array_append(
1425            r#"[
1426                1,
1427                2,
1428                3 // Trailing comment
1429            ]"#
1430            .unindent(),
1431            json!("new"),
1432            r#"[
1433                1,
1434                2,
1435                3 // Trailing comment
1436            ,
1437                "new"
1438            ]"#
1439            .unindent(),
1440        );
1441
1442        // Test empty array with comments
1443        check_array_append(
1444            r#"[
1445                // Empty array with comment
1446            ]"#
1447            .unindent(),
1448            json!("first"),
1449            r#"[
1450                // Empty array with comment
1451                "first"
1452            ]"#
1453            .unindent(),
1454        );
1455
1456        // Test with multiline block comment at end
1457        check_array_append(
1458            r#"[
1459                1,
1460                2
1461                /*
1462                 * This is a
1463                 * multiline comment
1464                 */
1465            ]"#
1466            .unindent(),
1467            json!(3),
1468            r#"[
1469                1,
1470                2
1471                /*
1472                 * This is a
1473                 * multiline comment
1474                 */
1475            ,
1476                3
1477            ]"#
1478            .unindent(),
1479        );
1480
1481        // Test with deep indentation
1482        check_array_append(
1483            r#"[
1484                1,
1485                    2,
1486                        3
1487            ]"#
1488            .unindent(),
1489            json!("deep"),
1490            r#"[
1491                1,
1492                    2,
1493                        3,
1494                        "deep"
1495            ]"#
1496            .unindent(),
1497        );
1498
1499        // Test with no spacing
1500        check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
1501
1502        // Test appending complex nested structure
1503        check_array_append(
1504            r#"[
1505                {"a": 1},
1506                {"b": 2}
1507            ]"#
1508            .unindent(),
1509            json!({"c": {"nested": [1, 2, 3]}}),
1510            r#"[
1511                {"a": 1},
1512                {"b": 2},
1513                {
1514                    "c": {
1515                        "nested": [
1516                            1,
1517                            2,
1518                            3
1519                        ]
1520                    }
1521                }
1522            ]"#
1523            .unindent(),
1524        );
1525
1526        // Test array ending with comment after bracket
1527        check_array_append(
1528            r#"[
1529                1,
1530                2,
1531                3
1532            ] // Comment after array"#
1533                .unindent(),
1534            json!(4),
1535            r#"[
1536                1,
1537                2,
1538                3,
1539                4
1540            ] // Comment after array"#
1541                .unindent(),
1542        );
1543
1544        // Test with inconsistent element formatting
1545        check_array_append(
1546            r#"[1,
1547               2,
1548                    3,
1549            ]"#
1550            .unindent(),
1551            json!(4),
1552            r#"[1,
1553               2,
1554                    3,
1555                    4
1556            ]"#
1557            .unindent(),
1558        );
1559
1560        // Test appending to single-line array with trailing comma
1561        check_array_append(
1562            r#"[1, 2, 3,]"#,
1563            json!({"key": "value"}),
1564            r#"[1, 2, 3, { "key": "value" }]"#,
1565        );
1566
1567        // Test appending null value
1568        check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
1569
1570        // Test appending to array with only comments
1571        check_array_append(
1572            r#"[
1573                // Just comments here
1574                // More comments
1575            ]"#
1576            .unindent(),
1577            json!(42),
1578            r#"[
1579                // Just comments here
1580                // More comments
1581                42
1582            ]"#
1583            .unindent(),
1584        );
1585    }
1586}