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