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