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