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