Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:01:19.937Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

settings_json.rs

2636 lines · 74.4 KB · rust
1use anyhow::Result;
2use serde::{Serialize, de::DeserializeOwned};
3use serde_json::Value;
4use std::{ops::Range, sync::LazyLock};
5use tree_sitter::{Query, StreamingIterator as _};
6use util::RangeExt;
7
8pub fn update_value_in_json_text<'a>(
9    text: &mut String,
10    key_path: &mut Vec<&'a str>,
11    tab_size: usize,
12    old_value: &'a Value,
13    new_value: &'a Value,
14    edits: &mut Vec<(Range<usize>, String)>,
15) {
16    // If the old and new values are both objects, then compare them key by key,
17    // preserving the comments and formatting of the unchanged parts. Otherwise,
18    // replace the old value with the new value.
19    if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
20        for (key, old_sub_value) in old_object.iter() {
21            key_path.push(key);
22            if let Some(new_sub_value) = new_object.get(key) {
23                // Key exists in both old and new, recursively update
24                update_value_in_json_text(
25                    text,
26                    key_path,
27                    tab_size,
28                    old_sub_value,
29                    new_sub_value,
30                    edits,
31                );
32            } else {
33                // Key was removed from new object, remove the entire key-value pair
34                let (range, replacement) =
35                    replace_value_in_json_text(text, key_path, 0, None, None);
36                text.replace_range(range.clone(), &replacement);
37                edits.push((range, replacement));
38            }
39            key_path.pop();
40        }
41        for (key, new_sub_value) in new_object.iter() {
42            key_path.push(key);
43            if !old_object.contains_key(key) {
44                update_value_in_json_text(
45                    text,
46                    key_path,
47                    tab_size,
48                    &Value::Null,
49                    new_sub_value,
50                    edits,
51                );
52            }
53            key_path.pop();
54        }
55    } else if old_value != new_value {
56        let mut new_value = new_value.clone();
57        if let Some(new_object) = new_value.as_object_mut() {
58            new_object.retain(|_, v| !v.is_null());
59        }
60        let (range, replacement) =
61            replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None);
62        text.replace_range(range.clone(), &replacement);
63        edits.push((range, replacement));
64    }
65}
66
67/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`.
68pub fn replace_value_in_json_text<T: AsRef<str>>(
69    text: &str,
70    key_path: &[T],
71    tab_size: usize,
72    new_value: Option<&Value>,
73    replace_key: Option<&str>,
74) -> (Range<usize>, String) {
75    static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
76        Query::new(
77            &tree_sitter_json::LANGUAGE.into(),
78            "(pair key: (string) @key value: (_) @value)",
79        )
80        .expect("Failed to create PAIR_QUERY")
81    });
82
83    let mut parser = tree_sitter::Parser::new();
84    parser
85        .set_language(&tree_sitter_json::LANGUAGE.into())
86        .unwrap();
87    let syntax_tree = parser.parse(text, None).unwrap();
88
89    let mut cursor = tree_sitter::QueryCursor::new();
90
91    let mut depth = 0;
92    let mut last_value_range = 0..0;
93    let mut first_key_start = None;
94    let mut existing_value_range = 0..text.len();
95
96    let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
97    while let Some(mat) = matches.next() {
98        if mat.captures.len() != 2 {
99            continue;
100        }
101
102        let key_range = mat.captures[0].node.byte_range();
103        let value_range = mat.captures[1].node.byte_range();
104
105        // Don't enter sub objects until we find an exact
106        // match for the current keypath
107        if last_value_range.contains_inclusive(&value_range) {
108            continue;
109        }
110
111        last_value_range = value_range.clone();
112
113        if key_range.start > existing_value_range.end {
114            break;
115        }
116
117        first_key_start.get_or_insert(key_range.start);
118
119        let found_key = text
120            .get(key_range.clone())
121            .zip(key_path.get(depth))
122            .and_then(|(key_text, key_path_value)| {
123                serde_json::to_string(key_path_value.as_ref())
124                    .ok()
125                    .map(|key_path| depth < key_path.len() && key_text == key_path)
126            })
127            .unwrap_or(false);
128
129        if found_key {
130            existing_value_range = value_range;
131            // Reset last value range when increasing in depth
132            last_value_range = existing_value_range.start..existing_value_range.start;
133            depth += 1;
134
135            if depth == key_path.len() {
136                break;
137            }
138
139            if let Some(array_replacement) = handle_possible_array_value(
140                &mat.captures[0].node,
141                &mat.captures[1].node,
142                text,
143                &key_path[depth..],
144                new_value,
145                replace_key,
146                tab_size,
147            ) {
148                return array_replacement;
149            }
150
151            first_key_start = None;
152        }
153    }
154
155    // We found the exact key we want
156    if depth == key_path.len() {
157        if let Some(new_value) = new_value {
158            let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
159            if let Some(replace_key) = replace_key.and_then(|str| serde_json::to_string(str).ok()) {
160                let new_key = format!("{}: ", replace_key);
161                if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
162                    if let Some(prev_key_start) = text[..key_start].rfind('"') {
163                        existing_value_range.start = prev_key_start;
164                    } else {
165                        existing_value_range.start = key_start;
166                    }
167                }
168                (existing_value_range, new_key + &new_val)
169            } else {
170                (existing_value_range, new_val)
171            }
172        } else {
173            let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
174            let mut removal_end = existing_value_range.end;
175
176            // Find the actual key position by looking for the key in the pair
177            // We need to extend the range to include the key, not just the value
178            if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
179                if let Some(prev_key_start) = text[..key_start].rfind('"') {
180                    removal_start = prev_key_start;
181                } else {
182                    removal_start = key_start;
183                }
184            }
185
186            let mut removed_comma = false;
187            // Look backward for a preceding comma first
188            let preceding_text = text.get(0..removal_start).unwrap_or("");
189            if let Some(comma_pos) = preceding_text.rfind(',') {
190                // Check if there are only whitespace characters between the comma and our key
191                let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
192                if between_comma_and_key.trim().is_empty() {
193                    removal_start = comma_pos;
194                    removed_comma = true;
195                }
196            }
197            if let Some(remaining_text) = text.get(existing_value_range.end..)
198                && !removed_comma
199            {
200                let mut chars = remaining_text.char_indices();
201                while let Some((offset, ch)) = chars.next() {
202                    if ch == ',' {
203                        removal_end = existing_value_range.end + offset + 1;
204                        // Also consume whitespace after the comma
205                        for (_, next_ch) in chars.by_ref() {
206                            if next_ch.is_whitespace() {
207                                removal_end += next_ch.len_utf8();
208                            } else {
209                                break;
210                            }
211                        }
212                        break;
213                    } else if !ch.is_whitespace() {
214                        break;
215                    }
216                }
217            }
218            (removal_start..removal_end, String::new())
219        }
220    } else {
221        if let Some(first_key_start) = first_key_start {
222            // We have key paths, construct the sub objects
223            let new_key = key_path[depth].as_ref();
224            // We don't have the key, construct the nested objects
225            let new_value = construct_json_value(&key_path[(depth + 1)..], new_value);
226
227            let mut row = 0;
228            let mut column = 0;
229            for (ix, char) in text.char_indices() {
230                if ix == first_key_start {
231                    break;
232                }
233                if char == '\n' {
234                    row += 1;
235                    column = 0;
236                } else {
237                    column += char.len_utf8();
238                }
239            }
240
241            if row > 0 {
242                // depth is 0 based, but division needs to be 1 based.
243                let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
244                let space = ' ';
245                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
246                (first_key_start..first_key_start, content)
247            } else {
248                let new_val = serde_json::to_string(&new_value).unwrap();
249                let mut content = format!(r#""{new_key}": {new_val},"#);
250                content.push(' ');
251                (first_key_start..first_key_start, content)
252            }
253        } else {
254            // We don't have the key, construct the nested objects
255            let new_value = construct_json_value(&key_path[depth..], new_value);
256            let indent_prefix_len = tab_size * depth;
257            let mut new_val = to_pretty_json(&new_value, tab_size, indent_prefix_len);
258            if depth == 0 {
259                new_val.push('\n');
260            }
261            // best effort to keep comments with best effort indentation
262            let mut replace_text = &text[existing_value_range.clone()];
263            while let Some(comment_start) = replace_text.rfind("//") {
264                if let Some(comment_end) = replace_text[comment_start..].find('\n') {
265                    let mut comment_with_indent_start = replace_text[..comment_start]
266                        .rfind('\n')
267                        .unwrap_or(comment_start);
268                    if !replace_text[comment_with_indent_start..comment_start]
269                        .trim()
270                        .is_empty()
271                    {
272                        comment_with_indent_start = comment_start;
273                    }
274                    new_val.insert_str(
275                        1,
276                        &replace_text[comment_with_indent_start..comment_start + comment_end],
277                    );
278                }
279                replace_text = &replace_text[..comment_start];
280            }
281
282            (existing_value_range, new_val)
283        }
284    }
285}
286
287fn construct_json_value(
288    key_path: &[impl AsRef<str>],
289    new_value: Option<&serde_json::Value>,
290) -> serde_json::Value {
291    let mut new_value =
292        serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
293    for key in key_path.iter().rev() {
294        if parse_index_key(key.as_ref()).is_some() {
295            new_value = serde_json::json!([new_value]);
296        } else {
297            new_value = serde_json::json!({ key.as_ref().to_string(): new_value });
298        }
299    }
300    return new_value;
301}
302
303fn parse_index_key(index_key: &str) -> Option<usize> {
304    index_key.strip_prefix('#')?.parse().ok()
305}
306
307fn handle_possible_array_value(
308    key_node: &tree_sitter::Node,
309    value_node: &tree_sitter::Node,
310    text: &str,
311    remaining_key_path: &[impl AsRef<str>],
312    new_value: Option<&Value>,
313    replace_key: Option<&str>,
314    tab_size: usize,
315) -> Option<(Range<usize>, String)> {
316    if remaining_key_path.is_empty() {
317        return None;
318    }
319    let key_path = remaining_key_path;
320    let index = parse_index_key(key_path[0].as_ref())?;
321
322    let value_is_array = value_node.kind() == TS_ARRAY_KIND;
323
324    let array_str = if value_is_array {
325        &text[value_node.byte_range()]
326    } else {
327        ""
328    };
329
330    let (mut replace_range, mut replace_value) = replace_top_level_array_value_in_json_text(
331        array_str,
332        &key_path[1..],
333        new_value,
334        replace_key,
335        index,
336        tab_size,
337    );
338
339    if value_is_array {
340        replace_range.start += value_node.start_byte();
341        replace_range.end += value_node.start_byte();
342    } else {
343        // replace the full value if it wasn't an array
344        replace_range = value_node.byte_range();
345    }
346    let non_whitespace_char_count = replace_value.len()
347        - replace_value
348            .chars()
349            .filter(char::is_ascii_whitespace)
350            .count();
351    let needs_indent = replace_value.ends_with('\n')
352        || (replace_value
353            .chars()
354            .zip(replace_value.chars().skip(1))
355            .any(|(c, next_c)| c == '\n' && !next_c.is_ascii_whitespace()));
356    let contains_comment = (replace_value.contains("//") && replace_value.contains('\n'))
357        || (replace_value.contains("/*") && replace_value.contains("*/"));
358    if needs_indent {
359        let indent_width = key_node.start_position().column;
360        let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
361        replace_value = replace_value.replace('\n', &increased_indent);
362    } else if non_whitespace_char_count < 32 && !contains_comment {
363        // remove indentation
364        while let Some(idx) = replace_value.find("\n ") {
365            replace_value.remove(idx);
366        }
367        while let Some(idx) = replace_value.find("  ") {
368            replace_value.remove(idx);
369        }
370    }
371    return Some((replace_range, replace_value));
372}
373
374const TS_DOCUMENT_KIND: &str = "document";
375const TS_ARRAY_KIND: &str = "array";
376const TS_COMMENT_KIND: &str = "comment";
377
378pub fn replace_top_level_array_value_in_json_text(
379    text: &str,
380    key_path: &[impl AsRef<str>],
381    new_value: Option<&Value>,
382    replace_key: Option<&str>,
383    array_index: usize,
384    tab_size: usize,
385) -> (Range<usize>, String) {
386    let mut parser = tree_sitter::Parser::new();
387    parser
388        .set_language(&tree_sitter_json::LANGUAGE.into())
389        .unwrap();
390
391    let syntax_tree = parser.parse(text, None).unwrap();
392
393    let mut cursor = syntax_tree.walk();
394
395    if cursor.node().kind() == TS_DOCUMENT_KIND {
396        cursor.goto_first_child();
397    }
398
399    while cursor.node().kind() != TS_ARRAY_KIND {
400        if !cursor.goto_next_sibling() {
401            let json_value = construct_json_value(key_path, new_value);
402            let json_value = serde_json::json!([json_value]);
403            return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
404        }
405    }
406
407    // false if no children
408    //
409    cursor.goto_first_child();
410    debug_assert_eq!(cursor.node().kind(), "[");
411
412    let mut index = 0;
413
414    while index <= array_index {
415        let node = cursor.node();
416        if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",")
417            && !node.is_extra()
418            && !node.is_missing()
419        {
420            if index == array_index {
421                break;
422            }
423            index += 1;
424        }
425        if !cursor.goto_next_sibling() {
426            if let Some(new_value) = new_value {
427                return append_top_level_array_value_in_json_text(text, new_value, tab_size);
428            } else {
429                return (0..0, String::new());
430            }
431        }
432    }
433
434    let range = cursor.node().range();
435    let indent_width = range.start_point.column;
436    let offset = range.start_byte;
437    let text_range = range.start_byte..range.end_byte;
438    let value_str = &text[text_range.clone()];
439    let needs_indent = range.start_point.row > 0;
440
441    if new_value.is_none() && key_path.is_empty() {
442        let mut remove_range = text_range;
443        if index == 0 {
444            while cursor.goto_next_sibling()
445                && (cursor.node().is_extra() || cursor.node().is_missing())
446            {}
447            if cursor.node().kind() == "," {
448                remove_range.end = cursor.node().range().end_byte;
449            }
450            if let Some(next_newline) = &text[remove_range.end + 1..].find('\n')
451                && text[remove_range.end + 1..remove_range.end + next_newline]
452                    .chars()
453                    .all(|c| c.is_ascii_whitespace())
454            {
455                remove_range.end = remove_range.end + next_newline;
456            }
457        } else {
458            while cursor.goto_previous_sibling()
459                && (cursor.node().is_extra() || cursor.node().is_missing())
460            {}
461            if cursor.node().kind() == "," {
462                remove_range.start = cursor.node().range().start_byte;
463            }
464        }
465        (remove_range, String::new())
466    } else {
467        if let Some(array_replacement) = handle_possible_array_value(
468            &cursor.node(),
469            &cursor.node(),
470            text,
471            key_path,
472            new_value,
473            replace_key,
474            tab_size,
475        ) {
476            return array_replacement;
477        }
478        let (mut replace_range, mut replace_value) =
479            replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
480
481        replace_range.start += offset;
482        replace_range.end += offset;
483
484        if needs_indent {
485            let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
486            replace_value = replace_value.replace('\n', &increased_indent);
487        } else {
488            while let Some(idx) = replace_value.find("\n ") {
489                replace_value.remove(idx + 1);
490            }
491            while let Some(idx) = replace_value.find("\n") {
492                replace_value.replace_range(idx..idx + 1, " ");
493            }
494        }
495
496        (replace_range, replace_value)
497    }
498}
499
500pub fn append_top_level_array_value_in_json_text(
501    text: &str,
502    new_value: &Value,
503    tab_size: usize,
504) -> (Range<usize>, String) {
505    let mut parser = tree_sitter::Parser::new();
506    parser
507        .set_language(&tree_sitter_json::LANGUAGE.into())
508        .unwrap();
509    let syntax_tree = parser.parse(text, None).unwrap();
510
511    let mut cursor = syntax_tree.walk();
512
513    if cursor.node().kind() == TS_DOCUMENT_KIND {
514        cursor.goto_first_child();
515    }
516
517    while cursor.node().kind() != TS_ARRAY_KIND {
518        if !cursor.goto_next_sibling() {
519            let json_value = serde_json::json!([new_value]);
520            return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
521        }
522    }
523
524    let went_to_last_child = cursor.goto_last_child();
525    debug_assert!(
526        went_to_last_child && cursor.node().kind() == "]",
527        "Malformed JSON syntax tree, expected `]` at end of array"
528    );
529    let close_bracket_start = cursor.node().start_byte();
530    while cursor.goto_previous_sibling()
531        && (cursor.node().is_extra() || cursor.node().is_missing())
532        && !cursor.node().is_error()
533    {}
534
535    let mut comma_range = None;
536    let mut prev_item_range = None;
537
538    if cursor.node().kind() == "," || is_error_of_kind(&mut cursor, ",") {
539        comma_range = Some(cursor.node().byte_range());
540        while cursor.goto_previous_sibling()
541            && (cursor.node().is_extra() || cursor.node().is_missing())
542        {}
543
544        debug_assert_ne!(cursor.node().kind(), "[");
545        prev_item_range = Some(cursor.node().range());
546    } else {
547        while (cursor.node().is_extra() || cursor.node().is_missing())
548            && cursor.goto_previous_sibling()
549        {}
550        if cursor.node().kind() != "[" {
551            prev_item_range = Some(cursor.node().range());
552        }
553    }
554
555    let (mut replace_range, mut replace_value) =
556        replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None);
557
558    replace_range.start = close_bracket_start;
559    replace_range.end = close_bracket_start;
560
561    let space = ' ';
562    if let Some(prev_item_range) = prev_item_range {
563        let needs_newline = prev_item_range.start_point.row > 0;
564        let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
565            prev_item_range.start_point.column,
566            |idx| {
567                prev_item_range.start_point.column
568                    - text[idx + 1..prev_item_range.start_byte].trim_start().len()
569            },
570        );
571
572        let prev_item_end = comma_range
573            .as_ref()
574            .map_or(prev_item_range.end_byte, |range| range.end);
575        if text[prev_item_end..replace_range.start].trim().is_empty() {
576            replace_range.start = prev_item_end;
577        }
578
579        if needs_newline {
580            let increased_indent = format!("\n{space:width$}", width = indent_width);
581            replace_value = replace_value.replace('\n', &increased_indent);
582            replace_value.push('\n');
583            replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
584        } else {
585            while let Some(idx) = replace_value.find("\n ") {
586                replace_value.remove(idx + 1);
587            }
588            while let Some(idx) = replace_value.find('\n') {
589                replace_value.replace_range(idx..idx + 1, " ");
590            }
591            replace_value.insert(0, ' ');
592        }
593
594        if comma_range.is_none() {
595            replace_value.insert(0, ',');
596        }
597    } else if replace_value.contains('\n') || text.contains('\n') {
598        if let Some(prev_newline) = text[..replace_range.start].rfind('\n')
599            && text[prev_newline..replace_range.start].trim().is_empty()
600        {
601            replace_range.start = prev_newline;
602        }
603        let indent = format!("\n{space:width$}", width = tab_size);
604        replace_value = replace_value.replace('\n', &indent);
605        replace_value.insert_str(0, &indent);
606        replace_value.push('\n');
607    }
608    return (replace_range, replace_value);
609
610    fn is_error_of_kind(cursor: &mut tree_sitter::TreeCursor<'_>, kind: &str) -> bool {
611        if cursor.node().kind() != "ERROR" {
612            return false;
613        }
614
615        let descendant_index = cursor.descendant_index();
616        let res = cursor.goto_first_child() && cursor.node().kind() == kind;
617        cursor.goto_descendant(descendant_index);
618        res
619    }
620}
621
622/// Infers the indentation size used in JSON text by analyzing the tree structure.
623/// Returns the detected indent size, or a default of 2 if no indentation is found.
624pub fn infer_json_indent_size(text: &str) -> usize {
625    const MAX_INDENT_SIZE: usize = 64;
626
627    let mut parser = tree_sitter::Parser::new();
628    parser
629        .set_language(&tree_sitter_json::LANGUAGE.into())
630        .unwrap();
631
632    let Some(syntax_tree) = parser.parse(text, None) else {
633        return 4;
634    };
635
636    let mut cursor = syntax_tree.walk();
637    let mut indent_counts = [0u32; MAX_INDENT_SIZE];
638
639    // Traverse the tree to find indentation patterns
640    fn visit_node(
641        cursor: &mut tree_sitter::TreeCursor,
642        indent_counts: &mut [u32; MAX_INDENT_SIZE],
643        depth: usize,
644    ) {
645        if depth >= 3 {
646            return;
647        }
648        let node = cursor.node();
649        let node_kind = node.kind();
650
651        // For objects and arrays, check the indentation of their first content child
652        if matches!(node_kind, "object" | "array") {
653            let container_column = node.start_position().column;
654            let container_row = node.start_position().row;
655
656            if cursor.goto_first_child() {
657                // Skip the opening bracket
658                loop {
659                    let child = cursor.node();
660                    let child_kind = child.kind();
661
662                    // Look for the first actual content (pair for objects, value for arrays)
663                    if (node_kind == "object" && child_kind == "pair")
664                        || (node_kind == "array"
665                            && !matches!(child_kind, "[" | "]" | "," | "comment"))
666                    {
667                        let child_column = child.start_position().column;
668                        let child_row = child.start_position().row;
669
670                        // Only count if the child is on a different line
671                        if child_row > container_row && child_column > container_column {
672                            let indent = child_column - container_column;
673                            if indent > 0 && indent < MAX_INDENT_SIZE {
674                                indent_counts[indent] += 1;
675                            }
676                        }
677                        break;
678                    }
679
680                    if !cursor.goto_next_sibling() {
681                        break;
682                    }
683                }
684                cursor.goto_parent();
685            }
686        }
687
688        // Recurse to children
689        if cursor.goto_first_child() {
690            loop {
691                visit_node(cursor, indent_counts, depth + 1);
692                if !cursor.goto_next_sibling() {
693                    break;
694                }
695            }
696            cursor.goto_parent();
697        }
698    }
699
700    visit_node(&mut cursor, &mut indent_counts, 0);
701
702    // Find the indent size with the highest count
703    let mut max_count = 0;
704    let mut max_indent = 4;
705
706    for (indent, &count) in indent_counts.iter().enumerate() {
707        if count > max_count {
708            max_count = count;
709            max_indent = indent;
710        }
711    }
712
713    if max_count == 0 { 2 } else { max_indent }
714}
715
716pub fn to_pretty_json(
717    value: &impl Serialize,
718    indent_size: usize,
719    indent_prefix_len: usize,
720) -> String {
721    let mut output = Vec::new();
722    let indent = " ".repeat(indent_size);
723    let mut ser = serde_json::Serializer::with_formatter(
724        &mut output,
725        serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()),
726    );
727
728    value.serialize(&mut ser).unwrap();
729    let text = String::from_utf8(output).unwrap();
730
731    let mut adjusted_text = String::new();
732    for (i, line) in text.split('\n').enumerate() {
733        if i > 0 {
734            adjusted_text.extend(std::iter::repeat(' ').take(indent_prefix_len));
735        }
736        adjusted_text.push_str(line);
737        adjusted_text.push('\n');
738    }
739    adjusted_text.pop();
740    adjusted_text
741}
742
743pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
744    let mut deserializer = serde_json_lenient::Deserializer::from_str(content);
745    Ok(serde_path_to_error::deserialize(&mut deserializer)?)
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use serde_json::{Value, json};
752    use unindent::Unindent;
753
754    #[test]
755    fn object_replace() {
756        #[track_caller]
757        fn check_object_replace(
758            input: String,
759            key_path: &[&str],
760            value: Option<Value>,
761            expected: String,
762        ) {
763            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
764            let mut result_str = input;
765            result_str.replace_range(result.0, &result.1);
766            pretty_assertions::assert_eq!(expected, result_str);
767        }
768        check_object_replace(
769            r#"{
770                "a": 1,
771                "b": 2
772            }"#
773            .unindent(),
774            &["b"],
775            Some(json!(3)),
776            r#"{
777                "a": 1,
778                "b": 3
779            }"#
780            .unindent(),
781        );
782        check_object_replace(
783            r#"{
784                "a": 1,
785                "b": 2
786            }"#
787            .unindent(),
788            &["b"],
789            None,
790            r#"{
791                "a": 1
792            }"#
793            .unindent(),
794        );
795        check_object_replace(
796            r#"{
797                "a": 1,
798                "b": 2
799            }"#
800            .unindent(),
801            &["c"],
802            Some(json!(3)),
803            r#"{
804                "c": 3,
805                "a": 1,
806                "b": 2
807            }"#
808            .unindent(),
809        );
810        check_object_replace(
811            r#"{
812                "a": 1,
813                "b": {
814                    "c": 2,
815                    "d": 3,
816                }
817            }"#
818            .unindent(),
819            &["b", "c"],
820            Some(json!([1, 2, 3])),
821            r#"{
822                "a": 1,
823                "b": {
824                    "c": [
825                        1,
826                        2,
827                        3
828                    ],
829                    "d": 3,
830                }
831            }"#
832            .unindent(),
833        );
834
835        check_object_replace(
836            r#"{
837                "name": "old_name",
838                "id": 123
839            }"#
840            .unindent(),
841            &["name"],
842            Some(json!("new_name")),
843            r#"{
844                "name": "new_name",
845                "id": 123
846            }"#
847            .unindent(),
848        );
849
850        check_object_replace(
851            r#"{
852                "enabled": false,
853                "count": 5
854            }"#
855            .unindent(),
856            &["enabled"],
857            Some(json!(true)),
858            r#"{
859                "enabled": true,
860                "count": 5
861            }"#
862            .unindent(),
863        );
864
865        check_object_replace(
866            r#"{
867                "value": null,
868                "other": "test"
869            }"#
870            .unindent(),
871            &["value"],
872            Some(json!(42)),
873            r#"{
874                "value": 42,
875                "other": "test"
876            }"#
877            .unindent(),
878        );
879
880        check_object_replace(
881            r#"{
882                "config": {
883                    "old": true
884                },
885                "name": "test"
886            }"#
887            .unindent(),
888            &["config"],
889            Some(json!({"new": false, "count": 3})),
890            r#"{
891                "config": {
892                    "new": false,
893                    "count": 3
894                },
895                "name": "test"
896            }"#
897            .unindent(),
898        );
899
900        check_object_replace(
901            r#"{
902                // This is a comment
903                "a": 1,
904                "b": 2 // Another comment
905            }"#
906            .unindent(),
907            &["b"],
908            Some(json!({"foo": "bar"})),
909            r#"{
910                // This is a comment
911                "a": 1,
912                "b": {
913                    "foo": "bar"
914                } // Another comment
915            }"#
916            .unindent(),
917        );
918
919        check_object_replace(
920            r#"{}"#.to_string(),
921            &["new_key"],
922            Some(json!("value")),
923            r#"{
924                "new_key": "value"
925            }
926            "#
927            .unindent(),
928        );
929
930        check_object_replace(
931            r#"{
932                "only_key": 123
933            }"#
934            .unindent(),
935            &["only_key"],
936            None,
937            "{\n    \n}".to_string(),
938        );
939
940        check_object_replace(
941            r#"{
942                "level1": {
943                    "level2": {
944                        "level3": {
945                            "target": "old"
946                        }
947                    }
948                }
949            }"#
950            .unindent(),
951            &["level1", "level2", "level3", "target"],
952            Some(json!("new")),
953            r#"{
954                "level1": {
955                    "level2": {
956                        "level3": {
957                            "target": "new"
958                        }
959                    }
960                }
961            }"#
962            .unindent(),
963        );
964
965        check_object_replace(
966            r#"{
967                "parent": {}
968            }"#
969            .unindent(),
970            &["parent", "child"],
971            Some(json!("value")),
972            r#"{
973                "parent": {
974                    "child": "value"
975                }
976            }"#
977            .unindent(),
978        );
979
980        check_object_replace(
981            r#"{
982                "a": 1,
983                "b": 2,
984            }"#
985            .unindent(),
986            &["b"],
987            Some(json!(3)),
988            r#"{
989                "a": 1,
990                "b": 3,
991            }"#
992            .unindent(),
993        );
994
995        check_object_replace(
996            r#"{
997                "items": [1, 2, 3],
998                "count": 3
999            }"#
1000            .unindent(),
1001            &["items", "1"],
1002            Some(json!(5)),
1003            r#"{
1004                "items": {
1005                    "1": 5
1006                },
1007                "count": 3
1008            }"#
1009            .unindent(),
1010        );
1011
1012        check_object_replace(
1013            r#"{
1014                "items": [1, 2, 3],
1015                "count": 3
1016            }"#
1017            .unindent(),
1018            &["items", "1"],
1019            None,
1020            r#"{
1021                "items": {
1022                    "1": null
1023                },
1024                "count": 3
1025            }"#
1026            .unindent(),
1027        );
1028
1029        check_object_replace(
1030            r#"{
1031                "items": [1, 2, 3],
1032                "count": 3
1033            }"#
1034            .unindent(),
1035            &["items"],
1036            Some(json!(["a", "b", "c", "d"])),
1037            r#"{
1038                "items": [
1039                    "a",
1040                    "b",
1041                    "c",
1042                    "d"
1043                ],
1044                "count": 3
1045            }"#
1046            .unindent(),
1047        );
1048
1049        check_object_replace(
1050            r#"{
1051                "0": "zero",
1052                "1": "one"
1053            }"#
1054            .unindent(),
1055            &["1"],
1056            Some(json!("ONE")),
1057            r#"{
1058                "0": "zero",
1059                "1": "ONE"
1060            }"#
1061            .unindent(),
1062        );
1063        // Test with comments between object members
1064        check_object_replace(
1065            r#"{
1066                "a": 1,
1067                // Comment between members
1068                "b": 2,
1069                /* Block comment */
1070                "c": 3
1071            }"#
1072            .unindent(),
1073            &["b"],
1074            Some(json!({"nested": true})),
1075            r#"{
1076                "a": 1,
1077                // Comment between members
1078                "b": {
1079                    "nested": true
1080                },
1081                /* Block comment */
1082                "c": 3
1083            }"#
1084            .unindent(),
1085        );
1086
1087        // Test with trailing comments on replaced value
1088        check_object_replace(
1089            r#"{
1090                "a": 1, // keep this comment
1091                "b": 2  // this should stay
1092            }"#
1093            .unindent(),
1094            &["a"],
1095            Some(json!("changed")),
1096            r#"{
1097                "a": "changed", // keep this comment
1098                "b": 2  // this should stay
1099            }"#
1100            .unindent(),
1101        );
1102
1103        // Test with deep indentation
1104        check_object_replace(
1105            r#"{
1106                        "deeply": {
1107                                "nested": {
1108                                        "value": "old"
1109                                }
1110                        }
1111                }"#
1112            .unindent(),
1113            &["deeply", "nested", "value"],
1114            Some(json!("new")),
1115            r#"{
1116                        "deeply": {
1117                                "nested": {
1118                                        "value": "new"
1119                                }
1120                        }
1121                }"#
1122            .unindent(),
1123        );
1124
1125        // Test removing value with comment preservation
1126        check_object_replace(
1127            r#"{
1128                // Header comment
1129                "a": 1,
1130                // This comment belongs to b
1131                "b": 2,
1132                // This comment belongs to c
1133                "c": 3
1134            }"#
1135            .unindent(),
1136            &["b"],
1137            None,
1138            r#"{
1139                // Header comment
1140                "a": 1,
1141                // This comment belongs to b
1142                // This comment belongs to c
1143                "c": 3
1144            }"#
1145            .unindent(),
1146        );
1147
1148        // Test with multiline block comments
1149        check_object_replace(
1150            r#"{
1151                /*
1152                 * This is a multiline
1153                 * block comment
1154                 */
1155                "value": "old",
1156                /* Another block */ "other": 123
1157            }"#
1158            .unindent(),
1159            &["value"],
1160            Some(json!("new")),
1161            r#"{
1162                /*
1163                 * This is a multiline
1164                 * block comment
1165                 */
1166                "value": "new",
1167                /* Another block */ "other": 123
1168            }"#
1169            .unindent(),
1170        );
1171
1172        check_object_replace(
1173            r#"{
1174                // This object is empty
1175            }"#
1176            .unindent(),
1177            &["key"],
1178            Some(json!("value")),
1179            r#"{
1180                // This object is empty
1181                "key": "value"
1182            }
1183            "#
1184            .unindent(),
1185        );
1186
1187        // Test replacing in object with only comments
1188        check_object_replace(
1189            r#"{
1190                // Comment 1
1191                // Comment 2
1192            }"#
1193            .unindent(),
1194            &["new"],
1195            Some(json!(42)),
1196            r#"{
1197                // Comment 1
1198                // Comment 2
1199                "new": 42
1200            }
1201            "#
1202            .unindent(),
1203        );
1204
1205        // Test with inconsistent spacing
1206        check_object_replace(
1207            r#"{
1208              "a":1,
1209                    "b"  :  2  ,
1210                "c":   3
1211            }"#
1212            .unindent(),
1213            &["b"],
1214            Some(json!("spaced")),
1215            r#"{
1216              "a":1,
1217                    "b"  :  "spaced"  ,
1218                "c":   3
1219            }"#
1220            .unindent(),
1221        );
1222    }
1223
1224    #[test]
1225    fn object_replace_array() {
1226        // Tests replacing values within arrays that are nested inside objects.
1227        // Uses "#N" syntax in key paths to indicate array indices.
1228        #[track_caller]
1229        fn check_object_replace_array(
1230            input: String,
1231            key_path: &[&str],
1232            value: Option<Value>,
1233            expected: String,
1234        ) {
1235            let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
1236            let mut result_str = input;
1237            result_str.replace_range(result.0, &result.1);
1238            pretty_assertions::assert_eq!(expected, result_str);
1239        }
1240
1241        // Basic array element replacement
1242        check_object_replace_array(
1243            r#"{
1244                "a": [1, 3],
1245            }"#
1246            .unindent(),
1247            &["a", "#1"],
1248            Some(json!(2)),
1249            r#"{
1250                "a": [1, 2],
1251            }"#
1252            .unindent(),
1253        );
1254
1255        // Replace first element
1256        check_object_replace_array(
1257            r#"{
1258                "items": [1, 2, 3]
1259            }"#
1260            .unindent(),
1261            &["items", "#0"],
1262            Some(json!(10)),
1263            r#"{
1264                "items": [10, 2, 3]
1265            }"#
1266            .unindent(),
1267        );
1268
1269        // Replace last element
1270        check_object_replace_array(
1271            r#"{
1272                "items": [1, 2, 3]
1273            }"#
1274            .unindent(),
1275            &["items", "#2"],
1276            Some(json!(30)),
1277            r#"{
1278                "items": [1, 2, 30]
1279            }"#
1280            .unindent(),
1281        );
1282
1283        // Replace string in array
1284        check_object_replace_array(
1285            r#"{
1286                "names": ["alice", "bob", "charlie"]
1287            }"#
1288            .unindent(),
1289            &["names", "#1"],
1290            Some(json!("robert")),
1291            r#"{
1292                "names": ["alice", "robert", "charlie"]
1293            }"#
1294            .unindent(),
1295        );
1296
1297        // Replace boolean
1298        check_object_replace_array(
1299            r#"{
1300                "flags": [true, false, true]
1301            }"#
1302            .unindent(),
1303            &["flags", "#0"],
1304            Some(json!(false)),
1305            r#"{
1306                "flags": [false, false, true]
1307            }"#
1308            .unindent(),
1309        );
1310
1311        // Replace null with value
1312        check_object_replace_array(
1313            r#"{
1314                "values": [null, 2, null]
1315            }"#
1316            .unindent(),
1317            &["values", "#0"],
1318            Some(json!(1)),
1319            r#"{
1320                "values": [1, 2, null]
1321            }"#
1322            .unindent(),
1323        );
1324
1325        // Replace value with null
1326        check_object_replace_array(
1327            r#"{
1328                "data": [1, 2, 3]
1329            }"#
1330            .unindent(),
1331            &["data", "#1"],
1332            Some(json!(null)),
1333            r#"{
1334                "data": [1, null, 3]
1335            }"#
1336            .unindent(),
1337        );
1338
1339        // Replace simple value with object
1340        check_object_replace_array(
1341            r#"{
1342                "list": [1, 2, 3]
1343            }"#
1344            .unindent(),
1345            &["list", "#1"],
1346            Some(json!({"value": 2, "label": "two"})),
1347            r#"{
1348                "list": [1, { "value": 2, "label": "two" }, 3]
1349            }"#
1350            .unindent(),
1351        );
1352
1353        // Replace simple value with nested array
1354        check_object_replace_array(
1355            r#"{
1356                "matrix": [1, 2, 3]
1357            }"#
1358            .unindent(),
1359            &["matrix", "#1"],
1360            Some(json!([20, 21, 22])),
1361            r#"{
1362                "matrix": [1, [ 20, 21, 22 ], 3]
1363            }"#
1364            .unindent(),
1365        );
1366
1367        // Replace object in array
1368        check_object_replace_array(
1369            r#"{
1370                "users": [
1371                    {"name": "alice"},
1372                    {"name": "bob"},
1373                    {"name": "charlie"}
1374                ]
1375            }"#
1376            .unindent(),
1377            &["users", "#1"],
1378            Some(json!({"name": "robert", "age": 30})),
1379            r#"{
1380                "users": [
1381                    {"name": "alice"},
1382                    { "name": "robert", "age": 30 },
1383                    {"name": "charlie"}
1384                ]
1385            }"#
1386            .unindent(),
1387        );
1388
1389        // Replace property within object in array
1390        check_object_replace_array(
1391            r#"{
1392                "users": [
1393                    {"name": "alice", "age": 25},
1394                    {"name": "bob", "age": 30},
1395                    {"name": "charlie", "age": 35}
1396                ]
1397            }"#
1398            .unindent(),
1399            &["users", "#1", "age"],
1400            Some(json!(31)),
1401            r#"{
1402                "users": [
1403                    {"name": "alice", "age": 25},
1404                    {"name": "bob", "age": 31},
1405                    {"name": "charlie", "age": 35}
1406                ]
1407            }"#
1408            .unindent(),
1409        );
1410
1411        // Add new property to object in array
1412        check_object_replace_array(
1413            r#"{
1414                "items": [
1415                    {"id": 1},
1416                    {"id": 2},
1417                    {"id": 3}
1418                ]
1419            }"#
1420            .unindent(),
1421            &["items", "#1", "name"],
1422            Some(json!("Item Two")),
1423            r#"{
1424                "items": [
1425                    {"id": 1},
1426                    {"name": "Item Two", "id": 2},
1427                    {"id": 3}
1428                ]
1429            }"#
1430            .unindent(),
1431        );
1432
1433        // Remove property from object in array
1434        check_object_replace_array(
1435            r#"{
1436                "items": [
1437                    {"id": 1, "name": "one"},
1438                    {"id": 2, "name": "two"},
1439                    {"id": 3, "name": "three"}
1440                ]
1441            }"#
1442            .unindent(),
1443            &["items", "#1", "name"],
1444            None,
1445            r#"{
1446                "items": [
1447                    {"id": 1, "name": "one"},
1448                    {"id": 2},
1449                    {"id": 3, "name": "three"}
1450                ]
1451            }"#
1452            .unindent(),
1453        );
1454
1455        // Deeply nested: array in object in array
1456        check_object_replace_array(
1457            r#"{
1458                "data": [
1459                    {
1460                        "values": [1, 2, 3]
1461                    },
1462                    {
1463                        "values": [4, 5, 6]
1464                    }
1465                ]
1466            }"#
1467            .unindent(),
1468            &["data", "#0", "values", "#1"],
1469            Some(json!(20)),
1470            r#"{
1471                "data": [
1472                    {
1473                        "values": [1, 20, 3]
1474                    },
1475                    {
1476                        "values": [4, 5, 6]
1477                    }
1478                ]
1479            }"#
1480            .unindent(),
1481        );
1482
1483        // Multiple levels of nesting
1484        check_object_replace_array(
1485            r#"{
1486                "root": {
1487                    "level1": [
1488                        {
1489                            "level2": {
1490                                "level3": [10, 20, 30]
1491                            }
1492                        }
1493                    ]
1494                }
1495            }"#
1496            .unindent(),
1497            &["root", "level1", "#0", "level2", "level3", "#2"],
1498            Some(json!(300)),
1499            r#"{
1500                "root": {
1501                    "level1": [
1502                        {
1503                            "level2": {
1504                                "level3": [10, 20, 300]
1505                            }
1506                        }
1507                    ]
1508                }
1509            }"#
1510            .unindent(),
1511        );
1512
1513        // Array with mixed types
1514        check_object_replace_array(
1515            r#"{
1516                "mixed": [1, "two", true, null, {"five": 5}]
1517            }"#
1518            .unindent(),
1519            &["mixed", "#3"],
1520            Some(json!({"four": 4})),
1521            r#"{
1522                "mixed": [1, "two", true, { "four": 4 }, {"five": 5}]
1523            }"#
1524            .unindent(),
1525        );
1526
1527        // Replace with complex object
1528        check_object_replace_array(
1529            r#"{
1530                "config": [
1531                    "simple",
1532                    "values"
1533                ]
1534            }"#
1535            .unindent(),
1536            &["config", "#0"],
1537            Some(json!({
1538                "type": "complex",
1539                "settings": {
1540                    "enabled": true,
1541                    "level": 5
1542                }
1543            })),
1544            r#"{
1545                "config": [
1546                    {
1547                        "type": "complex",
1548                        "settings": {
1549                            "enabled": true,
1550                            "level": 5
1551                        }
1552                    },
1553                    "values"
1554                ]
1555            }"#
1556            .unindent(),
1557        );
1558
1559        // Array with trailing comma
1560        check_object_replace_array(
1561            r#"{
1562                "items": [
1563                    1,
1564                    2,
1565                    3,
1566                ]
1567            }"#
1568            .unindent(),
1569            &["items", "#1"],
1570            Some(json!(20)),
1571            r#"{
1572                "items": [
1573                    1,
1574                    20,
1575                    3,
1576                ]
1577            }"#
1578            .unindent(),
1579        );
1580
1581        // Array with comments
1582        check_object_replace_array(
1583            r#"{
1584                "items": [
1585                    1, // first item
1586                    2, // second item
1587                    3  // third item
1588                ]
1589            }"#
1590            .unindent(),
1591            &["items", "#1"],
1592            Some(json!(20)),
1593            r#"{
1594                "items": [
1595                    1, // first item
1596                    20, // second item
1597                    3  // third item
1598                ]
1599            }"#
1600            .unindent(),
1601        );
1602
1603        // Multiple arrays in object
1604        check_object_replace_array(
1605            r#"{
1606                "first": [1, 2, 3],
1607                "second": [4, 5, 6],
1608                "third": [7, 8, 9]
1609            }"#
1610            .unindent(),
1611            &["second", "#1"],
1612            Some(json!(50)),
1613            r#"{
1614                "first": [1, 2, 3],
1615                "second": [4, 50, 6],
1616                "third": [7, 8, 9]
1617            }"#
1618            .unindent(),
1619        );
1620
1621        // Empty array - add first element
1622        check_object_replace_array(
1623            r#"{
1624                "empty": []
1625            }"#
1626            .unindent(),
1627            &["empty", "#0"],
1628            Some(json!("first")),
1629            r#"{
1630                "empty": ["first"]
1631            }"#
1632            .unindent(),
1633        );
1634
1635        // Array of arrays
1636        check_object_replace_array(
1637            r#"{
1638                "matrix": [
1639                    [1, 2],
1640                    [3, 4],
1641                    [5, 6]
1642                ]
1643            }"#
1644            .unindent(),
1645            &["matrix", "#1", "#0"],
1646            Some(json!(30)),
1647            r#"{
1648                "matrix": [
1649                    [1, 2],
1650                    [30, 4],
1651                    [5, 6]
1652                ]
1653            }"#
1654            .unindent(),
1655        );
1656
1657        // Replace nested object property in array element
1658        check_object_replace_array(
1659            r#"{
1660                "users": [
1661                    {
1662                        "name": "alice",
1663                        "address": {
1664                            "city": "NYC",
1665                            "zip": "10001"
1666                        }
1667                    }
1668                ]
1669            }"#
1670            .unindent(),
1671            &["users", "#0", "address", "city"],
1672            Some(json!("Boston")),
1673            r#"{
1674                "users": [
1675                    {
1676                        "name": "alice",
1677                        "address": {
1678                            "city": "Boston",
1679                            "zip": "10001"
1680                        }
1681                    }
1682                ]
1683            }"#
1684            .unindent(),
1685        );
1686
1687        // Add element past end of array
1688        check_object_replace_array(
1689            r#"{
1690                "items": [1, 2]
1691            }"#
1692            .unindent(),
1693            &["items", "#5"],
1694            Some(json!(6)),
1695            r#"{
1696                "items": [1, 2, 6]
1697            }"#
1698            .unindent(),
1699        );
1700
1701        // Complex nested structure
1702        check_object_replace_array(
1703            r#"{
1704                "app": {
1705                    "modules": [
1706                        {
1707                            "name": "auth",
1708                            "routes": [
1709                                {"path": "/login", "method": "POST"},
1710                                {"path": "/logout", "method": "POST"}
1711                            ]
1712                        },
1713                        {
1714                            "name": "api",
1715                            "routes": [
1716                                {"path": "/users", "method": "GET"},
1717                                {"path": "/users", "method": "POST"}
1718                            ]
1719                        }
1720                    ]
1721                }
1722            }"#
1723            .unindent(),
1724            &["app", "modules", "#1", "routes", "#0", "method"],
1725            Some(json!("PUT")),
1726            r#"{
1727                "app": {
1728                    "modules": [
1729                        {
1730                            "name": "auth",
1731                            "routes": [
1732                                {"path": "/login", "method": "POST"},
1733                                {"path": "/logout", "method": "POST"}
1734                            ]
1735                        },
1736                        {
1737                            "name": "api",
1738                            "routes": [
1739                                {"path": "/users", "method": "PUT"},
1740                                {"path": "/users", "method": "POST"}
1741                            ]
1742                        }
1743                    ]
1744                }
1745            }"#
1746            .unindent(),
1747        );
1748
1749        // Escaped strings in array
1750        check_object_replace_array(
1751            r#"{
1752                "messages": ["hello", "world"]
1753            }"#
1754            .unindent(),
1755            &["messages", "#0"],
1756            Some(json!("hello \"quoted\" world")),
1757            r#"{
1758                "messages": ["hello \"quoted\" world", "world"]
1759            }"#
1760            .unindent(),
1761        );
1762
1763        // Block comments
1764        check_object_replace_array(
1765            r#"{
1766                "data": [
1767                    /* first */ 1,
1768                    /* second */ 2,
1769                    /* third */ 3
1770                ]
1771            }"#
1772            .unindent(),
1773            &["data", "#1"],
1774            Some(json!(20)),
1775            r#"{
1776                "data": [
1777                    /* first */ 1,
1778                    /* second */ 20,
1779                    /* third */ 3
1780                ]
1781            }"#
1782            .unindent(),
1783        );
1784
1785        // Inline array
1786        check_object_replace_array(
1787            r#"{"items": [1, 2, 3], "count": 3}"#.to_string(),
1788            &["items", "#1"],
1789            Some(json!(20)),
1790            r#"{"items": [1, 20, 3], "count": 3}"#.to_string(),
1791        );
1792
1793        // Single element array
1794        check_object_replace_array(
1795            r#"{
1796                "single": [42]
1797            }"#
1798            .unindent(),
1799            &["single", "#0"],
1800            Some(json!(100)),
1801            r#"{
1802                "single": [100]
1803            }"#
1804            .unindent(),
1805        );
1806
1807        // Inconsistent formatting
1808        check_object_replace_array(
1809            r#"{
1810                "messy": [1,
1811                    2,
1812                        3,
1813                4]
1814            }"#
1815            .unindent(),
1816            &["messy", "#2"],
1817            Some(json!(30)),
1818            r#"{
1819                "messy": [1,
1820                    2,
1821                        30,
1822                4]
1823            }"#
1824            .unindent(),
1825        );
1826
1827        // Creates array if has numbered key
1828        check_object_replace_array(
1829            r#"{
1830                "array": {"foo": "bar"}
1831            }"#
1832            .unindent(),
1833            &["array", "#3"],
1834            Some(json!(4)),
1835            r#"{
1836                "array": [
1837                    4
1838                ]
1839            }"#
1840            .unindent(),
1841        );
1842
1843        // Replace non-array element within array with array
1844        check_object_replace_array(
1845            r#"{
1846                "matrix": [
1847                    [1, 2],
1848                    [3, 4],
1849                    [5, 6]
1850                ]
1851            }"#
1852            .unindent(),
1853            &["matrix", "#1", "#0"],
1854            Some(json!(["foo", "bar"])),
1855            r#"{
1856                "matrix": [
1857                    [1, 2],
1858                    [[ "foo", "bar" ], 4],
1859                    [5, 6]
1860                ]
1861            }"#
1862            .unindent(),
1863        );
1864        // Replace non-array element within array with array
1865        check_object_replace_array(
1866            r#"{
1867                "matrix": [
1868                    [1, 2],
1869                    [3, 4],
1870                    [5, 6]
1871                ]
1872            }"#
1873            .unindent(),
1874            &["matrix", "#1", "#0", "#3"],
1875            Some(json!(["foo", "bar"])),
1876            r#"{
1877                "matrix": [
1878                    [1, 2],
1879                    [[ [ "foo", "bar" ] ], 4],
1880                    [5, 6]
1881                ]
1882            }"#
1883            .unindent(),
1884        );
1885
1886        // Create array in key that doesn't exist
1887        check_object_replace_array(
1888            r#"{
1889                "foo": {}
1890            }"#
1891            .unindent(),
1892            &["foo", "bar", "#0"],
1893            Some(json!({"is_object": true})),
1894            r#"{
1895                "foo": {
1896                    "bar": [
1897                        {
1898                            "is_object": true
1899                        }
1900                    ]
1901                }
1902            }"#
1903            .unindent(),
1904        );
1905    }
1906
1907    #[test]
1908    fn array_replace() {
1909        #[track_caller]
1910        fn check_array_replace(
1911            input: impl ToString,
1912            index: usize,
1913            key_path: &[&str],
1914            value: Option<Value>,
1915            expected: impl ToString,
1916        ) {
1917            let input = input.to_string();
1918            let result = replace_top_level_array_value_in_json_text(
1919                &input,
1920                key_path,
1921                value.as_ref(),
1922                None,
1923                index,
1924                4,
1925            );
1926            let mut result_str = input;
1927            result_str.replace_range(result.0, &result.1);
1928            pretty_assertions::assert_eq!(expected.to_string(), result_str);
1929        }
1930
1931        check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1932        check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1933        check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1934        check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1935        check_array_replace(
1936            r#"[
1937                1,
1938                2,
1939                3,
1940            ]"#
1941            .unindent(),
1942            1,
1943            &[],
1944            Some(json!({"foo": "bar", "baz": "qux"})),
1945            r#"[
1946                1,
1947                {
1948                    "foo": "bar",
1949                    "baz": "qux"
1950                },
1951                3,
1952            ]"#
1953            .unindent(),
1954        );
1955        check_array_replace(
1956            r#"[1, 3, 3,]"#,
1957            1,
1958            &[],
1959            Some(json!({"foo": "bar", "baz": "qux"})),
1960            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1961        );
1962
1963        check_array_replace(
1964            r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1965            1,
1966            &["baz"],
1967            Some(json!({"qux": "quz"})),
1968            r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1969        );
1970
1971        check_array_replace(
1972            r#"[
1973                1,
1974                {
1975                    "foo": "bar",
1976                    "baz": "qux"
1977                },
1978                3
1979            ]"#,
1980            1,
1981            &["baz"],
1982            Some(json!({"qux": "quz"})),
1983            r#"[
1984                1,
1985                {
1986                    "foo": "bar",
1987                    "baz": {
1988                        "qux": "quz"
1989                    }
1990                },
1991                3
1992            ]"#,
1993        );
1994
1995        check_array_replace(
1996            r#"[
1997                1,
1998                {
1999                    "foo": "bar",
2000                    "baz": {
2001                        "qux": "quz"
2002                    }
2003                },
2004                3
2005            ]"#,
2006            1,
2007            &["baz"],
2008            Some(json!("qux")),
2009            r#"[
2010                1,
2011                {
2012                    "foo": "bar",
2013                    "baz": "qux"
2014                },
2015                3
2016            ]"#,
2017        );
2018
2019        check_array_replace(
2020            r#"[
2021                1,
2022                {
2023                    "foo": "bar",
2024                    // some comment to keep
2025                    "baz": {
2026                        // some comment to remove
2027                        "qux": "quz"
2028                    }
2029                    // some other comment to keep
2030                },
2031                3
2032            ]"#,
2033            1,
2034            &["baz"],
2035            Some(json!("qux")),
2036            r#"[
2037                1,
2038                {
2039                    "foo": "bar",
2040                    // some comment to keep
2041                    "baz": "qux"
2042                    // some other comment to keep
2043                },
2044                3
2045            ]"#,
2046        );
2047
2048        // Test with comments between array elements
2049        check_array_replace(
2050            r#"[
2051                1,
2052                // This is element 2
2053                2,
2054                /* Block comment */ 3,
2055                4 // Trailing comment
2056            ]"#,
2057            2,
2058            &[],
2059            Some(json!("replaced")),
2060            r#"[
2061                1,
2062                // This is element 2
2063                2,
2064                /* Block comment */ "replaced",
2065                4 // Trailing comment
2066            ]"#,
2067        );
2068
2069        // Test empty array with comments
2070        check_array_replace(
2071            r#"[
2072                // Empty array with comment
2073            ]"#
2074            .unindent(),
2075            0,
2076            &[],
2077            Some(json!("first")),
2078            r#"[
2079                // Empty array with comment
2080                "first"
2081            ]"#
2082            .unindent(),
2083        );
2084        check_array_replace(
2085            r#"[]"#.unindent(),
2086            0,
2087            &[],
2088            Some(json!("first")),
2089            r#"["first"]"#.unindent(),
2090        );
2091
2092        // Test array with leading comments
2093        check_array_replace(
2094            r#"[
2095                // Leading comment
2096                // Another leading comment
2097                1,
2098                2
2099            ]"#,
2100            0,
2101            &[],
2102            Some(json!({"new": "object"})),
2103            r#"[
2104                // Leading comment
2105                // Another leading comment
2106                {
2107                    "new": "object"
2108                },
2109                2
2110            ]"#,
2111        );
2112
2113        // Test with deep indentation
2114        check_array_replace(
2115            r#"[
2116                        1,
2117                        2,
2118                        3
2119                    ]"#,
2120            1,
2121            &[],
2122            Some(json!("deep")),
2123            r#"[
2124                        1,
2125                        "deep",
2126                        3
2127                    ]"#,
2128        );
2129
2130        // Test with mixed spacing
2131        check_array_replace(
2132            r#"[1,2,   3,    4]"#,
2133            2,
2134            &[],
2135            Some(json!("spaced")),
2136            r#"[1,2,   "spaced",    4]"#,
2137        );
2138
2139        // Test replacing nested array element
2140        check_array_replace(
2141            r#"[
2142                [1, 2, 3],
2143                [4, 5, 6],
2144                [7, 8, 9]
2145            ]"#,
2146            1,
2147            &[],
2148            Some(json!(["a", "b", "c", "d"])),
2149            r#"[
2150                [1, 2, 3],
2151                [
2152                    "a",
2153                    "b",
2154                    "c",
2155                    "d"
2156                ],
2157                [7, 8, 9]
2158            ]"#,
2159        );
2160
2161        // Test with multiline block comments
2162        check_array_replace(
2163            r#"[
2164                /*
2165                 * This is a
2166                 * multiline comment
2167                 */
2168                "first",
2169                "second"
2170            ]"#,
2171            0,
2172            &[],
2173            Some(json!("updated")),
2174            r#"[
2175                /*
2176                 * This is a
2177                 * multiline comment
2178                 */
2179                "updated",
2180                "second"
2181            ]"#,
2182        );
2183
2184        // Test replacing with null
2185        check_array_replace(
2186            r#"[true, false, true]"#,
2187            1,
2188            &[],
2189            Some(json!(null)),
2190            r#"[true, null, true]"#,
2191        );
2192
2193        // Test single element array
2194        check_array_replace(
2195            r#"[42]"#,
2196            0,
2197            &[],
2198            Some(json!({"answer": 42})),
2199            r#"[{ "answer": 42 }]"#,
2200        );
2201
2202        // Test array with only comments
2203        check_array_replace(
2204            r#"[
2205                // Comment 1
2206                // Comment 2
2207                // Comment 3
2208            ]"#
2209            .unindent(),
2210            10,
2211            &[],
2212            Some(json!(123)),
2213            r#"[
2214                // Comment 1
2215                // Comment 2
2216                // Comment 3
2217                123
2218            ]"#
2219            .unindent(),
2220        );
2221
2222        check_array_replace(
2223            r#"[
2224                {
2225                    "key": "value"
2226                },
2227                {
2228                    "key": "value2"
2229                }
2230            ]"#
2231            .unindent(),
2232            0,
2233            &[],
2234            None,
2235            r#"[
2236                {
2237                    "key": "value2"
2238                }
2239            ]"#
2240            .unindent(),
2241        );
2242
2243        check_array_replace(
2244            r#"[
2245                {
2246                    "key": "value"
2247                },
2248                {
2249                    "key": "value2"
2250                },
2251                {
2252                    "key": "value3"
2253                },
2254            ]"#
2255            .unindent(),
2256            1,
2257            &[],
2258            None,
2259            r#"[
2260                {
2261                    "key": "value"
2262                },
2263                {
2264                    "key": "value3"
2265                },
2266            ]"#
2267            .unindent(),
2268        );
2269
2270        check_array_replace(
2271            r#""#,
2272            2,
2273            &[],
2274            Some(json!(42)),
2275            r#"[
2276                42
2277            ]"#
2278            .unindent(),
2279        );
2280
2281        check_array_replace(
2282            r#""#,
2283            2,
2284            &["foo", "bar"],
2285            Some(json!(42)),
2286            r#"[
2287                {
2288                    "foo": {
2289                        "bar": 42
2290                    }
2291                }
2292            ]"#
2293            .unindent(),
2294        );
2295    }
2296
2297    #[test]
2298    fn array_append() {
2299        #[track_caller]
2300        fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
2301            let input = input.to_string();
2302            let result = append_top_level_array_value_in_json_text(&input, &value, 4);
2303            let mut result_str = input;
2304            result_str.replace_range(result.0, &result.1);
2305            pretty_assertions::assert_eq!(expected.to_string(), result_str);
2306        }
2307        check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
2308        check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
2309        check_array_append(r#"[1, 3, 3   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2310        check_array_append(r#"[1, 3, 3,   ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2311        check_array_append(
2312            r#"[
2313                1,
2314                2,
2315                3
2316            ]"#
2317            .unindent(),
2318            json!(4),
2319            r#"[
2320                1,
2321                2,
2322                3,
2323                4
2324            ]"#
2325            .unindent(),
2326        );
2327        check_array_append(
2328            r#"[
2329                1,
2330                2,
2331                3,
2332            ]"#
2333            .unindent(),
2334            json!(4),
2335            r#"[
2336                1,
2337                2,
2338                3,
2339                4
2340            ]"#
2341            .unindent(),
2342        );
2343        check_array_append(
2344            r#"[
2345                1,
2346                2,
2347                3,
2348            ]"#
2349            .unindent(),
2350            json!({"foo": "bar", "baz": "qux"}),
2351            r#"[
2352                1,
2353                2,
2354                3,
2355                {
2356                    "foo": "bar",
2357                    "baz": "qux"
2358                }
2359            ]"#
2360            .unindent(),
2361        );
2362        check_array_append(
2363            r#"[ 1, 2, 3, ]"#.unindent(),
2364            json!({"foo": "bar", "baz": "qux"}),
2365            r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
2366        );
2367        check_array_append(
2368            r#"[]"#,
2369            json!({"foo": "bar"}),
2370            r#"[
2371                {
2372                    "foo": "bar"
2373                }
2374            ]"#
2375            .unindent(),
2376        );
2377
2378        // Test with comments between array elements
2379        check_array_append(
2380            r#"[
2381                1,
2382                // Comment between elements
2383                2,
2384                /* Block comment */ 3
2385            ]"#
2386            .unindent(),
2387            json!(4),
2388            r#"[
2389                1,
2390                // Comment between elements
2391                2,
2392                /* Block comment */ 3,
2393                4
2394            ]"#
2395            .unindent(),
2396        );
2397
2398        // Test with trailing comment on last element
2399        check_array_append(
2400            r#"[
2401                1,
2402                2,
2403                3 // Trailing comment
2404            ]"#
2405            .unindent(),
2406            json!("new"),
2407            r#"[
2408                1,
2409                2,
2410                3 // Trailing comment
2411            ,
2412                "new"
2413            ]"#
2414            .unindent(),
2415        );
2416
2417        // Test empty array with comments
2418        check_array_append(
2419            r#"[
2420                // Empty array with comment
2421            ]"#
2422            .unindent(),
2423            json!("first"),
2424            r#"[
2425                // Empty array with comment
2426                "first"
2427            ]"#
2428            .unindent(),
2429        );
2430
2431        // Test with multiline block comment at end
2432        check_array_append(
2433            r#"[
2434                1,
2435                2
2436                /*
2437                 * This is a
2438                 * multiline comment
2439                 */
2440            ]"#
2441            .unindent(),
2442            json!(3),
2443            r#"[
2444                1,
2445                2
2446                /*
2447                 * This is a
2448                 * multiline comment
2449                 */
2450            ,
2451                3
2452            ]"#
2453            .unindent(),
2454        );
2455
2456        // Test with deep indentation
2457        check_array_append(
2458            r#"[
2459                1,
2460                    2,
2461                        3
2462            ]"#
2463            .unindent(),
2464            json!("deep"),
2465            r#"[
2466                1,
2467                    2,
2468                        3,
2469                        "deep"
2470            ]"#
2471            .unindent(),
2472        );
2473
2474        // Test with no spacing
2475        check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
2476
2477        // Test appending complex nested structure
2478        check_array_append(
2479            r#"[
2480                {"a": 1},
2481                {"b": 2}
2482            ]"#
2483            .unindent(),
2484            json!({"c": {"nested": [1, 2, 3]}}),
2485            r#"[
2486                {"a": 1},
2487                {"b": 2},
2488                {
2489                    "c": {
2490                        "nested": [
2491                            1,
2492                            2,
2493                            3
2494                        ]
2495                    }
2496                }
2497            ]"#
2498            .unindent(),
2499        );
2500
2501        // Test array ending with comment after bracket
2502        check_array_append(
2503            r#"[
2504                1,
2505                2,
2506                3
2507            ] // Comment after array"#
2508                .unindent(),
2509            json!(4),
2510            r#"[
2511                1,
2512                2,
2513                3,
2514                4
2515            ] // Comment after array"#
2516                .unindent(),
2517        );
2518
2519        // Test with inconsistent element formatting
2520        check_array_append(
2521            r#"[1,
2522               2,
2523                    3,
2524            ]"#
2525            .unindent(),
2526            json!(4),
2527            r#"[1,
2528               2,
2529                    3,
2530                    4
2531            ]"#
2532            .unindent(),
2533        );
2534
2535        // Test appending to single-line array with trailing comma
2536        check_array_append(
2537            r#"[1, 2, 3,]"#,
2538            json!({"key": "value"}),
2539            r#"[1, 2, 3, { "key": "value" }]"#,
2540        );
2541
2542        // Test appending null value
2543        check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
2544
2545        // Test appending to array with only comments
2546        check_array_append(
2547            r#"[
2548                // Just comments here
2549                // More comments
2550            ]"#
2551            .unindent(),
2552            json!(42),
2553            r#"[
2554                // Just comments here
2555                // More comments
2556                42
2557            ]"#
2558            .unindent(),
2559        );
2560
2561        check_array_append(
2562            r#""#,
2563            json!(42),
2564            r#"[
2565                42
2566            ]"#
2567            .unindent(),
2568        )
2569    }
2570
2571    #[test]
2572    fn test_infer_json_indent_size() {
2573        let json_2_spaces = r#"{
2574  "key1": "value1",
2575  "nested": {
2576    "key2": "value2",
2577    "array": [
2578      1,
2579      2,
2580      3
2581    ]
2582  }
2583}"#;
2584        assert_eq!(infer_json_indent_size(json_2_spaces), 2);
2585
2586        let json_4_spaces = r#"{
2587    "key1": "value1",
2588    "nested": {
2589        "key2": "value2",
2590        "array": [
2591            1,
2592            2,
2593            3
2594        ]
2595    }
2596}"#;
2597        assert_eq!(infer_json_indent_size(json_4_spaces), 4);
2598
2599        let json_8_spaces = r#"{
2600        "key1": "value1",
2601        "nested": {
2602                "key2": "value2"
2603        }
2604}"#;
2605        assert_eq!(infer_json_indent_size(json_8_spaces), 8);
2606
2607        let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#;
2608        assert_eq!(infer_json_indent_size(json_single_line), 2);
2609
2610        let json_empty = r#"{}"#;
2611        assert_eq!(infer_json_indent_size(json_empty), 2);
2612
2613        let json_array = r#"[
2614  {
2615    "id": 1,
2616    "name": "first"
2617  },
2618  {
2619    "id": 2,
2620    "name": "second"
2621  }
2622]"#;
2623        assert_eq!(infer_json_indent_size(json_array), 2);
2624
2625        let json_mixed = r#"{
2626  "a": {
2627    "b": {
2628        "c": "value"
2629    }
2630  },
2631  "d": "value2"
2632}"#;
2633        assert_eq!(infer_json_indent_size(json_mixed), 2);
2634    }
2635}
2636
Served at tenant.openagents/omega Member data and write actions are omitted.