use std::sync::LazyLock;
use ktrs_ast::{Ast, NodeId};
use ktrs_editorconfig::PropertyType;
use ktrs_editorconfig::property_type::boolean_value_parser;
use ktrs_syntax::SyntaxKind::{WHEN, WHEN_ENTRY};
use crate::ast_node_edit::AstNodeEdit;
use crate::ast_node_extension::{AstNodeExtension, AstNodeLines};
use crate::editorconfig::{EditorConfigProperty, PropertyRef};
use crate::rule::{About, EditorConfig, Emit, RuleId, RuleV2, TokenSet};
use crate::rules::STANDARD_RULE_ABOUT;
static LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY_TYPE: PropertyType<bool> = PropertyType {
name: "ij_kotlin_line_break_after_multiline_when_entry",
description: "Defines whether a blank line is to be added after a when entry. Contrary to default IDEA formatting, \
ktlint adds the blank line between all when-conditions if the when-statement contains at least one \
multiline when-condition. Or, it removes all blank lines between the when-conditions if the when-statement \
does not contain any multiline when-condition.",
parser: boolean_value_parser,
possible_values: &["true", "false"],
lower_casing: true,
};
pub static LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY: LazyLock<EditorConfigProperty<bool>> =
LazyLock::new(|| EditorConfigProperty::new(&LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY_TYPE, true));
pub struct BlankLineBetweenWhenConditions {
line_break_after_when_condition: bool,
}
impl BlankLineBetweenWhenConditions {
pub fn new() -> BlankLineBetweenWhenConditions {
BlankLineBetweenWhenConditions { line_break_after_when_condition: LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY.default_value }
}
}
impl Default for BlankLineBetweenWhenConditions {
fn default() -> Self {
Self::new()
}
}
const VISITED_TYPES: TokenSet = TokenSet::create(&[WHEN]);
impl RuleV2 for BlankLineBetweenWhenConditions {
fn rule_id(&self) -> RuleId {
RuleId("standard:blank-line-between-when-conditions")
}
fn visited_types(&self) -> Option<TokenSet> {
Some(VISITED_TYPES)
}
fn about(&self) -> About {
STANDARD_RULE_ABOUT
}
fn uses_editor_config_properties(&self) -> Vec<PropertyRef> {
vec![PropertyRef::from(&*LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY)]
}
fn before_first_node(&mut self, editor_config: &EditorConfig) {
self.line_break_after_when_condition = editor_config.get(&LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY);
}
fn before_visit_child_nodes(&mut self, ast: &mut Ast, node: NodeId, emit: &mut Emit<'_>) {
if ast.element_type(node) == WHEN {
self.visit_when_statement(ast, node, emit);
}
}
}
impl BlankLineBetweenWhenConditions {
fn visit_when_statement(&self, ast: &mut Ast, node: NodeId, emit_and_approve: &mut Emit<'_>) {
let has_multiline_when_condition = has_any_multiline_when_condition(ast, node);
if has_multiline_when_condition && self.line_break_after_when_condition {
add_blank_lines_between_when_conditions(ast, node, emit_and_approve);
} else {
remove_blank_lines_between_when_conditions(ast, node, emit_and_approve);
}
}
}
fn when_entries_but_first(ast: &Ast, node: NodeId) -> Vec<NodeId> {
ast.children(node).filter(|&it| ast.element_type(it) == WHEN_ENTRY).skip(1).collect()
}
fn add_blank_lines_between_when_conditions(ast: &mut Ast, node: NodeId, emit_and_approve: &mut Emit<'_>) {
for when_entry in when_entries_but_first(ast, node) {
let Some(prev_code_leaf) = ast.prev_code_leaf(when_entry) else { continue };
let last = ast
.leaves_forwards_including_self(prev_code_leaf)
.take_while(|&it| !ast.is_white_space_with_newline(it))
.last()
.expect("NoSuchElementException: Sequence is empty.");
let Some(whitespace_before_when_entry) = ast.next_leaf(last).filter(|&it| !contains_blank_line(ast, it)) else { continue };
emit_and_approve(
ast,
ast.start_offset(whitespace_before_when_entry) + 1,
"Add a blank line between all when-conditions in case at least one multiline when-condition is found in the statement",
true,
)
.if_autocorrect_allowed(|| {
let text = format!("\n{}", ast.indent(when_entry));
ast.upsert_whitespace_before_me(whitespace_before_when_entry, &text);
});
}
}
fn contains_blank_line(ast: &Ast, n: NodeId) -> bool {
ast.is_white_space(n) && ast.text(n).bytes().filter(|&b| b == b'\n').count() > 1
}
fn has_any_multiline_when_condition(ast: &Ast, n: NodeId) -> bool {
ast.children(n).any(|it| ast.element_type(it) == WHEN_ENTRY && (ast.text_contains(it, '\n') || is_preceded_by_comment(ast, it)))
}
fn is_preceded_by_comment(ast: &Ast, n: NodeId) -> bool {
let prev_non_whitespace = ast.prev_sibling_matching(n, |it| !ast.is_white_space(it));
let Some(prev_non_whitespace) = prev_non_whitespace.filter(|&it| ast.is_part_of_comment(it)) else { return false };
let whitespace_before_comment = ast.prev_sibling_matching(prev_non_whitespace, |it| ast.is_white_space(it));
whitespace_before_comment.is_some_and(|it| ast.text_contains(it, '\n'))
}
fn find_whitespace_after_previous_code_sibling(ast: &Ast, n: NodeId) -> Option<NodeId> {
ast.prev_code_sibling(n).map(|it| ast.last_child_leaf_or_self(it)).and_then(|it| ast.next_leaf_matching(it, |l| ast.is_white_space(l)))
}
fn remove_blank_lines_between_when_conditions(ast: &mut Ast, node: NodeId, emit_and_approve: &mut Emit<'_>) {
for when_entry in when_entries_but_first(ast, node) {
let Some(whitespace_before_when_entry) =
find_whitespace_after_previous_code_sibling(ast, when_entry).filter(|&it| contains_blank_line(ast, it))
else {
continue;
};
emit_and_approve(
ast,
ast.start_offset(whitespace_before_when_entry) + 1,
"Unexpected blank lines between when-condition if all when-conditions are single lines",
true,
)
.if_autocorrect_allowed(|| {
let text = format!("\n{}", ast.indent_without_newline_prefix(when_entry));
ast.upsert_whitespace_before_me(whitespace_before_when_entry, &text);
});
}
}