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