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    Ok(serde_json_lenient::from_str(content)?)
 567}
 568
 569#[cfg(test)]
 570mod tests {
 571    use super::*;
 572    use serde_json::{Value, json};
 573    use unindent::Unindent;
 574
 575    #[test]
 576    fn object_replace() {
 577        #[track_caller]
 578        fn check_object_replace(
 579            input: String,
 580            key_path: &[&str],
 581            value: Option<Value>,
 582            expected: String,
 583        ) {
 584            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
 585            let mut result_str = input;
 586            result_str.replace_range(result.0, &result.1);
 587            pretty_assertions::assert_eq!(expected, result_str);
 588        }
 589        check_object_replace(
 590            r#"{
 591                "a": 1,
 592                "b": 2
 593            }"#
 594            .unindent(),
 595            &["b"],
 596            Some(json!(3)),
 597            r#"{
 598                "a": 1,
 599                "b": 3
 600            }"#
 601            .unindent(),
 602        );
 603        check_object_replace(
 604            r#"{
 605                "a": 1,
 606                "b": 2
 607            }"#
 608            .unindent(),
 609            &["b"],
 610            None,
 611            r#"{
 612                "a": 1
 613            }"#
 614            .unindent(),
 615        );
 616        check_object_replace(
 617            r#"{
 618                "a": 1,
 619                "b": 2
 620            }"#
 621            .unindent(),
 622            &["c"],
 623            Some(json!(3)),
 624            r#"{
 625                "c": 3,
 626                "a": 1,
 627                "b": 2
 628            }"#
 629            .unindent(),
 630        );
 631        check_object_replace(
 632            r#"{
 633                "a": 1,
 634                "b": {
 635                    "c": 2,
 636                    "d": 3,
 637                }
 638            }"#
 639            .unindent(),
 640            &["b", "c"],
 641            Some(json!([1, 2, 3])),
 642            r#"{
 643                "a": 1,
 644                "b": {
 645                    "c": [
 646                        1,
 647                        2,
 648                        3
 649                    ],
 650                    "d": 3,
 651                }
 652            }"#
 653            .unindent(),
 654        );
 655
 656        check_object_replace(
 657            r#"{
 658                "name": "old_name",
 659                "id": 123
 660            }"#
 661            .unindent(),
 662            &["name"],
 663            Some(json!("new_name")),
 664            r#"{
 665                "name": "new_name",
 666                "id": 123
 667            }"#
 668            .unindent(),
 669        );
 670
 671        check_object_replace(
 672            r#"{
 673                "enabled": false,
 674                "count": 5
 675            }"#
 676            .unindent(),
 677            &["enabled"],
 678            Some(json!(true)),
 679            r#"{
 680                "enabled": true,
 681                "count": 5
 682            }"#
 683            .unindent(),
 684        );
 685
 686        check_object_replace(
 687            r#"{
 688                "value": null,
 689                "other": "test"
 690            }"#
 691            .unindent(),
 692            &["value"],
 693            Some(json!(42)),
 694            r#"{
 695                "value": 42,
 696                "other": "test"
 697            }"#
 698            .unindent(),
 699        );
 700
 701        check_object_replace(
 702            r#"{
 703                "config": {
 704                    "old": true
 705                },
 706                "name": "test"
 707            }"#
 708            .unindent(),
 709            &["config"],
 710            Some(json!({"new": false, "count": 3})),
 711            r#"{
 712                "config": {
 713                    "new": false,
 714                    "count": 3
 715                },
 716                "name": "test"
 717            }"#
 718            .unindent(),
 719        );
 720
 721        check_object_replace(
 722            r#"{
 723                // This is a comment
 724                "a": 1,
 725                "b": 2 // Another comment
 726            }"#
 727            .unindent(),
 728            &["b"],
 729            Some(json!({"foo": "bar"})),
 730            r#"{
 731                // This is a comment
 732                "a": 1,
 733                "b": {
 734                    "foo": "bar"
 735                } // Another comment
 736            }"#
 737            .unindent(),
 738        );
 739
 740        check_object_replace(
 741            r#"{}"#.to_string(),
 742            &["new_key"],
 743            Some(json!("value")),
 744            r#"{
 745                "new_key": "value"
 746            }
 747            "#
 748            .unindent(),
 749        );
 750
 751        check_object_replace(
 752            r#"{
 753                "only_key": 123
 754            }"#
 755            .unindent(),
 756            &["only_key"],
 757            None,
 758            "{\n    \n}".to_string(),
 759        );
 760
 761        check_object_replace(
 762            r#"{
 763                "level1": {
 764                    "level2": {
 765                        "level3": {
 766                            "target": "old"
 767                        }
 768                    }
 769                }
 770            }"#
 771            .unindent(),
 772            &["level1", "level2", "level3", "target"],
 773            Some(json!("new")),
 774            r#"{
 775                "level1": {
 776                    "level2": {
 777                        "level3": {
 778                            "target": "new"
 779                        }
 780                    }
 781                }
 782            }"#
 783            .unindent(),
 784        );
 785
 786        check_object_replace(
 787            r#"{
 788                "parent": {}
 789            }"#
 790            .unindent(),
 791            &["parent", "child"],
 792            Some(json!("value")),
 793            r#"{
 794                "parent": {
 795                    "child": "value"
 796                }
 797            }"#
 798            .unindent(),
 799        );
 800
 801        check_object_replace(
 802            r#"{
 803                "a": 1,
 804                "b": 2,
 805            }"#
 806            .unindent(),
 807            &["b"],
 808            Some(json!(3)),
 809            r#"{
 810                "a": 1,
 811                "b": 3,
 812            }"#
 813            .unindent(),
 814        );
 815
 816        check_object_replace(
 817            r#"{
 818                "items": [1, 2, 3],
 819                "count": 3
 820            }"#
 821            .unindent(),
 822            &["items", "1"],
 823            Some(json!(5)),
 824            r#"{
 825                "items": {
 826                    "1": 5
 827                },
 828                "count": 3
 829            }"#
 830            .unindent(),
 831        );
 832
 833        check_object_replace(
 834            r#"{
 835                "items": [1, 2, 3],
 836                "count": 3
 837            }"#
 838            .unindent(),
 839            &["items", "1"],
 840            None,
 841            r#"{
 842                "items": {
 843                    "1": null
 844                },
 845                "count": 3
 846            }"#
 847            .unindent(),
 848        );
 849
 850        check_object_replace(
 851            r#"{
 852                "items": [1, 2, 3],
 853                "count": 3
 854            }"#
 855            .unindent(),
 856            &["items"],
 857            Some(json!(["a", "b", "c", "d"])),
 858            r#"{
 859                "items": [
 860                    "a",
 861                    "b",
 862                    "c",
 863                    "d"
 864                ],
 865                "count": 3
 866            }"#
 867            .unindent(),
 868        );
 869
 870        check_object_replace(
 871            r#"{
 872                "0": "zero",
 873                "1": "one"
 874            }"#
 875            .unindent(),
 876            &["1"],
 877            Some(json!("ONE")),
 878            r#"{
 879                "0": "zero",
 880                "1": "ONE"
 881            }"#
 882            .unindent(),
 883        );
 884        // Test with comments between object members
 885        check_object_replace(
 886            r#"{
 887                "a": 1,
 888                // Comment between members
 889                "b": 2,
 890                /* Block comment */
 891                "c": 3
 892            }"#
 893            .unindent(),
 894            &["b"],
 895            Some(json!({"nested": true})),
 896            r#"{
 897                "a": 1,
 898                // Comment between members
 899                "b": {
 900                    "nested": true
 901                },
 902                /* Block comment */
 903                "c": 3
 904            }"#
 905            .unindent(),
 906        );
 907
 908        // Test with trailing comments on replaced value
 909        check_object_replace(
 910            r#"{
 911                "a": 1, // keep this comment
 912                "b": 2  // this should stay
 913            }"#
 914            .unindent(),
 915            &["a"],
 916            Some(json!("changed")),
 917            r#"{
 918                "a": "changed", // keep this comment
 919                "b": 2  // this should stay
 920            }"#
 921            .unindent(),
 922        );
 923
 924        // Test with deep indentation
 925        check_object_replace(
 926            r#"{
 927                        "deeply": {
 928                                "nested": {
 929                                        "value": "old"
 930                                }
 931                        }
 932                }"#
 933            .unindent(),
 934            &["deeply", "nested", "value"],
 935            Some(json!("new")),
 936            r#"{
 937                        "deeply": {
 938                                "nested": {
 939                                        "value": "new"
 940                                }
 941                        }
 942                }"#
 943            .unindent(),
 944        );
 945
 946        // Test removing value with comment preservation
 947        check_object_replace(
 948            r#"{
 949                // Header comment
 950                "a": 1,
 951                // This comment belongs to b
 952                "b": 2,
 953                // This comment belongs to c
 954                "c": 3
 955            }"#
 956            .unindent(),
 957            &["b"],
 958            None,
 959            r#"{
 960                // Header comment
 961                "a": 1,
 962                // This comment belongs to b
 963                // This comment belongs to c
 964                "c": 3
 965            }"#
 966            .unindent(),
 967        );
 968
 969        // Test with multiline block comments
 970        check_object_replace(
 971            r#"{
 972                /*
 973                 * This is a multiline
 974                 * block comment
 975                 */
 976                "value": "old",
 977                /* Another block */ "other": 123
 978            }"#
 979            .unindent(),
 980            &["value"],
 981            Some(json!("new")),
 982            r#"{
 983                /*
 984                 * This is a multiline
 985                 * block comment
 986                 */
 987                "value": "new",
 988                /* Another block */ "other": 123
 989            }"#
 990            .unindent(),
 991        );
 992
 993        check_object_replace(
 994            r#"{
 995                // This object is empty
 996            }"#
 997            .unindent(),
 998            &["key"],
 999            Some(json!("value")),
1000            r#"{
1001                // This object is empty
1002                "key": "value"
1003            }
1004            "#
1005            .unindent(),
1006        );
1007
1008        // Test replacing in object with only comments
1009        check_object_replace(
1010            r#"{
1011                // Comment 1
1012                // Comment 2
1013            }"#
1014            .unindent(),
1015            &["new"],
1016            Some(json!(42)),
1017            r#"{
1018                // Comment 1
1019                // Comment 2
1020                "new": 42
1021            }
1022            "#
1023            .unindent(),
1024        );
1025
1026        // Test with inconsistent spacing
1027        check_object_replace(
1028            r#"{
1029              "a":1,
1030                    "b"  :  2  ,
1031                "c":   3
1032            }"#
1033            .unindent(),
1034            &["b"],
1035            Some(json!("spaced")),
1036            r#"{
1037              "a":1,
1038                    "b"  :  "spaced"  ,
1039                "c":   3
1040            }"#
1041            .unindent(),
1042        );
1043    }
1044
1045    #[test]
1046    fn array_replace() {
1047        #[track_caller]
1048        fn check_array_replace(
1049            input: impl ToString,
1050            index: usize,
1051            key_path: &[&str],
1052            value: Option<Value>,
1053            expected: impl ToString,
1054        ) {
1055            let input = input.to_string();
1056            let result = replace_top_level_array_value_in_json_text(
1057                &input,
1058                key_path,
1059                value.as_ref(),
1060                None,
1061                index,
1062                4,
1063            )
1064            .expect("replace succeeded");
1065            let mut result_str = input;
1066            result_str.replace_range(result.0, &result.1);
1067            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1068        }
1069
1070        check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1071        check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1072        check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1073        check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1074        check_array_replace(
1075            r#"[
1076                1,
1077                2,
1078                3,
1079            ]"#
1080            .unindent(),
1081            1,
1082            &[],
1083            Some(json!({"foo": "bar", "baz": "qux"})),
1084            r#"[
1085                1,
1086                {
1087                    "foo": "bar",
1088                    "baz": "qux"
1089                },
1090                3,
1091            ]"#
1092            .unindent(),
1093        );
1094        check_array_replace(
1095            r#"[1, 3, 3,]"#,
1096            1,
1097            &[],
1098            Some(json!({"foo": "bar", "baz": "qux"})),
1099            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1100        );
1101
1102        check_array_replace(
1103            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1104            1,
1105            &["baz"],
1106            Some(json!({"qux": "quz"})),
1107            r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1108        );
1109
1110        check_array_replace(
1111            r#"[
1112                1,
1113                {
1114                    "foo": "bar",
1115                    "baz": "qux"
1116                },
1117                3
1118            ]"#,
1119            1,
1120            &["baz"],
1121            Some(json!({"qux": "quz"})),
1122            r#"[
1123                1,
1124                {
1125                    "foo": "bar",
1126                    "baz": {
1127                        "qux": "quz"
1128                    }
1129                },
1130                3
1131            ]"#,
1132        );
1133
1134        check_array_replace(
1135            r#"[
1136                1,
1137                {
1138                    "foo": "bar",
1139                    "baz": {
1140                        "qux": "quz"
1141                    }
1142                },
1143                3
1144            ]"#,
1145            1,
1146            &["baz"],
1147            Some(json!("qux")),
1148            r#"[
1149                1,
1150                {
1151                    "foo": "bar",
1152                    "baz": "qux"
1153                },
1154                3
1155            ]"#,
1156        );
1157
1158        check_array_replace(
1159            r#"[
1160                1,
1161                {
1162                    "foo": "bar",
1163                    // some comment to keep
1164                    "baz": {
1165                        // some comment to remove
1166                        "qux": "quz"
1167                    }
1168                    // some other comment to keep
1169                },
1170                3
1171            ]"#,
1172            1,
1173            &["baz"],
1174            Some(json!("qux")),
1175            r#"[
1176                1,
1177                {
1178                    "foo": "bar",
1179                    // some comment to keep
1180                    "baz": "qux"
1181                    // some other comment to keep
1182                },
1183                3
1184            ]"#,
1185        );
1186
1187        // Test with comments between array elements
1188        check_array_replace(
1189            r#"[
1190                1,
1191                // This is element 2
1192                2,
1193                /* Block comment */ 3,
1194                4 // Trailing comment
1195            ]"#,
1196            2,
1197            &[],
1198            Some(json!("replaced")),
1199            r#"[
1200                1,
1201                // This is element 2
1202                2,
1203                /* Block comment */ "replaced",
1204                4 // Trailing comment
1205            ]"#,
1206        );
1207
1208        // Test empty array with comments
1209        check_array_replace(
1210            r#"[
1211                // Empty array with comment
1212            ]"#
1213            .unindent(),
1214            0,
1215            &[],
1216            Some(json!("first")),
1217            r#"[
1218                // Empty array with comment
1219                "first"
1220            ]"#
1221            .unindent(),
1222        );
1223        check_array_replace(
1224            r#"[]"#.unindent(),
1225            0,
1226            &[],
1227            Some(json!("first")),
1228            r#"[
1229                "first"
1230            ]"#
1231            .unindent(),
1232        );
1233
1234        // Test array with leading comments
1235        check_array_replace(
1236            r#"[
1237                // Leading comment
1238                // Another leading comment
1239                1,
1240                2
1241            ]"#,
1242            0,
1243            &[],
1244            Some(json!({"new": "object"})),
1245            r#"[
1246                // Leading comment
1247                // Another leading comment
1248                {
1249                    "new": "object"
1250                },
1251                2
1252            ]"#,
1253        );
1254
1255        // Test with deep indentation
1256        check_array_replace(
1257            r#"[
1258                        1,
1259                        2,
1260                        3
1261                    ]"#,
1262            1,
1263            &[],
1264            Some(json!("deep")),
1265            r#"[
1266                        1,
1267                        "deep",
1268                        3
1269                    ]"#,
1270        );
1271
1272        // Test with mixed spacing
1273        check_array_replace(
1274            r#"[1,2,   3,    4]"#,
1275            2,
1276            &[],
1277            Some(json!("spaced")),
1278            r#"[1,2,   "spaced",    4]"#,
1279        );
1280
1281        // Test replacing nested array element
1282        check_array_replace(
1283            r#"[
1284                [1, 2, 3],
1285                [4, 5, 6],
1286                [7, 8, 9]
1287            ]"#,
1288            1,
1289            &[],
1290            Some(json!(["a", "b", "c", "d"])),
1291            r#"[
1292                [1, 2, 3],
1293                [
1294                    "a",
1295                    "b",
1296                    "c",
1297                    "d"
1298                ],
1299                [7, 8, 9]
1300            ]"#,
1301        );
1302
1303        // Test with multiline block comments
1304        check_array_replace(
1305            r#"[
1306                /*
1307                 * This is a
1308                 * multiline comment
1309                 */
1310                "first",
1311                "second"
1312            ]"#,
1313            0,
1314            &[],
1315            Some(json!("updated")),
1316            r#"[
1317                /*
1318                 * This is a
1319                 * multiline comment
1320                 */
1321                "updated",
1322                "second"
1323            ]"#,
1324        );
1325
1326        // Test replacing with null
1327        check_array_replace(
1328            r#"[true, false, true]"#,
1329            1,
1330            &[],
1331            Some(json!(null)),
1332            r#"[true, null, true]"#,
1333        );
1334
1335        // Test single element array
1336        check_array_replace(
1337            r#"[42]"#,
1338            0,
1339            &[],
1340            Some(json!({"answer": 42})),
1341            r#"[{ "answer": 42 }]"#,
1342        );
1343
1344        // Test array with only comments
1345        check_array_replace(
1346            r#"[
1347                // Comment 1
1348                // Comment 2
1349                // Comment 3
1350            ]"#
1351            .unindent(),
1352            10,
1353            &[],
1354            Some(json!(123)),
1355            r#"[
1356                // Comment 1
1357                // Comment 2
1358                // Comment 3
1359                123
1360            ]"#
1361            .unindent(),
1362        );
1363
1364        check_array_replace(
1365            r#"[
1366                {
1367                    "key": "value"
1368                },
1369                {
1370                    "key": "value2"
1371                }
1372            ]"#
1373            .unindent(),
1374            0,
1375            &[],
1376            None,
1377            r#"[
1378                {
1379                    "key": "value2"
1380                }
1381            ]"#
1382            .unindent(),
1383        );
1384
1385        check_array_replace(
1386            r#"[
1387                {
1388                    "key": "value"
1389                },
1390                {
1391                    "key": "value2"
1392                },
1393                {
1394                    "key": "value3"
1395                },
1396            ]"#
1397            .unindent(),
1398            1,
1399            &[],
1400            None,
1401            r#"[
1402                {
1403                    "key": "value"
1404                },
1405                {
1406                    "key": "value3"
1407                },
1408            ]"#
1409            .unindent(),
1410        );
1411    }
1412
1413    #[test]
1414    fn array_append() {
1415        #[track_caller]
1416        fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
1417            let input = input.to_string();
1418            let result = append_top_level_array_value_in_json_text(&input, &value, 4)
1419                .expect("append succeeded");
1420            let mut result_str = input;
1421            result_str.replace_range(result.0, &result.1);
1422            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1423        }
1424        check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
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(
1429            r#"[
1430                1,
1431                2,
1432                3
1433            ]"#
1434            .unindent(),
1435            json!(4),
1436            r#"[
1437                1,
1438                2,
1439                3,
1440                4
1441            ]"#
1442            .unindent(),
1443        );
1444        check_array_append(
1445            r#"[
1446                1,
1447                2,
1448                3,
1449            ]"#
1450            .unindent(),
1451            json!(4),
1452            r#"[
1453                1,
1454                2,
1455                3,
1456                4
1457            ]"#
1458            .unindent(),
1459        );
1460        check_array_append(
1461            r#"[
1462                1,
1463                2,
1464                3,
1465            ]"#
1466            .unindent(),
1467            json!({"foo": "bar", "baz": "qux"}),
1468            r#"[
1469                1,
1470                2,
1471                3,
1472                {
1473                    "foo": "bar",
1474                    "baz": "qux"
1475                }
1476            ]"#
1477            .unindent(),
1478        );
1479        check_array_append(
1480            r#"[ 1, 2, 3, ]"#.unindent(),
1481            json!({"foo": "bar", "baz": "qux"}),
1482            r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
1483        );
1484        check_array_append(
1485            r#"[]"#,
1486            json!({"foo": "bar"}),
1487            r#"[
1488                {
1489                    "foo": "bar"
1490                }
1491            ]"#
1492            .unindent(),
1493        );
1494
1495        // Test with comments between array elements
1496        check_array_append(
1497            r#"[
1498                1,
1499                // Comment between elements
1500                2,
1501                /* Block comment */ 3
1502            ]"#
1503            .unindent(),
1504            json!(4),
1505            r#"[
1506                1,
1507                // Comment between elements
1508                2,
1509                /* Block comment */ 3,
1510                4
1511            ]"#
1512            .unindent(),
1513        );
1514
1515        // Test with trailing comment on last element
1516        check_array_append(
1517            r#"[
1518                1,
1519                2,
1520                3 // Trailing comment
1521            ]"#
1522            .unindent(),
1523            json!("new"),
1524            r#"[
1525                1,
1526                2,
1527                3 // Trailing comment
1528            ,
1529                "new"
1530            ]"#
1531            .unindent(),
1532        );
1533
1534        // Test empty array with comments
1535        check_array_append(
1536            r#"[
1537                // Empty array with comment
1538            ]"#
1539            .unindent(),
1540            json!("first"),
1541            r#"[
1542                // Empty array with comment
1543                "first"
1544            ]"#
1545            .unindent(),
1546        );
1547
1548        // Test with multiline block comment at end
1549        check_array_append(
1550            r#"[
1551                1,
1552                2
1553                /*
1554                 * This is a
1555                 * multiline comment
1556                 */
1557            ]"#
1558            .unindent(),
1559            json!(3),
1560            r#"[
1561                1,
1562                2
1563                /*
1564                 * This is a
1565                 * multiline comment
1566                 */
1567            ,
1568                3
1569            ]"#
1570            .unindent(),
1571        );
1572
1573        // Test with deep indentation
1574        check_array_append(
1575            r#"[
1576                1,
1577                    2,
1578                        3
1579            ]"#
1580            .unindent(),
1581            json!("deep"),
1582            r#"[
1583                1,
1584                    2,
1585                        3,
1586                        "deep"
1587            ]"#
1588            .unindent(),
1589        );
1590
1591        // Test with no spacing
1592        check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
1593
1594        // Test appending complex nested structure
1595        check_array_append(
1596            r#"[
1597                {"a": 1},
1598                {"b": 2}
1599            ]"#
1600            .unindent(),
1601            json!({"c": {"nested": [1, 2, 3]}}),
1602            r#"[
1603                {"a": 1},
1604                {"b": 2},
1605                {
1606                    "c": {
1607                        "nested": [
1608                            1,
1609                            2,
1610                            3
1611                        ]
1612                    }
1613                }
1614            ]"#
1615            .unindent(),
1616        );
1617
1618        // Test array ending with comment after bracket
1619        check_array_append(
1620            r#"[
1621                1,
1622                2,
1623                3
1624            ] // Comment after array"#
1625                .unindent(),
1626            json!(4),
1627            r#"[
1628                1,
1629                2,
1630                3,
1631                4
1632            ] // Comment after array"#
1633                .unindent(),
1634        );
1635
1636        // Test with inconsistent element formatting
1637        check_array_append(
1638            r#"[1,
1639               2,
1640                    3,
1641            ]"#
1642            .unindent(),
1643            json!(4),
1644            r#"[1,
1645               2,
1646                    3,
1647                    4
1648            ]"#
1649            .unindent(),
1650        );
1651
1652        // Test appending to single-line array with trailing comma
1653        check_array_append(
1654            r#"[1, 2, 3,]"#,
1655            json!({"key": "value"}),
1656            r#"[1, 2, 3, { "key": "value" }]"#,
1657        );
1658
1659        // Test appending null value
1660        check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
1661
1662        // Test appending to array with only comments
1663        check_array_append(
1664            r#"[
1665                // Just comments here
1666                // More comments
1667            ]"#
1668            .unindent(),
1669            json!(42),
1670            r#"[
1671                // Just comments here
1672                // More comments
1673                42
1674            ]"#
1675            .unindent(),
1676        );
1677    }
1678}