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 text_range = range.start_byte..range.end_byte;
 357    let value_str = &text[text_range.clone()];
 358    let needs_indent = range.start_point.row > 0;
 359
 360    if new_value.is_none() && key_path.is_empty() {
 361        let mut remove_range = text_range.clone();
 362        if index == 0 {
 363            while cursor.goto_next_sibling()
 364                && (cursor.node().is_extra() || cursor.node().is_missing())
 365            {}
 366            if cursor.node().kind() == "," {
 367                remove_range.end = cursor.node().range().end_byte;
 368            }
 369            if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') {
 370                if text[remove_range.end + 1..remove_range.end + next_newline]
 371                    .chars()
 372                    .all(|c| c.is_ascii_whitespace())
 373                {
 374                    remove_range.end = remove_range.end + next_newline;
 375                }
 376            }
 377        } else {
 378            while cursor.goto_previous_sibling()
 379                && (cursor.node().is_extra() || cursor.node().is_missing())
 380            {}
 381            if cursor.node().kind() == "," {
 382                remove_range.start = cursor.node().range().start_byte;
 383            }
 384        }
 385        return Ok((remove_range, String::new()));
 386    } else {
 387        let (mut replace_range, mut replace_value) =
 388            replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
 389
 390        replace_range.start += offset;
 391        replace_range.end += offset;
 392
 393        if needs_indent {
 394            let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
 395            replace_value = replace_value.replace('\n', &increased_indent);
 396            // replace_value.push('\n');
 397        } else {
 398            while let Some(idx) = replace_value.find("\n ") {
 399                replace_value.remove(idx + 1);
 400            }
 401            while let Some(idx) = replace_value.find("\n") {
 402                replace_value.replace_range(idx..idx + 1, " ");
 403            }
 404        }
 405
 406        return Ok((replace_range, replace_value));
 407    }
 408}
 409
 410pub fn append_top_level_array_value_in_json_text(
 411    text: &str,
 412    new_value: &Value,
 413    tab_size: usize,
 414) -> Result<(Range<usize>, String)> {
 415    let mut parser = tree_sitter::Parser::new();
 416    parser
 417        .set_language(&tree_sitter_json::LANGUAGE.into())
 418        .unwrap();
 419    let syntax_tree = parser.parse(text, None).unwrap();
 420
 421    let mut cursor = syntax_tree.walk();
 422
 423    if cursor.node().kind() == TS_DOCUMENT_KIND {
 424        anyhow::ensure!(
 425            cursor.goto_first_child(),
 426            "Document empty - No top level array"
 427        );
 428    }
 429
 430    while cursor.node().kind() != TS_ARRAY_KIND {
 431        anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
 432    }
 433
 434    anyhow::ensure!(
 435        cursor.goto_last_child(),
 436        "Malformed JSON syntax tree, expected `]` at end of array"
 437    );
 438    debug_assert_eq!(cursor.node().kind(), "]");
 439    let close_bracket_start = cursor.node().start_byte();
 440    cursor.goto_previous_sibling();
 441    while (cursor.node().is_extra() || cursor.node().is_missing()) && cursor.goto_previous_sibling()
 442    {
 443    }
 444
 445    let mut comma_range = None;
 446    let mut prev_item_range = None;
 447
 448    if cursor.node().kind() == "," {
 449        comma_range = Some(cursor.node().byte_range());
 450        while cursor.goto_previous_sibling() && cursor.node().is_extra() {}
 451
 452        debug_assert_ne!(cursor.node().kind(), "[");
 453        prev_item_range = Some(cursor.node().range());
 454    } else {
 455        while (cursor.node().is_extra() || cursor.node().is_missing())
 456            && cursor.goto_previous_sibling()
 457        {}
 458        if cursor.node().kind() != "[" {
 459            prev_item_range = Some(cursor.node().range());
 460        }
 461    }
 462
 463    let (mut replace_range, mut replace_value) =
 464        replace_value_in_json_text("", &[], tab_size, Some(new_value), None);
 465
 466    replace_range.start = close_bracket_start;
 467    replace_range.end = close_bracket_start;
 468
 469    let space = ' ';
 470    if let Some(prev_item_range) = prev_item_range {
 471        let needs_newline = prev_item_range.start_point.row > 0;
 472        let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
 473            prev_item_range.start_point.column,
 474            |idx| {
 475                prev_item_range.start_point.column
 476                    - text[idx + 1..prev_item_range.start_byte].trim_start().len()
 477            },
 478        );
 479
 480        let prev_item_end = comma_range
 481            .as_ref()
 482            .map_or(prev_item_range.end_byte, |range| range.end);
 483        if text[prev_item_end..replace_range.start].trim().is_empty() {
 484            replace_range.start = prev_item_end;
 485        }
 486
 487        if needs_newline {
 488            let increased_indent = format!("\n{space:width$}", width = indent_width);
 489            replace_value = replace_value.replace('\n', &increased_indent);
 490            replace_value.push('\n');
 491            replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
 492        } else {
 493            while let Some(idx) = replace_value.find("\n ") {
 494                replace_value.remove(idx + 1);
 495            }
 496            while let Some(idx) = replace_value.find('\n') {
 497                replace_value.replace_range(idx..idx + 1, " ");
 498            }
 499            replace_value.insert(0, ' ');
 500        }
 501
 502        if comma_range.is_none() {
 503            replace_value.insert(0, ',');
 504        }
 505    } else {
 506        if let Some(prev_newline) = text[..replace_range.start].rfind('\n') {
 507            if text[prev_newline..replace_range.start].trim().is_empty() {
 508                replace_range.start = prev_newline;
 509            }
 510        }
 511        let indent = format!("\n{space:width$}", width = tab_size);
 512        replace_value = replace_value.replace('\n', &indent);
 513        replace_value.insert_str(0, &indent);
 514        replace_value.push('\n');
 515    }
 516    return Ok((replace_range, replace_value));
 517}
 518
 519pub fn to_pretty_json(
 520    value: &impl Serialize,
 521    indent_size: usize,
 522    indent_prefix_len: usize,
 523) -> String {
 524    const SPACES: [u8; 32] = [b' '; 32];
 525
 526    debug_assert!(indent_size <= SPACES.len());
 527    debug_assert!(indent_prefix_len <= SPACES.len());
 528
 529    let mut output = Vec::new();
 530    let mut ser = serde_json::Serializer::with_formatter(
 531        &mut output,
 532        serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
 533    );
 534
 535    value.serialize(&mut ser).unwrap();
 536    let text = String::from_utf8(output).unwrap();
 537
 538    let mut adjusted_text = String::new();
 539    for (i, line) in text.split('\n').enumerate() {
 540        if i > 0 {
 541            adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
 542        }
 543        adjusted_text.push_str(line);
 544        adjusted_text.push('\n');
 545    }
 546    adjusted_text.pop();
 547    adjusted_text
 548}
 549
 550pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
 551    Ok(serde_json_lenient::from_str(content)?)
 552}
 553
 554#[cfg(test)]
 555mod tests {
 556    use super::*;
 557    use serde_json::{Value, json};
 558    use unindent::Unindent;
 559
 560    #[test]
 561    fn object_replace() {
 562        #[track_caller]
 563        fn check_object_replace(
 564            input: String,
 565            key_path: &[&str],
 566            value: Option<Value>,
 567            expected: String,
 568        ) {
 569            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
 570            let mut result_str = input.to_string();
 571            result_str.replace_range(result.0, &result.1);
 572            pretty_assertions::assert_eq!(expected, result_str);
 573        }
 574        check_object_replace(
 575            r#"{
 576                "a": 1,
 577                "b": 2
 578            }"#
 579            .unindent(),
 580            &["b"],
 581            Some(json!(3)),
 582            r#"{
 583                "a": 1,
 584                "b": 3
 585            }"#
 586            .unindent(),
 587        );
 588        check_object_replace(
 589            r#"{
 590                "a": 1,
 591                "b": 2
 592            }"#
 593            .unindent(),
 594            &["b"],
 595            None,
 596            r#"{
 597                "a": 1
 598            }"#
 599            .unindent(),
 600        );
 601        check_object_replace(
 602            r#"{
 603                "a": 1,
 604                "b": 2
 605            }"#
 606            .unindent(),
 607            &["c"],
 608            Some(json!(3)),
 609            r#"{
 610                "c": 3,
 611                "a": 1,
 612                "b": 2
 613            }"#
 614            .unindent(),
 615        );
 616        check_object_replace(
 617            r#"{
 618                "a": 1,
 619                "b": {
 620                    "c": 2,
 621                    "d": 3,
 622                }
 623            }"#
 624            .unindent(),
 625            &["b", "c"],
 626            Some(json!([1, 2, 3])),
 627            r#"{
 628                "a": 1,
 629                "b": {
 630                    "c": [
 631                        1,
 632                        2,
 633                        3
 634                    ],
 635                    "d": 3,
 636                }
 637            }"#
 638            .unindent(),
 639        );
 640
 641        check_object_replace(
 642            r#"{
 643                "name": "old_name",
 644                "id": 123
 645            }"#
 646            .unindent(),
 647            &["name"],
 648            Some(json!("new_name")),
 649            r#"{
 650                "name": "new_name",
 651                "id": 123
 652            }"#
 653            .unindent(),
 654        );
 655
 656        check_object_replace(
 657            r#"{
 658                "enabled": false,
 659                "count": 5
 660            }"#
 661            .unindent(),
 662            &["enabled"],
 663            Some(json!(true)),
 664            r#"{
 665                "enabled": true,
 666                "count": 5
 667            }"#
 668            .unindent(),
 669        );
 670
 671        check_object_replace(
 672            r#"{
 673                "value": null,
 674                "other": "test"
 675            }"#
 676            .unindent(),
 677            &["value"],
 678            Some(json!(42)),
 679            r#"{
 680                "value": 42,
 681                "other": "test"
 682            }"#
 683            .unindent(),
 684        );
 685
 686        check_object_replace(
 687            r#"{
 688                "config": {
 689                    "old": true
 690                },
 691                "name": "test"
 692            }"#
 693            .unindent(),
 694            &["config"],
 695            Some(json!({"new": false, "count": 3})),
 696            r#"{
 697                "config": {
 698                    "new": false,
 699                    "count": 3
 700                },
 701                "name": "test"
 702            }"#
 703            .unindent(),
 704        );
 705
 706        check_object_replace(
 707            r#"{
 708                // This is a comment
 709                "a": 1,
 710                "b": 2 // Another comment
 711            }"#
 712            .unindent(),
 713            &["b"],
 714            Some(json!({"foo": "bar"})),
 715            r#"{
 716                // This is a comment
 717                "a": 1,
 718                "b": {
 719                    "foo": "bar"
 720                } // Another comment
 721            }"#
 722            .unindent(),
 723        );
 724
 725        check_object_replace(
 726            r#"{}"#.to_string(),
 727            &["new_key"],
 728            Some(json!("value")),
 729            r#"{
 730                "new_key": "value"
 731            }
 732            "#
 733            .unindent(),
 734        );
 735
 736        check_object_replace(
 737            r#"{
 738                "only_key": 123
 739            }"#
 740            .unindent(),
 741            &["only_key"],
 742            None,
 743            "{\n    \n}".to_string(),
 744        );
 745
 746        check_object_replace(
 747            r#"{
 748                "level1": {
 749                    "level2": {
 750                        "level3": {
 751                            "target": "old"
 752                        }
 753                    }
 754                }
 755            }"#
 756            .unindent(),
 757            &["level1", "level2", "level3", "target"],
 758            Some(json!("new")),
 759            r#"{
 760                "level1": {
 761                    "level2": {
 762                        "level3": {
 763                            "target": "new"
 764                        }
 765                    }
 766                }
 767            }"#
 768            .unindent(),
 769        );
 770
 771        check_object_replace(
 772            r#"{
 773                "parent": {}
 774            }"#
 775            .unindent(),
 776            &["parent", "child"],
 777            Some(json!("value")),
 778            r#"{
 779                "parent": {
 780                    "child": "value"
 781                }
 782            }"#
 783            .unindent(),
 784        );
 785
 786        check_object_replace(
 787            r#"{
 788                "a": 1,
 789                "b": 2,
 790            }"#
 791            .unindent(),
 792            &["b"],
 793            Some(json!(3)),
 794            r#"{
 795                "a": 1,
 796                "b": 3,
 797            }"#
 798            .unindent(),
 799        );
 800
 801        check_object_replace(
 802            r#"{
 803                "items": [1, 2, 3],
 804                "count": 3
 805            }"#
 806            .unindent(),
 807            &["items", "1"],
 808            Some(json!(5)),
 809            r#"{
 810                "items": {
 811                    "1": 5
 812                },
 813                "count": 3
 814            }"#
 815            .unindent(),
 816        );
 817
 818        check_object_replace(
 819            r#"{
 820                "items": [1, 2, 3],
 821                "count": 3
 822            }"#
 823            .unindent(),
 824            &["items", "1"],
 825            None,
 826            r#"{
 827                "items": {
 828                    "1": null
 829                },
 830                "count": 3
 831            }"#
 832            .unindent(),
 833        );
 834
 835        check_object_replace(
 836            r#"{
 837                "items": [1, 2, 3],
 838                "count": 3
 839            }"#
 840            .unindent(),
 841            &["items"],
 842            Some(json!(["a", "b", "c", "d"])),
 843            r#"{
 844                "items": [
 845                    "a",
 846                    "b",
 847                    "c",
 848                    "d"
 849                ],
 850                "count": 3
 851            }"#
 852            .unindent(),
 853        );
 854
 855        check_object_replace(
 856            r#"{
 857                "0": "zero",
 858                "1": "one"
 859            }"#
 860            .unindent(),
 861            &["1"],
 862            Some(json!("ONE")),
 863            r#"{
 864                "0": "zero",
 865                "1": "ONE"
 866            }"#
 867            .unindent(),
 868        );
 869        // Test with comments between object members
 870        check_object_replace(
 871            r#"{
 872                "a": 1,
 873                // Comment between members
 874                "b": 2,
 875                /* Block comment */
 876                "c": 3
 877            }"#
 878            .unindent(),
 879            &["b"],
 880            Some(json!({"nested": true})),
 881            r#"{
 882                "a": 1,
 883                // Comment between members
 884                "b": {
 885                    "nested": true
 886                },
 887                /* Block comment */
 888                "c": 3
 889            }"#
 890            .unindent(),
 891        );
 892
 893        // Test with trailing comments on replaced value
 894        check_object_replace(
 895            r#"{
 896                "a": 1, // keep this comment
 897                "b": 2  // this should stay
 898            }"#
 899            .unindent(),
 900            &["a"],
 901            Some(json!("changed")),
 902            r#"{
 903                "a": "changed", // keep this comment
 904                "b": 2  // this should stay
 905            }"#
 906            .unindent(),
 907        );
 908
 909        // Test with deep indentation
 910        check_object_replace(
 911            r#"{
 912                        "deeply": {
 913                                "nested": {
 914                                        "value": "old"
 915                                }
 916                        }
 917                }"#
 918            .unindent(),
 919            &["deeply", "nested", "value"],
 920            Some(json!("new")),
 921            r#"{
 922                        "deeply": {
 923                                "nested": {
 924                                        "value": "new"
 925                                }
 926                        }
 927                }"#
 928            .unindent(),
 929        );
 930
 931        // Test removing value with comment preservation
 932        check_object_replace(
 933            r#"{
 934                // Header comment
 935                "a": 1,
 936                // This comment belongs to b
 937                "b": 2,
 938                // This comment belongs to c
 939                "c": 3
 940            }"#
 941            .unindent(),
 942            &["b"],
 943            None,
 944            r#"{
 945                // Header comment
 946                "a": 1,
 947                // This comment belongs to b
 948                // This comment belongs to c
 949                "c": 3
 950            }"#
 951            .unindent(),
 952        );
 953
 954        // Test with multiline block comments
 955        check_object_replace(
 956            r#"{
 957                /*
 958                 * This is a multiline
 959                 * block comment
 960                 */
 961                "value": "old",
 962                /* Another block */ "other": 123
 963            }"#
 964            .unindent(),
 965            &["value"],
 966            Some(json!("new")),
 967            r#"{
 968                /*
 969                 * This is a multiline
 970                 * block comment
 971                 */
 972                "value": "new",
 973                /* Another block */ "other": 123
 974            }"#
 975            .unindent(),
 976        );
 977
 978        check_object_replace(
 979            r#"{
 980                // This object is empty
 981            }"#
 982            .unindent(),
 983            &["key"],
 984            Some(json!("value")),
 985            r#"{
 986                // This object is empty
 987                "key": "value"
 988            }
 989            "#
 990            .unindent(),
 991        );
 992
 993        // Test replacing in object with only comments
 994        check_object_replace(
 995            r#"{
 996                // Comment 1
 997                // Comment 2
 998            }"#
 999            .unindent(),
1000            &["new"],
1001            Some(json!(42)),
1002            r#"{
1003                // Comment 1
1004                // Comment 2
1005                "new": 42
1006            }
1007            "#
1008            .unindent(),
1009        );
1010
1011        // Test with inconsistent spacing
1012        check_object_replace(
1013            r#"{
1014              "a":1,
1015                    "b"  :  2  ,
1016                "c":   3
1017            }"#
1018            .unindent(),
1019            &["b"],
1020            Some(json!("spaced")),
1021            r#"{
1022              "a":1,
1023                    "b"  :  "spaced"  ,
1024                "c":   3
1025            }"#
1026            .unindent(),
1027        );
1028    }
1029
1030    #[test]
1031    fn array_replace() {
1032        #[track_caller]
1033        fn check_array_replace(
1034            input: impl ToString,
1035            index: usize,
1036            key_path: &[&str],
1037            value: Option<Value>,
1038            expected: impl ToString,
1039        ) {
1040            let input = input.to_string();
1041            let result = replace_top_level_array_value_in_json_text(
1042                &input,
1043                key_path,
1044                value.as_ref(),
1045                None,
1046                index,
1047                4,
1048            )
1049            .expect("replace succeeded");
1050            let mut result_str = input;
1051            result_str.replace_range(result.0, &result.1);
1052            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1053        }
1054
1055        check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1056        check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1057        check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1058        check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1059        check_array_replace(
1060            r#"[
1061                1,
1062                2,
1063                3,
1064            ]"#
1065            .unindent(),
1066            1,
1067            &[],
1068            Some(json!({"foo": "bar", "baz": "qux"})),
1069            r#"[
1070                1,
1071                {
1072                    "foo": "bar",
1073                    "baz": "qux"
1074                },
1075                3,
1076            ]"#
1077            .unindent(),
1078        );
1079        check_array_replace(
1080            r#"[1, 3, 3,]"#,
1081            1,
1082            &[],
1083            Some(json!({"foo": "bar", "baz": "qux"})),
1084            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1085        );
1086
1087        check_array_replace(
1088            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1089            1,
1090            &["baz"],
1091            Some(json!({"qux": "quz"})),
1092            r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1093        );
1094
1095        check_array_replace(
1096            r#"[
1097                1,
1098                {
1099                    "foo": "bar",
1100                    "baz": "qux"
1101                },
1102                3
1103            ]"#,
1104            1,
1105            &["baz"],
1106            Some(json!({"qux": "quz"})),
1107            r#"[
1108                1,
1109                {
1110                    "foo": "bar",
1111                    "baz": {
1112                        "qux": "quz"
1113                    }
1114                },
1115                3
1116            ]"#,
1117        );
1118
1119        check_array_replace(
1120            r#"[
1121                1,
1122                {
1123                    "foo": "bar",
1124                    "baz": {
1125                        "qux": "quz"
1126                    }
1127                },
1128                3
1129            ]"#,
1130            1,
1131            &["baz"],
1132            Some(json!("qux")),
1133            r#"[
1134                1,
1135                {
1136                    "foo": "bar",
1137                    "baz": "qux"
1138                },
1139                3
1140            ]"#,
1141        );
1142
1143        check_array_replace(
1144            r#"[
1145                1,
1146                {
1147                    "foo": "bar",
1148                    // some comment to keep
1149                    "baz": {
1150                        // some comment to remove
1151                        "qux": "quz"
1152                    }
1153                    // some other comment to keep
1154                },
1155                3
1156            ]"#,
1157            1,
1158            &["baz"],
1159            Some(json!("qux")),
1160            r#"[
1161                1,
1162                {
1163                    "foo": "bar",
1164                    // some comment to keep
1165                    "baz": "qux"
1166                    // some other comment to keep
1167                },
1168                3
1169            ]"#,
1170        );
1171
1172        // Test with comments between array elements
1173        check_array_replace(
1174            r#"[
1175                1,
1176                // This is element 2
1177                2,
1178                /* Block comment */ 3,
1179                4 // Trailing comment
1180            ]"#,
1181            2,
1182            &[],
1183            Some(json!("replaced")),
1184            r#"[
1185                1,
1186                // This is element 2
1187                2,
1188                /* Block comment */ "replaced",
1189                4 // Trailing comment
1190            ]"#,
1191        );
1192
1193        // Test empty array with comments
1194        check_array_replace(
1195            r#"[
1196                // Empty array with comment
1197            ]"#
1198            .unindent(),
1199            0,
1200            &[],
1201            Some(json!("first")),
1202            r#"[
1203                // Empty array with comment
1204                "first"
1205            ]"#
1206            .unindent(),
1207        );
1208        check_array_replace(
1209            r#"[]"#.unindent(),
1210            0,
1211            &[],
1212            Some(json!("first")),
1213            r#"[
1214                "first"
1215            ]"#
1216            .unindent(),
1217        );
1218
1219        // Test array with leading comments
1220        check_array_replace(
1221            r#"[
1222                // Leading comment
1223                // Another leading comment
1224                1,
1225                2
1226            ]"#,
1227            0,
1228            &[],
1229            Some(json!({"new": "object"})),
1230            r#"[
1231                // Leading comment
1232                // Another leading comment
1233                {
1234                    "new": "object"
1235                },
1236                2
1237            ]"#,
1238        );
1239
1240        // Test with deep indentation
1241        check_array_replace(
1242            r#"[
1243                        1,
1244                        2,
1245                        3
1246                    ]"#,
1247            1,
1248            &[],
1249            Some(json!("deep")),
1250            r#"[
1251                        1,
1252                        "deep",
1253                        3
1254                    ]"#,
1255        );
1256
1257        // Test with mixed spacing
1258        check_array_replace(
1259            r#"[1,2,   3,    4]"#,
1260            2,
1261            &[],
1262            Some(json!("spaced")),
1263            r#"[1,2,   "spaced",    4]"#,
1264        );
1265
1266        // Test replacing nested array element
1267        check_array_replace(
1268            r#"[
1269                [1, 2, 3],
1270                [4, 5, 6],
1271                [7, 8, 9]
1272            ]"#,
1273            1,
1274            &[],
1275            Some(json!(["a", "b", "c", "d"])),
1276            r#"[
1277                [1, 2, 3],
1278                [
1279                    "a",
1280                    "b",
1281                    "c",
1282                    "d"
1283                ],
1284                [7, 8, 9]
1285            ]"#,
1286        );
1287
1288        // Test with multiline block comments
1289        check_array_replace(
1290            r#"[
1291                /*
1292                 * This is a
1293                 * multiline comment
1294                 */
1295                "first",
1296                "second"
1297            ]"#,
1298            0,
1299            &[],
1300            Some(json!("updated")),
1301            r#"[
1302                /*
1303                 * This is a
1304                 * multiline comment
1305                 */
1306                "updated",
1307                "second"
1308            ]"#,
1309        );
1310
1311        // Test replacing with null
1312        check_array_replace(
1313            r#"[true, false, true]"#,
1314            1,
1315            &[],
1316            Some(json!(null)),
1317            r#"[true, null, true]"#,
1318        );
1319
1320        // Test single element array
1321        check_array_replace(
1322            r#"[42]"#,
1323            0,
1324            &[],
1325            Some(json!({"answer": 42})),
1326            r#"[{ "answer": 42 }]"#,
1327        );
1328
1329        // Test array with only comments
1330        check_array_replace(
1331            r#"[
1332                // Comment 1
1333                // Comment 2
1334                // Comment 3
1335            ]"#
1336            .unindent(),
1337            10,
1338            &[],
1339            Some(json!(123)),
1340            r#"[
1341                // Comment 1
1342                // Comment 2
1343                // Comment 3
1344                123
1345            ]"#
1346            .unindent(),
1347        );
1348
1349        check_array_replace(
1350            r#"[
1351                {
1352                    "key": "value"
1353                },
1354                {
1355                    "key": "value2"
1356                }
1357            ]"#
1358            .unindent(),
1359            0,
1360            &[],
1361            None,
1362            r#"[
1363                {
1364                    "key": "value2"
1365                }
1366            ]"#
1367            .unindent(),
1368        );
1369
1370        check_array_replace(
1371            r#"[
1372                {
1373                    "key": "value"
1374                },
1375                {
1376                    "key": "value2"
1377                },
1378                {
1379                    "key": "value3"
1380                },
1381            ]"#
1382            .unindent(),
1383            1,
1384            &[],
1385            None,
1386            r#"[
1387                {
1388                    "key": "value"
1389                },
1390                {
1391                    "key": "value3"
1392                },
1393            ]"#
1394            .unindent(),
1395        );
1396    }
1397
1398    #[test]
1399    fn array_append() {
1400        #[track_caller]
1401        fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
1402            let input = input.to_string();
1403            let result = append_top_level_array_value_in_json_text(&input, &value, 4)
1404                .expect("append succeeded");
1405            let mut result_str = input;
1406            result_str.replace_range(result.0, &result.1);
1407            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1408        }
1409        check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
1410        check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
1411        check_array_append(r#"[1, 3, 3   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
1412        check_array_append(r#"[1, 3, 3,   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
1413        check_array_append(
1414            r#"[
1415                1,
1416                2,
1417                3
1418            ]"#
1419            .unindent(),
1420            json!(4),
1421            r#"[
1422                1,
1423                2,
1424                3,
1425                4
1426            ]"#
1427            .unindent(),
1428        );
1429        check_array_append(
1430            r#"[
1431                1,
1432                2,
1433                3,
1434            ]"#
1435            .unindent(),
1436            json!(4),
1437            r#"[
1438                1,
1439                2,
1440                3,
1441                4
1442            ]"#
1443            .unindent(),
1444        );
1445        check_array_append(
1446            r#"[
1447                1,
1448                2,
1449                3,
1450            ]"#
1451            .unindent(),
1452            json!({"foo": "bar", "baz": "qux"}),
1453            r#"[
1454                1,
1455                2,
1456                3,
1457                {
1458                    "foo": "bar",
1459                    "baz": "qux"
1460                }
1461            ]"#
1462            .unindent(),
1463        );
1464        check_array_append(
1465            r#"[ 1, 2, 3, ]"#.unindent(),
1466            json!({"foo": "bar", "baz": "qux"}),
1467            r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
1468        );
1469        check_array_append(
1470            r#"[]"#,
1471            json!({"foo": "bar"}),
1472            r#"[
1473                {
1474                    "foo": "bar"
1475                }
1476            ]"#
1477            .unindent(),
1478        );
1479
1480        // Test with comments between array elements
1481        check_array_append(
1482            r#"[
1483                1,
1484                // Comment between elements
1485                2,
1486                /* Block comment */ 3
1487            ]"#
1488            .unindent(),
1489            json!(4),
1490            r#"[
1491                1,
1492                // Comment between elements
1493                2,
1494                /* Block comment */ 3,
1495                4
1496            ]"#
1497            .unindent(),
1498        );
1499
1500        // Test with trailing comment on last element
1501        check_array_append(
1502            r#"[
1503                1,
1504                2,
1505                3 // Trailing comment
1506            ]"#
1507            .unindent(),
1508            json!("new"),
1509            r#"[
1510                1,
1511                2,
1512                3 // Trailing comment
1513            ,
1514                "new"
1515            ]"#
1516            .unindent(),
1517        );
1518
1519        // Test empty array with comments
1520        check_array_append(
1521            r#"[
1522                // Empty array with comment
1523            ]"#
1524            .unindent(),
1525            json!("first"),
1526            r#"[
1527                // Empty array with comment
1528                "first"
1529            ]"#
1530            .unindent(),
1531        );
1532
1533        // Test with multiline block comment at end
1534        check_array_append(
1535            r#"[
1536                1,
1537                2
1538                /*
1539                 * This is a
1540                 * multiline comment
1541                 */
1542            ]"#
1543            .unindent(),
1544            json!(3),
1545            r#"[
1546                1,
1547                2
1548                /*
1549                 * This is a
1550                 * multiline comment
1551                 */
1552            ,
1553                3
1554            ]"#
1555            .unindent(),
1556        );
1557
1558        // Test with deep indentation
1559        check_array_append(
1560            r#"[
1561                1,
1562                    2,
1563                        3
1564            ]"#
1565            .unindent(),
1566            json!("deep"),
1567            r#"[
1568                1,
1569                    2,
1570                        3,
1571                        "deep"
1572            ]"#
1573            .unindent(),
1574        );
1575
1576        // Test with no spacing
1577        check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
1578
1579        // Test appending complex nested structure
1580        check_array_append(
1581            r#"[
1582                {"a": 1},
1583                {"b": 2}
1584            ]"#
1585            .unindent(),
1586            json!({"c": {"nested": [1, 2, 3]}}),
1587            r#"[
1588                {"a": 1},
1589                {"b": 2},
1590                {
1591                    "c": {
1592                        "nested": [
1593                            1,
1594                            2,
1595                            3
1596                        ]
1597                    }
1598                }
1599            ]"#
1600            .unindent(),
1601        );
1602
1603        // Test array ending with comment after bracket
1604        check_array_append(
1605            r#"[
1606                1,
1607                2,
1608                3
1609            ] // Comment after array"#
1610                .unindent(),
1611            json!(4),
1612            r#"[
1613                1,
1614                2,
1615                3,
1616                4
1617            ] // Comment after array"#
1618                .unindent(),
1619        );
1620
1621        // Test with inconsistent element formatting
1622        check_array_append(
1623            r#"[1,
1624               2,
1625                    3,
1626            ]"#
1627            .unindent(),
1628            json!(4),
1629            r#"[1,
1630               2,
1631                    3,
1632                    4
1633            ]"#
1634            .unindent(),
1635        );
1636
1637        // Test appending to single-line array with trailing comma
1638        check_array_append(
1639            r#"[1, 2, 3,]"#,
1640            json!({"key": "value"}),
1641            r#"[1, 2, 3, { "key": "value" }]"#,
1642        );
1643
1644        // Test appending null value
1645        check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
1646
1647        // Test appending to array with only comments
1648        check_array_append(
1649            r#"[
1650                // Just comments here
1651                // More comments
1652            ]"#
1653            .unindent(),
1654            json!(42),
1655            r#"[
1656                // Just comments here
1657                // More comments
1658                42
1659            ]"#
1660            .unindent(),
1661        );
1662    }
1663}