use rlsp_fmt::{Doc, concat, flat_alt, group, hard_line, indent, join, line, text};
use rlsp_yaml_parser::CollectionStyle;
use rlsp_yaml_parser::node::Node;
use rlsp_yaml_parser::{ScalarStyle, Span};
use super::options::YamlFormatOptions;
use super::scalar_render::{format_tag, is_core_schema_tag};
pub(super) fn prepend_collection_properties(
doc: Doc,
anchor: Option<&str>,
tag: Option<&str>,
style: CollectionStyle,
) -> Doc {
let tag_prefix = tag.and_then(|t| {
if is_core_schema_tag(t) {
None
} else {
Some(format_tag(t))
}
});
let props = match (anchor, tag_prefix.as_deref()) {
(Some(name), Some(t)) => Some(format!("&{name} {t}")),
(Some(name), None) => Some(format!("&{name}")),
(None, Some(t)) => Some(t.to_string()),
(None, None) => None,
};
let Some(props_str) = props else {
return doc;
};
match style {
CollectionStyle::Block => {
concat(vec![text(props_str), hard_line(), doc])
}
CollectionStyle::Flow => {
concat(vec![text(format!("{props_str} ")), doc])
}
}
}
pub(super) fn mapping_to_doc(
entries: &[(Node<Span>, Node<Span>)],
style: CollectionStyle,
options: &YamlFormatOptions,
) -> Doc {
if entries.is_empty() {
return text("{}");
}
let effective_style = if options.format_enforce_block_style {
CollectionStyle::Block
} else {
style
};
match effective_style {
CollectionStyle::Flow => flow_mapping_to_doc(entries, options),
CollectionStyle::Block => {
let pairs: Vec<Doc> = entries
.iter()
.map(|(key, value)| key_value_to_doc(key, value, options))
.collect();
join(&hard_line(), pairs)
}
}
}
pub(super) fn flow_mapping_to_doc(
entries: &[(Node<Span>, Node<Span>)],
options: &YamlFormatOptions,
) -> Doc {
let (open, close) = if options.bracket_spacing {
("{ ", " }")
} else {
("{", "}")
};
let items: Vec<Doc> = entries
.iter()
.map(|(key, value)| {
let key_doc = super::node_to_doc::flow_item_to_doc(key, options, true);
let val_doc = super::node_to_doc::flow_item_to_doc(value, options, false);
let sep = if key_needs_space_before_colon(key) {
text(" : ")
} else {
text(": ")
};
concat(vec![key_doc, sep, val_doc])
})
.collect();
let sep = concat(vec![text(","), line()]);
let inner = join(&sep, items);
group(concat(vec![
text(open),
indent(concat(vec![flat_alt(text(""), line()), inner])),
flat_alt(text(""), line()),
text(close),
]))
}
pub(super) const fn needs_explicit_key(key: &Node<Span>) -> bool {
match key {
Node::Mapping { entries, .. } if entries.is_empty() => false,
Node::Sequence { items, .. } if items.is_empty() => false,
Node::Scalar {
style: ScalarStyle::Plain | ScalarStyle::SingleQuoted | ScalarStyle::DoubleQuoted,
..
}
| Node::Alias { .. } => false,
Node::Mapping { .. }
| Node::Sequence { .. }
| Node::Scalar {
style: ScalarStyle::Literal(_) | ScalarStyle::Folded(_),
..
} => true,
}
}
pub(super) fn is_empty_key(key: &Node<Span>) -> bool {
match key {
Node::Scalar {
value, tag: None, ..
} if value.is_empty() => true,
Node::Scalar {
value,
tag: Some(t),
..
} if value.is_empty() && is_core_schema_tag(t) && key.tag_loc().is_none() => true,
Node::Scalar { .. } | Node::Mapping { .. } | Node::Sequence { .. } | Node::Alias { .. } => {
false
}
}
}
pub(super) fn key_needs_space_before_colon(key: &Node<Span>) -> bool {
match key {
Node::Scalar {
value,
tag: Some(t),
..
} if value.is_empty() => {
!(is_core_schema_tag(t) && key.tag_loc().is_none())
}
Node::Alias { .. } => true,
Node::Scalar { .. } | Node::Mapping { .. } | Node::Sequence { .. } => false,
}
}
pub(super) fn explicit_key_to_doc(
key: &Node<Span>,
value: &Node<Span>,
options: &YamlFormatOptions,
) -> Doc {
let key_doc = super::node_to_doc::node_to_doc(key, options, true);
let value_is_empty = matches!(value, Node::Scalar { value, .. } if value.is_empty());
let question_line = concat(vec![text("? "), indent(key_doc)]);
let colon_line = if value_is_empty {
text(":")
} else {
let effective_style = |style: CollectionStyle| {
if options.format_enforce_block_style {
CollectionStyle::Block
} else {
style
}
};
match value {
Node::Mapping {
entries,
style,
tag,
..
} if !entries.is_empty() && effective_style(*style) == CollectionStyle::Block => {
let user_tag = tag.as_ref().filter(|t| !is_core_schema_tag(t));
let colon_prefix = match (value.anchor(), user_tag) {
(Some(name), Some(t)) => format!(": &{name} {}", format_tag(t)),
(Some(name), None) => format!(": &{name}"),
(None, Some(t)) => format!(": {}", format_tag(t)),
(None, None) => ":".to_string(),
};
concat(vec![
text(colon_prefix),
indent(concat(vec![
hard_line(),
mapping_to_doc(entries, *style, options),
])),
])
}
Node::Sequence {
items, style, tag, ..
} if !items.is_empty() && effective_style(*style) == CollectionStyle::Block => {
let user_tag = tag.as_ref().filter(|t| !is_core_schema_tag(t));
let colon_prefix = match (value.anchor(), user_tag) {
(Some(name), Some(t)) => format!(": &{name} {}", format_tag(t)),
(Some(name), None) => format!(": &{name}"),
(None, Some(t)) => format!(": {}", format_tag(t)),
(None, None) => ":".to_string(),
};
let seq_doc = super::sequence_render::sequence_to_doc(items, *style, options);
if options.format_indent_sequences {
concat(vec![
text(colon_prefix),
indent(concat(vec![hard_line(), seq_doc])),
])
} else {
concat(vec![text(colon_prefix), hard_line(), seq_doc])
}
}
Node::Scalar { .. }
| Node::Mapping { .. }
| Node::Sequence { .. }
| Node::Alias { .. } => {
let value_doc = super::node_to_doc::node_to_doc(value, options, false);
concat(vec![text(": "), value_doc])
}
}
};
let colon_line = if let Some(tc) = value.trailing_comment() {
concat(vec![colon_line, text(format!(" {tc}"))])
} else {
colon_line
};
concat(vec![question_line, hard_line(), colon_line])
}
#[expect(
clippy::too_many_lines,
reason = "comprehensive match over all value variants"
)]
pub(super) fn key_value_to_doc(
key: &Node<Span>,
value: &Node<Span>,
options: &YamlFormatOptions,
) -> Doc {
let effective_style = |style: CollectionStyle| {
if options.format_enforce_block_style {
CollectionStyle::Block
} else {
style
}
};
let pair_doc = if needs_explicit_key(key) {
explicit_key_to_doc(key, value, options)
} else if is_empty_key(key) {
let value_doc = super::node_to_doc::node_to_doc(value, options, false);
if matches!(value, Node::Scalar { value, .. } if value.is_empty()) {
text(":")
} else {
concat(vec![text(": "), value_doc])
}
} else {
let key_doc = super::node_to_doc::node_to_doc(key, options, true);
match value {
Node::Mapping {
entries,
style,
tag,
..
} if !entries.is_empty() && effective_style(*style) == CollectionStyle::Block => {
let user_tag = tag.as_ref().filter(|t| !is_core_schema_tag(t));
let bare_colon = if key_needs_space_before_colon(key) {
" :"
} else {
":"
};
let colon = match (value.anchor(), user_tag) {
(Some(name), Some(t)) => text(format!(": &{name} {}", format_tag(t))),
(Some(name), None) => text(format!(": &{name}")),
(None, Some(t)) => text(format!(": {}", format_tag(t))),
(None, None) => text(bare_colon),
};
concat(vec![
key_doc,
colon,
indent(concat(vec![
hard_line(),
mapping_to_doc(entries, *style, options),
])),
])
}
Node::Sequence {
items, style, tag, ..
} if !items.is_empty() && effective_style(*style) == CollectionStyle::Block => {
let user_tag = tag.as_ref().filter(|t| !is_core_schema_tag(t));
let bare_colon = if key_needs_space_before_colon(key) {
" :"
} else {
":"
};
let colon = match (value.anchor(), user_tag) {
(Some(name), Some(t)) => text(format!(": &{name} {}", format_tag(t))),
(Some(name), None) => text(format!(": &{name}")),
(None, Some(t)) => text(format!(": {}", format_tag(t))),
(None, None) => text(bare_colon),
};
let seq_doc = super::sequence_render::sequence_to_doc(items, *style, options);
if options.format_indent_sequences {
concat(vec![
key_doc,
colon,
indent(concat(vec![hard_line(), seq_doc])),
])
} else {
concat(vec![key_doc, colon, hard_line(), seq_doc])
}
}
Node::Scalar { .. }
| Node::Mapping { .. }
| Node::Sequence { .. }
| Node::Alias { .. } => {
let value_doc = super::node_to_doc::node_to_doc(value, options, false);
let sep = if key_needs_space_before_colon(key) {
text(" : ")
} else {
text(": ")
};
concat(vec![key_doc, sep, value_doc])
}
}
};
let pair_doc = if !needs_explicit_key(key) && !is_empty_key(key) {
if let Some(tc) = value.trailing_comment() {
concat(vec![pair_doc, text(format!(" {tc}"))])
} else {
pair_doc
}
} else {
pair_doc
};
let leading = key.leading_comments();
if leading.is_empty() {
pair_doc
} else {
let mut parts: Vec<Doc> = Vec::new();
for lc in leading {
parts.push(text(lc.clone()));
parts.push(hard_line());
}
parts.push(pair_doc);
concat(parts)
}
}