settings_json.rs

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