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