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