When a keybind contains a backslash character (\\), it is parsed incorrectly, which results in an invalid keybind configuration. This patch fixes the issue by ensuring that backslashes are properly escaped during the parsing process. This allows them to be used as intended in keybind definitions. Release Notes: - Fixed an issue where keybinds containing a backslash character (\\) failed to be replaced correctly ## Screenshots <img width="912" height="530" alt="SCR-20250828-borp" src="https://github.com/user-attachments/assets/561a040f-575b-4222-ac75-17ab4fa71d07" /> <img width="912" height="530" alt="SCR-20250828-bosx" src="https://github.com/user-attachments/assets/b8e0fb99-549e-4fc9-8609-9b9aa2004656" />
1682 lines
47 KiB
Rust
1682 lines
47 KiB
Rust
use anyhow::Result;
|
|
use gpui::App;
|
|
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],
|
|
}
|
|
|
|
/// Value registered which specifies JSON schemas that are generated at runtime.
|
|
pub struct ParameterizedJsonSchema {
|
|
pub add_and_get_ref:
|
|
fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema,
|
|
}
|
|
|
|
inventory::collect!(ParameterizedJsonSchema);
|
|
|
|
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,
|
|
preserved_keys: &[&str],
|
|
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,
|
|
preserved_keys,
|
|
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,
|
|
preserved_keys,
|
|
edits,
|
|
);
|
|
}
|
|
key_path.pop();
|
|
}
|
|
} else if key_path
|
|
.last()
|
|
.is_some_and(|key| preserved_keys.contains(key))
|
|
|| 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())
|
|
.and_then(|key_text| {
|
|
serde_json::to_string(key_path[depth].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;
|
|
}
|
|
|
|
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 {
|
|
// 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 mut new_value =
|
|
serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
|
|
for key in key_path[(depth + 1)..].iter().rev() {
|
|
new_value = serde_json::json!({ key.as_ref().to_string(): new_value });
|
|
}
|
|
|
|
if let Some(first_key_start) = first_key_start {
|
|
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 {
|
|
new_value = serde_json::json!({ new_key.to_string(): new_value });
|
|
let indent_prefix_len = 4 * depth;
|
|
let mut new_val = to_pretty_json(&new_value, 4, 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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: &[&str],
|
|
new_value: Option<&Value>,
|
|
replace_key: Option<&str>,
|
|
array_index: usize,
|
|
tab_size: usize,
|
|
) -> Result<(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 {
|
|
anyhow::ensure!(
|
|
cursor.goto_first_child(),
|
|
"Document empty - No top level array"
|
|
);
|
|
}
|
|
|
|
while cursor.node().kind() != TS_ARRAY_KIND {
|
|
anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
|
|
}
|
|
|
|
// 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 Ok((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;
|
|
}
|
|
}
|
|
Ok((remove_range, String::new()))
|
|
} else {
|
|
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);
|
|
// replace_value.push('\n');
|
|
} 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, " ");
|
|
}
|
|
}
|
|
|
|
Ok((replace_range, replace_value))
|
|
}
|
|
}
|
|
|
|
pub fn append_top_level_array_value_in_json_text(
|
|
text: &str,
|
|
new_value: &Value,
|
|
tab_size: usize,
|
|
) -> Result<(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 {
|
|
anyhow::ensure!(
|
|
cursor.goto_first_child(),
|
|
"Document empty - No top level array"
|
|
);
|
|
}
|
|
|
|
while cursor.node().kind() != TS_ARRAY_KIND {
|
|
anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
|
|
}
|
|
|
|
anyhow::ensure!(
|
|
cursor.goto_last_child(),
|
|
"Malformed JSON syntax tree, expected `]` at end of array"
|
|
);
|
|
debug_assert_eq!(cursor.node().kind(), "]");
|
|
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 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 Ok((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
|
|
}
|
|
}
|
|
|
|
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 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,
|
|
)
|
|
.expect("replace succeeded");
|
|
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(),
|
|
);
|
|
}
|
|
|
|
#[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)
|
|
.expect("append succeeded");
|
|
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(),
|
|
);
|
|
}
|
|
}
|