use super::{BreakOpportunity, LanguageFormatError, LanguageFormatPolicy, SpacingRules, TextEdit};
use crate::{
core::physical_source::PhysicalSource,
parser::{Grammar, parse},
};
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct JsonFormatPolicy;
impl LanguageFormatPolicy for JsonFormatPolicy {
fn plan_spacing_edits(
&self,
_source: &PhysicalSource<'_>,
_rules: &SpacingRules,
) -> Result<Vec<TextEdit>, LanguageFormatError> {
Ok(Vec::new())
}
fn plan_break_opportunities(
&self,
source: &PhysicalSource<'_>,
) -> Result<Vec<BreakOpportunity>, LanguageFormatError> {
let text = source.text();
if serde_json::from_str::<serde_json::Value>(text).is_err() {
return Ok(Vec::new());
}
let Ok(tree) = parse(Grammar::Json, text) else {
return Ok(Vec::new());
};
let root = tree.root_node();
if root.has_error() || has_missing_or_unknown_node(root) {
return Ok(Vec::new());
}
Ok(json_token_seams(text, root))
}
}
fn has_missing_or_unknown_node(node: tree_sitter::Node<'_>) -> bool {
if node.is_missing()
|| !matches!(
node.kind(),
"document"
| "object"
| "array"
| "pair"
| "string"
| "string_content"
| "escape_sequence"
| "number"
| "true"
| "false"
| "null"
| "{"
| "}"
| "["
| "]"
| ","
| ":"
| "\""
)
{
return true;
}
let mut cursor = node.walk();
node.children(&mut cursor).any(has_missing_or_unknown_node)
}
const JSON_TOKEN_NODE_KINDS: &[&str] = &[
"string", "number", "true", "false", "null", "{", "}", "[", "]", ",", ":",
];
fn json_token_seams(source: &str, root: tree_sitter::Node<'_>) -> Vec<BreakOpportunity> {
let mut tokens = Vec::new();
collect_json_token_ranges(root, &mut tokens);
tokens
.windows(2)
.filter_map(|pair| {
let gap = pair[0].end..pair[1].start;
source[gap.clone()]
.bytes()
.all(is_horizontal_json_whitespace)
.then_some(BreakOpportunity {
replace: gap,
continuation: String::new(),
})
})
.collect()
}
fn collect_json_token_ranges(
node: tree_sitter::Node<'_>,
ranges: &mut Vec<std::ops::Range<usize>>,
) {
if JSON_TOKEN_NODE_KINDS.contains(&node.kind()) {
ranges.push(node.byte_range());
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_json_token_ranges(child, ranges);
}
}
fn is_horizontal_json_whitespace(byte: u8) -> bool {
matches!(byte, b' ' | b'\t')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plans_each_legal_seam_between_json_tokens() {
let policy = JsonFormatPolicy;
let source = r#"{"key" : [true, false, null, -12.34]}"#;
let source_index = PhysicalSource::new(source);
let seams = policy.plan_break_opportunities(&source_index).unwrap();
assert_eq!(
seams
.iter()
.map(|seam| seam.replace.clone())
.collect::<Vec<_>>(),
vec![
1..1,
6..7,
8..9,
10..10,
14..14,
15..16,
21..21,
22..23,
27..27,
28..29,
35..35,
36..36
]
);
}
#[test]
fn rejects_malformed_json_without_planning_edits_or_breaks() {
let policy = JsonFormatPolicy;
assert!(
policy
.plan_spacing_edits(
&PhysicalSource::new(r#"{"key":"value""#),
&SpacingRules::default(),
)
.unwrap()
.is_empty()
);
assert!(
policy
.plan_break_opportunities(&PhysicalSource::new(r#"{"key":"value""#))
.unwrap()
.is_empty()
);
assert!(
policy
.plan_break_opportunities(&PhysicalSource::new(r#""first" "second""#))
.unwrap()
.is_empty()
);
}
}