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