settings_json.rs

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