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 = tab_size * depth;
266 let mut new_val = to_pretty_json(&new_value, tab_size, 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
631/// Infers the indentation size used in JSON text by analyzing the tree structure.
632/// Returns the detected indent size, or a default of 2 if no indentation is found.
633pub fn infer_json_indent_size(text: &str) -> usize {
634 const MAX_INDENT_SIZE: usize = 64;
635
636 let mut parser = tree_sitter::Parser::new();
637 parser
638 .set_language(&tree_sitter_json::LANGUAGE.into())
639 .unwrap();
640
641 let Some(syntax_tree) = parser.parse(text, None) else {
642 return 4;
643 };
644
645 let mut cursor = syntax_tree.walk();
646 let mut indent_counts = [0u32; MAX_INDENT_SIZE];
647
648 // Traverse the tree to find indentation patterns
649 fn visit_node(
650 cursor: &mut tree_sitter::TreeCursor,
651 indent_counts: &mut [u32; MAX_INDENT_SIZE],
652 depth: usize,
653 ) {
654 if depth >= 3 {
655 return;
656 }
657 let node = cursor.node();
658 let node_kind = node.kind();
659
660 // For objects and arrays, check the indentation of their first content child
661 if matches!(node_kind, "object" | "array") {
662 let container_column = node.start_position().column;
663 let container_row = node.start_position().row;
664
665 if cursor.goto_first_child() {
666 // Skip the opening bracket
667 loop {
668 let child = cursor.node();
669 let child_kind = child.kind();
670
671 // Look for the first actual content (pair for objects, value for arrays)
672 if (node_kind == "object" && child_kind == "pair")
673 || (node_kind == "array"
674 && !matches!(child_kind, "[" | "]" | "," | "comment"))
675 {
676 let child_column = child.start_position().column;
677 let child_row = child.start_position().row;
678
679 // Only count if the child is on a different line
680 if child_row > container_row && child_column > container_column {
681 let indent = child_column - container_column;
682 if indent > 0 && indent < MAX_INDENT_SIZE {
683 indent_counts[indent] += 1;
684 }
685 }
686 break;
687 }
688
689 if !cursor.goto_next_sibling() {
690 break;
691 }
692 }
693 cursor.goto_parent();
694 }
695 }
696
697 // Recurse to children
698 if cursor.goto_first_child() {
699 loop {
700 visit_node(cursor, indent_counts, depth + 1);
701 if !cursor.goto_next_sibling() {
702 break;
703 }
704 }
705 cursor.goto_parent();
706 }
707 }
708
709 visit_node(&mut cursor, &mut indent_counts, 0);
710
711 // Find the indent size with the highest count
712 let mut max_count = 0;
713 let mut max_indent = 4;
714
715 for (indent, &count) in indent_counts.iter().enumerate() {
716 if count > max_count {
717 max_count = count;
718 max_indent = indent;
719 }
720 }
721
722 if max_count == 0 { 2 } else { max_indent }
723}
724
725pub fn to_pretty_json(
726 value: &impl Serialize,
727 indent_size: usize,
728 indent_prefix_len: usize,
729) -> String {
730 const SPACES: [u8; 32] = [b' '; 32];
731
732 debug_assert!(indent_size <= SPACES.len());
733 debug_assert!(indent_prefix_len <= SPACES.len());
734
735 let mut output = Vec::new();
736 let mut ser = serde_json::Serializer::with_formatter(
737 &mut output,
738 serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
739 );
740
741 value.serialize(&mut ser).unwrap();
742 let text = String::from_utf8(output).unwrap();
743
744 let mut adjusted_text = String::new();
745 for (i, line) in text.split('\n').enumerate() {
746 if i > 0 {
747 adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
748 }
749 adjusted_text.push_str(line);
750 adjusted_text.push('\n');
751 }
752 adjusted_text.pop();
753 adjusted_text
754}
755
756pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
757 let mut deserializer = serde_json_lenient::Deserializer::from_str(content);
758 Ok(serde_path_to_error::deserialize(&mut deserializer)?)
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764 use serde_json::{Value, json};
765 use unindent::Unindent;
766
767 #[test]
768 fn object_replace() {
769 #[track_caller]
770 fn check_object_replace(
771 input: String,
772 key_path: &[&str],
773 value: Option<Value>,
774 expected: String,
775 ) {
776 let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
777 let mut result_str = input;
778 result_str.replace_range(result.0, &result.1);
779 pretty_assertions::assert_eq!(expected, result_str);
780 }
781 check_object_replace(
782 r#"{
783 "a": 1,
784 "b": 2
785 }"#
786 .unindent(),
787 &["b"],
788 Some(json!(3)),
789 r#"{
790 "a": 1,
791 "b": 3
792 }"#
793 .unindent(),
794 );
795 check_object_replace(
796 r#"{
797 "a": 1,
798 "b": 2
799 }"#
800 .unindent(),
801 &["b"],
802 None,
803 r#"{
804 "a": 1
805 }"#
806 .unindent(),
807 );
808 check_object_replace(
809 r#"{
810 "a": 1,
811 "b": 2
812 }"#
813 .unindent(),
814 &["c"],
815 Some(json!(3)),
816 r#"{
817 "c": 3,
818 "a": 1,
819 "b": 2
820 }"#
821 .unindent(),
822 );
823 check_object_replace(
824 r#"{
825 "a": 1,
826 "b": {
827 "c": 2,
828 "d": 3,
829 }
830 }"#
831 .unindent(),
832 &["b", "c"],
833 Some(json!([1, 2, 3])),
834 r#"{
835 "a": 1,
836 "b": {
837 "c": [
838 1,
839 2,
840 3
841 ],
842 "d": 3,
843 }
844 }"#
845 .unindent(),
846 );
847
848 check_object_replace(
849 r#"{
850 "name": "old_name",
851 "id": 123
852 }"#
853 .unindent(),
854 &["name"],
855 Some(json!("new_name")),
856 r#"{
857 "name": "new_name",
858 "id": 123
859 }"#
860 .unindent(),
861 );
862
863 check_object_replace(
864 r#"{
865 "enabled": false,
866 "count": 5
867 }"#
868 .unindent(),
869 &["enabled"],
870 Some(json!(true)),
871 r#"{
872 "enabled": true,
873 "count": 5
874 }"#
875 .unindent(),
876 );
877
878 check_object_replace(
879 r#"{
880 "value": null,
881 "other": "test"
882 }"#
883 .unindent(),
884 &["value"],
885 Some(json!(42)),
886 r#"{
887 "value": 42,
888 "other": "test"
889 }"#
890 .unindent(),
891 );
892
893 check_object_replace(
894 r#"{
895 "config": {
896 "old": true
897 },
898 "name": "test"
899 }"#
900 .unindent(),
901 &["config"],
902 Some(json!({"new": false, "count": 3})),
903 r#"{
904 "config": {
905 "new": false,
906 "count": 3
907 },
908 "name": "test"
909 }"#
910 .unindent(),
911 );
912
913 check_object_replace(
914 r#"{
915 // This is a comment
916 "a": 1,
917 "b": 2 // Another comment
918 }"#
919 .unindent(),
920 &["b"],
921 Some(json!({"foo": "bar"})),
922 r#"{
923 // This is a comment
924 "a": 1,
925 "b": {
926 "foo": "bar"
927 } // Another comment
928 }"#
929 .unindent(),
930 );
931
932 check_object_replace(
933 r#"{}"#.to_string(),
934 &["new_key"],
935 Some(json!("value")),
936 r#"{
937 "new_key": "value"
938 }
939 "#
940 .unindent(),
941 );
942
943 check_object_replace(
944 r#"{
945 "only_key": 123
946 }"#
947 .unindent(),
948 &["only_key"],
949 None,
950 "{\n \n}".to_string(),
951 );
952
953 check_object_replace(
954 r#"{
955 "level1": {
956 "level2": {
957 "level3": {
958 "target": "old"
959 }
960 }
961 }
962 }"#
963 .unindent(),
964 &["level1", "level2", "level3", "target"],
965 Some(json!("new")),
966 r#"{
967 "level1": {
968 "level2": {
969 "level3": {
970 "target": "new"
971 }
972 }
973 }
974 }"#
975 .unindent(),
976 );
977
978 check_object_replace(
979 r#"{
980 "parent": {}
981 }"#
982 .unindent(),
983 &["parent", "child"],
984 Some(json!("value")),
985 r#"{
986 "parent": {
987 "child": "value"
988 }
989 }"#
990 .unindent(),
991 );
992
993 check_object_replace(
994 r#"{
995 "a": 1,
996 "b": 2,
997 }"#
998 .unindent(),
999 &["b"],
1000 Some(json!(3)),
1001 r#"{
1002 "a": 1,
1003 "b": 3,
1004 }"#
1005 .unindent(),
1006 );
1007
1008 check_object_replace(
1009 r#"{
1010 "items": [1, 2, 3],
1011 "count": 3
1012 }"#
1013 .unindent(),
1014 &["items", "1"],
1015 Some(json!(5)),
1016 r#"{
1017 "items": {
1018 "1": 5
1019 },
1020 "count": 3
1021 }"#
1022 .unindent(),
1023 );
1024
1025 check_object_replace(
1026 r#"{
1027 "items": [1, 2, 3],
1028 "count": 3
1029 }"#
1030 .unindent(),
1031 &["items", "1"],
1032 None,
1033 r#"{
1034 "items": {
1035 "1": null
1036 },
1037 "count": 3
1038 }"#
1039 .unindent(),
1040 );
1041
1042 check_object_replace(
1043 r#"{
1044 "items": [1, 2, 3],
1045 "count": 3
1046 }"#
1047 .unindent(),
1048 &["items"],
1049 Some(json!(["a", "b", "c", "d"])),
1050 r#"{
1051 "items": [
1052 "a",
1053 "b",
1054 "c",
1055 "d"
1056 ],
1057 "count": 3
1058 }"#
1059 .unindent(),
1060 );
1061
1062 check_object_replace(
1063 r#"{
1064 "0": "zero",
1065 "1": "one"
1066 }"#
1067 .unindent(),
1068 &["1"],
1069 Some(json!("ONE")),
1070 r#"{
1071 "0": "zero",
1072 "1": "ONE"
1073 }"#
1074 .unindent(),
1075 );
1076 // Test with comments between object members
1077 check_object_replace(
1078 r#"{
1079 "a": 1,
1080 // Comment between members
1081 "b": 2,
1082 /* Block comment */
1083 "c": 3
1084 }"#
1085 .unindent(),
1086 &["b"],
1087 Some(json!({"nested": true})),
1088 r#"{
1089 "a": 1,
1090 // Comment between members
1091 "b": {
1092 "nested": true
1093 },
1094 /* Block comment */
1095 "c": 3
1096 }"#
1097 .unindent(),
1098 );
1099
1100 // Test with trailing comments on replaced value
1101 check_object_replace(
1102 r#"{
1103 "a": 1, // keep this comment
1104 "b": 2 // this should stay
1105 }"#
1106 .unindent(),
1107 &["a"],
1108 Some(json!("changed")),
1109 r#"{
1110 "a": "changed", // keep this comment
1111 "b": 2 // this should stay
1112 }"#
1113 .unindent(),
1114 );
1115
1116 // Test with deep indentation
1117 check_object_replace(
1118 r#"{
1119 "deeply": {
1120 "nested": {
1121 "value": "old"
1122 }
1123 }
1124 }"#
1125 .unindent(),
1126 &["deeply", "nested", "value"],
1127 Some(json!("new")),
1128 r#"{
1129 "deeply": {
1130 "nested": {
1131 "value": "new"
1132 }
1133 }
1134 }"#
1135 .unindent(),
1136 );
1137
1138 // Test removing value with comment preservation
1139 check_object_replace(
1140 r#"{
1141 // Header comment
1142 "a": 1,
1143 // This comment belongs to b
1144 "b": 2,
1145 // This comment belongs to c
1146 "c": 3
1147 }"#
1148 .unindent(),
1149 &["b"],
1150 None,
1151 r#"{
1152 // Header comment
1153 "a": 1,
1154 // This comment belongs to b
1155 // This comment belongs to c
1156 "c": 3
1157 }"#
1158 .unindent(),
1159 );
1160
1161 // Test with multiline block comments
1162 check_object_replace(
1163 r#"{
1164 /*
1165 * This is a multiline
1166 * block comment
1167 */
1168 "value": "old",
1169 /* Another block */ "other": 123
1170 }"#
1171 .unindent(),
1172 &["value"],
1173 Some(json!("new")),
1174 r#"{
1175 /*
1176 * This is a multiline
1177 * block comment
1178 */
1179 "value": "new",
1180 /* Another block */ "other": 123
1181 }"#
1182 .unindent(),
1183 );
1184
1185 check_object_replace(
1186 r#"{
1187 // This object is empty
1188 }"#
1189 .unindent(),
1190 &["key"],
1191 Some(json!("value")),
1192 r#"{
1193 // This object is empty
1194 "key": "value"
1195 }
1196 "#
1197 .unindent(),
1198 );
1199
1200 // Test replacing in object with only comments
1201 check_object_replace(
1202 r#"{
1203 // Comment 1
1204 // Comment 2
1205 }"#
1206 .unindent(),
1207 &["new"],
1208 Some(json!(42)),
1209 r#"{
1210 // Comment 1
1211 // Comment 2
1212 "new": 42
1213 }
1214 "#
1215 .unindent(),
1216 );
1217
1218 // Test with inconsistent spacing
1219 check_object_replace(
1220 r#"{
1221 "a":1,
1222 "b" : 2 ,
1223 "c": 3
1224 }"#
1225 .unindent(),
1226 &["b"],
1227 Some(json!("spaced")),
1228 r#"{
1229 "a":1,
1230 "b" : "spaced" ,
1231 "c": 3
1232 }"#
1233 .unindent(),
1234 );
1235 }
1236
1237 #[test]
1238 fn object_replace_array() {
1239 // Tests replacing values within arrays that are nested inside objects.
1240 // Uses "#N" syntax in key paths to indicate array indices.
1241 #[track_caller]
1242 fn check_object_replace_array(
1243 input: String,
1244 key_path: &[&str],
1245 value: Option<Value>,
1246 expected: String,
1247 ) {
1248 let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
1249 let mut result_str = input;
1250 result_str.replace_range(result.0, &result.1);
1251 pretty_assertions::assert_eq!(expected, result_str);
1252 }
1253
1254 // Basic array element replacement
1255 check_object_replace_array(
1256 r#"{
1257 "a": [1, 3],
1258 }"#
1259 .unindent(),
1260 &["a", "#1"],
1261 Some(json!(2)),
1262 r#"{
1263 "a": [1, 2],
1264 }"#
1265 .unindent(),
1266 );
1267
1268 // Replace first element
1269 check_object_replace_array(
1270 r#"{
1271 "items": [1, 2, 3]
1272 }"#
1273 .unindent(),
1274 &["items", "#0"],
1275 Some(json!(10)),
1276 r#"{
1277 "items": [10, 2, 3]
1278 }"#
1279 .unindent(),
1280 );
1281
1282 // Replace last element
1283 check_object_replace_array(
1284 r#"{
1285 "items": [1, 2, 3]
1286 }"#
1287 .unindent(),
1288 &["items", "#2"],
1289 Some(json!(30)),
1290 r#"{
1291 "items": [1, 2, 30]
1292 }"#
1293 .unindent(),
1294 );
1295
1296 // Replace string in array
1297 check_object_replace_array(
1298 r#"{
1299 "names": ["alice", "bob", "charlie"]
1300 }"#
1301 .unindent(),
1302 &["names", "#1"],
1303 Some(json!("robert")),
1304 r#"{
1305 "names": ["alice", "robert", "charlie"]
1306 }"#
1307 .unindent(),
1308 );
1309
1310 // Replace boolean
1311 check_object_replace_array(
1312 r#"{
1313 "flags": [true, false, true]
1314 }"#
1315 .unindent(),
1316 &["flags", "#0"],
1317 Some(json!(false)),
1318 r#"{
1319 "flags": [false, false, true]
1320 }"#
1321 .unindent(),
1322 );
1323
1324 // Replace null with value
1325 check_object_replace_array(
1326 r#"{
1327 "values": [null, 2, null]
1328 }"#
1329 .unindent(),
1330 &["values", "#0"],
1331 Some(json!(1)),
1332 r#"{
1333 "values": [1, 2, null]
1334 }"#
1335 .unindent(),
1336 );
1337
1338 // Replace value with null
1339 check_object_replace_array(
1340 r#"{
1341 "data": [1, 2, 3]
1342 }"#
1343 .unindent(),
1344 &["data", "#1"],
1345 Some(json!(null)),
1346 r#"{
1347 "data": [1, null, 3]
1348 }"#
1349 .unindent(),
1350 );
1351
1352 // Replace simple value with object
1353 check_object_replace_array(
1354 r#"{
1355 "list": [1, 2, 3]
1356 }"#
1357 .unindent(),
1358 &["list", "#1"],
1359 Some(json!({"value": 2, "label": "two"})),
1360 r#"{
1361 "list": [1, { "value": 2, "label": "two" }, 3]
1362 }"#
1363 .unindent(),
1364 );
1365
1366 // Replace simple value with nested array
1367 check_object_replace_array(
1368 r#"{
1369 "matrix": [1, 2, 3]
1370 }"#
1371 .unindent(),
1372 &["matrix", "#1"],
1373 Some(json!([20, 21, 22])),
1374 r#"{
1375 "matrix": [1, [ 20, 21, 22 ], 3]
1376 }"#
1377 .unindent(),
1378 );
1379
1380 // Replace object in array
1381 check_object_replace_array(
1382 r#"{
1383 "users": [
1384 {"name": "alice"},
1385 {"name": "bob"},
1386 {"name": "charlie"}
1387 ]
1388 }"#
1389 .unindent(),
1390 &["users", "#1"],
1391 Some(json!({"name": "robert", "age": 30})),
1392 r#"{
1393 "users": [
1394 {"name": "alice"},
1395 { "name": "robert", "age": 30 },
1396 {"name": "charlie"}
1397 ]
1398 }"#
1399 .unindent(),
1400 );
1401
1402 // Replace property within object in array
1403 check_object_replace_array(
1404 r#"{
1405 "users": [
1406 {"name": "alice", "age": 25},
1407 {"name": "bob", "age": 30},
1408 {"name": "charlie", "age": 35}
1409 ]
1410 }"#
1411 .unindent(),
1412 &["users", "#1", "age"],
1413 Some(json!(31)),
1414 r#"{
1415 "users": [
1416 {"name": "alice", "age": 25},
1417 {"name": "bob", "age": 31},
1418 {"name": "charlie", "age": 35}
1419 ]
1420 }"#
1421 .unindent(),
1422 );
1423
1424 // Add new property to object in array
1425 check_object_replace_array(
1426 r#"{
1427 "items": [
1428 {"id": 1},
1429 {"id": 2},
1430 {"id": 3}
1431 ]
1432 }"#
1433 .unindent(),
1434 &["items", "#1", "name"],
1435 Some(json!("Item Two")),
1436 r#"{
1437 "items": [
1438 {"id": 1},
1439 {"name": "Item Two", "id": 2},
1440 {"id": 3}
1441 ]
1442 }"#
1443 .unindent(),
1444 );
1445
1446 // Remove property from object in array
1447 check_object_replace_array(
1448 r#"{
1449 "items": [
1450 {"id": 1, "name": "one"},
1451 {"id": 2, "name": "two"},
1452 {"id": 3, "name": "three"}
1453 ]
1454 }"#
1455 .unindent(),
1456 &["items", "#1", "name"],
1457 None,
1458 r#"{
1459 "items": [
1460 {"id": 1, "name": "one"},
1461 {"id": 2},
1462 {"id": 3, "name": "three"}
1463 ]
1464 }"#
1465 .unindent(),
1466 );
1467
1468 // Deeply nested: array in object in array
1469 check_object_replace_array(
1470 r#"{
1471 "data": [
1472 {
1473 "values": [1, 2, 3]
1474 },
1475 {
1476 "values": [4, 5, 6]
1477 }
1478 ]
1479 }"#
1480 .unindent(),
1481 &["data", "#0", "values", "#1"],
1482 Some(json!(20)),
1483 r#"{
1484 "data": [
1485 {
1486 "values": [1, 20, 3]
1487 },
1488 {
1489 "values": [4, 5, 6]
1490 }
1491 ]
1492 }"#
1493 .unindent(),
1494 );
1495
1496 // Multiple levels of nesting
1497 check_object_replace_array(
1498 r#"{
1499 "root": {
1500 "level1": [
1501 {
1502 "level2": {
1503 "level3": [10, 20, 30]
1504 }
1505 }
1506 ]
1507 }
1508 }"#
1509 .unindent(),
1510 &["root", "level1", "#0", "level2", "level3", "#2"],
1511 Some(json!(300)),
1512 r#"{
1513 "root": {
1514 "level1": [
1515 {
1516 "level2": {
1517 "level3": [10, 20, 300]
1518 }
1519 }
1520 ]
1521 }
1522 }"#
1523 .unindent(),
1524 );
1525
1526 // Array with mixed types
1527 check_object_replace_array(
1528 r#"{
1529 "mixed": [1, "two", true, null, {"five": 5}]
1530 }"#
1531 .unindent(),
1532 &["mixed", "#3"],
1533 Some(json!({"four": 4})),
1534 r#"{
1535 "mixed": [1, "two", true, { "four": 4 }, {"five": 5}]
1536 }"#
1537 .unindent(),
1538 );
1539
1540 // Replace with complex object
1541 check_object_replace_array(
1542 r#"{
1543 "config": [
1544 "simple",
1545 "values"
1546 ]
1547 }"#
1548 .unindent(),
1549 &["config", "#0"],
1550 Some(json!({
1551 "type": "complex",
1552 "settings": {
1553 "enabled": true,
1554 "level": 5
1555 }
1556 })),
1557 r#"{
1558 "config": [
1559 {
1560 "type": "complex",
1561 "settings": {
1562 "enabled": true,
1563 "level": 5
1564 }
1565 },
1566 "values"
1567 ]
1568 }"#
1569 .unindent(),
1570 );
1571
1572 // Array with trailing comma
1573 check_object_replace_array(
1574 r#"{
1575 "items": [
1576 1,
1577 2,
1578 3,
1579 ]
1580 }"#
1581 .unindent(),
1582 &["items", "#1"],
1583 Some(json!(20)),
1584 r#"{
1585 "items": [
1586 1,
1587 20,
1588 3,
1589 ]
1590 }"#
1591 .unindent(),
1592 );
1593
1594 // Array with comments
1595 check_object_replace_array(
1596 r#"{
1597 "items": [
1598 1, // first item
1599 2, // second item
1600 3 // third item
1601 ]
1602 }"#
1603 .unindent(),
1604 &["items", "#1"],
1605 Some(json!(20)),
1606 r#"{
1607 "items": [
1608 1, // first item
1609 20, // second item
1610 3 // third item
1611 ]
1612 }"#
1613 .unindent(),
1614 );
1615
1616 // Multiple arrays in object
1617 check_object_replace_array(
1618 r#"{
1619 "first": [1, 2, 3],
1620 "second": [4, 5, 6],
1621 "third": [7, 8, 9]
1622 }"#
1623 .unindent(),
1624 &["second", "#1"],
1625 Some(json!(50)),
1626 r#"{
1627 "first": [1, 2, 3],
1628 "second": [4, 50, 6],
1629 "third": [7, 8, 9]
1630 }"#
1631 .unindent(),
1632 );
1633
1634 // Empty array - add first element
1635 check_object_replace_array(
1636 r#"{
1637 "empty": []
1638 }"#
1639 .unindent(),
1640 &["empty", "#0"],
1641 Some(json!("first")),
1642 r#"{
1643 "empty": ["first"]
1644 }"#
1645 .unindent(),
1646 );
1647
1648 // Array of arrays
1649 check_object_replace_array(
1650 r#"{
1651 "matrix": [
1652 [1, 2],
1653 [3, 4],
1654 [5, 6]
1655 ]
1656 }"#
1657 .unindent(),
1658 &["matrix", "#1", "#0"],
1659 Some(json!(30)),
1660 r#"{
1661 "matrix": [
1662 [1, 2],
1663 [30, 4],
1664 [5, 6]
1665 ]
1666 }"#
1667 .unindent(),
1668 );
1669
1670 // Replace nested object property in array element
1671 check_object_replace_array(
1672 r#"{
1673 "users": [
1674 {
1675 "name": "alice",
1676 "address": {
1677 "city": "NYC",
1678 "zip": "10001"
1679 }
1680 }
1681 ]
1682 }"#
1683 .unindent(),
1684 &["users", "#0", "address", "city"],
1685 Some(json!("Boston")),
1686 r#"{
1687 "users": [
1688 {
1689 "name": "alice",
1690 "address": {
1691 "city": "Boston",
1692 "zip": "10001"
1693 }
1694 }
1695 ]
1696 }"#
1697 .unindent(),
1698 );
1699
1700 // Add element past end of array
1701 check_object_replace_array(
1702 r#"{
1703 "items": [1, 2]
1704 }"#
1705 .unindent(),
1706 &["items", "#5"],
1707 Some(json!(6)),
1708 r#"{
1709 "items": [1, 2, 6]
1710 }"#
1711 .unindent(),
1712 );
1713
1714 // Complex nested structure
1715 check_object_replace_array(
1716 r#"{
1717 "app": {
1718 "modules": [
1719 {
1720 "name": "auth",
1721 "routes": [
1722 {"path": "/login", "method": "POST"},
1723 {"path": "/logout", "method": "POST"}
1724 ]
1725 },
1726 {
1727 "name": "api",
1728 "routes": [
1729 {"path": "/users", "method": "GET"},
1730 {"path": "/users", "method": "POST"}
1731 ]
1732 }
1733 ]
1734 }
1735 }"#
1736 .unindent(),
1737 &["app", "modules", "#1", "routes", "#0", "method"],
1738 Some(json!("PUT")),
1739 r#"{
1740 "app": {
1741 "modules": [
1742 {
1743 "name": "auth",
1744 "routes": [
1745 {"path": "/login", "method": "POST"},
1746 {"path": "/logout", "method": "POST"}
1747 ]
1748 },
1749 {
1750 "name": "api",
1751 "routes": [
1752 {"path": "/users", "method": "PUT"},
1753 {"path": "/users", "method": "POST"}
1754 ]
1755 }
1756 ]
1757 }
1758 }"#
1759 .unindent(),
1760 );
1761
1762 // Escaped strings in array
1763 check_object_replace_array(
1764 r#"{
1765 "messages": ["hello", "world"]
1766 }"#
1767 .unindent(),
1768 &["messages", "#0"],
1769 Some(json!("hello \"quoted\" world")),
1770 r#"{
1771 "messages": ["hello \"quoted\" world", "world"]
1772 }"#
1773 .unindent(),
1774 );
1775
1776 // Block comments
1777 check_object_replace_array(
1778 r#"{
1779 "data": [
1780 /* first */ 1,
1781 /* second */ 2,
1782 /* third */ 3
1783 ]
1784 }"#
1785 .unindent(),
1786 &["data", "#1"],
1787 Some(json!(20)),
1788 r#"{
1789 "data": [
1790 /* first */ 1,
1791 /* second */ 20,
1792 /* third */ 3
1793 ]
1794 }"#
1795 .unindent(),
1796 );
1797
1798 // Inline array
1799 check_object_replace_array(
1800 r#"{"items": [1, 2, 3], "count": 3}"#.to_string(),
1801 &["items", "#1"],
1802 Some(json!(20)),
1803 r#"{"items": [1, 20, 3], "count": 3}"#.to_string(),
1804 );
1805
1806 // Single element array
1807 check_object_replace_array(
1808 r#"{
1809 "single": [42]
1810 }"#
1811 .unindent(),
1812 &["single", "#0"],
1813 Some(json!(100)),
1814 r#"{
1815 "single": [100]
1816 }"#
1817 .unindent(),
1818 );
1819
1820 // Inconsistent formatting
1821 check_object_replace_array(
1822 r#"{
1823 "messy": [1,
1824 2,
1825 3,
1826 4]
1827 }"#
1828 .unindent(),
1829 &["messy", "#2"],
1830 Some(json!(30)),
1831 r#"{
1832 "messy": [1,
1833 2,
1834 30,
1835 4]
1836 }"#
1837 .unindent(),
1838 );
1839
1840 // Creates array if has numbered key
1841 check_object_replace_array(
1842 r#"{
1843 "array": {"foo": "bar"}
1844 }"#
1845 .unindent(),
1846 &["array", "#3"],
1847 Some(json!(4)),
1848 r#"{
1849 "array": [
1850 4
1851 ]
1852 }"#
1853 .unindent(),
1854 );
1855
1856 // Replace non-array element within array with array
1857 check_object_replace_array(
1858 r#"{
1859 "matrix": [
1860 [1, 2],
1861 [3, 4],
1862 [5, 6]
1863 ]
1864 }"#
1865 .unindent(),
1866 &["matrix", "#1", "#0"],
1867 Some(json!(["foo", "bar"])),
1868 r#"{
1869 "matrix": [
1870 [1, 2],
1871 [[ "foo", "bar" ], 4],
1872 [5, 6]
1873 ]
1874 }"#
1875 .unindent(),
1876 );
1877 // Replace non-array element within array with array
1878 check_object_replace_array(
1879 r#"{
1880 "matrix": [
1881 [1, 2],
1882 [3, 4],
1883 [5, 6]
1884 ]
1885 }"#
1886 .unindent(),
1887 &["matrix", "#1", "#0", "#3"],
1888 Some(json!(["foo", "bar"])),
1889 r#"{
1890 "matrix": [
1891 [1, 2],
1892 [[ [ "foo", "bar" ] ], 4],
1893 [5, 6]
1894 ]
1895 }"#
1896 .unindent(),
1897 );
1898
1899 // Create array in key that doesn't exist
1900 check_object_replace_array(
1901 r#"{
1902 "foo": {}
1903 }"#
1904 .unindent(),
1905 &["foo", "bar", "#0"],
1906 Some(json!({"is_object": true})),
1907 r#"{
1908 "foo": {
1909 "bar": [
1910 {
1911 "is_object": true
1912 }
1913 ]
1914 }
1915 }"#
1916 .unindent(),
1917 );
1918 }
1919
1920 #[test]
1921 fn array_replace() {
1922 #[track_caller]
1923 fn check_array_replace(
1924 input: impl ToString,
1925 index: usize,
1926 key_path: &[&str],
1927 value: Option<Value>,
1928 expected: impl ToString,
1929 ) {
1930 let input = input.to_string();
1931 let result = replace_top_level_array_value_in_json_text(
1932 &input,
1933 key_path,
1934 value.as_ref(),
1935 None,
1936 index,
1937 4,
1938 );
1939 let mut result_str = input;
1940 result_str.replace_range(result.0, &result.1);
1941 pretty_assertions::assert_eq!(expected.to_string(), result_str);
1942 }
1943
1944 check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1945 check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1946 check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1947 check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1948 check_array_replace(
1949 r#"[
1950 1,
1951 2,
1952 3,
1953 ]"#
1954 .unindent(),
1955 1,
1956 &[],
1957 Some(json!({"foo": "bar", "baz": "qux"})),
1958 r#"[
1959 1,
1960 {
1961 "foo": "bar",
1962 "baz": "qux"
1963 },
1964 3,
1965 ]"#
1966 .unindent(),
1967 );
1968 check_array_replace(
1969 r#"[1, 3, 3,]"#,
1970 1,
1971 &[],
1972 Some(json!({"foo": "bar", "baz": "qux"})),
1973 r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1974 );
1975
1976 check_array_replace(
1977 r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1978 1,
1979 &["baz"],
1980 Some(json!({"qux": "quz"})),
1981 r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1982 );
1983
1984 check_array_replace(
1985 r#"[
1986 1,
1987 {
1988 "foo": "bar",
1989 "baz": "qux"
1990 },
1991 3
1992 ]"#,
1993 1,
1994 &["baz"],
1995 Some(json!({"qux": "quz"})),
1996 r#"[
1997 1,
1998 {
1999 "foo": "bar",
2000 "baz": {
2001 "qux": "quz"
2002 }
2003 },
2004 3
2005 ]"#,
2006 );
2007
2008 check_array_replace(
2009 r#"[
2010 1,
2011 {
2012 "foo": "bar",
2013 "baz": {
2014 "qux": "quz"
2015 }
2016 },
2017 3
2018 ]"#,
2019 1,
2020 &["baz"],
2021 Some(json!("qux")),
2022 r#"[
2023 1,
2024 {
2025 "foo": "bar",
2026 "baz": "qux"
2027 },
2028 3
2029 ]"#,
2030 );
2031
2032 check_array_replace(
2033 r#"[
2034 1,
2035 {
2036 "foo": "bar",
2037 // some comment to keep
2038 "baz": {
2039 // some comment to remove
2040 "qux": "quz"
2041 }
2042 // some other comment to keep
2043 },
2044 3
2045 ]"#,
2046 1,
2047 &["baz"],
2048 Some(json!("qux")),
2049 r#"[
2050 1,
2051 {
2052 "foo": "bar",
2053 // some comment to keep
2054 "baz": "qux"
2055 // some other comment to keep
2056 },
2057 3
2058 ]"#,
2059 );
2060
2061 // Test with comments between array elements
2062 check_array_replace(
2063 r#"[
2064 1,
2065 // This is element 2
2066 2,
2067 /* Block comment */ 3,
2068 4 // Trailing comment
2069 ]"#,
2070 2,
2071 &[],
2072 Some(json!("replaced")),
2073 r#"[
2074 1,
2075 // This is element 2
2076 2,
2077 /* Block comment */ "replaced",
2078 4 // Trailing comment
2079 ]"#,
2080 );
2081
2082 // Test empty array with comments
2083 check_array_replace(
2084 r#"[
2085 // Empty array with comment
2086 ]"#
2087 .unindent(),
2088 0,
2089 &[],
2090 Some(json!("first")),
2091 r#"[
2092 // Empty array with comment
2093 "first"
2094 ]"#
2095 .unindent(),
2096 );
2097 check_array_replace(
2098 r#"[]"#.unindent(),
2099 0,
2100 &[],
2101 Some(json!("first")),
2102 r#"["first"]"#.unindent(),
2103 );
2104
2105 // Test array with leading comments
2106 check_array_replace(
2107 r#"[
2108 // Leading comment
2109 // Another leading comment
2110 1,
2111 2
2112 ]"#,
2113 0,
2114 &[],
2115 Some(json!({"new": "object"})),
2116 r#"[
2117 // Leading comment
2118 // Another leading comment
2119 {
2120 "new": "object"
2121 },
2122 2
2123 ]"#,
2124 );
2125
2126 // Test with deep indentation
2127 check_array_replace(
2128 r#"[
2129 1,
2130 2,
2131 3
2132 ]"#,
2133 1,
2134 &[],
2135 Some(json!("deep")),
2136 r#"[
2137 1,
2138 "deep",
2139 3
2140 ]"#,
2141 );
2142
2143 // Test with mixed spacing
2144 check_array_replace(
2145 r#"[1,2, 3, 4]"#,
2146 2,
2147 &[],
2148 Some(json!("spaced")),
2149 r#"[1,2, "spaced", 4]"#,
2150 );
2151
2152 // Test replacing nested array element
2153 check_array_replace(
2154 r#"[
2155 [1, 2, 3],
2156 [4, 5, 6],
2157 [7, 8, 9]
2158 ]"#,
2159 1,
2160 &[],
2161 Some(json!(["a", "b", "c", "d"])),
2162 r#"[
2163 [1, 2, 3],
2164 [
2165 "a",
2166 "b",
2167 "c",
2168 "d"
2169 ],
2170 [7, 8, 9]
2171 ]"#,
2172 );
2173
2174 // Test with multiline block comments
2175 check_array_replace(
2176 r#"[
2177 /*
2178 * This is a
2179 * multiline comment
2180 */
2181 "first",
2182 "second"
2183 ]"#,
2184 0,
2185 &[],
2186 Some(json!("updated")),
2187 r#"[
2188 /*
2189 * This is a
2190 * multiline comment
2191 */
2192 "updated",
2193 "second"
2194 ]"#,
2195 );
2196
2197 // Test replacing with null
2198 check_array_replace(
2199 r#"[true, false, true]"#,
2200 1,
2201 &[],
2202 Some(json!(null)),
2203 r#"[true, null, true]"#,
2204 );
2205
2206 // Test single element array
2207 check_array_replace(
2208 r#"[42]"#,
2209 0,
2210 &[],
2211 Some(json!({"answer": 42})),
2212 r#"[{ "answer": 42 }]"#,
2213 );
2214
2215 // Test array with only comments
2216 check_array_replace(
2217 r#"[
2218 // Comment 1
2219 // Comment 2
2220 // Comment 3
2221 ]"#
2222 .unindent(),
2223 10,
2224 &[],
2225 Some(json!(123)),
2226 r#"[
2227 // Comment 1
2228 // Comment 2
2229 // Comment 3
2230 123
2231 ]"#
2232 .unindent(),
2233 );
2234
2235 check_array_replace(
2236 r#"[
2237 {
2238 "key": "value"
2239 },
2240 {
2241 "key": "value2"
2242 }
2243 ]"#
2244 .unindent(),
2245 0,
2246 &[],
2247 None,
2248 r#"[
2249 {
2250 "key": "value2"
2251 }
2252 ]"#
2253 .unindent(),
2254 );
2255
2256 check_array_replace(
2257 r#"[
2258 {
2259 "key": "value"
2260 },
2261 {
2262 "key": "value2"
2263 },
2264 {
2265 "key": "value3"
2266 },
2267 ]"#
2268 .unindent(),
2269 1,
2270 &[],
2271 None,
2272 r#"[
2273 {
2274 "key": "value"
2275 },
2276 {
2277 "key": "value3"
2278 },
2279 ]"#
2280 .unindent(),
2281 );
2282
2283 check_array_replace(
2284 r#""#,
2285 2,
2286 &[],
2287 Some(json!(42)),
2288 r#"[
2289 42
2290 ]"#
2291 .unindent(),
2292 );
2293
2294 check_array_replace(
2295 r#""#,
2296 2,
2297 &["foo", "bar"],
2298 Some(json!(42)),
2299 r#"[
2300 {
2301 "foo": {
2302 "bar": 42
2303 }
2304 }
2305 ]"#
2306 .unindent(),
2307 );
2308 }
2309
2310 #[test]
2311 fn array_append() {
2312 #[track_caller]
2313 fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
2314 let input = input.to_string();
2315 let result = append_top_level_array_value_in_json_text(&input, &value, 4);
2316 let mut result_str = input;
2317 result_str.replace_range(result.0, &result.1);
2318 pretty_assertions::assert_eq!(expected.to_string(), result_str);
2319 }
2320 check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
2321 check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
2322 check_array_append(r#"[1, 3, 3 ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2323 check_array_append(r#"[1, 3, 3, ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2324 check_array_append(
2325 r#"[
2326 1,
2327 2,
2328 3
2329 ]"#
2330 .unindent(),
2331 json!(4),
2332 r#"[
2333 1,
2334 2,
2335 3,
2336 4
2337 ]"#
2338 .unindent(),
2339 );
2340 check_array_append(
2341 r#"[
2342 1,
2343 2,
2344 3,
2345 ]"#
2346 .unindent(),
2347 json!(4),
2348 r#"[
2349 1,
2350 2,
2351 3,
2352 4
2353 ]"#
2354 .unindent(),
2355 );
2356 check_array_append(
2357 r#"[
2358 1,
2359 2,
2360 3,
2361 ]"#
2362 .unindent(),
2363 json!({"foo": "bar", "baz": "qux"}),
2364 r#"[
2365 1,
2366 2,
2367 3,
2368 {
2369 "foo": "bar",
2370 "baz": "qux"
2371 }
2372 ]"#
2373 .unindent(),
2374 );
2375 check_array_append(
2376 r#"[ 1, 2, 3, ]"#.unindent(),
2377 json!({"foo": "bar", "baz": "qux"}),
2378 r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
2379 );
2380 check_array_append(
2381 r#"[]"#,
2382 json!({"foo": "bar"}),
2383 r#"[
2384 {
2385 "foo": "bar"
2386 }
2387 ]"#
2388 .unindent(),
2389 );
2390
2391 // Test with comments between array elements
2392 check_array_append(
2393 r#"[
2394 1,
2395 // Comment between elements
2396 2,
2397 /* Block comment */ 3
2398 ]"#
2399 .unindent(),
2400 json!(4),
2401 r#"[
2402 1,
2403 // Comment between elements
2404 2,
2405 /* Block comment */ 3,
2406 4
2407 ]"#
2408 .unindent(),
2409 );
2410
2411 // Test with trailing comment on last element
2412 check_array_append(
2413 r#"[
2414 1,
2415 2,
2416 3 // Trailing comment
2417 ]"#
2418 .unindent(),
2419 json!("new"),
2420 r#"[
2421 1,
2422 2,
2423 3 // Trailing comment
2424 ,
2425 "new"
2426 ]"#
2427 .unindent(),
2428 );
2429
2430 // Test empty array with comments
2431 check_array_append(
2432 r#"[
2433 // Empty array with comment
2434 ]"#
2435 .unindent(),
2436 json!("first"),
2437 r#"[
2438 // Empty array with comment
2439 "first"
2440 ]"#
2441 .unindent(),
2442 );
2443
2444 // Test with multiline block comment at end
2445 check_array_append(
2446 r#"[
2447 1,
2448 2
2449 /*
2450 * This is a
2451 * multiline comment
2452 */
2453 ]"#
2454 .unindent(),
2455 json!(3),
2456 r#"[
2457 1,
2458 2
2459 /*
2460 * This is a
2461 * multiline comment
2462 */
2463 ,
2464 3
2465 ]"#
2466 .unindent(),
2467 );
2468
2469 // Test with deep indentation
2470 check_array_append(
2471 r#"[
2472 1,
2473 2,
2474 3
2475 ]"#
2476 .unindent(),
2477 json!("deep"),
2478 r#"[
2479 1,
2480 2,
2481 3,
2482 "deep"
2483 ]"#
2484 .unindent(),
2485 );
2486
2487 // Test with no spacing
2488 check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
2489
2490 // Test appending complex nested structure
2491 check_array_append(
2492 r#"[
2493 {"a": 1},
2494 {"b": 2}
2495 ]"#
2496 .unindent(),
2497 json!({"c": {"nested": [1, 2, 3]}}),
2498 r#"[
2499 {"a": 1},
2500 {"b": 2},
2501 {
2502 "c": {
2503 "nested": [
2504 1,
2505 2,
2506 3
2507 ]
2508 }
2509 }
2510 ]"#
2511 .unindent(),
2512 );
2513
2514 // Test array ending with comment after bracket
2515 check_array_append(
2516 r#"[
2517 1,
2518 2,
2519 3
2520 ] // Comment after array"#
2521 .unindent(),
2522 json!(4),
2523 r#"[
2524 1,
2525 2,
2526 3,
2527 4
2528 ] // Comment after array"#
2529 .unindent(),
2530 );
2531
2532 // Test with inconsistent element formatting
2533 check_array_append(
2534 r#"[1,
2535 2,
2536 3,
2537 ]"#
2538 .unindent(),
2539 json!(4),
2540 r#"[1,
2541 2,
2542 3,
2543 4
2544 ]"#
2545 .unindent(),
2546 );
2547
2548 // Test appending to single-line array with trailing comma
2549 check_array_append(
2550 r#"[1, 2, 3,]"#,
2551 json!({"key": "value"}),
2552 r#"[1, 2, 3, { "key": "value" }]"#,
2553 );
2554
2555 // Test appending null value
2556 check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
2557
2558 // Test appending to array with only comments
2559 check_array_append(
2560 r#"[
2561 // Just comments here
2562 // More comments
2563 ]"#
2564 .unindent(),
2565 json!(42),
2566 r#"[
2567 // Just comments here
2568 // More comments
2569 42
2570 ]"#
2571 .unindent(),
2572 );
2573
2574 check_array_append(
2575 r#""#,
2576 json!(42),
2577 r#"[
2578 42
2579 ]"#
2580 .unindent(),
2581 )
2582 }
2583
2584 #[test]
2585 fn test_infer_json_indent_size() {
2586 let json_2_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_2_spaces), 2);
2598
2599 let json_4_spaces = r#"{
2600 "key1": "value1",
2601 "nested": {
2602 "key2": "value2",
2603 "array": [
2604 1,
2605 2,
2606 3
2607 ]
2608 }
2609}"#;
2610 assert_eq!(infer_json_indent_size(json_4_spaces), 4);
2611
2612 let json_8_spaces = r#"{
2613 "key1": "value1",
2614 "nested": {
2615 "key2": "value2"
2616 }
2617}"#;
2618 assert_eq!(infer_json_indent_size(json_8_spaces), 8);
2619
2620 let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#;
2621 assert_eq!(infer_json_indent_size(json_single_line), 2);
2622
2623 let json_empty = r#"{}"#;
2624 assert_eq!(infer_json_indent_size(json_empty), 2);
2625
2626 let json_array = r#"[
2627 {
2628 "id": 1,
2629 "name": "first"
2630 },
2631 {
2632 "id": 2,
2633 "name": "second"
2634 }
2635]"#;
2636 assert_eq!(infer_json_indent_size(json_array), 2);
2637
2638 let json_mixed = r#"{
2639 "a": {
2640 "b": {
2641 "c": "value"
2642 }
2643 },
2644 "d": "value2"
2645}"#;
2646 assert_eq!(infer_json_indent_size(json_mixed), 2);
2647 }
2648}