Closes #40270 Release Notes: - Fixed an issue with the settings migration to flatten `code_actions` format steps where comments would cause enabled code actions to be omitted from the migrated settings. If you were effected, restoring the settings file backup and allowing the migration to re-run will result in a valid settings file - Fixed an issue where automated settings and keymap file updates would occasionally assume 4-space indentation
2649 lines
73 KiB
Rust
2649 lines
73 KiB
Rust
use anyhow::Result;
|
|
use gpui::SharedString;
|
|
use serde::{Serialize, de::DeserializeOwned};
|
|
use serde_json::Value;
|
|
use std::{ops::Range, sync::LazyLock};
|
|
use tree_sitter::{Query, StreamingIterator as _};
|
|
use util::RangeExt;
|
|
|
|
/// Parameters that are used when generating some JSON schemas at runtime.
|
|
pub struct SettingsJsonSchemaParams<'a> {
|
|
pub language_names: &'a [String],
|
|
pub font_names: &'a [String],
|
|
pub theme_names: &'a [SharedString],
|
|
pub icon_theme_names: &'a [SharedString],
|
|
}
|
|
|
|
pub fn update_value_in_json_text<'a>(
|
|
text: &mut String,
|
|
key_path: &mut Vec<&'a str>,
|
|
tab_size: usize,
|
|
old_value: &'a Value,
|
|
new_value: &'a Value,
|
|
edits: &mut Vec<(Range<usize>, String)>,
|
|
) {
|
|
// If the old and new values are both objects, then compare them key by key,
|
|
// preserving the comments and formatting of the unchanged parts. Otherwise,
|
|
// replace the old value with the new value.
|
|
if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
|
|
for (key, old_sub_value) in old_object.iter() {
|
|
key_path.push(key);
|
|
if let Some(new_sub_value) = new_object.get(key) {
|
|
// Key exists in both old and new, recursively update
|
|
update_value_in_json_text(
|
|
text,
|
|
key_path,
|
|
tab_size,
|
|
old_sub_value,
|
|
new_sub_value,
|
|
edits,
|
|
);
|
|
} else {
|
|
// Key was removed from new object, remove the entire key-value pair
|
|
let (range, replacement) =
|
|
replace_value_in_json_text(text, key_path, 0, None, None);
|
|
text.replace_range(range.clone(), &replacement);
|
|
edits.push((range, replacement));
|
|
}
|
|
key_path.pop();
|
|
}
|
|
for (key, new_sub_value) in new_object.iter() {
|
|
key_path.push(key);
|
|
if !old_object.contains_key(key) {
|
|
update_value_in_json_text(
|
|
text,
|
|
key_path,
|
|
tab_size,
|
|
&Value::Null,
|
|
new_sub_value,
|
|
edits,
|
|
);
|
|
}
|
|
key_path.pop();
|
|
}
|
|
} else if old_value != new_value {
|
|
let mut new_value = new_value.clone();
|
|
if let Some(new_object) = new_value.as_object_mut() {
|
|
new_object.retain(|_, v| !v.is_null());
|
|
}
|
|
let (range, replacement) =
|
|
replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None);
|
|
text.replace_range(range.clone(), &replacement);
|
|
edits.push((range, replacement));
|
|
}
|
|
}
|
|
|
|
/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`.
|
|
pub fn replace_value_in_json_text<T: AsRef<str>>(
|
|
text: &str,
|
|
key_path: &[T],
|
|
tab_size: usize,
|
|
new_value: Option<&Value>,
|
|
replace_key: Option<&str>,
|
|
) -> (Range<usize>, String) {
|
|
static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
|
|
Query::new(
|
|
&tree_sitter_json::LANGUAGE.into(),
|
|
"(pair key: (string) @key value: (_) @value)",
|
|
)
|
|
.expect("Failed to create PAIR_QUERY")
|
|
});
|
|
|
|
let mut parser = tree_sitter::Parser::new();
|
|
parser
|
|
.set_language(&tree_sitter_json::LANGUAGE.into())
|
|
.unwrap();
|
|
let syntax_tree = parser.parse(text, None).unwrap();
|
|
|
|
let mut cursor = tree_sitter::QueryCursor::new();
|
|
|
|
let mut depth = 0;
|
|
let mut last_value_range = 0..0;
|
|
let mut first_key_start = None;
|
|
let mut existing_value_range = 0..text.len();
|
|
|
|
let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
|
|
while let Some(mat) = matches.next() {
|
|
if mat.captures.len() != 2 {
|
|
continue;
|
|
}
|
|
|
|
let key_range = mat.captures[0].node.byte_range();
|
|
let value_range = mat.captures[1].node.byte_range();
|
|
|
|
// Don't enter sub objects until we find an exact
|
|
// match for the current keypath
|
|
if last_value_range.contains_inclusive(&value_range) {
|
|
continue;
|
|
}
|
|
|
|
last_value_range = value_range.clone();
|
|
|
|
if key_range.start > existing_value_range.end {
|
|
break;
|
|
}
|
|
|
|
first_key_start.get_or_insert(key_range.start);
|
|
|
|
let found_key = text
|
|
.get(key_range.clone())
|
|
.zip(key_path.get(depth))
|
|
.and_then(|(key_text, key_path_value)| {
|
|
serde_json::to_string(key_path_value.as_ref())
|
|
.ok()
|
|
.map(|key_path| depth < key_path.len() && key_text == key_path)
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
if found_key {
|
|
existing_value_range = value_range;
|
|
// Reset last value range when increasing in depth
|
|
last_value_range = existing_value_range.start..existing_value_range.start;
|
|
depth += 1;
|
|
|
|
if depth == key_path.len() {
|
|
break;
|
|
}
|
|
|
|
if let Some(array_replacement) = handle_possible_array_value(
|
|
&mat.captures[0].node,
|
|
&mat.captures[1].node,
|
|
text,
|
|
&key_path[depth..],
|
|
new_value,
|
|
replace_key,
|
|
tab_size,
|
|
) {
|
|
return array_replacement;
|
|
}
|
|
|
|
first_key_start = None;
|
|
}
|
|
}
|
|
|
|
// We found the exact key we want
|
|
if depth == key_path.len() {
|
|
if let Some(new_value) = new_value {
|
|
let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
|
|
if let Some(replace_key) = replace_key.and_then(|str| serde_json::to_string(str).ok()) {
|
|
let new_key = format!("{}: ", replace_key);
|
|
if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
|
|
if let Some(prev_key_start) = text[..key_start].rfind('"') {
|
|
existing_value_range.start = prev_key_start;
|
|
} else {
|
|
existing_value_range.start = key_start;
|
|
}
|
|
}
|
|
(existing_value_range, new_key + &new_val)
|
|
} else {
|
|
(existing_value_range, new_val)
|
|
}
|
|
} else {
|
|
let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
|
|
let mut removal_end = existing_value_range.end;
|
|
|
|
// Find the actual key position by looking for the key in the pair
|
|
// We need to extend the range to include the key, not just the value
|
|
if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
|
|
if let Some(prev_key_start) = text[..key_start].rfind('"') {
|
|
removal_start = prev_key_start;
|
|
} else {
|
|
removal_start = key_start;
|
|
}
|
|
}
|
|
|
|
let mut removed_comma = false;
|
|
// Look backward for a preceding comma first
|
|
let preceding_text = text.get(0..removal_start).unwrap_or("");
|
|
if let Some(comma_pos) = preceding_text.rfind(',') {
|
|
// Check if there are only whitespace characters between the comma and our key
|
|
let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
|
|
if between_comma_and_key.trim().is_empty() {
|
|
removal_start = comma_pos;
|
|
removed_comma = true;
|
|
}
|
|
}
|
|
if let Some(remaining_text) = text.get(existing_value_range.end..)
|
|
&& !removed_comma
|
|
{
|
|
let mut chars = remaining_text.char_indices();
|
|
while let Some((offset, ch)) = chars.next() {
|
|
if ch == ',' {
|
|
removal_end = existing_value_range.end + offset + 1;
|
|
// Also consume whitespace after the comma
|
|
for (_, next_ch) in chars.by_ref() {
|
|
if next_ch.is_whitespace() {
|
|
removal_end += next_ch.len_utf8();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
} else if !ch.is_whitespace() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
(removal_start..removal_end, String::new())
|
|
}
|
|
} else {
|
|
if let Some(first_key_start) = first_key_start {
|
|
// We have key paths, construct the sub objects
|
|
let new_key = key_path[depth].as_ref();
|
|
// We don't have the key, construct the nested objects
|
|
let new_value = construct_json_value(&key_path[(depth + 1)..], new_value);
|
|
|
|
let mut row = 0;
|
|
let mut column = 0;
|
|
for (ix, char) in text.char_indices() {
|
|
if ix == first_key_start {
|
|
break;
|
|
}
|
|
if char == '\n' {
|
|
row += 1;
|
|
column = 0;
|
|
} else {
|
|
column += char.len_utf8();
|
|
}
|
|
}
|
|
|
|
if row > 0 {
|
|
// depth is 0 based, but division needs to be 1 based.
|
|
let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
|
|
let space = ' ';
|
|
let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
|
|
(first_key_start..first_key_start, content)
|
|
} else {
|
|
let new_val = serde_json::to_string(&new_value).unwrap();
|
|
let mut content = format!(r#""{new_key}": {new_val},"#);
|
|
content.push(' ');
|
|
(first_key_start..first_key_start, content)
|
|
}
|
|
} else {
|
|
// We don't have the key, construct the nested objects
|
|
let new_value = construct_json_value(&key_path[depth..], new_value);
|
|
let indent_prefix_len = tab_size * depth;
|
|
let mut new_val = to_pretty_json(&new_value, tab_size, indent_prefix_len);
|
|
if depth == 0 {
|
|
new_val.push('\n');
|
|
}
|
|
// best effort to keep comments with best effort indentation
|
|
let mut replace_text = &text[existing_value_range.clone()];
|
|
while let Some(comment_start) = replace_text.rfind("//") {
|
|
if let Some(comment_end) = replace_text[comment_start..].find('\n') {
|
|
let mut comment_with_indent_start = replace_text[..comment_start]
|
|
.rfind('\n')
|
|
.unwrap_or(comment_start);
|
|
if !replace_text[comment_with_indent_start..comment_start]
|
|
.trim()
|
|
.is_empty()
|
|
{
|
|
comment_with_indent_start = comment_start;
|
|
}
|
|
new_val.insert_str(
|
|
1,
|
|
&replace_text[comment_with_indent_start..comment_start + comment_end],
|
|
);
|
|
}
|
|
replace_text = &replace_text[..comment_start];
|
|
}
|
|
|
|
(existing_value_range, new_val)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn construct_json_value(
|
|
key_path: &[impl AsRef<str>],
|
|
new_value: Option<&serde_json::Value>,
|
|
) -> serde_json::Value {
|
|
let mut new_value =
|
|
serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
|
|
for key in key_path.iter().rev() {
|
|
if parse_index_key(key.as_ref()).is_some() {
|
|
new_value = serde_json::json!([new_value]);
|
|
} else {
|
|
new_value = serde_json::json!({ key.as_ref().to_string(): new_value });
|
|
}
|
|
}
|
|
return new_value;
|
|
}
|
|
|
|
fn parse_index_key(index_key: &str) -> Option<usize> {
|
|
index_key.strip_prefix('#')?.parse().ok()
|
|
}
|
|
|
|
fn handle_possible_array_value(
|
|
key_node: &tree_sitter::Node,
|
|
value_node: &tree_sitter::Node,
|
|
text: &str,
|
|
remaining_key_path: &[impl AsRef<str>],
|
|
new_value: Option<&Value>,
|
|
replace_key: Option<&str>,
|
|
tab_size: usize,
|
|
) -> Option<(Range<usize>, String)> {
|
|
if remaining_key_path.is_empty() {
|
|
return None;
|
|
}
|
|
let key_path = remaining_key_path;
|
|
let index = parse_index_key(key_path[0].as_ref())?;
|
|
|
|
let value_is_array = value_node.kind() == TS_ARRAY_KIND;
|
|
|
|
let array_str = if value_is_array {
|
|
&text[value_node.byte_range()]
|
|
} else {
|
|
""
|
|
};
|
|
|
|
let (mut replace_range, mut replace_value) = replace_top_level_array_value_in_json_text(
|
|
array_str,
|
|
&key_path[1..],
|
|
new_value,
|
|
replace_key,
|
|
index,
|
|
tab_size,
|
|
);
|
|
|
|
if value_is_array {
|
|
replace_range.start += value_node.start_byte();
|
|
replace_range.end += value_node.start_byte();
|
|
} else {
|
|
// replace the full value if it wasn't an array
|
|
replace_range = value_node.byte_range();
|
|
}
|
|
let non_whitespace_char_count = replace_value.len()
|
|
- replace_value
|
|
.chars()
|
|
.filter(char::is_ascii_whitespace)
|
|
.count();
|
|
let needs_indent = replace_value.ends_with('\n')
|
|
|| (replace_value
|
|
.chars()
|
|
.zip(replace_value.chars().skip(1))
|
|
.any(|(c, next_c)| c == '\n' && !next_c.is_ascii_whitespace()));
|
|
let contains_comment = (replace_value.contains("//") && replace_value.contains('\n'))
|
|
|| (replace_value.contains("/*") && replace_value.contains("*/"));
|
|
if needs_indent {
|
|
let indent_width = key_node.start_position().column;
|
|
let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
|
|
replace_value = replace_value.replace('\n', &increased_indent);
|
|
} else if non_whitespace_char_count < 32 && !contains_comment {
|
|
// remove indentation
|
|
while let Some(idx) = replace_value.find("\n ") {
|
|
replace_value.remove(idx);
|
|
}
|
|
while let Some(idx) = replace_value.find(" ") {
|
|
replace_value.remove(idx);
|
|
}
|
|
}
|
|
return Some((replace_range, replace_value));
|
|
}
|
|
|
|
const TS_DOCUMENT_KIND: &str = "document";
|
|
const TS_ARRAY_KIND: &str = "array";
|
|
const TS_COMMENT_KIND: &str = "comment";
|
|
|
|
pub fn replace_top_level_array_value_in_json_text(
|
|
text: &str,
|
|
key_path: &[impl AsRef<str>],
|
|
new_value: Option<&Value>,
|
|
replace_key: Option<&str>,
|
|
array_index: usize,
|
|
tab_size: usize,
|
|
) -> (Range<usize>, String) {
|
|
let mut parser = tree_sitter::Parser::new();
|
|
parser
|
|
.set_language(&tree_sitter_json::LANGUAGE.into())
|
|
.unwrap();
|
|
|
|
let syntax_tree = parser.parse(text, None).unwrap();
|
|
|
|
let mut cursor = syntax_tree.walk();
|
|
|
|
if cursor.node().kind() == TS_DOCUMENT_KIND {
|
|
cursor.goto_first_child();
|
|
}
|
|
|
|
while cursor.node().kind() != TS_ARRAY_KIND {
|
|
if !cursor.goto_next_sibling() {
|
|
let json_value = construct_json_value(key_path, new_value);
|
|
let json_value = serde_json::json!([json_value]);
|
|
return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
|
|
}
|
|
}
|
|
|
|
// false if no children
|
|
//
|
|
cursor.goto_first_child();
|
|
debug_assert_eq!(cursor.node().kind(), "[");
|
|
|
|
let mut index = 0;
|
|
|
|
while index <= array_index {
|
|
let node = cursor.node();
|
|
if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",")
|
|
&& !node.is_extra()
|
|
&& !node.is_missing()
|
|
{
|
|
if index == array_index {
|
|
break;
|
|
}
|
|
index += 1;
|
|
}
|
|
if !cursor.goto_next_sibling() {
|
|
if let Some(new_value) = new_value {
|
|
return append_top_level_array_value_in_json_text(text, new_value, tab_size);
|
|
} else {
|
|
return (0..0, String::new());
|
|
}
|
|
}
|
|
}
|
|
|
|
let range = cursor.node().range();
|
|
let indent_width = range.start_point.column;
|
|
let offset = range.start_byte;
|
|
let text_range = range.start_byte..range.end_byte;
|
|
let value_str = &text[text_range.clone()];
|
|
let needs_indent = range.start_point.row > 0;
|
|
|
|
if new_value.is_none() && key_path.is_empty() {
|
|
let mut remove_range = text_range;
|
|
if index == 0 {
|
|
while cursor.goto_next_sibling()
|
|
&& (cursor.node().is_extra() || cursor.node().is_missing())
|
|
{}
|
|
if cursor.node().kind() == "," {
|
|
remove_range.end = cursor.node().range().end_byte;
|
|
}
|
|
if let Some(next_newline) = &text[remove_range.end + 1..].find('\n')
|
|
&& text[remove_range.end + 1..remove_range.end + next_newline]
|
|
.chars()
|
|
.all(|c| c.is_ascii_whitespace())
|
|
{
|
|
remove_range.end = remove_range.end + next_newline;
|
|
}
|
|
} else {
|
|
while cursor.goto_previous_sibling()
|
|
&& (cursor.node().is_extra() || cursor.node().is_missing())
|
|
{}
|
|
if cursor.node().kind() == "," {
|
|
remove_range.start = cursor.node().range().start_byte;
|
|
}
|
|
}
|
|
(remove_range, String::new())
|
|
} else {
|
|
if let Some(array_replacement) = handle_possible_array_value(
|
|
&cursor.node(),
|
|
&cursor.node(),
|
|
text,
|
|
key_path,
|
|
new_value,
|
|
replace_key,
|
|
tab_size,
|
|
) {
|
|
return array_replacement;
|
|
}
|
|
let (mut replace_range, mut replace_value) =
|
|
replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
|
|
|
|
replace_range.start += offset;
|
|
replace_range.end += offset;
|
|
|
|
if needs_indent {
|
|
let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
|
|
replace_value = replace_value.replace('\n', &increased_indent);
|
|
} else {
|
|
while let Some(idx) = replace_value.find("\n ") {
|
|
replace_value.remove(idx + 1);
|
|
}
|
|
while let Some(idx) = replace_value.find("\n") {
|
|
replace_value.replace_range(idx..idx + 1, " ");
|
|
}
|
|
}
|
|
|
|
(replace_range, replace_value)
|
|
}
|
|
}
|
|
|
|
pub fn append_top_level_array_value_in_json_text(
|
|
text: &str,
|
|
new_value: &Value,
|
|
tab_size: usize,
|
|
) -> (Range<usize>, String) {
|
|
let mut parser = tree_sitter::Parser::new();
|
|
parser
|
|
.set_language(&tree_sitter_json::LANGUAGE.into())
|
|
.unwrap();
|
|
let syntax_tree = parser.parse(text, None).unwrap();
|
|
|
|
let mut cursor = syntax_tree.walk();
|
|
|
|
if cursor.node().kind() == TS_DOCUMENT_KIND {
|
|
cursor.goto_first_child();
|
|
}
|
|
|
|
while cursor.node().kind() != TS_ARRAY_KIND {
|
|
if !cursor.goto_next_sibling() {
|
|
let json_value = serde_json::json!([new_value]);
|
|
return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
|
|
}
|
|
}
|
|
|
|
let went_to_last_child = cursor.goto_last_child();
|
|
debug_assert!(
|
|
went_to_last_child && cursor.node().kind() == "]",
|
|
"Malformed JSON syntax tree, expected `]` at end of array"
|
|
);
|
|
let close_bracket_start = cursor.node().start_byte();
|
|
while cursor.goto_previous_sibling()
|
|
&& (cursor.node().is_extra() || cursor.node().is_missing())
|
|
&& !cursor.node().is_error()
|
|
{}
|
|
|
|
let mut comma_range = None;
|
|
let mut prev_item_range = None;
|
|
|
|
if cursor.node().kind() == "," || is_error_of_kind(&mut cursor, ",") {
|
|
comma_range = Some(cursor.node().byte_range());
|
|
while cursor.goto_previous_sibling()
|
|
&& (cursor.node().is_extra() || cursor.node().is_missing())
|
|
{}
|
|
|
|
debug_assert_ne!(cursor.node().kind(), "[");
|
|
prev_item_range = Some(cursor.node().range());
|
|
} else {
|
|
while (cursor.node().is_extra() || cursor.node().is_missing())
|
|
&& cursor.goto_previous_sibling()
|
|
{}
|
|
if cursor.node().kind() != "[" {
|
|
prev_item_range = Some(cursor.node().range());
|
|
}
|
|
}
|
|
|
|
let (mut replace_range, mut replace_value) =
|
|
replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None);
|
|
|
|
replace_range.start = close_bracket_start;
|
|
replace_range.end = close_bracket_start;
|
|
|
|
let space = ' ';
|
|
if let Some(prev_item_range) = prev_item_range {
|
|
let needs_newline = prev_item_range.start_point.row > 0;
|
|
let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
|
|
prev_item_range.start_point.column,
|
|
|idx| {
|
|
prev_item_range.start_point.column
|
|
- text[idx + 1..prev_item_range.start_byte].trim_start().len()
|
|
},
|
|
);
|
|
|
|
let prev_item_end = comma_range
|
|
.as_ref()
|
|
.map_or(prev_item_range.end_byte, |range| range.end);
|
|
if text[prev_item_end..replace_range.start].trim().is_empty() {
|
|
replace_range.start = prev_item_end;
|
|
}
|
|
|
|
if needs_newline {
|
|
let increased_indent = format!("\n{space:width$}", width = indent_width);
|
|
replace_value = replace_value.replace('\n', &increased_indent);
|
|
replace_value.push('\n');
|
|
replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
|
|
} else {
|
|
while let Some(idx) = replace_value.find("\n ") {
|
|
replace_value.remove(idx + 1);
|
|
}
|
|
while let Some(idx) = replace_value.find('\n') {
|
|
replace_value.replace_range(idx..idx + 1, " ");
|
|
}
|
|
replace_value.insert(0, ' ');
|
|
}
|
|
|
|
if comma_range.is_none() {
|
|
replace_value.insert(0, ',');
|
|
}
|
|
} else if replace_value.contains('\n') || text.contains('\n') {
|
|
if let Some(prev_newline) = text[..replace_range.start].rfind('\n')
|
|
&& text[prev_newline..replace_range.start].trim().is_empty()
|
|
{
|
|
replace_range.start = prev_newline;
|
|
}
|
|
let indent = format!("\n{space:width$}", width = tab_size);
|
|
replace_value = replace_value.replace('\n', &indent);
|
|
replace_value.insert_str(0, &indent);
|
|
replace_value.push('\n');
|
|
}
|
|
return (replace_range, replace_value);
|
|
|
|
fn is_error_of_kind(cursor: &mut tree_sitter::TreeCursor<'_>, kind: &str) -> bool {
|
|
if cursor.node().kind() != "ERROR" {
|
|
return false;
|
|
}
|
|
|
|
let descendant_index = cursor.descendant_index();
|
|
let res = cursor.goto_first_child() && cursor.node().kind() == kind;
|
|
cursor.goto_descendant(descendant_index);
|
|
res
|
|
}
|
|
}
|
|
|
|
/// Infers the indentation size used in JSON text by analyzing the tree structure.
|
|
/// Returns the detected indent size, or a default of 2 if no indentation is found.
|
|
pub fn infer_json_indent_size(text: &str) -> usize {
|
|
const MAX_INDENT_SIZE: usize = 64;
|
|
|
|
let mut parser = tree_sitter::Parser::new();
|
|
parser
|
|
.set_language(&tree_sitter_json::LANGUAGE.into())
|
|
.unwrap();
|
|
|
|
let Some(syntax_tree) = parser.parse(text, None) else {
|
|
return 4;
|
|
};
|
|
|
|
let mut cursor = syntax_tree.walk();
|
|
let mut indent_counts = [0u32; MAX_INDENT_SIZE];
|
|
|
|
// Traverse the tree to find indentation patterns
|
|
fn visit_node(
|
|
cursor: &mut tree_sitter::TreeCursor,
|
|
indent_counts: &mut [u32; MAX_INDENT_SIZE],
|
|
depth: usize,
|
|
) {
|
|
if depth >= 3 {
|
|
return;
|
|
}
|
|
let node = cursor.node();
|
|
let node_kind = node.kind();
|
|
|
|
// For objects and arrays, check the indentation of their first content child
|
|
if matches!(node_kind, "object" | "array") {
|
|
let container_column = node.start_position().column;
|
|
let container_row = node.start_position().row;
|
|
|
|
if cursor.goto_first_child() {
|
|
// Skip the opening bracket
|
|
loop {
|
|
let child = cursor.node();
|
|
let child_kind = child.kind();
|
|
|
|
// Look for the first actual content (pair for objects, value for arrays)
|
|
if (node_kind == "object" && child_kind == "pair")
|
|
|| (node_kind == "array"
|
|
&& !matches!(child_kind, "[" | "]" | "," | "comment"))
|
|
{
|
|
let child_column = child.start_position().column;
|
|
let child_row = child.start_position().row;
|
|
|
|
// Only count if the child is on a different line
|
|
if child_row > container_row && child_column > container_column {
|
|
let indent = child_column - container_column;
|
|
if indent > 0 && indent < MAX_INDENT_SIZE {
|
|
indent_counts[indent] += 1;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if !cursor.goto_next_sibling() {
|
|
break;
|
|
}
|
|
}
|
|
cursor.goto_parent();
|
|
}
|
|
}
|
|
|
|
// Recurse to children
|
|
if cursor.goto_first_child() {
|
|
loop {
|
|
visit_node(cursor, indent_counts, depth + 1);
|
|
if !cursor.goto_next_sibling() {
|
|
break;
|
|
}
|
|
}
|
|
cursor.goto_parent();
|
|
}
|
|
}
|
|
|
|
visit_node(&mut cursor, &mut indent_counts, 0);
|
|
|
|
// Find the indent size with the highest count
|
|
let mut max_count = 0;
|
|
let mut max_indent = 4;
|
|
|
|
for (indent, &count) in indent_counts.iter().enumerate() {
|
|
if count > max_count {
|
|
max_count = count;
|
|
max_indent = indent;
|
|
}
|
|
}
|
|
|
|
if max_count == 0 { 2 } else { max_indent }
|
|
}
|
|
|
|
pub fn to_pretty_json(
|
|
value: &impl Serialize,
|
|
indent_size: usize,
|
|
indent_prefix_len: usize,
|
|
) -> String {
|
|
const SPACES: [u8; 32] = [b' '; 32];
|
|
|
|
debug_assert!(indent_size <= SPACES.len());
|
|
debug_assert!(indent_prefix_len <= SPACES.len());
|
|
|
|
let mut output = Vec::new();
|
|
let mut ser = serde_json::Serializer::with_formatter(
|
|
&mut output,
|
|
serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
|
|
);
|
|
|
|
value.serialize(&mut ser).unwrap();
|
|
let text = String::from_utf8(output).unwrap();
|
|
|
|
let mut adjusted_text = String::new();
|
|
for (i, line) in text.split('\n').enumerate() {
|
|
if i > 0 {
|
|
adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
|
|
}
|
|
adjusted_text.push_str(line);
|
|
adjusted_text.push('\n');
|
|
}
|
|
adjusted_text.pop();
|
|
adjusted_text
|
|
}
|
|
|
|
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
|
let mut deserializer = serde_json_lenient::Deserializer::from_str(content);
|
|
Ok(serde_path_to_error::deserialize(&mut deserializer)?)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::{Value, json};
|
|
use unindent::Unindent;
|
|
|
|
#[test]
|
|
fn object_replace() {
|
|
#[track_caller]
|
|
fn check_object_replace(
|
|
input: String,
|
|
key_path: &[&str],
|
|
value: Option<Value>,
|
|
expected: String,
|
|
) {
|
|
let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
|
|
let mut result_str = input;
|
|
result_str.replace_range(result.0, &result.1);
|
|
pretty_assertions::assert_eq!(expected, result_str);
|
|
}
|
|
check_object_replace(
|
|
r#"{
|
|
"a": 1,
|
|
"b": 2
|
|
}"#
|
|
.unindent(),
|
|
&["b"],
|
|
Some(json!(3)),
|
|
r#"{
|
|
"a": 1,
|
|
"b": 3
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
check_object_replace(
|
|
r#"{
|
|
"a": 1,
|
|
"b": 2
|
|
}"#
|
|
.unindent(),
|
|
&["b"],
|
|
None,
|
|
r#"{
|
|
"a": 1
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
check_object_replace(
|
|
r#"{
|
|
"a": 1,
|
|
"b": 2
|
|
}"#
|
|
.unindent(),
|
|
&["c"],
|
|
Some(json!(3)),
|
|
r#"{
|
|
"c": 3,
|
|
"a": 1,
|
|
"b": 2
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
check_object_replace(
|
|
r#"{
|
|
"a": 1,
|
|
"b": {
|
|
"c": 2,
|
|
"d": 3,
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
&["b", "c"],
|
|
Some(json!([1, 2, 3])),
|
|
r#"{
|
|
"a": 1,
|
|
"b": {
|
|
"c": [
|
|
1,
|
|
2,
|
|
3
|
|
],
|
|
"d": 3,
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"name": "old_name",
|
|
"id": 123
|
|
}"#
|
|
.unindent(),
|
|
&["name"],
|
|
Some(json!("new_name")),
|
|
r#"{
|
|
"name": "new_name",
|
|
"id": 123
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"enabled": false,
|
|
"count": 5
|
|
}"#
|
|
.unindent(),
|
|
&["enabled"],
|
|
Some(json!(true)),
|
|
r#"{
|
|
"enabled": true,
|
|
"count": 5
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"value": null,
|
|
"other": "test"
|
|
}"#
|
|
.unindent(),
|
|
&["value"],
|
|
Some(json!(42)),
|
|
r#"{
|
|
"value": 42,
|
|
"other": "test"
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"config": {
|
|
"old": true
|
|
},
|
|
"name": "test"
|
|
}"#
|
|
.unindent(),
|
|
&["config"],
|
|
Some(json!({"new": false, "count": 3})),
|
|
r#"{
|
|
"config": {
|
|
"new": false,
|
|
"count": 3
|
|
},
|
|
"name": "test"
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
// This is a comment
|
|
"a": 1,
|
|
"b": 2 // Another comment
|
|
}"#
|
|
.unindent(),
|
|
&["b"],
|
|
Some(json!({"foo": "bar"})),
|
|
r#"{
|
|
// This is a comment
|
|
"a": 1,
|
|
"b": {
|
|
"foo": "bar"
|
|
} // Another comment
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{}"#.to_string(),
|
|
&["new_key"],
|
|
Some(json!("value")),
|
|
r#"{
|
|
"new_key": "value"
|
|
}
|
|
"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"only_key": 123
|
|
}"#
|
|
.unindent(),
|
|
&["only_key"],
|
|
None,
|
|
"{\n \n}".to_string(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"level1": {
|
|
"level2": {
|
|
"level3": {
|
|
"target": "old"
|
|
}
|
|
}
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
&["level1", "level2", "level3", "target"],
|
|
Some(json!("new")),
|
|
r#"{
|
|
"level1": {
|
|
"level2": {
|
|
"level3": {
|
|
"target": "new"
|
|
}
|
|
}
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"parent": {}
|
|
}"#
|
|
.unindent(),
|
|
&["parent", "child"],
|
|
Some(json!("value")),
|
|
r#"{
|
|
"parent": {
|
|
"child": "value"
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"a": 1,
|
|
"b": 2,
|
|
}"#
|
|
.unindent(),
|
|
&["b"],
|
|
Some(json!(3)),
|
|
r#"{
|
|
"a": 1,
|
|
"b": 3,
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"items": [1, 2, 3],
|
|
"count": 3
|
|
}"#
|
|
.unindent(),
|
|
&["items", "1"],
|
|
Some(json!(5)),
|
|
r#"{
|
|
"items": {
|
|
"1": 5
|
|
},
|
|
"count": 3
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"items": [1, 2, 3],
|
|
"count": 3
|
|
}"#
|
|
.unindent(),
|
|
&["items", "1"],
|
|
None,
|
|
r#"{
|
|
"items": {
|
|
"1": null
|
|
},
|
|
"count": 3
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"items": [1, 2, 3],
|
|
"count": 3
|
|
}"#
|
|
.unindent(),
|
|
&["items"],
|
|
Some(json!(["a", "b", "c", "d"])),
|
|
r#"{
|
|
"items": [
|
|
"a",
|
|
"b",
|
|
"c",
|
|
"d"
|
|
],
|
|
"count": 3
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
"0": "zero",
|
|
"1": "one"
|
|
}"#
|
|
.unindent(),
|
|
&["1"],
|
|
Some(json!("ONE")),
|
|
r#"{
|
|
"0": "zero",
|
|
"1": "ONE"
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
// Test with comments between object members
|
|
check_object_replace(
|
|
r#"{
|
|
"a": 1,
|
|
// Comment between members
|
|
"b": 2,
|
|
/* Block comment */
|
|
"c": 3
|
|
}"#
|
|
.unindent(),
|
|
&["b"],
|
|
Some(json!({"nested": true})),
|
|
r#"{
|
|
"a": 1,
|
|
// Comment between members
|
|
"b": {
|
|
"nested": true
|
|
},
|
|
/* Block comment */
|
|
"c": 3
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with trailing comments on replaced value
|
|
check_object_replace(
|
|
r#"{
|
|
"a": 1, // keep this comment
|
|
"b": 2 // this should stay
|
|
}"#
|
|
.unindent(),
|
|
&["a"],
|
|
Some(json!("changed")),
|
|
r#"{
|
|
"a": "changed", // keep this comment
|
|
"b": 2 // this should stay
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with deep indentation
|
|
check_object_replace(
|
|
r#"{
|
|
"deeply": {
|
|
"nested": {
|
|
"value": "old"
|
|
}
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
&["deeply", "nested", "value"],
|
|
Some(json!("new")),
|
|
r#"{
|
|
"deeply": {
|
|
"nested": {
|
|
"value": "new"
|
|
}
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test removing value with comment preservation
|
|
check_object_replace(
|
|
r#"{
|
|
// Header comment
|
|
"a": 1,
|
|
// This comment belongs to b
|
|
"b": 2,
|
|
// This comment belongs to c
|
|
"c": 3
|
|
}"#
|
|
.unindent(),
|
|
&["b"],
|
|
None,
|
|
r#"{
|
|
// Header comment
|
|
"a": 1,
|
|
// This comment belongs to b
|
|
// This comment belongs to c
|
|
"c": 3
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with multiline block comments
|
|
check_object_replace(
|
|
r#"{
|
|
/*
|
|
* This is a multiline
|
|
* block comment
|
|
*/
|
|
"value": "old",
|
|
/* Another block */ "other": 123
|
|
}"#
|
|
.unindent(),
|
|
&["value"],
|
|
Some(json!("new")),
|
|
r#"{
|
|
/*
|
|
* This is a multiline
|
|
* block comment
|
|
*/
|
|
"value": "new",
|
|
/* Another block */ "other": 123
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_object_replace(
|
|
r#"{
|
|
// This object is empty
|
|
}"#
|
|
.unindent(),
|
|
&["key"],
|
|
Some(json!("value")),
|
|
r#"{
|
|
// This object is empty
|
|
"key": "value"
|
|
}
|
|
"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test replacing in object with only comments
|
|
check_object_replace(
|
|
r#"{
|
|
// Comment 1
|
|
// Comment 2
|
|
}"#
|
|
.unindent(),
|
|
&["new"],
|
|
Some(json!(42)),
|
|
r#"{
|
|
// Comment 1
|
|
// Comment 2
|
|
"new": 42
|
|
}
|
|
"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with inconsistent spacing
|
|
check_object_replace(
|
|
r#"{
|
|
"a":1,
|
|
"b" : 2 ,
|
|
"c": 3
|
|
}"#
|
|
.unindent(),
|
|
&["b"],
|
|
Some(json!("spaced")),
|
|
r#"{
|
|
"a":1,
|
|
"b" : "spaced" ,
|
|
"c": 3
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn object_replace_array() {
|
|
// Tests replacing values within arrays that are nested inside objects.
|
|
// Uses "#N" syntax in key paths to indicate array indices.
|
|
#[track_caller]
|
|
fn check_object_replace_array(
|
|
input: String,
|
|
key_path: &[&str],
|
|
value: Option<Value>,
|
|
expected: String,
|
|
) {
|
|
let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
|
|
let mut result_str = input;
|
|
result_str.replace_range(result.0, &result.1);
|
|
pretty_assertions::assert_eq!(expected, result_str);
|
|
}
|
|
|
|
// Basic array element replacement
|
|
check_object_replace_array(
|
|
r#"{
|
|
"a": [1, 3],
|
|
}"#
|
|
.unindent(),
|
|
&["a", "#1"],
|
|
Some(json!(2)),
|
|
r#"{
|
|
"a": [1, 2],
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace first element
|
|
check_object_replace_array(
|
|
r#"{
|
|
"items": [1, 2, 3]
|
|
}"#
|
|
.unindent(),
|
|
&["items", "#0"],
|
|
Some(json!(10)),
|
|
r#"{
|
|
"items": [10, 2, 3]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace last element
|
|
check_object_replace_array(
|
|
r#"{
|
|
"items": [1, 2, 3]
|
|
}"#
|
|
.unindent(),
|
|
&["items", "#2"],
|
|
Some(json!(30)),
|
|
r#"{
|
|
"items": [1, 2, 30]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace string in array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"names": ["alice", "bob", "charlie"]
|
|
}"#
|
|
.unindent(),
|
|
&["names", "#1"],
|
|
Some(json!("robert")),
|
|
r#"{
|
|
"names": ["alice", "robert", "charlie"]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace boolean
|
|
check_object_replace_array(
|
|
r#"{
|
|
"flags": [true, false, true]
|
|
}"#
|
|
.unindent(),
|
|
&["flags", "#0"],
|
|
Some(json!(false)),
|
|
r#"{
|
|
"flags": [false, false, true]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace null with value
|
|
check_object_replace_array(
|
|
r#"{
|
|
"values": [null, 2, null]
|
|
}"#
|
|
.unindent(),
|
|
&["values", "#0"],
|
|
Some(json!(1)),
|
|
r#"{
|
|
"values": [1, 2, null]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace value with null
|
|
check_object_replace_array(
|
|
r#"{
|
|
"data": [1, 2, 3]
|
|
}"#
|
|
.unindent(),
|
|
&["data", "#1"],
|
|
Some(json!(null)),
|
|
r#"{
|
|
"data": [1, null, 3]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace simple value with object
|
|
check_object_replace_array(
|
|
r#"{
|
|
"list": [1, 2, 3]
|
|
}"#
|
|
.unindent(),
|
|
&["list", "#1"],
|
|
Some(json!({"value": 2, "label": "two"})),
|
|
r#"{
|
|
"list": [1, { "value": 2, "label": "two" }, 3]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace simple value with nested array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"matrix": [1, 2, 3]
|
|
}"#
|
|
.unindent(),
|
|
&["matrix", "#1"],
|
|
Some(json!([20, 21, 22])),
|
|
r#"{
|
|
"matrix": [1, [ 20, 21, 22 ], 3]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace object in array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"users": [
|
|
{"name": "alice"},
|
|
{"name": "bob"},
|
|
{"name": "charlie"}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["users", "#1"],
|
|
Some(json!({"name": "robert", "age": 30})),
|
|
r#"{
|
|
"users": [
|
|
{"name": "alice"},
|
|
{ "name": "robert", "age": 30 },
|
|
{"name": "charlie"}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace property within object in array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"users": [
|
|
{"name": "alice", "age": 25},
|
|
{"name": "bob", "age": 30},
|
|
{"name": "charlie", "age": 35}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["users", "#1", "age"],
|
|
Some(json!(31)),
|
|
r#"{
|
|
"users": [
|
|
{"name": "alice", "age": 25},
|
|
{"name": "bob", "age": 31},
|
|
{"name": "charlie", "age": 35}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Add new property to object in array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"items": [
|
|
{"id": 1},
|
|
{"id": 2},
|
|
{"id": 3}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["items", "#1", "name"],
|
|
Some(json!("Item Two")),
|
|
r#"{
|
|
"items": [
|
|
{"id": 1},
|
|
{"name": "Item Two", "id": 2},
|
|
{"id": 3}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Remove property from object in array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"items": [
|
|
{"id": 1, "name": "one"},
|
|
{"id": 2, "name": "two"},
|
|
{"id": 3, "name": "three"}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["items", "#1", "name"],
|
|
None,
|
|
r#"{
|
|
"items": [
|
|
{"id": 1, "name": "one"},
|
|
{"id": 2},
|
|
{"id": 3, "name": "three"}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Deeply nested: array in object in array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"data": [
|
|
{
|
|
"values": [1, 2, 3]
|
|
},
|
|
{
|
|
"values": [4, 5, 6]
|
|
}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["data", "#0", "values", "#1"],
|
|
Some(json!(20)),
|
|
r#"{
|
|
"data": [
|
|
{
|
|
"values": [1, 20, 3]
|
|
},
|
|
{
|
|
"values": [4, 5, 6]
|
|
}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Multiple levels of nesting
|
|
check_object_replace_array(
|
|
r#"{
|
|
"root": {
|
|
"level1": [
|
|
{
|
|
"level2": {
|
|
"level3": [10, 20, 30]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
&["root", "level1", "#0", "level2", "level3", "#2"],
|
|
Some(json!(300)),
|
|
r#"{
|
|
"root": {
|
|
"level1": [
|
|
{
|
|
"level2": {
|
|
"level3": [10, 20, 300]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Array with mixed types
|
|
check_object_replace_array(
|
|
r#"{
|
|
"mixed": [1, "two", true, null, {"five": 5}]
|
|
}"#
|
|
.unindent(),
|
|
&["mixed", "#3"],
|
|
Some(json!({"four": 4})),
|
|
r#"{
|
|
"mixed": [1, "two", true, { "four": 4 }, {"five": 5}]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace with complex object
|
|
check_object_replace_array(
|
|
r#"{
|
|
"config": [
|
|
"simple",
|
|
"values"
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["config", "#0"],
|
|
Some(json!({
|
|
"type": "complex",
|
|
"settings": {
|
|
"enabled": true,
|
|
"level": 5
|
|
}
|
|
})),
|
|
r#"{
|
|
"config": [
|
|
{
|
|
"type": "complex",
|
|
"settings": {
|
|
"enabled": true,
|
|
"level": 5
|
|
}
|
|
},
|
|
"values"
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Array with trailing comma
|
|
check_object_replace_array(
|
|
r#"{
|
|
"items": [
|
|
1,
|
|
2,
|
|
3,
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["items", "#1"],
|
|
Some(json!(20)),
|
|
r#"{
|
|
"items": [
|
|
1,
|
|
20,
|
|
3,
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Array with comments
|
|
check_object_replace_array(
|
|
r#"{
|
|
"items": [
|
|
1, // first item
|
|
2, // second item
|
|
3 // third item
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["items", "#1"],
|
|
Some(json!(20)),
|
|
r#"{
|
|
"items": [
|
|
1, // first item
|
|
20, // second item
|
|
3 // third item
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Multiple arrays in object
|
|
check_object_replace_array(
|
|
r#"{
|
|
"first": [1, 2, 3],
|
|
"second": [4, 5, 6],
|
|
"third": [7, 8, 9]
|
|
}"#
|
|
.unindent(),
|
|
&["second", "#1"],
|
|
Some(json!(50)),
|
|
r#"{
|
|
"first": [1, 2, 3],
|
|
"second": [4, 50, 6],
|
|
"third": [7, 8, 9]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Empty array - add first element
|
|
check_object_replace_array(
|
|
r#"{
|
|
"empty": []
|
|
}"#
|
|
.unindent(),
|
|
&["empty", "#0"],
|
|
Some(json!("first")),
|
|
r#"{
|
|
"empty": ["first"]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Array of arrays
|
|
check_object_replace_array(
|
|
r#"{
|
|
"matrix": [
|
|
[1, 2],
|
|
[3, 4],
|
|
[5, 6]
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["matrix", "#1", "#0"],
|
|
Some(json!(30)),
|
|
r#"{
|
|
"matrix": [
|
|
[1, 2],
|
|
[30, 4],
|
|
[5, 6]
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace nested object property in array element
|
|
check_object_replace_array(
|
|
r#"{
|
|
"users": [
|
|
{
|
|
"name": "alice",
|
|
"address": {
|
|
"city": "NYC",
|
|
"zip": "10001"
|
|
}
|
|
}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["users", "#0", "address", "city"],
|
|
Some(json!("Boston")),
|
|
r#"{
|
|
"users": [
|
|
{
|
|
"name": "alice",
|
|
"address": {
|
|
"city": "Boston",
|
|
"zip": "10001"
|
|
}
|
|
}
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Add element past end of array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"items": [1, 2]
|
|
}"#
|
|
.unindent(),
|
|
&["items", "#5"],
|
|
Some(json!(6)),
|
|
r#"{
|
|
"items": [1, 2, 6]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Complex nested structure
|
|
check_object_replace_array(
|
|
r#"{
|
|
"app": {
|
|
"modules": [
|
|
{
|
|
"name": "auth",
|
|
"routes": [
|
|
{"path": "/login", "method": "POST"},
|
|
{"path": "/logout", "method": "POST"}
|
|
]
|
|
},
|
|
{
|
|
"name": "api",
|
|
"routes": [
|
|
{"path": "/users", "method": "GET"},
|
|
{"path": "/users", "method": "POST"}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
&["app", "modules", "#1", "routes", "#0", "method"],
|
|
Some(json!("PUT")),
|
|
r#"{
|
|
"app": {
|
|
"modules": [
|
|
{
|
|
"name": "auth",
|
|
"routes": [
|
|
{"path": "/login", "method": "POST"},
|
|
{"path": "/logout", "method": "POST"}
|
|
]
|
|
},
|
|
{
|
|
"name": "api",
|
|
"routes": [
|
|
{"path": "/users", "method": "PUT"},
|
|
{"path": "/users", "method": "POST"}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Escaped strings in array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"messages": ["hello", "world"]
|
|
}"#
|
|
.unindent(),
|
|
&["messages", "#0"],
|
|
Some(json!("hello \"quoted\" world")),
|
|
r#"{
|
|
"messages": ["hello \"quoted\" world", "world"]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Block comments
|
|
check_object_replace_array(
|
|
r#"{
|
|
"data": [
|
|
/* first */ 1,
|
|
/* second */ 2,
|
|
/* third */ 3
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["data", "#1"],
|
|
Some(json!(20)),
|
|
r#"{
|
|
"data": [
|
|
/* first */ 1,
|
|
/* second */ 20,
|
|
/* third */ 3
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Inline array
|
|
check_object_replace_array(
|
|
r#"{"items": [1, 2, 3], "count": 3}"#.to_string(),
|
|
&["items", "#1"],
|
|
Some(json!(20)),
|
|
r#"{"items": [1, 20, 3], "count": 3}"#.to_string(),
|
|
);
|
|
|
|
// Single element array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"single": [42]
|
|
}"#
|
|
.unindent(),
|
|
&["single", "#0"],
|
|
Some(json!(100)),
|
|
r#"{
|
|
"single": [100]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Inconsistent formatting
|
|
check_object_replace_array(
|
|
r#"{
|
|
"messy": [1,
|
|
2,
|
|
3,
|
|
4]
|
|
}"#
|
|
.unindent(),
|
|
&["messy", "#2"],
|
|
Some(json!(30)),
|
|
r#"{
|
|
"messy": [1,
|
|
2,
|
|
30,
|
|
4]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Creates array if has numbered key
|
|
check_object_replace_array(
|
|
r#"{
|
|
"array": {"foo": "bar"}
|
|
}"#
|
|
.unindent(),
|
|
&["array", "#3"],
|
|
Some(json!(4)),
|
|
r#"{
|
|
"array": [
|
|
4
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Replace non-array element within array with array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"matrix": [
|
|
[1, 2],
|
|
[3, 4],
|
|
[5, 6]
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["matrix", "#1", "#0"],
|
|
Some(json!(["foo", "bar"])),
|
|
r#"{
|
|
"matrix": [
|
|
[1, 2],
|
|
[[ "foo", "bar" ], 4],
|
|
[5, 6]
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
// Replace non-array element within array with array
|
|
check_object_replace_array(
|
|
r#"{
|
|
"matrix": [
|
|
[1, 2],
|
|
[3, 4],
|
|
[5, 6]
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
&["matrix", "#1", "#0", "#3"],
|
|
Some(json!(["foo", "bar"])),
|
|
r#"{
|
|
"matrix": [
|
|
[1, 2],
|
|
[[ [ "foo", "bar" ] ], 4],
|
|
[5, 6]
|
|
]
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Create array in key that doesn't exist
|
|
check_object_replace_array(
|
|
r#"{
|
|
"foo": {}
|
|
}"#
|
|
.unindent(),
|
|
&["foo", "bar", "#0"],
|
|
Some(json!({"is_object": true})),
|
|
r#"{
|
|
"foo": {
|
|
"bar": [
|
|
{
|
|
"is_object": true
|
|
}
|
|
]
|
|
}
|
|
}"#
|
|
.unindent(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn array_replace() {
|
|
#[track_caller]
|
|
fn check_array_replace(
|
|
input: impl ToString,
|
|
index: usize,
|
|
key_path: &[&str],
|
|
value: Option<Value>,
|
|
expected: impl ToString,
|
|
) {
|
|
let input = input.to_string();
|
|
let result = replace_top_level_array_value_in_json_text(
|
|
&input,
|
|
key_path,
|
|
value.as_ref(),
|
|
None,
|
|
index,
|
|
4,
|
|
);
|
|
let mut result_str = input;
|
|
result_str.replace_range(result.0, &result.1);
|
|
pretty_assertions::assert_eq!(expected.to_string(), result_str);
|
|
}
|
|
|
|
check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
|
|
check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
|
|
check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
|
|
check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
|
|
check_array_replace(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
]"#
|
|
.unindent(),
|
|
1,
|
|
&[],
|
|
Some(json!({"foo": "bar", "baz": "qux"})),
|
|
r#"[
|
|
1,
|
|
{
|
|
"foo": "bar",
|
|
"baz": "qux"
|
|
},
|
|
3,
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
check_array_replace(
|
|
r#"[1, 3, 3,]"#,
|
|
1,
|
|
&[],
|
|
Some(json!({"foo": "bar", "baz": "qux"})),
|
|
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
|
|
);
|
|
|
|
check_array_replace(
|
|
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
|
|
1,
|
|
&["baz"],
|
|
Some(json!({"qux": "quz"})),
|
|
r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
|
|
);
|
|
|
|
check_array_replace(
|
|
r#"[
|
|
1,
|
|
{
|
|
"foo": "bar",
|
|
"baz": "qux"
|
|
},
|
|
3
|
|
]"#,
|
|
1,
|
|
&["baz"],
|
|
Some(json!({"qux": "quz"})),
|
|
r#"[
|
|
1,
|
|
{
|
|
"foo": "bar",
|
|
"baz": {
|
|
"qux": "quz"
|
|
}
|
|
},
|
|
3
|
|
]"#,
|
|
);
|
|
|
|
check_array_replace(
|
|
r#"[
|
|
1,
|
|
{
|
|
"foo": "bar",
|
|
"baz": {
|
|
"qux": "quz"
|
|
}
|
|
},
|
|
3
|
|
]"#,
|
|
1,
|
|
&["baz"],
|
|
Some(json!("qux")),
|
|
r#"[
|
|
1,
|
|
{
|
|
"foo": "bar",
|
|
"baz": "qux"
|
|
},
|
|
3
|
|
]"#,
|
|
);
|
|
|
|
check_array_replace(
|
|
r#"[
|
|
1,
|
|
{
|
|
"foo": "bar",
|
|
// some comment to keep
|
|
"baz": {
|
|
// some comment to remove
|
|
"qux": "quz"
|
|
}
|
|
// some other comment to keep
|
|
},
|
|
3
|
|
]"#,
|
|
1,
|
|
&["baz"],
|
|
Some(json!("qux")),
|
|
r#"[
|
|
1,
|
|
{
|
|
"foo": "bar",
|
|
// some comment to keep
|
|
"baz": "qux"
|
|
// some other comment to keep
|
|
},
|
|
3
|
|
]"#,
|
|
);
|
|
|
|
// Test with comments between array elements
|
|
check_array_replace(
|
|
r#"[
|
|
1,
|
|
// This is element 2
|
|
2,
|
|
/* Block comment */ 3,
|
|
4 // Trailing comment
|
|
]"#,
|
|
2,
|
|
&[],
|
|
Some(json!("replaced")),
|
|
r#"[
|
|
1,
|
|
// This is element 2
|
|
2,
|
|
/* Block comment */ "replaced",
|
|
4 // Trailing comment
|
|
]"#,
|
|
);
|
|
|
|
// Test empty array with comments
|
|
check_array_replace(
|
|
r#"[
|
|
// Empty array with comment
|
|
]"#
|
|
.unindent(),
|
|
0,
|
|
&[],
|
|
Some(json!("first")),
|
|
r#"[
|
|
// Empty array with comment
|
|
"first"
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
check_array_replace(
|
|
r#"[]"#.unindent(),
|
|
0,
|
|
&[],
|
|
Some(json!("first")),
|
|
r#"["first"]"#.unindent(),
|
|
);
|
|
|
|
// Test array with leading comments
|
|
check_array_replace(
|
|
r#"[
|
|
// Leading comment
|
|
// Another leading comment
|
|
1,
|
|
2
|
|
]"#,
|
|
0,
|
|
&[],
|
|
Some(json!({"new": "object"})),
|
|
r#"[
|
|
// Leading comment
|
|
// Another leading comment
|
|
{
|
|
"new": "object"
|
|
},
|
|
2
|
|
]"#,
|
|
);
|
|
|
|
// Test with deep indentation
|
|
check_array_replace(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3
|
|
]"#,
|
|
1,
|
|
&[],
|
|
Some(json!("deep")),
|
|
r#"[
|
|
1,
|
|
"deep",
|
|
3
|
|
]"#,
|
|
);
|
|
|
|
// Test with mixed spacing
|
|
check_array_replace(
|
|
r#"[1,2, 3, 4]"#,
|
|
2,
|
|
&[],
|
|
Some(json!("spaced")),
|
|
r#"[1,2, "spaced", 4]"#,
|
|
);
|
|
|
|
// Test replacing nested array element
|
|
check_array_replace(
|
|
r#"[
|
|
[1, 2, 3],
|
|
[4, 5, 6],
|
|
[7, 8, 9]
|
|
]"#,
|
|
1,
|
|
&[],
|
|
Some(json!(["a", "b", "c", "d"])),
|
|
r#"[
|
|
[1, 2, 3],
|
|
[
|
|
"a",
|
|
"b",
|
|
"c",
|
|
"d"
|
|
],
|
|
[7, 8, 9]
|
|
]"#,
|
|
);
|
|
|
|
// Test with multiline block comments
|
|
check_array_replace(
|
|
r#"[
|
|
/*
|
|
* This is a
|
|
* multiline comment
|
|
*/
|
|
"first",
|
|
"second"
|
|
]"#,
|
|
0,
|
|
&[],
|
|
Some(json!("updated")),
|
|
r#"[
|
|
/*
|
|
* This is a
|
|
* multiline comment
|
|
*/
|
|
"updated",
|
|
"second"
|
|
]"#,
|
|
);
|
|
|
|
// Test replacing with null
|
|
check_array_replace(
|
|
r#"[true, false, true]"#,
|
|
1,
|
|
&[],
|
|
Some(json!(null)),
|
|
r#"[true, null, true]"#,
|
|
);
|
|
|
|
// Test single element array
|
|
check_array_replace(
|
|
r#"[42]"#,
|
|
0,
|
|
&[],
|
|
Some(json!({"answer": 42})),
|
|
r#"[{ "answer": 42 }]"#,
|
|
);
|
|
|
|
// Test array with only comments
|
|
check_array_replace(
|
|
r#"[
|
|
// Comment 1
|
|
// Comment 2
|
|
// Comment 3
|
|
]"#
|
|
.unindent(),
|
|
10,
|
|
&[],
|
|
Some(json!(123)),
|
|
r#"[
|
|
// Comment 1
|
|
// Comment 2
|
|
// Comment 3
|
|
123
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_array_replace(
|
|
r#"[
|
|
{
|
|
"key": "value"
|
|
},
|
|
{
|
|
"key": "value2"
|
|
}
|
|
]"#
|
|
.unindent(),
|
|
0,
|
|
&[],
|
|
None,
|
|
r#"[
|
|
{
|
|
"key": "value2"
|
|
}
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_array_replace(
|
|
r#"[
|
|
{
|
|
"key": "value"
|
|
},
|
|
{
|
|
"key": "value2"
|
|
},
|
|
{
|
|
"key": "value3"
|
|
},
|
|
]"#
|
|
.unindent(),
|
|
1,
|
|
&[],
|
|
None,
|
|
r#"[
|
|
{
|
|
"key": "value"
|
|
},
|
|
{
|
|
"key": "value3"
|
|
},
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_array_replace(
|
|
r#""#,
|
|
2,
|
|
&[],
|
|
Some(json!(42)),
|
|
r#"[
|
|
42
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_array_replace(
|
|
r#""#,
|
|
2,
|
|
&["foo", "bar"],
|
|
Some(json!(42)),
|
|
r#"[
|
|
{
|
|
"foo": {
|
|
"bar": 42
|
|
}
|
|
}
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn array_append() {
|
|
#[track_caller]
|
|
fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
|
|
let input = input.to_string();
|
|
let result = append_top_level_array_value_in_json_text(&input, &value, 4);
|
|
let mut result_str = input;
|
|
result_str.replace_range(result.0, &result.1);
|
|
pretty_assertions::assert_eq!(expected.to_string(), result_str);
|
|
}
|
|
check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
|
|
check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
|
|
check_array_append(r#"[1, 3, 3 ]"#, json!(4), r#"[1, 3, 3, 4]"#);
|
|
check_array_append(r#"[1, 3, 3, ]"#, json!(4), r#"[1, 3, 3, 4]"#);
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3
|
|
]"#
|
|
.unindent(),
|
|
json!(4),
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
4
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
]"#
|
|
.unindent(),
|
|
json!(4),
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
4
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
]"#
|
|
.unindent(),
|
|
json!({"foo": "bar", "baz": "qux"}),
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
{
|
|
"foo": "bar",
|
|
"baz": "qux"
|
|
}
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
check_array_append(
|
|
r#"[ 1, 2, 3, ]"#.unindent(),
|
|
json!({"foo": "bar", "baz": "qux"}),
|
|
r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
|
|
);
|
|
check_array_append(
|
|
r#"[]"#,
|
|
json!({"foo": "bar"}),
|
|
r#"[
|
|
{
|
|
"foo": "bar"
|
|
}
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with comments between array elements
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
// Comment between elements
|
|
2,
|
|
/* Block comment */ 3
|
|
]"#
|
|
.unindent(),
|
|
json!(4),
|
|
r#"[
|
|
1,
|
|
// Comment between elements
|
|
2,
|
|
/* Block comment */ 3,
|
|
4
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with trailing comment on last element
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3 // Trailing comment
|
|
]"#
|
|
.unindent(),
|
|
json!("new"),
|
|
r#"[
|
|
1,
|
|
2,
|
|
3 // Trailing comment
|
|
,
|
|
"new"
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test empty array with comments
|
|
check_array_append(
|
|
r#"[
|
|
// Empty array with comment
|
|
]"#
|
|
.unindent(),
|
|
json!("first"),
|
|
r#"[
|
|
// Empty array with comment
|
|
"first"
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with multiline block comment at end
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
2
|
|
/*
|
|
* This is a
|
|
* multiline comment
|
|
*/
|
|
]"#
|
|
.unindent(),
|
|
json!(3),
|
|
r#"[
|
|
1,
|
|
2
|
|
/*
|
|
* This is a
|
|
* multiline comment
|
|
*/
|
|
,
|
|
3
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with deep indentation
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3
|
|
]"#
|
|
.unindent(),
|
|
json!("deep"),
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
"deep"
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with no spacing
|
|
check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
|
|
|
|
// Test appending complex nested structure
|
|
check_array_append(
|
|
r#"[
|
|
{"a": 1},
|
|
{"b": 2}
|
|
]"#
|
|
.unindent(),
|
|
json!({"c": {"nested": [1, 2, 3]}}),
|
|
r#"[
|
|
{"a": 1},
|
|
{"b": 2},
|
|
{
|
|
"c": {
|
|
"nested": [
|
|
1,
|
|
2,
|
|
3
|
|
]
|
|
}
|
|
}
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test array ending with comment after bracket
|
|
check_array_append(
|
|
r#"[
|
|
1,
|
|
2,
|
|
3
|
|
] // Comment after array"#
|
|
.unindent(),
|
|
json!(4),
|
|
r#"[
|
|
1,
|
|
2,
|
|
3,
|
|
4
|
|
] // Comment after array"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test with inconsistent element formatting
|
|
check_array_append(
|
|
r#"[1,
|
|
2,
|
|
3,
|
|
]"#
|
|
.unindent(),
|
|
json!(4),
|
|
r#"[1,
|
|
2,
|
|
3,
|
|
4
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
// Test appending to single-line array with trailing comma
|
|
check_array_append(
|
|
r#"[1, 2, 3,]"#,
|
|
json!({"key": "value"}),
|
|
r#"[1, 2, 3, { "key": "value" }]"#,
|
|
);
|
|
|
|
// Test appending null value
|
|
check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
|
|
|
|
// Test appending to array with only comments
|
|
check_array_append(
|
|
r#"[
|
|
// Just comments here
|
|
// More comments
|
|
]"#
|
|
.unindent(),
|
|
json!(42),
|
|
r#"[
|
|
// Just comments here
|
|
// More comments
|
|
42
|
|
]"#
|
|
.unindent(),
|
|
);
|
|
|
|
check_array_append(
|
|
r#""#,
|
|
json!(42),
|
|
r#"[
|
|
42
|
|
]"#
|
|
.unindent(),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_infer_json_indent_size() {
|
|
let json_2_spaces = r#"{
|
|
"key1": "value1",
|
|
"nested": {
|
|
"key2": "value2",
|
|
"array": [
|
|
1,
|
|
2,
|
|
3
|
|
]
|
|
}
|
|
}"#;
|
|
assert_eq!(infer_json_indent_size(json_2_spaces), 2);
|
|
|
|
let json_4_spaces = r#"{
|
|
"key1": "value1",
|
|
"nested": {
|
|
"key2": "value2",
|
|
"array": [
|
|
1,
|
|
2,
|
|
3
|
|
]
|
|
}
|
|
}"#;
|
|
assert_eq!(infer_json_indent_size(json_4_spaces), 4);
|
|
|
|
let json_8_spaces = r#"{
|
|
"key1": "value1",
|
|
"nested": {
|
|
"key2": "value2"
|
|
}
|
|
}"#;
|
|
assert_eq!(infer_json_indent_size(json_8_spaces), 8);
|
|
|
|
let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#;
|
|
assert_eq!(infer_json_indent_size(json_single_line), 2);
|
|
|
|
let json_empty = r#"{}"#;
|
|
assert_eq!(infer_json_indent_size(json_empty), 2);
|
|
|
|
let json_array = r#"[
|
|
{
|
|
"id": 1,
|
|
"name": "first"
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "second"
|
|
}
|
|
]"#;
|
|
assert_eq!(infer_json_indent_size(json_array), 2);
|
|
|
|
let json_mixed = r#"{
|
|
"a": {
|
|
"b": {
|
|
"c": "value"
|
|
}
|
|
},
|
|
"d": "value2"
|
|
}"#;
|
|
assert_eq!(infer_json_indent_size(json_mixed), 2);
|
|
}
|
|
}
|