settings_json.rs

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