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 text_range = range.start_byte..range.end_byte;
357 let value_str = &text[text_range.clone()];
358 let needs_indent = range.start_point.row > 0;
359
360 if new_value.is_none() && key_path.is_empty() {
361 let mut remove_range = text_range.clone();
362 if index == 0 {
363 while cursor.goto_next_sibling()
364 && (cursor.node().is_extra() || cursor.node().is_missing())
365 {}
366 if cursor.node().kind() == "," {
367 remove_range.end = cursor.node().range().end_byte;
368 }
369 if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') {
370 if text[remove_range.end + 1..remove_range.end + next_newline]
371 .chars()
372 .all(|c| c.is_ascii_whitespace())
373 {
374 remove_range.end = remove_range.end + next_newline;
375 }
376 }
377 } else {
378 while cursor.goto_previous_sibling()
379 && (cursor.node().is_extra() || cursor.node().is_missing())
380 {}
381 if cursor.node().kind() == "," {
382 remove_range.start = cursor.node().range().start_byte;
383 }
384 }
385 return Ok((remove_range, String::new()));
386 } else {
387 let (mut replace_range, mut replace_value) =
388 replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
389
390 replace_range.start += offset;
391 replace_range.end += offset;
392
393 if needs_indent {
394 let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
395 replace_value = replace_value.replace('\n', &increased_indent);
396 // replace_value.push('\n');
397 } else {
398 while let Some(idx) = replace_value.find("\n ") {
399 replace_value.remove(idx + 1);
400 }
401 while let Some(idx) = replace_value.find("\n") {
402 replace_value.replace_range(idx..idx + 1, " ");
403 }
404 }
405
406 return Ok((replace_range, replace_value));
407 }
408}
409
410pub fn append_top_level_array_value_in_json_text(
411 text: &str,
412 new_value: &Value,
413 tab_size: usize,
414) -> Result<(Range<usize>, String)> {
415 let mut parser = tree_sitter::Parser::new();
416 parser
417 .set_language(&tree_sitter_json::LANGUAGE.into())
418 .unwrap();
419 let syntax_tree = parser.parse(text, None).unwrap();
420
421 let mut cursor = syntax_tree.walk();
422
423 if cursor.node().kind() == TS_DOCUMENT_KIND {
424 anyhow::ensure!(
425 cursor.goto_first_child(),
426 "Document empty - No top level array"
427 );
428 }
429
430 while cursor.node().kind() != TS_ARRAY_KIND {
431 anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
432 }
433
434 anyhow::ensure!(
435 cursor.goto_last_child(),
436 "Malformed JSON syntax tree, expected `]` at end of array"
437 );
438 debug_assert_eq!(cursor.node().kind(), "]");
439 let close_bracket_start = cursor.node().start_byte();
440 cursor.goto_previous_sibling();
441 while (cursor.node().is_extra() || cursor.node().is_missing()) && cursor.goto_previous_sibling()
442 {
443 }
444
445 let mut comma_range = None;
446 let mut prev_item_range = None;
447
448 if cursor.node().kind() == "," {
449 comma_range = Some(cursor.node().byte_range());
450 while cursor.goto_previous_sibling() && cursor.node().is_extra() {}
451
452 debug_assert_ne!(cursor.node().kind(), "[");
453 prev_item_range = Some(cursor.node().range());
454 } else {
455 while (cursor.node().is_extra() || cursor.node().is_missing())
456 && cursor.goto_previous_sibling()
457 {}
458 if cursor.node().kind() != "[" {
459 prev_item_range = Some(cursor.node().range());
460 }
461 }
462
463 let (mut replace_range, mut replace_value) =
464 replace_value_in_json_text("", &[], tab_size, Some(new_value), None);
465
466 replace_range.start = close_bracket_start;
467 replace_range.end = close_bracket_start;
468
469 let space = ' ';
470 if let Some(prev_item_range) = prev_item_range {
471 let needs_newline = prev_item_range.start_point.row > 0;
472 let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
473 prev_item_range.start_point.column,
474 |idx| {
475 prev_item_range.start_point.column
476 - text[idx + 1..prev_item_range.start_byte].trim_start().len()
477 },
478 );
479
480 let prev_item_end = comma_range
481 .as_ref()
482 .map_or(prev_item_range.end_byte, |range| range.end);
483 if text[prev_item_end..replace_range.start].trim().is_empty() {
484 replace_range.start = prev_item_end;
485 }
486
487 if needs_newline {
488 let increased_indent = format!("\n{space:width$}", width = indent_width);
489 replace_value = replace_value.replace('\n', &increased_indent);
490 replace_value.push('\n');
491 replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
492 } else {
493 while let Some(idx) = replace_value.find("\n ") {
494 replace_value.remove(idx + 1);
495 }
496 while let Some(idx) = replace_value.find('\n') {
497 replace_value.replace_range(idx..idx + 1, " ");
498 }
499 replace_value.insert(0, ' ');
500 }
501
502 if comma_range.is_none() {
503 replace_value.insert(0, ',');
504 }
505 } else {
506 if let Some(prev_newline) = text[..replace_range.start].rfind('\n') {
507 if text[prev_newline..replace_range.start].trim().is_empty() {
508 replace_range.start = prev_newline;
509 }
510 }
511 let indent = format!("\n{space:width$}", width = tab_size);
512 replace_value = replace_value.replace('\n', &indent);
513 replace_value.insert_str(0, &indent);
514 replace_value.push('\n');
515 }
516 return Ok((replace_range, replace_value));
517}
518
519pub fn to_pretty_json(
520 value: &impl Serialize,
521 indent_size: usize,
522 indent_prefix_len: usize,
523) -> String {
524 const SPACES: [u8; 32] = [b' '; 32];
525
526 debug_assert!(indent_size <= SPACES.len());
527 debug_assert!(indent_prefix_len <= SPACES.len());
528
529 let mut output = Vec::new();
530 let mut ser = serde_json::Serializer::with_formatter(
531 &mut output,
532 serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
533 );
534
535 value.serialize(&mut ser).unwrap();
536 let text = String::from_utf8(output).unwrap();
537
538 let mut adjusted_text = String::new();
539 for (i, line) in text.split('\n').enumerate() {
540 if i > 0 {
541 adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
542 }
543 adjusted_text.push_str(line);
544 adjusted_text.push('\n');
545 }
546 adjusted_text.pop();
547 adjusted_text
548}
549
550pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
551 Ok(serde_json_lenient::from_str(content)?)
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use serde_json::{Value, json};
558 use unindent::Unindent;
559
560 #[test]
561 fn object_replace() {
562 #[track_caller]
563 fn check_object_replace(
564 input: String,
565 key_path: &[&str],
566 value: Option<Value>,
567 expected: String,
568 ) {
569 let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
570 let mut result_str = input.to_string();
571 result_str.replace_range(result.0, &result.1);
572 pretty_assertions::assert_eq!(expected, result_str);
573 }
574 check_object_replace(
575 r#"{
576 "a": 1,
577 "b": 2
578 }"#
579 .unindent(),
580 &["b"],
581 Some(json!(3)),
582 r#"{
583 "a": 1,
584 "b": 3
585 }"#
586 .unindent(),
587 );
588 check_object_replace(
589 r#"{
590 "a": 1,
591 "b": 2
592 }"#
593 .unindent(),
594 &["b"],
595 None,
596 r#"{
597 "a": 1
598 }"#
599 .unindent(),
600 );
601 check_object_replace(
602 r#"{
603 "a": 1,
604 "b": 2
605 }"#
606 .unindent(),
607 &["c"],
608 Some(json!(3)),
609 r#"{
610 "c": 3,
611 "a": 1,
612 "b": 2
613 }"#
614 .unindent(),
615 );
616 check_object_replace(
617 r#"{
618 "a": 1,
619 "b": {
620 "c": 2,
621 "d": 3,
622 }
623 }"#
624 .unindent(),
625 &["b", "c"],
626 Some(json!([1, 2, 3])),
627 r#"{
628 "a": 1,
629 "b": {
630 "c": [
631 1,
632 2,
633 3
634 ],
635 "d": 3,
636 }
637 }"#
638 .unindent(),
639 );
640
641 check_object_replace(
642 r#"{
643 "name": "old_name",
644 "id": 123
645 }"#
646 .unindent(),
647 &["name"],
648 Some(json!("new_name")),
649 r#"{
650 "name": "new_name",
651 "id": 123
652 }"#
653 .unindent(),
654 );
655
656 check_object_replace(
657 r#"{
658 "enabled": false,
659 "count": 5
660 }"#
661 .unindent(),
662 &["enabled"],
663 Some(json!(true)),
664 r#"{
665 "enabled": true,
666 "count": 5
667 }"#
668 .unindent(),
669 );
670
671 check_object_replace(
672 r#"{
673 "value": null,
674 "other": "test"
675 }"#
676 .unindent(),
677 &["value"],
678 Some(json!(42)),
679 r#"{
680 "value": 42,
681 "other": "test"
682 }"#
683 .unindent(),
684 );
685
686 check_object_replace(
687 r#"{
688 "config": {
689 "old": true
690 },
691 "name": "test"
692 }"#
693 .unindent(),
694 &["config"],
695 Some(json!({"new": false, "count": 3})),
696 r#"{
697 "config": {
698 "new": false,
699 "count": 3
700 },
701 "name": "test"
702 }"#
703 .unindent(),
704 );
705
706 check_object_replace(
707 r#"{
708 // This is a comment
709 "a": 1,
710 "b": 2 // Another comment
711 }"#
712 .unindent(),
713 &["b"],
714 Some(json!({"foo": "bar"})),
715 r#"{
716 // This is a comment
717 "a": 1,
718 "b": {
719 "foo": "bar"
720 } // Another comment
721 }"#
722 .unindent(),
723 );
724
725 check_object_replace(
726 r#"{}"#.to_string(),
727 &["new_key"],
728 Some(json!("value")),
729 r#"{
730 "new_key": "value"
731 }
732 "#
733 .unindent(),
734 );
735
736 check_object_replace(
737 r#"{
738 "only_key": 123
739 }"#
740 .unindent(),
741 &["only_key"],
742 None,
743 "{\n \n}".to_string(),
744 );
745
746 check_object_replace(
747 r#"{
748 "level1": {
749 "level2": {
750 "level3": {
751 "target": "old"
752 }
753 }
754 }
755 }"#
756 .unindent(),
757 &["level1", "level2", "level3", "target"],
758 Some(json!("new")),
759 r#"{
760 "level1": {
761 "level2": {
762 "level3": {
763 "target": "new"
764 }
765 }
766 }
767 }"#
768 .unindent(),
769 );
770
771 check_object_replace(
772 r#"{
773 "parent": {}
774 }"#
775 .unindent(),
776 &["parent", "child"],
777 Some(json!("value")),
778 r#"{
779 "parent": {
780 "child": "value"
781 }
782 }"#
783 .unindent(),
784 );
785
786 check_object_replace(
787 r#"{
788 "a": 1,
789 "b": 2,
790 }"#
791 .unindent(),
792 &["b"],
793 Some(json!(3)),
794 r#"{
795 "a": 1,
796 "b": 3,
797 }"#
798 .unindent(),
799 );
800
801 check_object_replace(
802 r#"{
803 "items": [1, 2, 3],
804 "count": 3
805 }"#
806 .unindent(),
807 &["items", "1"],
808 Some(json!(5)),
809 r#"{
810 "items": {
811 "1": 5
812 },
813 "count": 3
814 }"#
815 .unindent(),
816 );
817
818 check_object_replace(
819 r#"{
820 "items": [1, 2, 3],
821 "count": 3
822 }"#
823 .unindent(),
824 &["items", "1"],
825 None,
826 r#"{
827 "items": {
828 "1": null
829 },
830 "count": 3
831 }"#
832 .unindent(),
833 );
834
835 check_object_replace(
836 r#"{
837 "items": [1, 2, 3],
838 "count": 3
839 }"#
840 .unindent(),
841 &["items"],
842 Some(json!(["a", "b", "c", "d"])),
843 r#"{
844 "items": [
845 "a",
846 "b",
847 "c",
848 "d"
849 ],
850 "count": 3
851 }"#
852 .unindent(),
853 );
854
855 check_object_replace(
856 r#"{
857 "0": "zero",
858 "1": "one"
859 }"#
860 .unindent(),
861 &["1"],
862 Some(json!("ONE")),
863 r#"{
864 "0": "zero",
865 "1": "ONE"
866 }"#
867 .unindent(),
868 );
869 // Test with comments between object members
870 check_object_replace(
871 r#"{
872 "a": 1,
873 // Comment between members
874 "b": 2,
875 /* Block comment */
876 "c": 3
877 }"#
878 .unindent(),
879 &["b"],
880 Some(json!({"nested": true})),
881 r#"{
882 "a": 1,
883 // Comment between members
884 "b": {
885 "nested": true
886 },
887 /* Block comment */
888 "c": 3
889 }"#
890 .unindent(),
891 );
892
893 // Test with trailing comments on replaced value
894 check_object_replace(
895 r#"{
896 "a": 1, // keep this comment
897 "b": 2 // this should stay
898 }"#
899 .unindent(),
900 &["a"],
901 Some(json!("changed")),
902 r#"{
903 "a": "changed", // keep this comment
904 "b": 2 // this should stay
905 }"#
906 .unindent(),
907 );
908
909 // Test with deep indentation
910 check_object_replace(
911 r#"{
912 "deeply": {
913 "nested": {
914 "value": "old"
915 }
916 }
917 }"#
918 .unindent(),
919 &["deeply", "nested", "value"],
920 Some(json!("new")),
921 r#"{
922 "deeply": {
923 "nested": {
924 "value": "new"
925 }
926 }
927 }"#
928 .unindent(),
929 );
930
931 // Test removing value with comment preservation
932 check_object_replace(
933 r#"{
934 // Header comment
935 "a": 1,
936 // This comment belongs to b
937 "b": 2,
938 // This comment belongs to c
939 "c": 3
940 }"#
941 .unindent(),
942 &["b"],
943 None,
944 r#"{
945 // Header comment
946 "a": 1,
947 // This comment belongs to b
948 // This comment belongs to c
949 "c": 3
950 }"#
951 .unindent(),
952 );
953
954 // Test with multiline block comments
955 check_object_replace(
956 r#"{
957 /*
958 * This is a multiline
959 * block comment
960 */
961 "value": "old",
962 /* Another block */ "other": 123
963 }"#
964 .unindent(),
965 &["value"],
966 Some(json!("new")),
967 r#"{
968 /*
969 * This is a multiline
970 * block comment
971 */
972 "value": "new",
973 /* Another block */ "other": 123
974 }"#
975 .unindent(),
976 );
977
978 check_object_replace(
979 r#"{
980 // This object is empty
981 }"#
982 .unindent(),
983 &["key"],
984 Some(json!("value")),
985 r#"{
986 // This object is empty
987 "key": "value"
988 }
989 "#
990 .unindent(),
991 );
992
993 // Test replacing in object with only comments
994 check_object_replace(
995 r#"{
996 // Comment 1
997 // Comment 2
998 }"#
999 .unindent(),
1000 &["new"],
1001 Some(json!(42)),
1002 r#"{
1003 // Comment 1
1004 // Comment 2
1005 "new": 42
1006 }
1007 "#
1008 .unindent(),
1009 );
1010
1011 // Test with inconsistent spacing
1012 check_object_replace(
1013 r#"{
1014 "a":1,
1015 "b" : 2 ,
1016 "c": 3
1017 }"#
1018 .unindent(),
1019 &["b"],
1020 Some(json!("spaced")),
1021 r#"{
1022 "a":1,
1023 "b" : "spaced" ,
1024 "c": 3
1025 }"#
1026 .unindent(),
1027 );
1028 }
1029
1030 #[test]
1031 fn array_replace() {
1032 #[track_caller]
1033 fn check_array_replace(
1034 input: impl ToString,
1035 index: usize,
1036 key_path: &[&str],
1037 value: Option<Value>,
1038 expected: impl ToString,
1039 ) {
1040 let input = input.to_string();
1041 let result = replace_top_level_array_value_in_json_text(
1042 &input,
1043 key_path,
1044 value.as_ref(),
1045 None,
1046 index,
1047 4,
1048 )
1049 .expect("replace succeeded");
1050 let mut result_str = input;
1051 result_str.replace_range(result.0, &result.1);
1052 pretty_assertions::assert_eq!(expected.to_string(), result_str);
1053 }
1054
1055 check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1056 check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1057 check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1058 check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1059 check_array_replace(
1060 r#"[
1061 1,
1062 2,
1063 3,
1064 ]"#
1065 .unindent(),
1066 1,
1067 &[],
1068 Some(json!({"foo": "bar", "baz": "qux"})),
1069 r#"[
1070 1,
1071 {
1072 "foo": "bar",
1073 "baz": "qux"
1074 },
1075 3,
1076 ]"#
1077 .unindent(),
1078 );
1079 check_array_replace(
1080 r#"[1, 3, 3,]"#,
1081 1,
1082 &[],
1083 Some(json!({"foo": "bar", "baz": "qux"})),
1084 r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1085 );
1086
1087 check_array_replace(
1088 r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1089 1,
1090 &["baz"],
1091 Some(json!({"qux": "quz"})),
1092 r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1093 );
1094
1095 check_array_replace(
1096 r#"[
1097 1,
1098 {
1099 "foo": "bar",
1100 "baz": "qux"
1101 },
1102 3
1103 ]"#,
1104 1,
1105 &["baz"],
1106 Some(json!({"qux": "quz"})),
1107 r#"[
1108 1,
1109 {
1110 "foo": "bar",
1111 "baz": {
1112 "qux": "quz"
1113 }
1114 },
1115 3
1116 ]"#,
1117 );
1118
1119 check_array_replace(
1120 r#"[
1121 1,
1122 {
1123 "foo": "bar",
1124 "baz": {
1125 "qux": "quz"
1126 }
1127 },
1128 3
1129 ]"#,
1130 1,
1131 &["baz"],
1132 Some(json!("qux")),
1133 r#"[
1134 1,
1135 {
1136 "foo": "bar",
1137 "baz": "qux"
1138 },
1139 3
1140 ]"#,
1141 );
1142
1143 check_array_replace(
1144 r#"[
1145 1,
1146 {
1147 "foo": "bar",
1148 // some comment to keep
1149 "baz": {
1150 // some comment to remove
1151 "qux": "quz"
1152 }
1153 // some other comment to keep
1154 },
1155 3
1156 ]"#,
1157 1,
1158 &["baz"],
1159 Some(json!("qux")),
1160 r#"[
1161 1,
1162 {
1163 "foo": "bar",
1164 // some comment to keep
1165 "baz": "qux"
1166 // some other comment to keep
1167 },
1168 3
1169 ]"#,
1170 );
1171
1172 // Test with comments between array elements
1173 check_array_replace(
1174 r#"[
1175 1,
1176 // This is element 2
1177 2,
1178 /* Block comment */ 3,
1179 4 // Trailing comment
1180 ]"#,
1181 2,
1182 &[],
1183 Some(json!("replaced")),
1184 r#"[
1185 1,
1186 // This is element 2
1187 2,
1188 /* Block comment */ "replaced",
1189 4 // Trailing comment
1190 ]"#,
1191 );
1192
1193 // Test empty array with comments
1194 check_array_replace(
1195 r#"[
1196 // Empty array with comment
1197 ]"#
1198 .unindent(),
1199 0,
1200 &[],
1201 Some(json!("first")),
1202 r#"[
1203 // Empty array with comment
1204 "first"
1205 ]"#
1206 .unindent(),
1207 );
1208 check_array_replace(
1209 r#"[]"#.unindent(),
1210 0,
1211 &[],
1212 Some(json!("first")),
1213 r#"[
1214 "first"
1215 ]"#
1216 .unindent(),
1217 );
1218
1219 // Test array with leading comments
1220 check_array_replace(
1221 r#"[
1222 // Leading comment
1223 // Another leading comment
1224 1,
1225 2
1226 ]"#,
1227 0,
1228 &[],
1229 Some(json!({"new": "object"})),
1230 r#"[
1231 // Leading comment
1232 // Another leading comment
1233 {
1234 "new": "object"
1235 },
1236 2
1237 ]"#,
1238 );
1239
1240 // Test with deep indentation
1241 check_array_replace(
1242 r#"[
1243 1,
1244 2,
1245 3
1246 ]"#,
1247 1,
1248 &[],
1249 Some(json!("deep")),
1250 r#"[
1251 1,
1252 "deep",
1253 3
1254 ]"#,
1255 );
1256
1257 // Test with mixed spacing
1258 check_array_replace(
1259 r#"[1,2, 3, 4]"#,
1260 2,
1261 &[],
1262 Some(json!("spaced")),
1263 r#"[1,2, "spaced", 4]"#,
1264 );
1265
1266 // Test replacing nested array element
1267 check_array_replace(
1268 r#"[
1269 [1, 2, 3],
1270 [4, 5, 6],
1271 [7, 8, 9]
1272 ]"#,
1273 1,
1274 &[],
1275 Some(json!(["a", "b", "c", "d"])),
1276 r#"[
1277 [1, 2, 3],
1278 [
1279 "a",
1280 "b",
1281 "c",
1282 "d"
1283 ],
1284 [7, 8, 9]
1285 ]"#,
1286 );
1287
1288 // Test with multiline block comments
1289 check_array_replace(
1290 r#"[
1291 /*
1292 * This is a
1293 * multiline comment
1294 */
1295 "first",
1296 "second"
1297 ]"#,
1298 0,
1299 &[],
1300 Some(json!("updated")),
1301 r#"[
1302 /*
1303 * This is a
1304 * multiline comment
1305 */
1306 "updated",
1307 "second"
1308 ]"#,
1309 );
1310
1311 // Test replacing with null
1312 check_array_replace(
1313 r#"[true, false, true]"#,
1314 1,
1315 &[],
1316 Some(json!(null)),
1317 r#"[true, null, true]"#,
1318 );
1319
1320 // Test single element array
1321 check_array_replace(
1322 r#"[42]"#,
1323 0,
1324 &[],
1325 Some(json!({"answer": 42})),
1326 r#"[{ "answer": 42 }]"#,
1327 );
1328
1329 // Test array with only comments
1330 check_array_replace(
1331 r#"[
1332 // Comment 1
1333 // Comment 2
1334 // Comment 3
1335 ]"#
1336 .unindent(),
1337 10,
1338 &[],
1339 Some(json!(123)),
1340 r#"[
1341 // Comment 1
1342 // Comment 2
1343 // Comment 3
1344 123
1345 ]"#
1346 .unindent(),
1347 );
1348
1349 check_array_replace(
1350 r#"[
1351 {
1352 "key": "value"
1353 },
1354 {
1355 "key": "value2"
1356 }
1357 ]"#
1358 .unindent(),
1359 0,
1360 &[],
1361 None,
1362 r#"[
1363 {
1364 "key": "value2"
1365 }
1366 ]"#
1367 .unindent(),
1368 );
1369
1370 check_array_replace(
1371 r#"[
1372 {
1373 "key": "value"
1374 },
1375 {
1376 "key": "value2"
1377 },
1378 {
1379 "key": "value3"
1380 },
1381 ]"#
1382 .unindent(),
1383 1,
1384 &[],
1385 None,
1386 r#"[
1387 {
1388 "key": "value"
1389 },
1390 {
1391 "key": "value3"
1392 },
1393 ]"#
1394 .unindent(),
1395 );
1396 }
1397
1398 #[test]
1399 fn array_append() {
1400 #[track_caller]
1401 fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
1402 let input = input.to_string();
1403 let result = append_top_level_array_value_in_json_text(&input, &value, 4)
1404 .expect("append succeeded");
1405 let mut result_str = input;
1406 result_str.replace_range(result.0, &result.1);
1407 pretty_assertions::assert_eq!(expected.to_string(), result_str);
1408 }
1409 check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
1410 check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
1411 check_array_append(r#"[1, 3, 3 ]"#, json!(4), r#"[1, 3, 3, 4]"#);
1412 check_array_append(r#"[1, 3, 3, ]"#, json!(4), r#"[1, 3, 3, 4]"#);
1413 check_array_append(
1414 r#"[
1415 1,
1416 2,
1417 3
1418 ]"#
1419 .unindent(),
1420 json!(4),
1421 r#"[
1422 1,
1423 2,
1424 3,
1425 4
1426 ]"#
1427 .unindent(),
1428 );
1429 check_array_append(
1430 r#"[
1431 1,
1432 2,
1433 3,
1434 ]"#
1435 .unindent(),
1436 json!(4),
1437 r#"[
1438 1,
1439 2,
1440 3,
1441 4
1442 ]"#
1443 .unindent(),
1444 );
1445 check_array_append(
1446 r#"[
1447 1,
1448 2,
1449 3,
1450 ]"#
1451 .unindent(),
1452 json!({"foo": "bar", "baz": "qux"}),
1453 r#"[
1454 1,
1455 2,
1456 3,
1457 {
1458 "foo": "bar",
1459 "baz": "qux"
1460 }
1461 ]"#
1462 .unindent(),
1463 );
1464 check_array_append(
1465 r#"[ 1, 2, 3, ]"#.unindent(),
1466 json!({"foo": "bar", "baz": "qux"}),
1467 r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
1468 );
1469 check_array_append(
1470 r#"[]"#,
1471 json!({"foo": "bar"}),
1472 r#"[
1473 {
1474 "foo": "bar"
1475 }
1476 ]"#
1477 .unindent(),
1478 );
1479
1480 // Test with comments between array elements
1481 check_array_append(
1482 r#"[
1483 1,
1484 // Comment between elements
1485 2,
1486 /* Block comment */ 3
1487 ]"#
1488 .unindent(),
1489 json!(4),
1490 r#"[
1491 1,
1492 // Comment between elements
1493 2,
1494 /* Block comment */ 3,
1495 4
1496 ]"#
1497 .unindent(),
1498 );
1499
1500 // Test with trailing comment on last element
1501 check_array_append(
1502 r#"[
1503 1,
1504 2,
1505 3 // Trailing comment
1506 ]"#
1507 .unindent(),
1508 json!("new"),
1509 r#"[
1510 1,
1511 2,
1512 3 // Trailing comment
1513 ,
1514 "new"
1515 ]"#
1516 .unindent(),
1517 );
1518
1519 // Test empty array with comments
1520 check_array_append(
1521 r#"[
1522 // Empty array with comment
1523 ]"#
1524 .unindent(),
1525 json!("first"),
1526 r#"[
1527 // Empty array with comment
1528 "first"
1529 ]"#
1530 .unindent(),
1531 );
1532
1533 // Test with multiline block comment at end
1534 check_array_append(
1535 r#"[
1536 1,
1537 2
1538 /*
1539 * This is a
1540 * multiline comment
1541 */
1542 ]"#
1543 .unindent(),
1544 json!(3),
1545 r#"[
1546 1,
1547 2
1548 /*
1549 * This is a
1550 * multiline comment
1551 */
1552 ,
1553 3
1554 ]"#
1555 .unindent(),
1556 );
1557
1558 // Test with deep indentation
1559 check_array_append(
1560 r#"[
1561 1,
1562 2,
1563 3
1564 ]"#
1565 .unindent(),
1566 json!("deep"),
1567 r#"[
1568 1,
1569 2,
1570 3,
1571 "deep"
1572 ]"#
1573 .unindent(),
1574 );
1575
1576 // Test with no spacing
1577 check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
1578
1579 // Test appending complex nested structure
1580 check_array_append(
1581 r#"[
1582 {"a": 1},
1583 {"b": 2}
1584 ]"#
1585 .unindent(),
1586 json!({"c": {"nested": [1, 2, 3]}}),
1587 r#"[
1588 {"a": 1},
1589 {"b": 2},
1590 {
1591 "c": {
1592 "nested": [
1593 1,
1594 2,
1595 3
1596 ]
1597 }
1598 }
1599 ]"#
1600 .unindent(),
1601 );
1602
1603 // Test array ending with comment after bracket
1604 check_array_append(
1605 r#"[
1606 1,
1607 2,
1608 3
1609 ] // Comment after array"#
1610 .unindent(),
1611 json!(4),
1612 r#"[
1613 1,
1614 2,
1615 3,
1616 4
1617 ] // Comment after array"#
1618 .unindent(),
1619 );
1620
1621 // Test with inconsistent element formatting
1622 check_array_append(
1623 r#"[1,
1624 2,
1625 3,
1626 ]"#
1627 .unindent(),
1628 json!(4),
1629 r#"[1,
1630 2,
1631 3,
1632 4
1633 ]"#
1634 .unindent(),
1635 );
1636
1637 // Test appending to single-line array with trailing comma
1638 check_array_append(
1639 r#"[1, 2, 3,]"#,
1640 json!({"key": "value"}),
1641 r#"[1, 2, 3, { "key": "value" }]"#,
1642 );
1643
1644 // Test appending null value
1645 check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
1646
1647 // Test appending to array with only comments
1648 check_array_append(
1649 r#"[
1650 // Just comments here
1651 // More comments
1652 ]"#
1653 .unindent(),
1654 json!(42),
1655 r#"[
1656 // Just comments here
1657 // More comments
1658 42
1659 ]"#
1660 .unindent(),
1661 );
1662 }
1663}