diff --git a/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/PerformanceTypingIndentationTestGenerated.java b/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/PerformanceTypingIndentationTestGenerated.java index 748d3f6696b..d77564e6a32 100644 --- a/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/PerformanceTypingIndentationTestGenerated.java +++ b/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/PerformanceTypingIndentationTestGenerated.java @@ -233,6 +233,11 @@ public class PerformanceTypingIndentationTestGenerated extends AbstractPerforman runTest("idea/testData/indentationOnNewline/KT20783.kt"); } + @TestMetadata("LargeFile.kt") + public void testLargeFile() throws Exception { + runTest("idea/testData/indentationOnNewline/LargeFile.kt"); + } + @TestMetadata("ModifierListInUnfinishedDeclaration.kt") public void testModifierListInUnfinishedDeclaration() throws Exception { runTest("idea/testData/indentationOnNewline/ModifierListInUnfinishedDeclaration.kt"); diff --git a/idea/testData/indentationOnNewline/LargeFile.after.kt b/idea/testData/indentationOnNewline/LargeFile.after.kt new file mode 100644 index 00000000000..ec7d53e3829 --- /dev/null +++ b/idea/testData/indentationOnNewline/LargeFile.after.kt @@ -0,0 +1,2323 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.formatter.uber + +import com.intellij.application.options.CodeStyle +import com.intellij.formatting.* +import com.intellij.formatting.DependentSpacingRule.Anchor +import com.intellij.formatting.DependentSpacingRule.Trigger +import com.intellij.formatting.SpacingBuilder.RuleBuilder +import com.intellij.lang.ASTNode +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.* +import com.intellij.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.codeStyle.CommonCodeStyleSettings +import com.intellij.psi.impl.source.tree.TreeUtil +import com.intellij.psi.tree.IElementType +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiUtilCore +import com.intellij.util.text.TextRangeUtil +import org.jetbrains.annotations.NonNls +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.KtNodeTypes.* +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.idea.core.formatter.KotlinCodeStyleSettings +import org.jetbrains.kotlin.idea.formatter.CommonAlignmentStrategy +import org.jetbrains.kotlin.idea.formatter.KotlinCommonCodeStyleSettings +import org.jetbrains.kotlin.idea.formatter.uber.NodeIndentStrategy.Companion.strategy +import org.jetbrains.kotlin.kdoc.lexer.KDocTokens +import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.lexer.KtTokens.* +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.psi.stubs.elements.KtModifierListElementType +import org.jetbrains.kotlin.utils.addToStdlib.cast +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import kotlin.math.max + +abstract class NodeIndentStrategy { + + abstract fun getIndent(node: ASTNode, settings: CodeStyleSettings): Indent? + + class ConstIndentStrategy(private val indent: Indent) : NodeIndentStrategy() { + + override fun getIndent(node: ASTNode, settings: CodeStyleSettings): Indent? { + return indent + } + } + + class PositionStrategy(private val debugInfo: String?) : NodeIndentStrategy() { + private var indentCallback: (CodeStyleSettings) -> Indent = { Indent.getNoneIndent() } + + private val within = ArrayList() + private var withinCallback: ((ASTNode) -> Boolean)? = null + + private val notIn = ArrayList() + + private val forElement = ArrayList() + private val notForElement = ArrayList() + private var forElementCallback: ((ASTNode) -> Boolean)? = null + + override fun toString(): String { + return "PositionStrategy " + (debugInfo ?: "No debug info") + } + + fun set(indent: Indent): PositionStrategy { + indentCallback = { indent } + return this + } + + fun set(indentCallback: (CodeStyleSettings) -> Indent): PositionStrategy { + this.indentCallback = indentCallback + return this + } + + fun within(parents: TokenSet): PositionStrategy { + val types = parents.types + if (types.isEmpty()) { + throw IllegalArgumentException("Empty token set is unexpected") + } + + fillTypes(within, types[0], types.copyOfRange(1, types.size)) + return this + } + + fun within(parentType: IElementType, vararg orParentTypes: IElementType): PositionStrategy { + fillTypes(within, parentType, orParentTypes) + return this + } + + fun within(callback: (ASTNode) -> Boolean): PositionStrategy { + withinCallback = callback + return this + } + + fun notWithin(parentType: IElementType, vararg orParentTypes: IElementType): PositionStrategy { + fillTypes(notIn, parentType, orParentTypes) + return this + } + + fun withinAny(): PositionStrategy { + within.clear() + notIn.clear() + return this + } + + fun forType(elementType: IElementType, vararg otherTypes: IElementType): PositionStrategy { + fillTypes(forElement, elementType, otherTypes) + return this + } + + fun notForType(elementType: IElementType, vararg otherTypes: IElementType): PositionStrategy { + fillTypes(notForElement, elementType, otherTypes) + return this + } + + fun forAny(): PositionStrategy { + forElement.clear() + notForElement.clear() + return this + } + + fun forElement(callback: (ASTNode) -> Boolean): PositionStrategy { + forElementCallback = callback + return this + } + + override fun getIndent(node: ASTNode, settings: CodeStyleSettings): Indent? { + if (!isValidIndent(forElement, notForElement, node, forElementCallback)) return null + + val parent = node.treeParent + if (parent != null) { + if (!isValidIndent(within, notIn, parent, withinCallback)) return null + } else if (within.isNotEmpty()) return null + + return indentCallback(settings) + } + + private fun fillTypes(resultCollection: MutableList, singleType: IElementType, otherTypes: Array) { + resultCollection.clear() + resultCollection.add(singleType) + resultCollection.addAll(otherTypes) + } + } + + companion object { + fun constIndent(indent: Indent): NodeIndentStrategy { + return ConstIndentStrategy(indent) + } + + fun strategy(@NonNls debugInfo: String?): PositionStrategy { + return PositionStrategy(debugInfo) + } + } +} + +private fun isValidIndent( + elements: ArrayList, + excludeElements: ArrayList, + node: ASTNode, + callback: ((ASTNode) -> Boolean)? +): Boolean { + if (elements.isNotEmpty() && !elements.contains(node.elementType)) return false + if (excludeElements.contains(node.elementType)) return false + if (callback?.invoke(node) == false) return false + return true +} + +class SyntheticKotlinBlock( + private val node: ASTNode, + private val subBlocks: List, + private val alignment: Alignment?, + private val indent: Indent?, + private val wrap: Wrap?, + private val spacingBuilder: KotlinSpacingBuilder, + private val createParentSyntheticSpacingBlock: (ASTNode) -> ASTBlock +) : ASTBlock { + + private val textRange = TextRange( + subBlocks.first().textRange.startOffset, + subBlocks.last().textRange.endOffset + ) + + override fun getTextRange(): TextRange = textRange + override fun getSubBlocks() = subBlocks + override fun getWrap() = wrap + override fun getIndent() = indent + override fun getAlignment() = alignment + override fun getChildAttributes(newChildIndex: Int) = ChildAttributes(getIndent(), null) + override fun isIncomplete() = getSubBlocks().last().isIncomplete + override fun isLeaf() = false + override fun getNode() = node + override fun getSpacing(child1: Block?, child2: Block): Spacing? { + return spacingBuilder.getSpacing(createParentSyntheticSpacingBlock(node), child1, child2) + } + + + override fun toString(): String { + var child = subBlocks.first() + var treeNode: ASTNode? = null + + loop@ + while (treeNode == null) when (child) { + is SyntheticKotlinBlock -> child = child.getSubBlocks().first() + + else -> treeNode = child.node + } + + val textRange = getTextRange() + val psi = treeNode.psi + if (psi != null) { + val file = psi.containingFile + if (file != null) { + return file.text!!.subSequence(textRange.startOffset, textRange.endOffset).toString() + " " + textRange + } + } + + return this::class.java.name + ": " + textRange + } +} + +/* + * ASTBlock.node is nullable, this extension was introduced to minimize changes + */ +fun ASTBlock.requireNode() = node ?: error("ASTBlock.getNode() returned null") + +/** + * Can be removed with all usages after moving master to 1.3 with new default code style settings. + */ +val isDefaultOfficialCodeStyle by lazy { !KotlinCodeStyleSettings.defaultSettings().CONTINUATION_INDENT_FOR_CHAINED_CALLS } + +fun PsiElement.getLineCount(): Int { + val doc = containingFile?.let { PsiDocumentManager.getInstance(project).getDocument(it) } + if (doc != null) { + val spaceRange = textRange ?: TextRange.EMPTY_RANGE + + if (spaceRange.endOffset <= doc.textLength && spaceRange.startOffset < spaceRange.endOffset) { + val startLine = doc.getLineNumber(spaceRange.startOffset) + val endLine = doc.getLineNumber(spaceRange.endOffset - 1) + + return endLine - startLine + 1 + } + } + + return StringUtil.getLineBreakCount(text ?: "") + 1 +} + +fun PsiElement.isMultiline() = getLineCount() > 1 + +fun KtFunctionLiteral.needTrailingComma(settings: CodeStyleSettings?, checkExistingTrailingComma: Boolean = true): Boolean = + needTrailingComma( + settings = settings, + trailingComma = { if (checkExistingTrailingComma) valueParameterList?.trailingComma else null }, + globalStartOffset = { valueParameterList?.startOffset }, + globalEndOffset = { arrow?.endOffset }, + ) + +fun KtWhenEntry.needTrailingComma(settings: CodeStyleSettings?, checkExistingTrailingComma: Boolean = true): Boolean = needTrailingComma( + settings = settings, + trailingComma = { if (checkExistingTrailingComma) trailingComma else null }, + additionalCheck = { !isElse && parent.cast().leftParenthesis != null }, + globalEndOffset = { arrow?.endOffset }, +) + +fun KtDestructuringDeclaration.needTrailingComma(settings: CodeStyleSettings?, checkExistingTrailingComma: Boolean = true): Boolean = + needTrailingComma( + settings = settings, + trailingComma = { if (checkExistingTrailingComma) trailingComma else null }, + globalStartOffset = { lPar?.startOffset }, + globalEndOffset = { rPar?.endOffset }, + ) + +fun T.needTrailingComma( + settings: CodeStyleSettings?, + trailingComma: T.() -> PsiElement?, + additionalCheck: () -> Boolean = { true }, + globalStartOffset: T.() -> Int? = PsiElement::startOffset, + globalEndOffset: T.() -> Int? = PsiElement::endOffset, +): Boolean { + if (trailingComma() == null && settings?.kotlinCustomSettings?.addTrailingCommaIsAllowedFor(this) == false) return false + if (!additionalCheck()) return false + + val startOffset = globalStartOffset() ?: return false + val endOffset = globalEndOffset() ?: return false + return containsLineBreakInThis(startOffset, endOffset) +} + +fun PsiElement.containsLineBreakInThis(globalStartOffset: Int, globalEndOffset: Int): Boolean { + val textRange = TextRange.create(globalStartOffset, globalEndOffset).shiftLeft(startOffset) + return StringUtil.containsLineBreak(textRange.subSequence(text)) +} + +fun trailingCommaIsAllowedOnCallSite(): Boolean = Registry.`is`("kotlin.formatter.allowTrailingCommaOnCallSite") + +private val TYPES_WITH_TRAILING_COMMA = TokenSet.create( + KtNodeTypes.TYPE_PARAMETER_LIST, + KtNodeTypes.DESTRUCTURING_DECLARATION, + KtNodeTypes.WHEN_ENTRY, + KtNodeTypes.FUNCTION_LITERAL, + KtNodeTypes.VALUE_PARAMETER_LIST, +) + +private val TYPES_WITH_TRAILING_COMMA_ON_CALL_SITE = TokenSet.create( + KtNodeTypes.COLLECTION_LITERAL_EXPRESSION, + KtNodeTypes.TYPE_ARGUMENT_LIST, + KtNodeTypes.INDICES, + KtNodeTypes.VALUE_ARGUMENT_LIST, +) + +fun UserDataHolder.addTrailingCommaIsAllowedForThis(): Boolean { + val type = when (this) { + is ASTNode -> PsiUtilCore.getElementType(this) + is PsiElement -> PsiUtilCore.getElementType(this) + else -> return false + } + + return type in TYPES_WITH_TRAILING_COMMA || trailingCommaIsAllowedOnCallSite() && type in TYPES_WITH_TRAILING_COMMA_ON_CALL_SITE +} + +fun KotlinCodeStyleSettings.addTrailingCommaIsAllowedFor(element: UserDataHolder): Boolean = + ALLOW_TRAILING_COMMA && element.addTrailingCommaIsAllowedForThis() + +data class KtCodeStyleSettings( + val custom: KotlinCodeStyleSettings, + val common: KotlinCommonCodeStyleSettings, + val all: CodeStyleSettings +) + +fun KtCodeStyleSettings.canRestore(): Boolean { + return custom.canRestore() || common.canRestore() +} + +fun KtCodeStyleSettings.hasDefaultLoadScheme(): Boolean { + return custom.CODE_STYLE_DEFAULTS == null || common.CODE_STYLE_DEFAULTS == null +} + +fun KtCodeStyleSettings.restore() { + custom.restore() + common.restore() +} + +fun ktCodeStyleSettings(project: Project): KtCodeStyleSettings? { + val settings = CodeStyle.getSettings(project) + + val ktCommonSettings = settings.getCommonSettings(KotlinLanguage.INSTANCE) as KotlinCommonCodeStyleSettings + val ktCustomSettings = settings.getCustomSettings(KotlinCodeStyleSettings::class.java) + + return KtCodeStyleSettings(ktCustomSettings, ktCommonSettings, settings) +} + +val CodeStyleSettings.kotlinCommonSettings: KotlinCommonCodeStyleSettings + get() = getCommonSettings(KotlinLanguage.INSTANCE) as KotlinCommonCodeStyleSettings + +val CodeStyleSettings.kotlinCustomSettings: KotlinCodeStyleSettings + get() = getCustomSettings(KotlinCodeStyleSettings::class.java) + +fun CodeStyleSettings.kotlinCodeStyleDefaults(): String? { + return kotlinCustomSettings.CODE_STYLE_DEFAULTS ?: kotlinCommonSettings.CODE_STYLE_DEFAULTS +} + +fun CommonCodeStyleSettings.createSpaceBeforeRBrace(numSpacesOtherwise: Int, textRange: TextRange): Spacing? { + return Spacing.createDependentLFSpacing( + numSpacesOtherwise, numSpacesOtherwise, textRange, + KEEP_LINE_BREAKS, + KEEP_BLANK_LINES_BEFORE_RBRACE + ) +} + +class KotlinSpacingBuilder(val commonCodeStyleSettings: CommonCodeStyleSettings, val spacingBuilderUtil: KotlinSpacingBuilderUtil) { + private val builders = ArrayList() + + private interface Builder { + fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? + } + + inner class BasicSpacingBuilder : SpacingBuilder(commonCodeStyleSettings), Builder { + override fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? { + return super.getSpacing(parent, left, right) + } + } + + private data class Condition( + val parent: IElementType? = null, + val left: IElementType? = null, + val right: IElementType? = null, + val parentSet: TokenSet? = null, + val leftSet: TokenSet? = null, + val rightSet: TokenSet? = null + ) : (ASTBlock, ASTBlock, ASTBlock) -> Boolean { + override fun invoke(p: ASTBlock, l: ASTBlock, r: ASTBlock): Boolean = + (parent == null || p.requireNode().elementType == parent) && + (left == null || l.requireNode().elementType == left) && + (right == null || r.requireNode().elementType == right) && + (parentSet == null || parentSet.contains(p.requireNode().elementType)) && + (leftSet == null || leftSet.contains(l.requireNode().elementType)) && + (rightSet == null || rightSet.contains(r.requireNode().elementType)) + } + + private data class Rule( + val conditions: List, + val action: (ASTBlock, ASTBlock, ASTBlock) -> Spacing? + ) : (ASTBlock, ASTBlock, ASTBlock) -> Spacing? { + override fun invoke(p: ASTBlock, l: ASTBlock, r: ASTBlock): Spacing? = + if (conditions.all { it(p, l, r) }) action(p, l, r) else null + } + + inner class CustomSpacingBuilder : Builder { + private val rules = ArrayList() + private var conditions = ArrayList() + + override fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? { + for (rule in rules) { + val spacing = rule(parent, left, right) + if (spacing != null) { + return spacing + } + } + return null + } + + fun inPosition( + parent: IElementType? = null, left: IElementType? = null, right: IElementType? = null, + parentSet: TokenSet? = null, leftSet: TokenSet? = null, rightSet: TokenSet? = null + ): CustomSpacingBuilder { + conditions.add(Condition(parent, left, right, parentSet, leftSet, rightSet)) + return this + } + + fun lineBreakIfLineBreakInParent(numSpacesOtherwise: Int, allowBlankLines: Boolean = true) { + newRule { p, _, _ -> + Spacing.createDependentLFSpacing( + numSpacesOtherwise, numSpacesOtherwise, p.textRange, + commonCodeStyleSettings.KEEP_LINE_BREAKS, + if (allowBlankLines) commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE else 0 + ) + } + } + + fun emptyLinesIfLineBreakInLeft(emptyLines: Int, numberOfLineFeedsOtherwise: Int = 1, numSpacesOtherwise: Int = 0) { + newRule { _: ASTBlock, left: ASTBlock, _: ASTBlock -> + val lastChild = left.node?.psi?.lastChild + val leftEndsWithComment = lastChild is PsiComment && lastChild.tokenType == KtTokens.EOL_COMMENT + val dependentSpacingRule = DependentSpacingRule(Trigger.HAS_LINE_FEEDS).registerData(Anchor.MIN_LINE_FEEDS, emptyLines + 1) + val textRange = left.node + ?.startOfDeclaration() + ?.startOffset + ?.let { TextRange.create(it, left.textRange.endOffset) } + ?: left.textRange + + spacingBuilderUtil.createLineFeedDependentSpacing( + numSpacesOtherwise, + numSpacesOtherwise, + if (leftEndsWithComment) max(1, numberOfLineFeedsOtherwise) else numberOfLineFeedsOtherwise, + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_DECLARATIONS, + textRange, + dependentSpacingRule + ) + } + } + + fun spacing(spacing: Spacing) { + newRule { _, _, _ -> spacing } + } + + fun customRule(block: (parent: ASTBlock, left: ASTBlock, right: ASTBlock) -> Spacing?) { + newRule(block) + } + + private fun newRule(rule: (ASTBlock, ASTBlock, ASTBlock) -> Spacing?) { + val savedConditions = ArrayList(conditions) + rules.add(Rule(savedConditions, rule)) + conditions.clear() + } + } + + fun getSpacing(parent: Block, child1: Block?, child2: Block): Spacing? { + if (parent !is ASTBlock || child1 !is ASTBlock || child2 !is ASTBlock) { + return null + } + + for (builder in builders) { + val spacing = builder.getSpacing(parent, child1, child2) + + if (spacing != null) { + // TODO: it's a severe hack but I don't know how to implement it in other way + if (child1.requireNode().elementType == KtTokens.EOL_COMMENT && spacing.toString().contains("minLineFeeds=0")) { + val isBeforeBlock = + child2.requireNode().elementType == KtNodeTypes.BLOCK || child2.requireNode().firstChildNode + ?.elementType == KtNodeTypes.BLOCK + val keepBlankLines = if (isBeforeBlock) 0 else commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + return createSpacing(0, minLineFeeds = 1, keepLineBreaks = true, keepBlankLines = keepBlankLines) + } + return spacing + } + } + return null + } + + fun simple(init: BasicSpacingBuilder.() -> Unit) { + val builder = BasicSpacingBuilder() + builder.init() + builders.add(builder) + } + + fun custom(init: CustomSpacingBuilder.() -> Unit) { + val builder = CustomSpacingBuilder() + builder.init() + builders.add(builder) + } + + fun createSpacing( + minSpaces: Int, + maxSpaces: Int = minSpaces, + minLineFeeds: Int = 0, + keepLineBreaks: Boolean = commonCodeStyleSettings.KEEP_LINE_BREAKS, + keepBlankLines: Int = commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ): Spacing { + return Spacing.createSpacing(minSpaces, maxSpaces, minLineFeeds, keepLineBreaks, keepBlankLines) + } +} + +interface KotlinSpacingBuilderUtil { + fun createLineFeedDependentSpacing( + minSpaces: Int, + maxSpaces: Int, + minimumLineFeeds: Int, + keepLineBreaks: Boolean, + keepBlankLines: Int, + dependency: TextRange, + rule: DependentSpacingRule + ): Spacing + + fun getPreviousNonWhitespaceLeaf(node: ASTNode?): ASTNode? + + fun isWhitespaceOrEmpty(node: ASTNode?): Boolean +} + +fun rules( + commonCodeStyleSettings: CommonCodeStyleSettings, + builderUtil: KotlinSpacingBuilderUtil, + init: KotlinSpacingBuilder.() -> Unit +): KotlinSpacingBuilder { + val builder = KotlinSpacingBuilder(commonCodeStyleSettings, builderUtil) + builder.init() + return builder +} + +internal fun ASTNode.startOfDeclaration(): ASTNode? = children().firstOrNull { + val elementType = it.elementType + elementType !is KtModifierListElementType<*> && elementType !in KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET +} + +val MODIFIERS_LIST_ENTRIES = TokenSet.orSet(TokenSet.create(ANNOTATION_ENTRY, ANNOTATION), MODIFIER_KEYWORDS) + +val EXTEND_COLON_ELEMENTS = + TokenSet.create(TYPE_CONSTRAINT, CLASS, OBJECT_DECLARATION, TYPE_PARAMETER, ENUM_ENTRY, SECONDARY_CONSTRUCTOR) + +val DECLARATIONS = TokenSet.create(PROPERTY, FUN, CLASS, OBJECT_DECLARATION, ENUM_ENTRY, SECONDARY_CONSTRUCTOR, CLASS_INITIALIZER) + +fun SpacingBuilder.beforeInside(element: IElementType, tokenSet: TokenSet, spacingFun: RuleBuilder.() -> Unit) { + tokenSet.types.forEach { inType -> beforeInside(element, inType).spacingFun() } +} + +fun SpacingBuilder.afterInside(element: IElementType, tokenSet: TokenSet, spacingFun: RuleBuilder.() -> Unit) { + tokenSet.types.forEach { inType -> afterInside(element, inType).spacingFun() } +} + +fun RuleBuilder.spacesNoLineBreak(spaces: Int): SpacingBuilder? = + spacing(spaces, spaces, 0, false, 0) + +fun createSpacingBuilder(settings: CodeStyleSettings, builderUtil: KotlinSpacingBuilderUtil): KotlinSpacingBuilder { + val kotlinCommonSettings = settings.kotlinCommonSettings + val kotlinCustomSettings = settings.kotlinCustomSettings + return rules(kotlinCommonSettings, builderUtil) { + simple { + before(FILE_ANNOTATION_LIST).lineBreakInCode() + between(IMPORT_DIRECTIVE, IMPORT_DIRECTIVE).lineBreakInCode() + after(IMPORT_LIST).blankLines(1) + } + + custom { + fun commentSpacing(minSpaces: Int): Spacing { + if (kotlinCommonSettings.KEEP_FIRST_COLUMN_COMMENT) { + return Spacing.createKeepingFirstColumnSpacing( + minSpaces, + Int.MAX_VALUE, + settings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + } + return Spacing.createSpacing( + minSpaces, + Int.MAX_VALUE, + 0, + settings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + } + + // Several line comments happened to be generated in one line + inPosition(parent = null, left = EOL_COMMENT, right = EOL_COMMENT).customRule { _, _, right -> + val nodeBeforeRight = right.requireNode().treePrev + if (nodeBeforeRight is PsiWhiteSpace && !nodeBeforeRight.textContains('\n')) { + createSpacing(0, minLineFeeds = 1) + } else { + null + } + } + + inPosition(right = BLOCK_COMMENT).spacing(commentSpacing(0)) + inPosition(right = EOL_COMMENT).spacing(commentSpacing(1)) + inPosition(parent = FUNCTION_LITERAL, right = BLOCK).customRule { _, _, right -> + when (right.node?.children()?.firstOrNull()?.elementType) { + BLOCK_COMMENT -> commentSpacing(0) + EOL_COMMENT -> commentSpacing(1) + else -> null + } + } + } + + simple { + after(FILE_ANNOTATION_LIST).blankLines(1) + after(PACKAGE_DIRECTIVE).blankLines(1) + } + + custom { + inPosition(leftSet = DECLARATIONS, rightSet = DECLARATIONS).customRule(fun( + _: ASTBlock, + _: ASTBlock, + right: ASTBlock + ): Spacing? { + val node = right.node ?: return null + val elementStart = node.startOfDeclaration() ?: return null + return if (StringUtil.containsLineBreak( + node.text.subSequence(0, elementStart.startOffset - node.startOffset).trimStart() + ) + ) + createSpacing(0, minLineFeeds = 2) + else + null + }) + + inPosition(left = CLASS, right = CLASS).emptyLinesIfLineBreakInLeft(1) + inPosition(left = CLASS, right = OBJECT_DECLARATION).emptyLinesIfLineBreakInLeft(1) + inPosition(left = OBJECT_DECLARATION, right = OBJECT_DECLARATION).emptyLinesIfLineBreakInLeft(1) + inPosition(left = OBJECT_DECLARATION, right = CLASS).emptyLinesIfLineBreakInLeft(1) + inPosition(left = FUN, right = FUN).emptyLinesIfLineBreakInLeft(1) + inPosition(left = PROPERTY, right = FUN).emptyLinesIfLineBreakInLeft(1) + inPosition(left = FUN, right = PROPERTY).emptyLinesIfLineBreakInLeft(1) + inPosition(left = SECONDARY_CONSTRUCTOR, right = SECONDARY_CONSTRUCTOR).emptyLinesIfLineBreakInLeft(1) + inPosition(left = TYPEALIAS, right = TYPEALIAS).emptyLinesIfLineBreakInLeft(1) + + // Case left for alternative constructors + inPosition(left = FUN, right = CLASS).emptyLinesIfLineBreakInLeft(1) + + inPosition(left = ENUM_ENTRY, right = ENUM_ENTRY).emptyLinesIfLineBreakInLeft( + emptyLines = 0, numberOfLineFeedsOtherwise = 0, numSpacesOtherwise = 1 + ) + + inPosition(parent = CLASS_BODY, left = SEMICOLON).customRule { parent, _, right -> + val klass = parent.requireNode().treeParent.psi as? KtClass ?: return@customRule null + if (klass.isEnum() && right.requireNode().elementType in DECLARATIONS) { + createSpacing(0, minLineFeeds = 2, keepBlankLines = settings.KEEP_BLANK_LINES_IN_DECLARATIONS) + } else null + } + + inPosition(parent = CLASS_BODY, left = LBRACE).customRule { parent, left, right -> + if (right.requireNode().elementType == RBRACE) { + return@customRule createSpacing(0) + } + val classBody = parent.requireNode().psi as KtClassBody + val parentPsi = classBody.parent as? KtClassOrObject ?: return@customRule null + if (kotlinCommonSettings.BLANK_LINES_AFTER_CLASS_HEADER == 0 || parentPsi.isObjectLiteral()) { + null + } else { + val minLineFeeds = if (right.requireNode().elementType == FUN || right.requireNode().elementType == PROPERTY) + kotlinCommonSettings.BLANK_LINES_AFTER_CLASS_HEADER + 1 + else + 0 + + builderUtil.createLineFeedDependentSpacing( + 1, + 1, + minLineFeeds, + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_DECLARATIONS, + TextRange(parentPsi.textRange.startOffset, left.requireNode().psi.textRange.startOffset), + DependentSpacingRule(DependentSpacingRule.Trigger.HAS_LINE_FEEDS) + .registerData( + DependentSpacingRule.Anchor.MIN_LINE_FEEDS, + kotlinCommonSettings.BLANK_LINES_AFTER_CLASS_HEADER + 1 + ) + ) + } + } + + val parameterWithDocCommentRule = { _: ASTBlock, _: ASTBlock, right: ASTBlock -> + if (right.requireNode().firstChildNode.elementType == DOC_COMMENT) { + createSpacing(0, minLineFeeds = 1, keepLineBreaks = true, keepBlankLines = settings.KEEP_BLANK_LINES_IN_DECLARATIONS) + } else { + null + } + } + inPosition(parent = VALUE_PARAMETER_LIST, right = VALUE_PARAMETER).customRule(parameterWithDocCommentRule) + + inPosition(parent = PROPERTY, right = PROPERTY_ACCESSOR).customRule { parent, _, _ -> + val startNode = parent.requireNode().psi.firstChild + .siblings() + .dropWhile { it is PsiComment || it is PsiWhiteSpace }.firstOrNull() ?: parent.requireNode().psi + Spacing.createDependentLFSpacing( + 1, 1, + TextRange(startNode.textRange.startOffset, parent.textRange.endOffset), + false, 0 + ) + } + + if (!kotlinCustomSettings.ALLOW_TRAILING_COMMA) { + inPosition(parent = VALUE_ARGUMENT_LIST, left = LPAR).customRule { parent, _, _ -> + if (kotlinCommonSettings.CALL_PARAMETERS_LPAREN_ON_NEXT_LINE && org.jetbrains.kotlin.idea.formatter.needWrapArgumentList( + parent.requireNode().psi + ) + ) { + Spacing.createDependentLFSpacing( + 0, 0, + excludeLambdasAndObjects(parent), + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ) + } else { + createSpacing(0) + } + } + + inPosition(parent = VALUE_ARGUMENT_LIST, right = RPAR).customRule { parent, left, _ -> + when { + kotlinCommonSettings.CALL_PARAMETERS_RPAREN_ON_NEXT_LINE -> + Spacing.createDependentLFSpacing( + 0, 0, + excludeLambdasAndObjects(parent), + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ) + left.requireNode().elementType == COMMA -> // incomplete call being edited + createSpacing(1) + else -> + createSpacing(0) + } + } + } + + inPosition(left = CONDITION, right = RPAR).customRule { _, left, _ -> + if (kotlinCustomSettings.IF_RPAREN_ON_NEW_LINE) { + Spacing.createDependentLFSpacing( + 0, 0, + excludeLambdasAndObjects(left), + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ) + } else { + createSpacing(0) + } + } + + inPosition(left = VALUE_PARAMETER, right = COMMA).customRule { _, left, _ -> + if (left.node?.lastChildNode?.elementType === EOL_COMMENT) + createSpacing(0, minLineFeeds = 1) + else + null + } + + inPosition(parent = LONG_STRING_TEMPLATE_ENTRY, right = LONG_TEMPLATE_ENTRY_END).lineBreakIfLineBreakInParent(0) + inPosition(parent = LONG_STRING_TEMPLATE_ENTRY, left = LONG_TEMPLATE_ENTRY_START).lineBreakIfLineBreakInParent(0) + } + + simple { + // ============ Line breaks ============== + before(DOC_COMMENT).lineBreakInCode() + between(PROPERTY, PROPERTY).lineBreakInCode() + + // CLASS - CLASS, CLASS - OBJECT_DECLARATION are exception + between(CLASS, DECLARATIONS).blankLines(1) + + // FUN - FUN, FUN - PROPERTY, FUN - CLASS are exceptions + between(FUN, DECLARATIONS).blankLines(1) + + // PROPERTY - PROPERTY, PROPERTY - FUN are exceptions + between(PROPERTY, DECLARATIONS).blankLines(1) + + // OBJECT_DECLARATION - OBJECT_DECLARATION, CLASS - OBJECT_DECLARATION are exception + between(OBJECT_DECLARATION, DECLARATIONS).blankLines(1) + between(SECONDARY_CONSTRUCTOR, DECLARATIONS).blankLines(1) + between(CLASS_INITIALIZER, DECLARATIONS).blankLines(1) + + // TYPEALIAS - TYPEALIAS is an exception + between(TYPEALIAS, DECLARATIONS).blankLines(1) + + // ENUM_ENTRY - ENUM_ENTRY is exception + between(ENUM_ENTRY, DECLARATIONS).blankLines(1) + + between(ENUM_ENTRY, SEMICOLON).spaces(0) + + between(COMMA, SEMICOLON).lineBreakInCodeIf(kotlinCustomSettings.ALLOW_TRAILING_COMMA) + + beforeInside(FUN, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(SECONDARY_CONSTRUCTOR, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(CLASS, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(OBJECT_DECLARATION, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(PROPERTY, WHEN).spaces(0) + beforeInside(PROPERTY, LABELED_EXPRESSION).spacesNoLineBreak(1) + before(PROPERTY).lineBreakInCode() + + after(DOC_COMMENT).lineBreakInCode() + + // =============== Spacing ================ + between(EOL_COMMENT, COMMA).lineBreakInCode() + before(COMMA).spacesNoLineBreak(if (kotlinCommonSettings.SPACE_BEFORE_COMMA) 1 else 0) + after(COMMA).spaceIf(kotlinCommonSettings.SPACE_AFTER_COMMA) + + val spacesAroundAssignment = if (kotlinCommonSettings.SPACE_AROUND_ASSIGNMENT_OPERATORS) 1 else 0 + beforeInside(EQ, PROPERTY).spacesNoLineBreak(spacesAroundAssignment) + beforeInside(EQ, FUN).spacing(spacesAroundAssignment, spacesAroundAssignment, 0, false, 0) + + around( + TokenSet.create(EQ, MULTEQ, DIVEQ, PLUSEQ, MINUSEQ, PERCEQ) + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_ASSIGNMENT_OPERATORS) + around(TokenSet.create(ANDAND, OROR)).spaceIf(kotlinCommonSettings.SPACE_AROUND_LOGICAL_OPERATORS) + around(TokenSet.create(EQEQ, EXCLEQ, EQEQEQ, EXCLEQEQEQ)).spaceIf(kotlinCommonSettings.SPACE_AROUND_EQUALITY_OPERATORS) + aroundInside( + TokenSet.create(LT, GT, LTEQ, GTEQ), BINARY_EXPRESSION + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_RELATIONAL_OPERATORS) + aroundInside(TokenSet.create(PLUS, MINUS), BINARY_EXPRESSION).spaceIf(kotlinCommonSettings.SPACE_AROUND_ADDITIVE_OPERATORS) + aroundInside( + TokenSet.create(MUL, DIV, PERC), BINARY_EXPRESSION + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_MULTIPLICATIVE_OPERATORS) + around( + TokenSet.create(PLUSPLUS, MINUSMINUS, EXCLEXCL, MINUS, PLUS, EXCL) + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_UNARY_OPERATOR) + before(ELVIS).spaces(1) + after(ELVIS).spacesNoLineBreak(1) + around(RANGE).spaceIf(kotlinCustomSettings.SPACE_AROUND_RANGE) + + after(MODIFIER_LIST).spaces(1) + + beforeInside(IDENTIFIER, CLASS).spaces(1) + beforeInside(IDENTIFIER, OBJECT_DECLARATION).spaces(1) + + after(VAL_KEYWORD).spaces(1) + after(VAR_KEYWORD).spaces(1) + betweenInside(TYPE_PARAMETER_LIST, IDENTIFIER, PROPERTY).spaces(1) + betweenInside(TYPE_REFERENCE, DOT, PROPERTY).spacing(0, 0, 0, false, 0) + betweenInside(DOT, IDENTIFIER, PROPERTY).spacing(0, 0, 0, false, 0) + + betweenInside(RETURN_KEYWORD, LABEL_QUALIFIER, RETURN).spaces(0) + afterInside(RETURN_KEYWORD, RETURN).spaces(1) + afterInside(LABEL_QUALIFIER, RETURN).spaces(1) + betweenInside(LABEL_QUALIFIER, EOL_COMMENT, LABELED_EXPRESSION).spacing( + 0, Int.MAX_VALUE, 0, true, kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + betweenInside(LABEL_QUALIFIER, BLOCK_COMMENT, LABELED_EXPRESSION).spacing( + 0, Int.MAX_VALUE, 0, true, kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + betweenInside(LABEL_QUALIFIER, LAMBDA_EXPRESSION, LABELED_EXPRESSION).spaces(0) + afterInside(LABEL_QUALIFIER, LABELED_EXPRESSION).spaces(1) + + betweenInside(FUN_KEYWORD, VALUE_PARAMETER_LIST, FUN).spacing(0, 0, 0, false, 0) + after(FUN_KEYWORD).spaces(1) + betweenInside(TYPE_PARAMETER_LIST, TYPE_REFERENCE, FUN).spaces(1) + betweenInside(TYPE_PARAMETER_LIST, IDENTIFIER, FUN).spaces(1) + betweenInside(TYPE_REFERENCE, DOT, FUN).spacing(0, 0, 0, false, 0) + betweenInside(DOT, IDENTIFIER, FUN).spacing(0, 0, 0, false, 0) + afterInside(IDENTIFIER, FUN).spacing(0, 0, 0, false, 0) + aroundInside(DOT, USER_TYPE).spaces(0) + + around(AS_KEYWORD).spaces(1) + around(AS_SAFE).spaces(1) + around(IS_KEYWORD).spaces(1) + around(NOT_IS).spaces(1) + around(IN_KEYWORD).spaces(1) + around(NOT_IN).spaces(1) + aroundInside(IDENTIFIER, BINARY_EXPRESSION).spaces(1) + + // before LPAR in constructor(): this() {} + after(CONSTRUCTOR_DELEGATION_REFERENCE).spacing(0, 0, 0, false, 0) + + // class A() - no space before LPAR of PRIMARY_CONSTRUCTOR + // class A private() - one space before modifier + custom { + inPosition(right = PRIMARY_CONSTRUCTOR).customRule { _, _, r -> + val spacesCount = if (r.requireNode().findLeafElementAt(0)?.elementType != LPAR) 1 else 0 + createSpacing(spacesCount, minLineFeeds = 0, keepLineBreaks = true, keepBlankLines = 0) + } + } + + afterInside(CONSTRUCTOR_KEYWORD, PRIMARY_CONSTRUCTOR).spaces(0) + betweenInside(IDENTIFIER, TYPE_PARAMETER_LIST, CLASS).spaces(0) + + beforeInside(DOT, DOT_QUALIFIED_EXPRESSION).spaces(0) + afterInside(DOT, DOT_QUALIFIED_EXPRESSION).spacesNoLineBreak(0) + beforeInside(SAFE_ACCESS, SAFE_ACCESS_EXPRESSION).spaces(0) + afterInside(SAFE_ACCESS, SAFE_ACCESS_EXPRESSION).spacesNoLineBreak(0) + + between(MODIFIERS_LIST_ENTRIES, MODIFIERS_LIST_ENTRIES).spaces(1) + + after(LBRACKET).spaces(0) + before(RBRACKET).spaces(0) + + afterInside(LPAR, VALUE_PARAMETER_LIST).spaces(0, kotlinCommonSettings.METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE) + beforeInside(RPAR, VALUE_PARAMETER_LIST).spaces(0, kotlinCommonSettings.METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE) + afterInside(LT, TYPE_PARAMETER_LIST).spaces(0) + beforeInside(GT, TYPE_PARAMETER_LIST).spaces(0) + afterInside(LT, TYPE_ARGUMENT_LIST).spaces(0) + beforeInside(GT, TYPE_ARGUMENT_LIST).spaces(0) + before(TYPE_ARGUMENT_LIST).spaces(0) + + after(LPAR).spaces(0) + before(RPAR).spaces(0) + + betweenInside(FOR_KEYWORD, LPAR, FOR).spaceIf(kotlinCommonSettings.SPACE_BEFORE_FOR_PARENTHESES) + betweenInside(IF_KEYWORD, LPAR, IF).spaceIf(kotlinCommonSettings.SPACE_BEFORE_IF_PARENTHESES) + betweenInside(WHILE_KEYWORD, LPAR, WHILE).spaceIf(kotlinCommonSettings.SPACE_BEFORE_WHILE_PARENTHESES) + betweenInside(WHILE_KEYWORD, LPAR, DO_WHILE).spaceIf(kotlinCommonSettings.SPACE_BEFORE_WHILE_PARENTHESES) + betweenInside(WHEN_KEYWORD, LPAR, WHEN).spaceIf(kotlinCustomSettings.SPACE_BEFORE_WHEN_PARENTHESES) + betweenInside(CATCH_KEYWORD, VALUE_PARAMETER_LIST, CATCH).spaceIf(kotlinCommonSettings.SPACE_BEFORE_CATCH_PARENTHESES) + + betweenInside(LPAR, VALUE_PARAMETER, FOR).spaces(0) + betweenInside(LPAR, DESTRUCTURING_DECLARATION, FOR).spaces(0) + betweenInside(LOOP_RANGE, RPAR, FOR).spaces(0) + + afterInside(ANNOTATION_ENTRY, ANNOTATED_EXPRESSION).spaces(1) + + before(SEMICOLON).spaces(0) + + beforeInside(INITIALIZER_LIST, ENUM_ENTRY).spaces(0) + + beforeInside(QUEST, NULLABLE_TYPE).spaces(0) + + val TYPE_COLON_ELEMENTS = TokenSet.create(PROPERTY, FUN, VALUE_PARAMETER, DESTRUCTURING_DECLARATION_ENTRY, FUNCTION_LITERAL) + beforeInside(COLON, TYPE_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_BEFORE_TYPE_COLON) } + afterInside(COLON, TYPE_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_AFTER_TYPE_COLON) } + + afterInside(COLON, EXTEND_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_AFTER_EXTEND_COLON) } + + beforeInside(ARROW, FUNCTION_LITERAL).spaceIf(kotlinCustomSettings.SPACE_BEFORE_LAMBDA_ARROW) + + aroundInside(ARROW, FUNCTION_TYPE).spaceIf(kotlinCustomSettings.SPACE_AROUND_FUNCTION_TYPE_ARROW) + + before(VALUE_ARGUMENT_LIST).spaces(0) + between(VALUE_ARGUMENT_LIST, LAMBDA_ARGUMENT).spaces(1) + betweenInside(REFERENCE_EXPRESSION, LAMBDA_ARGUMENT, CALL_EXPRESSION).spaces(1) + betweenInside(TYPE_ARGUMENT_LIST, LAMBDA_ARGUMENT, CALL_EXPRESSION).spaces(1) + + around(COLONCOLON).spaces(0) + + around(BY_KEYWORD).spaces(1) + betweenInside(IDENTIFIER, PROPERTY_DELEGATE, PROPERTY).spaces(1) + betweenInside(TYPE_REFERENCE, PROPERTY_DELEGATE, PROPERTY).spaces(1) + + before(INDICES).spaces(0) + before(WHERE_KEYWORD).spaces(1) + + afterInside(GET_KEYWORD, PROPERTY_ACCESSOR).spaces(0) + afterInside(SET_KEYWORD, PROPERTY_ACCESSOR).spaces(0) + } + custom { + + fun KotlinSpacingBuilder.CustomSpacingBuilder.ruleForKeywordOnNewLine( + shouldBeOnNewLine: Boolean, + keyword: IElementType, + parent: IElementType, + afterBlockFilter: (wordParent: ASTNode, block: ASTNode) -> Boolean = { _, _ -> true } + ) { + if (shouldBeOnNewLine) { + inPosition(parent = parent, right = keyword) + .lineBreakIfLineBreakInParent(numSpacesOtherwise = 1, allowBlankLines = false) + } else { + inPosition(parent = parent, right = keyword).customRule { _, _, right -> + + val previousLeaf = builderUtil.getPreviousNonWhitespaceLeaf(right.requireNode()) + val leftBlock = if ( + previousLeaf != null && + previousLeaf.elementType == RBRACE && + previousLeaf.treeParent?.elementType == BLOCK + ) { + previousLeaf.treeParent!! + } else null + + val removeLineBreaks = leftBlock != null && afterBlockFilter(right.node?.treeParent!!, leftBlock) + createSpacing(1, minLineFeeds = 0, keepLineBreaks = !removeLineBreaks, keepBlankLines = 0) + } + } + } + + ruleForKeywordOnNewLine(kotlinCommonSettings.ELSE_ON_NEW_LINE, keyword = ELSE_KEYWORD, parent = IF) { keywordParent, block -> + block.treeParent?.elementType == THEN && block.treeParent?.treeParent == keywordParent + } + ruleForKeywordOnNewLine( + kotlinCommonSettings.WHILE_ON_NEW_LINE, + keyword = WHILE_KEYWORD, + parent = DO_WHILE + ) { keywordParent, block -> + block.treeParent?.elementType == BODY && block.treeParent?.treeParent == keywordParent + } + ruleForKeywordOnNewLine(kotlinCommonSettings.CATCH_ON_NEW_LINE, keyword = CATCH, parent = TRY) + ruleForKeywordOnNewLine(kotlinCommonSettings.FINALLY_ON_NEW_LINE, keyword = FINALLY, parent = TRY) + + + fun spacingForLeftBrace(block: ASTNode?, blockType: IElementType = BLOCK): Spacing? { + if (block != null && block.elementType == blockType) { + val leftBrace = block.findChildByType(LBRACE) + if (leftBrace != null) { + val previousLeaf = builderUtil.getPreviousNonWhitespaceLeaf(leftBrace) + val isAfterEolComment = previousLeaf != null && (previousLeaf.elementType == EOL_COMMENT) + val keepLineBreaks = kotlinCustomSettings.LBRACE_ON_NEXT_LINE || isAfterEolComment + val minimumLF = if (kotlinCustomSettings.LBRACE_ON_NEXT_LINE) 1 else 0 + return createSpacing(1, minLineFeeds = minimumLF, keepLineBreaks = keepLineBreaks, keepBlankLines = 0) + } + } + return createSpacing(1) + } + + fun leftBraceRule(blockType: IElementType = BLOCK) = { _: ASTBlock, _: ASTBlock, right: ASTBlock -> + spacingForLeftBrace(right.node, blockType) + } + + val leftBraceRuleIfBlockIsWrapped = { _: ASTBlock, _: ASTBlock, right: ASTBlock -> + spacingForLeftBrace(right.requireNode().firstChildNode) + } + + // Add space after a semicolon if there is another child at the same line + inPosition(left = SEMICOLON).customRule { _, left, _ -> + val nodeAfterLeft = left.requireNode().treeNext + if (nodeAfterLeft is PsiWhiteSpace && !nodeAfterLeft.textContains('\n')) { + createSpacing(1) + } else { + null + } + } + + inPosition(parent = IF, right = THEN).customRule(leftBraceRuleIfBlockIsWrapped) + inPosition(parent = IF, right = ELSE).customRule(leftBraceRuleIfBlockIsWrapped) + + inPosition(parent = FOR, right = BODY).customRule(leftBraceRuleIfBlockIsWrapped) + inPosition(parent = WHILE, right = BODY).customRule(leftBraceRuleIfBlockIsWrapped) + inPosition(parent = DO_WHILE, right = BODY).customRule(leftBraceRuleIfBlockIsWrapped) + + inPosition(parent = TRY, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = CATCH, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = FINALLY, right = BLOCK).customRule(leftBraceRule()) + + inPosition(parent = FUN, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = SECONDARY_CONSTRUCTOR, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = CLASS_INITIALIZER, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = PROPERTY_ACCESSOR, right = BLOCK).customRule(leftBraceRule()) + + inPosition(right = CLASS_BODY).customRule(leftBraceRule(blockType = CLASS_BODY)) + + inPosition(left = WHEN_ENTRY, right = WHEN_ENTRY).customRule { _, left, right -> + val leftEntry = left.requireNode().psi as KtWhenEntry + val rightEntry = right.requireNode().psi as KtWhenEntry + val blankLines = if (leftEntry.expression is KtBlockExpression || rightEntry.expression is KtBlockExpression) + settings.kotlinCustomSettings.BLANK_LINES_AROUND_BLOCK_WHEN_BRANCHES + else + 0 + + createSpacing(0, minLineFeeds = blankLines + 1) + } + + inPosition(parent = WHEN_ENTRY, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = WHEN, right = LBRACE).customRule { parent, _, _ -> + spacingForLeftBrace(block = parent.requireNode(), blockType = WHEN) + } + + inPosition(left = LBRACE, right = WHEN_ENTRY).customRule { _, _, _ -> + createSpacing(0, minLineFeeds = 1) + } + + val spacesInSimpleFunction = if (kotlinCustomSettings.INSERT_WHITESPACES_IN_SIMPLE_ONE_LINE_METHOD) 1 else 0 + inPosition( + parent = FUNCTION_LITERAL, + left = LBRACE, + right = BLOCK + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = spacesInSimpleFunction) + + inPosition( + parent = FUNCTION_LITERAL, + left = ARROW, + right = BLOCK + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = 1) + + inPosition( + parent = FUNCTION_LITERAL, + left = LBRACE, + right = RBRACE + ).spacing(createSpacing(minSpaces = 0, maxSpaces = 1)) + + inPosition( + parent = FUNCTION_LITERAL, + right = RBRACE + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = spacesInSimpleFunction) + + inPosition( + parent = FUNCTION_LITERAL, + left = LBRACE + ).customRule { _, _, right -> + val rightNode = right.requireNode() + val rightType = rightNode.elementType + if (rightType == VALUE_PARAMETER_LIST) { + createSpacing(spacesInSimpleFunction, keepLineBreaks = false) + } else { + createSpacing(spacesInSimpleFunction) + } + } + + inPosition(parent = CLASS_BODY, right = RBRACE).customRule { parent, _, _ -> + kotlinCommonSettings.createSpaceBeforeRBrace(1, parent.textRange) + } + + inPosition(parent = BLOCK, right = RBRACE).customRule { block, left, _ -> + val psiElement = block.requireNode().treeParent.psi + + val empty = left.requireNode().elementType == LBRACE + + when (psiElement) { + is KtFunction -> { + if (psiElement.name != null && !empty) return@customRule null + } + is KtPropertyAccessor -> + if (!empty) return@customRule null + else -> + return@customRule null + } + + val spaces = if (empty) 0 else spacesInSimpleFunction + kotlinCommonSettings.createSpaceBeforeRBrace(spaces, psiElement.textRangeWithoutComments) + } + + inPosition(parent = BLOCK, left = LBRACE).customRule { parent, _, _ -> + val psiElement = parent.requireNode().treeParent.psi + val funNode = psiElement as? KtFunction ?: return@customRule null + + if (funNode.name != null) return@customRule null + + // Empty block is covered in above rule + Spacing.createDependentLFSpacing( + spacesInSimpleFunction, spacesInSimpleFunction, funNode.textRangeWithoutComments, + kotlinCommonSettings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + } + + inPosition(parentSet = EXTEND_COLON_ELEMENTS, left = PRIMARY_CONSTRUCTOR, right = COLON).customRule { _, left, _ -> + val primaryConstructor = left.requireNode().psi as KtPrimaryConstructor + val rightParenthesis = primaryConstructor.valueParameterList?.rightParenthesis + val prevSibling = rightParenthesis?.prevSibling + val spaces = if (kotlinCustomSettings.SPACE_BEFORE_EXTEND_COLON) 1 else 0 + // TODO This should use DependentSpacingRule, but it doesn't set keepLineBreaks to false if max LFs is 0 + if ((prevSibling as? PsiWhiteSpace)?.textContains('\n') == true || kotlinCommonSettings + .METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE + ) { + createSpacing(spaces, keepLineBreaks = false) + } else { + createSpacing(spaces) + } + } + + inPosition( + parent = CLASS_BODY, + left = LBRACE, + right = ENUM_ENTRY + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = 1) + } + + simple { + afterInside(LBRACE, BLOCK).lineBreakInCode() + beforeInside(RBRACE, BLOCK).spacing( + 1, 0, 1, + kotlinCommonSettings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_BEFORE_RBRACE + ) + between(LBRACE, ENUM_ENTRY).spacing(1, 0, 0, true, kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE) + beforeInside(RBRACE, WHEN).lineBreakInCode() + between(RPAR, BODY).spaces(1) + + // if when entry has block, spacing after arrow should be set by lbrace rule + aroundInside(ARROW, WHEN_ENTRY).spaceIf(kotlinCustomSettings.SPACE_AROUND_WHEN_ARROW) + + beforeInside(COLON, EXTEND_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_BEFORE_EXTEND_COLON) } + + after(EOL_COMMENT).lineBreakInCode() + } + } +} + +private fun excludeLambdasAndObjects(parent: ASTBlock): List { + val rangesToExclude = mutableListOf() + parent.requireNode().psi.accept(object : KtTreeVisitorVoid() { + override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { + super.visitLambdaExpression(lambdaExpression) + rangesToExclude.add(lambdaExpression.textRange) + } + + override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) { + super.visitObjectLiteralExpression(expression) + rangesToExclude.add(expression.textRange) + } + + override fun visitNamedFunction(function: KtNamedFunction) { + super.visitNamedFunction(function) + if (function.name == null) { + rangesToExclude.add(function.textRange) + } + } + }) + return TextRangeUtil.excludeRanges(parent.textRange, rangesToExclude).toList() +} + +private val QUALIFIED_OPERATION = TokenSet.create(DOT, SAFE_ACCESS) +private val QUALIFIED_EXPRESSIONS = TokenSet.create(DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION) +private val ELVIS_SET = TokenSet.create(ELVIS) +private val QUALIFIED_EXPRESSIONS_WITHOUT_WRAP = TokenSet.create(IMPORT_DIRECTIVE, PACKAGE_DIRECTIVE) + +private const val KDOC_COMMENT_INDENT = 1 + +private val BINARY_EXPRESSIONS = TokenSet.create(BINARY_EXPRESSION, BINARY_WITH_TYPE, IS_EXPRESSION) +private val KDOC_CONTENT = TokenSet.create(KDocTokens.KDOC, KDocElementTypes.KDOC_SECTION, KDocElementTypes.KDOC_TAG) + +private val CODE_BLOCKS = TokenSet.create(BLOCK, CLASS_BODY, FUNCTION_LITERAL) + +private val ALIGN_FOR_BINARY_OPERATIONS = TokenSet.create(MUL, DIV, PERC, PLUS, MINUS, ELVIS, LT, GT, LTEQ, GTEQ, ANDAND, OROR) +private val ANNOTATIONS = TokenSet.create(ANNOTATION_ENTRY, ANNOTATION) + +typealias WrappingStrategy = (childElement: ASTNode) -> Wrap? + +fun noWrapping(childElement: ASTNode): Wrap? = null + +abstract class KotlinCommonBlock( + private val node: ASTNode, + private val settings: CodeStyleSettings, + private val spacingBuilder: KotlinSpacingBuilder, + private val alignmentStrategy: CommonAlignmentStrategy, + private val overrideChildren: Sequence? = null, +) { + @Volatile + private var mySubBlocks: List? = null + + fun getTextRange(): TextRange { + if (overrideChildren != null) { + return TextRange(overrideChildren.first().startOffset, overrideChildren.last().textRange.endOffset) + } + return node.textRange + } + + protected abstract fun createBlock( + node: ASTNode, + alignmentStrategy: CommonAlignmentStrategy, + indent: Indent?, + wrap: Wrap?, + settings: CodeStyleSettings, + spacingBuilder: KotlinSpacingBuilder, + overrideChildren: Sequence? = null, + ): ASTBlock + + protected abstract fun createSyntheticSpacingNodeBlock(node: ASTNode): ASTBlock + + protected abstract fun getSubBlocks(): List + + protected abstract fun getSuperChildAttributes(newChildIndex: Int): ChildAttributes + + protected abstract fun isIncompleteInSuper(): Boolean + + protected abstract fun getAlignmentForCaseBranch(shouldAlignInColumns: Boolean): CommonAlignmentStrategy + + protected abstract fun getAlignment(): Alignment? + + protected abstract fun createAlignmentStrategy(alignOption: Boolean, defaultAlignment: Alignment?): CommonAlignmentStrategy + + protected abstract fun getNullAlignmentStrategy(): CommonAlignmentStrategy + + fun isLeaf(): Boolean = node.firstChildNode == null + + fun isIncomplete(): Boolean { + if (isIncompleteInSuper()) { + return true + } + + // An incomplete declaration is the reason when modifier list can become a class body child, otherwise + // it's going to be a declaration child. + return node.elementType == MODIFIER_LIST && node.treeParent?.elementType == CLASS_BODY + } + + fun buildChildren(): List { + if (mySubBlocks != null) { + return mySubBlocks!! + } + + var nodeSubBlocks = buildSubBlocks() + + if (node.elementType in QUALIFIED_EXPRESSIONS) { + nodeSubBlocks = splitSubBlocksOnDot(nodeSubBlocks) + } else { + val psi = node.psi + if (psi is KtBinaryExpression && psi.operationToken == ELVIS) { + nodeSubBlocks = splitSubBlocksOnElvis(nodeSubBlocks) + } + } + + mySubBlocks = nodeSubBlocks + + return nodeSubBlocks + } + + private fun splitSubBlocksOnDot(nodeSubBlocks: List): List { + if (node.treeParent?.isQualifier == true || node.isCallChainWithoutWrap) return nodeSubBlocks + + val operationBlockIndex = nodeSubBlocks.indexOfBlockWithType(QUALIFIED_OPERATION) + if (operationBlockIndex == -1) return nodeSubBlocks + + val block = nodeSubBlocks.first() + val wrap = createWrapForQualifierExpression(node) + val enforceIndentToChildren = anyCallInCallChainIsWrapped(node) + val indent = createIndentForQualifierExpression(enforceIndentToChildren) + val newBlock = block.processBlock(wrap, enforceIndentToChildren) + return nodeSubBlocks.replaceBlock(newBlock, 0).splitAtIndex(operationBlockIndex, indent, wrap) + } + + private fun ASTBlock.processBlock(wrap: Wrap?, enforceIndentToChildren: Boolean): ASTBlock { + val currentNode = requireNode() + val enforceIndent = enforceIndentToChildren && anyCallInCallChainIsWrapped(currentNode) + val indent = createIndentForQualifierExpression(enforceIndent) + + @Suppress("UNCHECKED_CAST") + val subBlocks = subBlocks as List + val elementType = currentNode.elementType + if (elementType != POSTFIX_EXPRESSION && elementType !in QUALIFIED_EXPRESSIONS) return this + + val index = 0 + val resultWrap = if (currentNode.wrapForFirstCallInChainIsAllowed) + wrap ?: createWrapForQualifierExpression(currentNode) + else + null + + val newBlock = subBlocks.elementAt(index).processBlock(resultWrap, enforceIndent) + return subBlocks.replaceBlock(newBlock, index).let { + val operationIndex = subBlocks.indexOfBlockWithType(QUALIFIED_OPERATION) + if (operationIndex != -1) + it.splitAtIndex(operationIndex, indent, resultWrap) + else + it + }.wrapToBlock(currentNode, this) + } + + private fun List.replaceBlock(block: ASTBlock, index: Int = 0): List = toMutableList().apply { this[index] = block } + + private val ASTNode.wrapForFirstCallInChainIsAllowed: Boolean + get() { + if (unwrapQualifier()?.isCall != true) return false + return settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN || receiverIsCall() + } + + private fun createWrapForQualifierExpression(node: ASTNode): Wrap? = + if (node.wrapForFirstCallInChainIsAllowed && node.receiverIsCall()) + Wrap.createWrap( + settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP, + true /* wrapFirstElement */, + ) + else + null + + // enforce indent to children when there's a line break before the dot in any call in the chain (meaning that + // the call chain following that call is indented) + private fun createIndentForQualifierExpression(enforceIndentToChildren: Boolean): Indent { + val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) { + if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST + } else { + Indent.Type.NORMAL + } + + return Indent.getIndent( + indentType, false, + enforceIndentToChildren, + ) + } + + private fun List.wrapToBlock( + anchor: ASTNode?, + parentBlock: ASTBlock?, + ): ASTBlock = splitAtIndex(0, null, null, anchor, parentBlock).single() + + private fun List.splitAtIndex( + index: Int, + indent: Indent?, + wrap: Wrap?, + anchor: ASTNode? = null, + parentBlock: ASTBlock? = null, + ): List { + val operationBlock = this[index] + val createParentSyntheticSpacingBlock: (ASTNode) -> ASTBlock = if (parentBlock != null) { + { parentBlock } + } else { + { + val parent = it.treeParent ?: node + val skipOperationNodeParent = if (parent.elementType === OPERATION_REFERENCE) { + parent.treeParent ?: parent + } else { + parent + } + createSyntheticSpacingNodeBlock(skipOperationNodeParent) + } + } + val operationSyntheticBlock = SyntheticKotlinBlock( + anchor ?: operationBlock.requireNode(), + subList(index, size), + null, indent, wrap, spacingBuilder, createParentSyntheticSpacingBlock, + ) + + return subList(0, index) + operationSyntheticBlock + } + + private fun splitSubBlocksOnElvis(nodeSubBlocks: List): List { + val elvisIndex = nodeSubBlocks.indexOfBlockWithType(ELVIS_SET) + if (elvisIndex >= 0) { + val indent = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_ELVIS) { + Indent.getContinuationIndent() + } else { + Indent.getNormalIndent() + } + + return nodeSubBlocks.splitAtIndex( + elvisIndex, + indent, + null, + ) + } + return nodeSubBlocks + } + + private fun createChildIndent(child: ASTNode): Indent? { + val childParent = child.treeParent + val childType = child.elementType + + if (childParent != null && isInCodeChunk(childParent)) { + return Indent.getNoneIndent() + } + + // do not indent child after heading comments inside declaration + if (childParent != null && childParent.psi is KtDeclaration) { + val prev = getPrevWithoutWhitespace(child) + if (prev != null && COMMENTS.contains(prev.elementType) && getSiblingWithoutWhitespaceAndComments(prev) == null) { + return Indent.getNoneIndent() + } + } + + for (strategy in INDENT_RULES) { + val indent = strategy.getIndent(child, settings) + if (indent != null) { + return indent + } + } + + // TODO: Try to rewrite other rules to declarative style + if (childParent != null) { + val parentType = childParent.elementType + + if (parentType === VALUE_PARAMETER_LIST || parentType === VALUE_ARGUMENT_LIST) { + val prev = getPrevWithoutWhitespace(child) + if (childType === RPAR && (prev == null || prev.elementType !== COMMA || !hasDoubleLineBreakBefore(child))) { + return Indent.getNoneIndent() + } + + return if (settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_ARGUMENT_LISTS) + Indent.getContinuationWithoutFirstIndent() + else + Indent.getNormalIndent() + } + + if (parentType === TYPE_PARAMETER_LIST || parentType === TYPE_ARGUMENT_LIST) { + return Indent.getContinuationWithoutFirstIndent() + } + } + + return Indent.getNoneIndent() + } + + private fun isInCodeChunk(node: ASTNode): Boolean { + val parent = node.treeParent ?: return false + + if (node.elementType != BLOCK) { + return false + } + + val parentType = parent.elementType + return parentType == SCRIPT + || parentType == BLOCK_CODE_FRAGMENT + || parentType == EXPRESSION_CODE_FRAGMENT + || parentType == TYPE_CODE_FRAGMENT + } + + fun getChildAttributes(newChildIndex: Int): ChildAttributes { + val type = node.elementType + + if (isInCodeChunk(node)) { + return ChildAttributes(Indent.getNoneIndent(), null) + } + + if (type == IF) { + val elseBlock = mySubBlocks?.getOrNull(newChildIndex) + if (elseBlock != null && elseBlock.requireNode().elementType == ELSE_KEYWORD) { + return ChildAttributes.DELEGATE_TO_NEXT_CHILD + } + } + + if (newChildIndex > 0) { + val prevBlock = mySubBlocks?.get(newChildIndex - 1) + if (prevBlock?.node?.elementType == MODIFIER_LIST) { + return ChildAttributes(Indent.getNoneIndent(), null) + } + } + + return when (type) { + in CODE_BLOCKS, WHEN, IF, FOR, WHILE, DO_WHILE, WHEN_ENTRY -> ChildAttributes( + Indent.getNormalIndent(), + null, + ) + + TRY -> ChildAttributes(Indent.getNoneIndent(), null) + + in QUALIFIED_EXPRESSIONS -> ChildAttributes(Indent.getContinuationWithoutFirstIndent(), null) + + VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> { + val subBlocks = getSubBlocks() + if (newChildIndex != 1 && newChildIndex != 0 && newChildIndex < subBlocks.size) { + val block = subBlocks[newChildIndex] + ChildAttributes(block.indent, block.alignment) + } else { + val indent = + if ((type == VALUE_PARAMETER_LIST && !settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_PARAMETER_LISTS) || + (type == VALUE_ARGUMENT_LIST && !settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_ARGUMENT_LISTS) + ) { + Indent.getNormalIndent() + } else { + Indent.getContinuationIndent() + } + ChildAttributes(indent, null) + } + } + + DOC_COMMENT -> ChildAttributes(Indent.getSpaceIndent(KDOC_COMMENT_INDENT), null) + + PARENTHESIZED -> getSuperChildAttributes(newChildIndex) + + else -> { + val blocks = getSubBlocks() + if (newChildIndex != 0) { + val isIncomplete = if (newChildIndex < blocks.size) blocks[newChildIndex - 1].isIncomplete else isIncompleteInSuper() + if (isIncomplete) { + if (blocks.size == newChildIndex && !settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_EXPRESSION_BODIES) { + val lastInParent = blocks.last() + if (lastInParent is ASTBlock && lastInParent.node?.elementType in ALL_ASSIGNMENTS) { + return ChildAttributes(Indent.getNormalIndent(), null) + } + } + + return getSuperChildAttributes(newChildIndex) + } + } + + if (blocks.size > newChildIndex) { + val block = blocks[newChildIndex] + return ChildAttributes(block.indent, block.alignment) + } + + ChildAttributes(Indent.getNoneIndent(), null) + } + } + } + + private fun getChildrenAlignmentStrategy(): CommonAlignmentStrategy { + val kotlinCommonSettings = settings.kotlinCommonSettings + val kotlinCustomSettings = settings.kotlinCustomSettings + val parentType = node.elementType + return when { + parentType === VALUE_PARAMETER_LIST -> + getAlignmentForChildInParenthesis( + kotlinCommonSettings.ALIGN_MULTILINE_PARAMETERS, VALUE_PARAMETER, COMMA, + kotlinCommonSettings.ALIGN_MULTILINE_METHOD_BRACKETS, LPAR, RPAR, + ) + + parentType === VALUE_ARGUMENT_LIST -> + getAlignmentForChildInParenthesis( + kotlinCommonSettings.ALIGN_MULTILINE_PARAMETERS_IN_CALLS, VALUE_ARGUMENT, COMMA, + kotlinCommonSettings.ALIGN_MULTILINE_METHOD_BRACKETS, LPAR, RPAR, + ) + + parentType === WHEN -> + getAlignmentForCaseBranch(kotlinCustomSettings.ALIGN_IN_COLUMNS_CASE_BRANCH) + + parentType === WHEN_ENTRY -> + alignmentStrategy + + parentType in BINARY_EXPRESSIONS && getOperationType(node) in ALIGN_FOR_BINARY_OPERATIONS -> + createAlignmentStrategy(kotlinCommonSettings.ALIGN_MULTILINE_BINARY_OPERATION, getAlignment()) + + parentType === SUPER_TYPE_LIST -> + createAlignmentStrategy(kotlinCommonSettings.ALIGN_MULTILINE_EXTENDS_LIST, getAlignment()) + + parentType === PARENTHESIZED -> + object : CommonAlignmentStrategy() { + private var bracketsAlignment: Alignment? = + if (kotlinCommonSettings.ALIGN_MULTILINE_BINARY_OPERATION) Alignment.createAlignment() else null + + override fun getAlignment(node: ASTNode): Alignment? { + val childNodeType = node.elementType + val prev = getPrevWithoutWhitespace(node) + + if (prev != null && prev.elementType === TokenType.ERROR_ELEMENT || childNodeType === TokenType.ERROR_ELEMENT) { + return bracketsAlignment + } + + if (childNodeType === LPAR || childNodeType === RPAR) { + return bracketsAlignment + } + + return null + } + } + + parentType == TYPE_CONSTRAINT_LIST -> + createAlignmentStrategy(true, getAlignment()) + + else -> + getNullAlignmentStrategy() + } + } + + + private fun buildSubBlock( + child: ASTNode, + alignmentStrategy: CommonAlignmentStrategy, + wrappingStrategy: WrappingStrategy, + overrideChildren: Sequence? = null, + ): ASTBlock { + val childWrap = wrappingStrategy(child) + + // Skip one sub-level for operators, so type of block node is an element type of operator + if (child.elementType === OPERATION_REFERENCE) { + val operationNode = child.firstChildNode + if (operationNode != null) { + return createBlock( + operationNode, + alignmentStrategy, + createChildIndent(child), + childWrap, + settings, + spacingBuilder, + overrideChildren, + ) + } + } + + return createBlock(child, alignmentStrategy, createChildIndent(child), childWrap, settings, spacingBuilder, overrideChildren) + } + + private fun buildSubBlocks(): List { + val childrenAlignmentStrategy = getChildrenAlignmentStrategy() + val wrappingStrategy = getWrappingStrategy() + + val childNodes = when { + overrideChildren != null -> overrideChildren.asSequence() + node.elementType == BINARY_EXPRESSION -> { + val binaryExpression = node.psi as? KtBinaryExpression + if (binaryExpression != null && ALL_ASSIGNMENTS.contains(binaryExpression.operationToken)) { + node.children() + } else { + val binaryExpressionChildren = mutableListOf() + collectBinaryExpressionChildren(node, binaryExpressionChildren) + binaryExpressionChildren.asSequence() + } + } + else -> node.children() + } + + return childNodes + .filter { it.textRange.length > 0 && it.elementType != TokenType.WHITE_SPACE } + .flatMap { buildSubBlocksForChildNode(it, childrenAlignmentStrategy, wrappingStrategy) } + .toList() + } + + private fun buildSubBlocksForChildNode( + node: ASTNode, + childrenAlignmentStrategy: CommonAlignmentStrategy, + wrappingStrategy: WrappingStrategy, + ): Sequence { + if (node.elementType == FUN && false /* TODO fix tests and restore */) { + val filteredChildren = node.children().filter { + it.textRange.length > 0 && it.elementType != TokenType.WHITE_SPACE + } + val significantChildren = filteredChildren.dropWhile { it.elementType == EOL_COMMENT } + val funIndent = extractIndent(significantChildren.first()) + val eolComments = filteredChildren.takeWhile { + it.elementType == EOL_COMMENT && extractIndent(it) != funIndent + }.toList() + val remainingChildren = filteredChildren.drop(eolComments.size) + + val blocks = eolComments.map { buildSubBlock(it, childrenAlignmentStrategy, wrappingStrategy) } + + sequenceOf(buildSubBlock(node, childrenAlignmentStrategy, wrappingStrategy, remainingChildren)) + val blockList = blocks.toList() + return blockList.asSequence() + } + + return sequenceOf(buildSubBlock(node, childrenAlignmentStrategy, wrappingStrategy)) + } + + private fun collectBinaryExpressionChildren(node: ASTNode, result: MutableList) { + for (child in node.children()) { + if (child.elementType == BINARY_EXPRESSION) { + collectBinaryExpressionChildren(child, result) + } else { + result.add(child) + } + } + } + + private fun getWrappingStrategy(): WrappingStrategy { + val commonSettings = settings.kotlinCommonSettings + val elementType = node.elementType + val parentElementType = node.treeParent?.elementType + val nodePsi = node.psi + + when { + elementType === VALUE_ARGUMENT_LIST -> { + val wrapSetting = commonSettings.CALL_PARAMETERS_WRAP + if (!node.addTrailingComma && + (wrapSetting == CommonCodeStyleSettings.WRAP_AS_NEEDED || wrapSetting == CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM) && + !needWrapArgumentList(nodePsi) + ) { + return ::noWrapping + } + return getWrappingStrategyForItemList( + wrapSetting, + VALUE_ARGUMENT, + node.addTrailingComma, + additionalWrap = trailingCommaWrappingStrategyWithMultiLineCheck(LPAR, RPAR), + ) + } + + elementType === VALUE_PARAMETER_LIST -> { + when (parentElementType) { + FUN, PRIMARY_CONSTRUCTOR, SECONDARY_CONSTRUCTOR -> return getWrappingStrategyForItemList( + commonSettings.METHOD_PARAMETERS_WRAP, + VALUE_PARAMETER, + node.addTrailingComma, + additionalWrap = trailingCommaWrappingStrategyWithMultiLineCheck(LPAR, RPAR), + ) + FUNCTION_TYPE -> return defaultTrailingCommaWrappingStrategy(LPAR, RPAR) + FUNCTION_LITERAL -> { + if (nodePsi.parent?.safeAs()?.needTrailingComma(settings) == true) { + val check = thisOrPrevIsMultiLineElement(COMMA, LBRACE /* not necessary */, ARROW /* not necessary */) + return { childElement -> + createWrapAlwaysIf(getSiblingWithoutWhitespaceAndComments(childElement) == null || check(childElement)) + } + } + } + } + } + + elementType === FUNCTION_LITERAL -> { + if (nodePsi.cast().needTrailingComma(settings)) + return trailingCommaWrappingStrategy(leftAnchor = LBRACE, rightAnchor = ARROW) + } + + elementType === WHEN_ENTRY -> { + // with argument + if (nodePsi.cast().needTrailingComma(settings)) { + val check = thisOrPrevIsMultiLineElement(COMMA, LBRACE /* not necessary */, ARROW /* not necessary */) + return trailingCommaWrappingStrategy(rightAnchor = ARROW) { + getSiblingWithoutWhitespaceAndComments(it, true) != null && check(it) + } + } + } + + elementType === DESTRUCTURING_DECLARATION -> { + nodePsi as KtDestructuringDeclaration + if (nodePsi.valOrVarKeyword == null) return defaultTrailingCommaWrappingStrategy(LPAR, RPAR) + else if (nodePsi.needTrailingComma(settings)) { + val check = thisOrPrevIsMultiLineElement(COMMA, LPAR, RPAR) + return trailingCommaWrappingStrategy(leftAnchor = LPAR, rightAnchor = RPAR, filter = { it.elementType !== EQ }) { + getSiblingWithoutWhitespaceAndComments(it, true) != null && check(it) + } + } + } + + elementType === INDICES -> return defaultTrailingCommaWrappingStrategy(LBRACKET, RBRACKET) + + elementType === TYPE_PARAMETER_LIST -> return defaultTrailingCommaWrappingStrategy(LT, GT) + + elementType === TYPE_ARGUMENT_LIST -> return defaultTrailingCommaWrappingStrategy(LT, GT) + + elementType === COLLECTION_LITERAL_EXPRESSION -> return defaultTrailingCommaWrappingStrategy(LBRACKET, RBRACKET) + + elementType === SUPER_TYPE_LIST -> { + val wrap = Wrap.createWrap(commonSettings.EXTENDS_LIST_WRAP, false) + return { childElement -> if (childElement.psi is KtSuperTypeListEntry) wrap else null } + } + + elementType === CLASS_BODY -> return getWrappingStrategyForItemList(commonSettings.ENUM_CONSTANTS_WRAP, ENUM_ENTRY) + + elementType === MODIFIER_LIST -> { + when (val parent = node.treeParent.psi) { + is KtParameter -> + return getWrappingStrategyForItemList( + commonSettings.PARAMETER_ANNOTATION_WRAP, + ANNOTATIONS, + !node.treeParent.isFirstParameter(), + ) + is KtClassOrObject, is KtTypeAlias -> + return getWrappingStrategyForItemList( + commonSettings.CLASS_ANNOTATION_WRAP, + ANNOTATIONS, + ) + + is KtNamedFunction, is KtSecondaryConstructor -> + return getWrappingStrategyForItemList( + commonSettings.METHOD_ANNOTATION_WRAP, + ANNOTATIONS, + ) + + is KtProperty -> + return getWrappingStrategyForItemList( + if (parent.isLocal) + commonSettings.VARIABLE_ANNOTATION_WRAP + else + commonSettings.FIELD_ANNOTATION_WRAP, + ANNOTATIONS, + ) + } + } + + elementType === VALUE_PARAMETER -> + return wrapAfterAnnotation(commonSettings.PARAMETER_ANNOTATION_WRAP) + + nodePsi is KtClassOrObject || nodePsi is KtTypeAlias -> + return wrapAfterAnnotation(commonSettings.CLASS_ANNOTATION_WRAP) + + nodePsi is KtNamedFunction || nodePsi is KtSecondaryConstructor -> + return wrap@{ childElement -> + getWrapAfterAnnotation(childElement, commonSettings.METHOD_ANNOTATION_WRAP)?.let { + return@wrap it + } + if (getSiblingWithoutWhitespaceAndComments(childElement)?.elementType == EQ) { + return@wrap Wrap.createWrap(settings.kotlinCustomSettings.WRAP_EXPRESSION_BODY_FUNCTIONS, true) + } + null + } + + nodePsi is KtProperty -> + return wrap@{ childElement -> + val wrapSetting = if (nodePsi.isLocal) commonSettings.VARIABLE_ANNOTATION_WRAP else commonSettings.FIELD_ANNOTATION_WRAP + getWrapAfterAnnotation(childElement, wrapSetting)?.let { + return@wrap it + } + if (getSiblingWithoutWhitespaceAndComments(childElement)?.elementType == EQ) { + return@wrap Wrap.createWrap(settings.kotlinCommonSettings.ASSIGNMENT_WRAP, true) + } + null + } + + nodePsi is KtBinaryExpression -> { + if (nodePsi.operationToken == EQ) { + return { childElement -> + if (getSiblingWithoutWhitespaceAndComments(childElement)?.elementType == OPERATION_REFERENCE) { + Wrap.createWrap(settings.kotlinCommonSettings.ASSIGNMENT_WRAP, true) + } else { + null + } + } + } + if (nodePsi.operationToken == ELVIS && nodePsi.getStrictParentOfType() == null) { + return { childElement -> + if (childElement.elementType == OPERATION_REFERENCE && (childElement.psi as? KtOperationReferenceExpression)?.operationSignTokenType == ELVIS) { + Wrap.createWrap(settings.kotlinCustomSettings.WRAP_ELVIS_EXPRESSIONS, true) + } else { + null + } + } + } + return ::noWrapping + } + } + + return ::noWrapping + } + + private fun defaultTrailingCommaWrappingStrategy(leftAnchor: IElementType, rightAnchor: IElementType): WrappingStrategy = + fun(childElement: ASTNode): Wrap? = trailingCommaWrappingStrategyWithMultiLineCheck(leftAnchor, rightAnchor)(childElement) + + private val ASTNode.addTrailingComma: Boolean + get() = (settings.kotlinCustomSettings.addTrailingCommaIsAllowedFor(this) || + lastChildNode?.let { getSiblingWithoutWhitespaceAndComments(it) }?.elementType === COMMA) && + psi?.let(PsiElement::isMultiline) == true + + + private fun ASTNode.notDelimiterSiblingNodeInSequence( + forward: Boolean, + delimiterType: IElementType, + typeOfLastElement: IElementType, + ): ASTNode? { + var sibling: ASTNode? = null + for (element in siblings(forward).filter { it.elementType != WHITE_SPACE }.takeWhile { it.elementType != typeOfLastElement }) { + val elementType = element.elementType + if (!forward) { + sibling = element + if (elementType != delimiterType && elementType !in COMMENTS) break + } else { + if (elementType !in COMMENTS) break + sibling = element + } + } + + return sibling + } + + private fun thisOrPrevIsMultiLineElement( + delimiterType: IElementType, + typeOfFirstElement: IElementType, + typeOfLastElement: IElementType, + ) = fun(childElement: ASTNode): Boolean { + when (childElement.elementType) { + typeOfFirstElement, + typeOfLastElement, + delimiterType, + in WHITE_SPACE_OR_COMMENT_BIT_SET, + -> return false + } + + val psi = childElement.psi ?: return false + if (psi.isMultiline()) return true + + val startOffset = childElement.notDelimiterSiblingNodeInSequence(false, delimiterType, typeOfFirstElement)?.startOffset + ?: psi.startOffset + val endOffset = childElement.notDelimiterSiblingNodeInSequence(true, delimiterType, typeOfLastElement)?.psi?.endOffset + ?: psi.endOffset + return psi.parent.containsLineBreakInThis(startOffset, endOffset) + } + + private fun trailingCommaWrappingStrategyWithMultiLineCheck( + leftAnchor: IElementType, + rightAnchor: IElementType, + ) = trailingCommaWrappingStrategy( + leftAnchor = leftAnchor, + rightAnchor = rightAnchor, + checkTrailingComma = true, + additionalCheck = thisOrPrevIsMultiLineElement(COMMA, leftAnchor, rightAnchor), + ) + + private fun trailingCommaWrappingStrategy( + leftAnchor: IElementType? = null, + rightAnchor: IElementType? = null, + checkTrailingComma: Boolean = false, + filter: (ASTNode) -> Boolean = { true }, + additionalCheck: (ASTNode) -> Boolean = { false }, + ): WrappingStrategy = fun(childElement: ASTNode): Wrap? { + if (!filter(childElement)) return null + val childElementType = childElement.elementType + return createWrapAlwaysIf( + (!checkTrailingComma || childElement.treeParent.addTrailingComma) && ( + rightAnchor != null && rightAnchor === childElementType || + leftAnchor != null && leftAnchor === getSiblingWithoutWhitespaceAndComments(childElement)?.elementType || + additionalCheck(childElement) + ), + ) + } +} + +private fun ASTNode.qualifierReceiver(): ASTNode? = unwrapQualifier()?.psi + ?.safeAs() + ?.receiverExpression + ?.node + ?.unwrapQualifier() + +private tailrec fun ASTNode.unwrapQualifier(): ASTNode? { + if (elementType in QUALIFIED_EXPRESSIONS) return this + + val psi = psi as? KtPostfixExpression ?: return null + if (psi.operationToken != EXCLEXCL) return null + + return psi.baseExpression?.node?.unwrapQualifier() +} + +private fun ASTNode.receiverIsCall(): Boolean = qualifierReceiver()?.isCall == true + +private val ASTNode.isCallChainWithoutWrap: Boolean + get() { + val callChainParent = parents().firstOrNull { !it.isQualifier } ?: return true + return callChainParent.elementType in QUALIFIED_EXPRESSIONS_WITHOUT_WRAP + } + +private val ASTNode.isQualifier: Boolean + get() { + var currentNode: ASTNode? = this + while (currentNode != null) { + if (currentNode.elementType in QUALIFIED_EXPRESSIONS) return true + if (currentNode.psi?.safeAs()?.operationToken != EXCLEXCL) return false + + currentNode = currentNode.treeParent + } + + return false + } + +private val ASTNode.isCall: Boolean + get() = unwrapQualifier()?.lastChildNode?.elementType == CALL_EXPRESSION + +private fun anyCallInCallChainIsWrapped(node: ASTNode): Boolean { + val sequentialNodes = generateSequence(node) { + when (it.elementType) { + POSTFIX_EXPRESSION, in QUALIFIED_EXPRESSIONS -> it.firstChildNode + PARENTHESIZED -> getSiblingWithoutWhitespaceAndComments(it.firstChildNode, true) + else -> null + } + } + + return sequentialNodes.any { + val checkedElement = when (it.elementType) { + in QUALIFIED_EXPRESSIONS -> it.findChildByType(QUALIFIED_OPERATION) + PARENTHESIZED -> it.lastChildNode + else -> null + } + + checkedElement != null && hasLineBreakBefore(checkedElement) + } +} + +private fun ASTNode.isFirstParameter(): Boolean = treePrev?.elementType == LPAR + +private fun wrapAfterAnnotation(wrapType: Int): WrappingStrategy { + return { childElement -> getWrapAfterAnnotation(childElement, wrapType) } +} + +private fun getWrapAfterAnnotation(childElement: ASTNode, wrapType: Int): Wrap? { + if (childElement.elementType in COMMENTS) return null + var prevLeaf = childElement.treePrev + while (prevLeaf?.elementType == TokenType.WHITE_SPACE) { + prevLeaf = prevLeaf.treePrev + } + if (prevLeaf?.elementType == MODIFIER_LIST) { + if (prevLeaf?.lastChildNode?.elementType in ANNOTATIONS) { + return Wrap.createWrap(wrapType, true) + } + } + return null +} + +fun needWrapArgumentList(psi: PsiElement): Boolean { + val args = (psi as? KtValueArgumentList)?.arguments + return args?.singleOrNull()?.getArgumentExpression() !is KtObjectLiteralExpression +} + +private fun hasLineBreakBefore(node: ASTNode): Boolean { + val prevSibling = node.leaves(false) + .dropWhile { it.psi is PsiComment } + .firstOrNull() + return prevSibling?.elementType == TokenType.WHITE_SPACE && prevSibling?.textContains('\n') == true +} + +private fun hasDoubleLineBreakBefore(node: ASTNode): Boolean { + val prevSibling = node.leaves(false).firstOrNull() ?: return false + + return prevSibling.text.count { it == '\n' } >= 2 +} + +fun NodeIndentStrategy.PositionStrategy.continuationIf( + option: (KotlinCodeStyleSettings) -> Boolean, + indentFirst: Boolean = false, +): NodeIndentStrategy { + return set { settings -> + if (option(settings.kotlinCustomSettings)) { + if (indentFirst) + Indent.getContinuationIndent() + else + Indent.getContinuationWithoutFirstIndent() + } else + Indent.getNormalIndent() + } +} + +private val INDENT_RULES = arrayOf( + strategy("No indent for braces in blocks") + .within(BLOCK, CLASS_BODY, FUNCTION_LITERAL) + .forType(RBRACE, LBRACE) + .set(Indent.getNoneIndent()), + + strategy("Indent for block content") + .within(BLOCK, CLASS_BODY, FUNCTION_LITERAL) + .notForType(RBRACE, LBRACE, BLOCK) + .set(Indent.getNormalIndent()), + + strategy("Indent for template content") + .within(LONG_STRING_TEMPLATE_ENTRY) + .notForType(LONG_TEMPLATE_ENTRY_START, LONG_TEMPLATE_ENTRY_END) + .set(Indent.getNormalIndent()), + + strategy("No indent for braces in template") + .within(LONG_STRING_TEMPLATE_ENTRY) + .forType(LONG_TEMPLATE_ENTRY_START, LONG_TEMPLATE_ENTRY_END) + .set(Indent.getNoneIndent()), + + strategy("Indent for property accessors") + .within(PROPERTY).forType(PROPERTY_ACCESSOR) + .set(Indent.getNormalIndent()), + + strategy("For a single statement in 'for'") + .within(BODY).notForType(BLOCK) + .set(Indent.getNormalIndent()), + + strategy("For WHEN content") + .within(WHEN) + .notForType(RBRACE, LBRACE, WHEN_KEYWORD) + .set(Indent.getNormalIndent()), + + strategy("For single statement in THEN and ELSE") + .within(THEN, ELSE).notForType(BLOCK) + .set(Indent.getNormalIndent()), + + strategy("Expression body") + .within(FUN) + .forElement { + (it.psi is KtExpression && it.psi !is KtBlockExpression) + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES, indentFirst = true), + + strategy("Line comment at expression body position") + .forElement { node -> + val psi = node.psi + val parent = psi.parent + if (psi is PsiComment && parent is KtDeclarationWithInitializer) { + psi.getNextSiblingIgnoringWhitespace() == parent.initializer + } else { + false + } + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES, indentFirst = true), + + strategy("If condition") + .within(CONDITION) + .set { settings -> + val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_IF_CONDITIONS) + Indent.Type.CONTINUATION + else + Indent.Type.NORMAL + Indent.getIndent(indentType, false, true) + }, + + strategy("Property accessor expression body") + .within(PROPERTY_ACCESSOR) + .forElement { + it.psi is KtExpression && it.psi !is KtBlockExpression + } + .set(Indent.getNormalIndent()), + + strategy("Property initializer") + .within(PROPERTY) + .forElement { + it.psi is KtExpression + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES), + + strategy("Destructuring declaration") + .within(DESTRUCTURING_DECLARATION) + .forElement { + it.psi is KtExpression + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES), + + strategy("Assignment expressions") + .within(BINARY_EXPRESSION) + .within { + val binaryExpression = it.psi as? KtBinaryExpression + ?: return@within false + + return@within ALL_ASSIGNMENTS.contains(binaryExpression.operationToken) + } + .forElement { + val psi = it.psi + val binaryExpression = psi?.parent as? KtBinaryExpression + binaryExpression?.right == psi + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES), + + strategy("Indent for parts") + .within(PROPERTY, FUN, DESTRUCTURING_DECLARATION, SECONDARY_CONSTRUCTOR) + .notForType( + BLOCK, FUN_KEYWORD, VAL_KEYWORD, VAR_KEYWORD, CONSTRUCTOR_KEYWORD, RPAR, + EOL_COMMENT, + ) + .set(Indent.getContinuationWithoutFirstIndent()), + + strategy("Chained calls") + .within(QUALIFIED_EXPRESSIONS) + .forType(EOL_COMMENT, BLOCK_COMMENT, DOC_COMMENT, SHEBANG_COMMENT) + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_CHAINED_CALLS), + + strategy("Colon of delegation list") + .within(CLASS, OBJECT_DECLARATION) + .forType(COLON) + .set(Indent.getNormalIndent()), + + strategy("Delegation list") + .within(SUPER_TYPE_LIST) + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_IN_SUPERTYPE_LISTS, indentFirst = true), + + strategy("Indices") + .within(INDICES) + .notForType(RBRACKET) + .set(Indent.getContinuationIndent(false)), + + strategy("Binary expressions") + .within(BINARY_EXPRESSIONS) + .forElement { node -> + !node.suppressBinaryExpressionIndent() + } + .set(Indent.getContinuationWithoutFirstIndent(false)), + + strategy("Parenthesized expression") + .within(PARENTHESIZED) + .set(Indent.getContinuationWithoutFirstIndent(false)), + + strategy("Opening parenthesis for conditions") + .forType(LPAR) + .within(IF, WHEN_ENTRY, WHILE, DO_WHILE) + .set(Indent.getContinuationWithoutFirstIndent(true)), + + strategy("Closing parenthesis for conditions") + .forType(RPAR) + .forElement { node -> !hasErrorElementBefore(node) } + .within(IF, WHEN_ENTRY, WHILE, DO_WHILE) + .set(Indent.getNoneIndent()), + + strategy("Closing parenthesis for incomplete conditions") + .forType(RPAR) + .forElement { node -> hasErrorElementBefore(node) } + .within(IF, WHEN_ENTRY, WHILE, DO_WHILE) + .set(Indent.getContinuationWithoutFirstIndent()), + + strategy("KDoc comment indent") + .within(KDOC_CONTENT) + .forType(KDocTokens.LEADING_ASTERISK, KDocTokens.END) + .set(Indent.getSpaceIndent(KDOC_COMMENT_INDENT)), + + strategy("Block in when entry") + .within(WHEN_ENTRY) + .notForType( + BLOCK, + WHEN_CONDITION_EXPRESSION, + WHEN_CONDITION_IN_RANGE, + WHEN_CONDITION_IS_PATTERN, + ELSE_KEYWORD, + ARROW, + ) + .set(Indent.getNormalIndent()), + + strategy("Parameter list") + .within(VALUE_PARAMETER_LIST) + .forElement { it.elementType == VALUE_PARAMETER && it.psi.prevSibling != null } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_IN_PARAMETER_LISTS, indentFirst = true), + + strategy("Where clause") + .within(CLASS, FUN, PROPERTY) + .forType(WHERE_KEYWORD) + .set(Indent.getContinuationIndent()), + + strategy("Array literals") + .within(COLLECTION_LITERAL_EXPRESSION) + .notForType(LBRACKET, RBRACKET) + .set(Indent.getNormalIndent()), + + strategy("Type aliases") + .within(TYPEALIAS) + .notForType( + TYPE_ALIAS_KEYWORD, EOL_COMMENT, MODIFIER_LIST, BLOCK_COMMENT, + DOC_COMMENT, + ) + .set(Indent.getContinuationIndent()), + + strategy("Default parameter values") + .within(VALUE_PARAMETER) + .forElement { node -> node.psi != null && node.psi == (node.psi.parent as? KtParameter)?.defaultValue } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES, indentFirst = true), +) + + +private fun getOperationType(node: ASTNode): IElementType? = + node.findChildByType(OPERATION_REFERENCE)?.firstChildNode?.elementType + +fun hasErrorElementBefore(node: ASTNode): Boolean { + val prevSibling = getPrevWithoutWhitespace(node) + ?: return false + + if (prevSibling.elementType == TokenType.ERROR_ELEMENT) + return true + + val lastChild = TreeUtil.getLastChild(prevSibling) + return lastChild?.elementType == TokenType.ERROR_ELEMENT +} + +/** + * Suppress indent for binary expressions when there is a block higher in the tree that forces + * its indent to children ('if' condition or elvis). + */ +private fun ASTNode.suppressBinaryExpressionIndent(): Boolean { + var psi = psi.parent as? KtBinaryExpression ?: return false + while (psi.parent is KtBinaryExpression) { + psi = psi.parent as KtBinaryExpression + } + return psi.parent?.node?.elementType == CONDITION || psi.operationToken == ELVIS +} \ No newline at end of file diff --git a/idea/testData/indentationOnNewline/LargeFile.kt b/idea/testData/indentationOnNewline/LargeFile.kt new file mode 100644 index 00000000000..25a7786c758 --- /dev/null +++ b/idea/testData/indentationOnNewline/LargeFile.kt @@ -0,0 +1,2322 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.formatter.uber + +import com.intellij.application.options.CodeStyle +import com.intellij.formatting.* +import com.intellij.formatting.DependentSpacingRule.Anchor +import com.intellij.formatting.DependentSpacingRule.Trigger +import com.intellij.formatting.SpacingBuilder.RuleBuilder +import com.intellij.lang.ASTNode +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.* +import com.intellij.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.codeStyle.CommonCodeStyleSettings +import com.intellij.psi.impl.source.tree.TreeUtil +import com.intellij.psi.tree.IElementType +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiUtilCore +import com.intellij.util.text.TextRangeUtil +import org.jetbrains.annotations.NonNls +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.KtNodeTypes.* +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.idea.core.formatter.KotlinCodeStyleSettings +import org.jetbrains.kotlin.idea.formatter.CommonAlignmentStrategy +import org.jetbrains.kotlin.idea.formatter.KotlinCommonCodeStyleSettings +import org.jetbrains.kotlin.idea.formatter.uber.NodeIndentStrategy.Companion.strategy +import org.jetbrains.kotlin.kdoc.lexer.KDocTokens +import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.lexer.KtTokens.* +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.psi.stubs.elements.KtModifierListElementType +import org.jetbrains.kotlin.utils.addToStdlib.cast +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import kotlin.math.max + +abstract class NodeIndentStrategy { + + abstract fun getIndent(node: ASTNode, settings: CodeStyleSettings): Indent? + + class ConstIndentStrategy(private val indent: Indent) : NodeIndentStrategy() { + + override fun getIndent(node: ASTNode, settings: CodeStyleSettings): Indent? { + return indent + } + } + + class PositionStrategy(private val debugInfo: String?) : NodeIndentStrategy() { + private var indentCallback: (CodeStyleSettings) -> Indent = { Indent.getNoneIndent() } + + private val within = ArrayList() + private var withinCallback: ((ASTNode) -> Boolean)? = null + + private val notIn = ArrayList() + + private val forElement = ArrayList() + private val notForElement = ArrayList() + private var forElementCallback: ((ASTNode) -> Boolean)? = null + + override fun toString(): String { + return "PositionStrategy " + (debugInfo ?: "No debug info") + } + + fun set(indent: Indent): PositionStrategy { + indentCallback = { indent } + return this + } + + fun set(indentCallback: (CodeStyleSettings) -> Indent): PositionStrategy { + this.indentCallback = indentCallback + return this + } + + fun within(parents: TokenSet): PositionStrategy { + val types = parents.types + if (types.isEmpty()) { + throw IllegalArgumentException("Empty token set is unexpected") + } + + fillTypes(within, types[0], types.copyOfRange(1, types.size)) + return this + } + + fun within(parentType: IElementType, vararg orParentTypes: IElementType): PositionStrategy { + fillTypes(within, parentType, orParentTypes) + return this + } + + fun within(callback: (ASTNode) -> Boolean): PositionStrategy { + withinCallback = callback + return this + } + + fun notWithin(parentType: IElementType, vararg orParentTypes: IElementType): PositionStrategy { + fillTypes(notIn, parentType, orParentTypes) + return this + } + + fun withinAny(): PositionStrategy { + within.clear() + notIn.clear() + return this + } + + fun forType(elementType: IElementType, vararg otherTypes: IElementType): PositionStrategy { + fillTypes(forElement, elementType, otherTypes) + return this + } + + fun notForType(elementType: IElementType, vararg otherTypes: IElementType): PositionStrategy { + fillTypes(notForElement, elementType, otherTypes) + return this + } + + fun forAny(): PositionStrategy { + forElement.clear() + notForElement.clear() + return this + } + + fun forElement(callback: (ASTNode) -> Boolean): PositionStrategy { + forElementCallback = callback + return this + } + + override fun getIndent(node: ASTNode, settings: CodeStyleSettings): Indent? { + if (!isValidIndent(forElement, notForElement, node, forElementCallback)) return null + + val parent = node.treeParent + if (parent != null) { + if (!isValidIndent(within, notIn, parent, withinCallback)) return null + } else if (within.isNotEmpty()) return null + + return indentCallback(settings) + } + + private fun fillTypes(resultCollection: MutableList, singleType: IElementType, otherTypes: Array) { + resultCollection.clear() + resultCollection.add(singleType) + resultCollection.addAll(otherTypes) + } + } + + companion object { + fun constIndent(indent: Indent): NodeIndentStrategy { + return ConstIndentStrategy(indent) + } + + fun strategy(@NonNls debugInfo: String?): PositionStrategy { + return PositionStrategy(debugInfo) + } + } +} + +private fun isValidIndent( + elements: ArrayList, + excludeElements: ArrayList, + node: ASTNode, + callback: ((ASTNode) -> Boolean)? +): Boolean { + if (elements.isNotEmpty() && !elements.contains(node.elementType)) return false + if (excludeElements.contains(node.elementType)) return false + if (callback?.invoke(node) == false) return false + return true +} + +class SyntheticKotlinBlock( + private val node: ASTNode, + private val subBlocks: List, + private val alignment: Alignment?, + private val indent: Indent?, + private val wrap: Wrap?, + private val spacingBuilder: KotlinSpacingBuilder, + private val createParentSyntheticSpacingBlock: (ASTNode) -> ASTBlock +) : ASTBlock { + + private val textRange = TextRange( + subBlocks.first().textRange.startOffset, + subBlocks.last().textRange.endOffset + ) + + override fun getTextRange(): TextRange = textRange + override fun getSubBlocks() = subBlocks + override fun getWrap() = wrap + override fun getIndent() = indent + override fun getAlignment() = alignment + override fun getChildAttributes(newChildIndex: Int) = ChildAttributes(getIndent(), null) + override fun isIncomplete() = getSubBlocks().last().isIncomplete + override fun isLeaf() = false + override fun getNode() = node + override fun getSpacing(child1: Block?, child2: Block): Spacing? { + return spacingBuilder.getSpacing(createParentSyntheticSpacingBlock(node), child1, child2) + } + + + override fun toString(): String { + var child = subBlocks.first() + var treeNode: ASTNode? = null + + loop@ + while (treeNode == null) when (child) { + is SyntheticKotlinBlock -> child = child.getSubBlocks().first() + + else -> treeNode = child.node + } + + val textRange = getTextRange() + val psi = treeNode.psi + if (psi != null) { + val file = psi.containingFile + if (file != null) { + return file.text!!.subSequence(textRange.startOffset, textRange.endOffset).toString() + " " + textRange + } + } + + return this::class.java.name + ": " + textRange + } +} + +/* + * ASTBlock.node is nullable, this extension was introduced to minimize changes + */ +fun ASTBlock.requireNode() = node ?: error("ASTBlock.getNode() returned null") + +/** + * Can be removed with all usages after moving master to 1.3 with new default code style settings. + */ +val isDefaultOfficialCodeStyle by lazy { !KotlinCodeStyleSettings.defaultSettings().CONTINUATION_INDENT_FOR_CHAINED_CALLS } + +fun PsiElement.getLineCount(): Int { + val doc = containingFile?.let { PsiDocumentManager.getInstance(project).getDocument(it) } + if (doc != null) { + val spaceRange = textRange ?: TextRange.EMPTY_RANGE + + if (spaceRange.endOffset <= doc.textLength && spaceRange.startOffset < spaceRange.endOffset) { + val startLine = doc.getLineNumber(spaceRange.startOffset) + val endLine = doc.getLineNumber(spaceRange.endOffset - 1) + + return endLine - startLine + 1 + } + } + + return StringUtil.getLineBreakCount(text ?: "") + 1 +} + +fun PsiElement.isMultiline() = getLineCount() > 1 + +fun KtFunctionLiteral.needTrailingComma(settings: CodeStyleSettings?, checkExistingTrailingComma: Boolean = true): Boolean = + needTrailingComma( + settings = settings, + trailingComma = { if (checkExistingTrailingComma) valueParameterList?.trailingComma else null }, + globalStartOffset = { valueParameterList?.startOffset }, + globalEndOffset = { arrow?.endOffset }, + ) + +fun KtWhenEntry.needTrailingComma(settings: CodeStyleSettings?, checkExistingTrailingComma: Boolean = true): Boolean = needTrailingComma( + settings = settings, + trailingComma = { if (checkExistingTrailingComma) trailingComma else null }, + additionalCheck = { !isElse && parent.cast().leftParenthesis != null }, + globalEndOffset = { arrow?.endOffset }, +) + +fun KtDestructuringDeclaration.needTrailingComma(settings: CodeStyleSettings?, checkExistingTrailingComma: Boolean = true): Boolean = + needTrailingComma( + settings = settings, + trailingComma = { if (checkExistingTrailingComma) trailingComma else null }, + globalStartOffset = { lPar?.startOffset }, + globalEndOffset = { rPar?.endOffset }, + ) + +fun T.needTrailingComma( + settings: CodeStyleSettings?, + trailingComma: T.() -> PsiElement?, + additionalCheck: () -> Boolean = { true }, + globalStartOffset: T.() -> Int? = PsiElement::startOffset, + globalEndOffset: T.() -> Int? = PsiElement::endOffset, +): Boolean { + if (trailingComma() == null && settings?.kotlinCustomSettings?.addTrailingCommaIsAllowedFor(this) == false) return false + if (!additionalCheck()) return false + + val startOffset = globalStartOffset() ?: return false + val endOffset = globalEndOffset() ?: return false + return containsLineBreakInThis(startOffset, endOffset) +} + +fun PsiElement.containsLineBreakInThis(globalStartOffset: Int, globalEndOffset: Int): Boolean { + val textRange = TextRange.create(globalStartOffset, globalEndOffset).shiftLeft(startOffset) + return StringUtil.containsLineBreak(textRange.subSequence(text)) +} + +fun trailingCommaIsAllowedOnCallSite(): Boolean = Registry.`is`("kotlin.formatter.allowTrailingCommaOnCallSite") + +private val TYPES_WITH_TRAILING_COMMA = TokenSet.create( + KtNodeTypes.TYPE_PARAMETER_LIST, + KtNodeTypes.DESTRUCTURING_DECLARATION, + KtNodeTypes.WHEN_ENTRY, + KtNodeTypes.FUNCTION_LITERAL, + KtNodeTypes.VALUE_PARAMETER_LIST, +) + +private val TYPES_WITH_TRAILING_COMMA_ON_CALL_SITE = TokenSet.create( + KtNodeTypes.COLLECTION_LITERAL_EXPRESSION, + KtNodeTypes.TYPE_ARGUMENT_LIST, + KtNodeTypes.INDICES, + KtNodeTypes.VALUE_ARGUMENT_LIST,) + +fun UserDataHolder.addTrailingCommaIsAllowedForThis(): Boolean { + val type = when (this) { + is ASTNode -> PsiUtilCore.getElementType(this) + is PsiElement -> PsiUtilCore.getElementType(this) + else -> return false + } + + return type in TYPES_WITH_TRAILING_COMMA || trailingCommaIsAllowedOnCallSite() && type in TYPES_WITH_TRAILING_COMMA_ON_CALL_SITE +} + +fun KotlinCodeStyleSettings.addTrailingCommaIsAllowedFor(element: UserDataHolder): Boolean = + ALLOW_TRAILING_COMMA && element.addTrailingCommaIsAllowedForThis() + +data class KtCodeStyleSettings( + val custom: KotlinCodeStyleSettings, + val common: KotlinCommonCodeStyleSettings, + val all: CodeStyleSettings +) + +fun KtCodeStyleSettings.canRestore(): Boolean { + return custom.canRestore() || common.canRestore() +} + +fun KtCodeStyleSettings.hasDefaultLoadScheme(): Boolean { + return custom.CODE_STYLE_DEFAULTS == null || common.CODE_STYLE_DEFAULTS == null +} + +fun KtCodeStyleSettings.restore() { + custom.restore() + common.restore() +} + +fun ktCodeStyleSettings(project: Project): KtCodeStyleSettings? { + val settings = CodeStyle.getSettings(project) + + val ktCommonSettings = settings.getCommonSettings(KotlinLanguage.INSTANCE) as KotlinCommonCodeStyleSettings + val ktCustomSettings = settings.getCustomSettings(KotlinCodeStyleSettings::class.java) + + return KtCodeStyleSettings(ktCustomSettings, ktCommonSettings, settings) +} + +val CodeStyleSettings.kotlinCommonSettings: KotlinCommonCodeStyleSettings + get() = getCommonSettings(KotlinLanguage.INSTANCE) as KotlinCommonCodeStyleSettings + +val CodeStyleSettings.kotlinCustomSettings: KotlinCodeStyleSettings + get() = getCustomSettings(KotlinCodeStyleSettings::class.java) + +fun CodeStyleSettings.kotlinCodeStyleDefaults(): String? { + return kotlinCustomSettings.CODE_STYLE_DEFAULTS ?: kotlinCommonSettings.CODE_STYLE_DEFAULTS +} + +fun CommonCodeStyleSettings.createSpaceBeforeRBrace(numSpacesOtherwise: Int, textRange: TextRange): Spacing? { + return Spacing.createDependentLFSpacing( + numSpacesOtherwise, numSpacesOtherwise, textRange, + KEEP_LINE_BREAKS, + KEEP_BLANK_LINES_BEFORE_RBRACE + ) +} + +class KotlinSpacingBuilder(val commonCodeStyleSettings: CommonCodeStyleSettings, val spacingBuilderUtil: KotlinSpacingBuilderUtil) { + private val builders = ArrayList() + + private interface Builder { + fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? + } + + inner class BasicSpacingBuilder : SpacingBuilder(commonCodeStyleSettings), Builder { + override fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? { + return super.getSpacing(parent, left, right) + } + } + + private data class Condition( + val parent: IElementType? = null, + val left: IElementType? = null, + val right: IElementType? = null, + val parentSet: TokenSet? = null, + val leftSet: TokenSet? = null, + val rightSet: TokenSet? = null + ) : (ASTBlock, ASTBlock, ASTBlock) -> Boolean { + override fun invoke(p: ASTBlock, l: ASTBlock, r: ASTBlock): Boolean = + (parent == null || p.requireNode().elementType == parent) && + (left == null || l.requireNode().elementType == left) && + (right == null || r.requireNode().elementType == right) && + (parentSet == null || parentSet.contains(p.requireNode().elementType)) && + (leftSet == null || leftSet.contains(l.requireNode().elementType)) && + (rightSet == null || rightSet.contains(r.requireNode().elementType)) + } + + private data class Rule( + val conditions: List, + val action: (ASTBlock, ASTBlock, ASTBlock) -> Spacing? + ) : (ASTBlock, ASTBlock, ASTBlock) -> Spacing? { + override fun invoke(p: ASTBlock, l: ASTBlock, r: ASTBlock): Spacing? = + if (conditions.all { it(p, l, r) }) action(p, l, r) else null + } + + inner class CustomSpacingBuilder : Builder { + private val rules = ArrayList() + private var conditions = ArrayList() + + override fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? { + for (rule in rules) { + val spacing = rule(parent, left, right) + if (spacing != null) { + return spacing + } + } + return null + } + + fun inPosition( + parent: IElementType? = null, left: IElementType? = null, right: IElementType? = null, + parentSet: TokenSet? = null, leftSet: TokenSet? = null, rightSet: TokenSet? = null + ): CustomSpacingBuilder { + conditions.add(Condition(parent, left, right, parentSet, leftSet, rightSet)) + return this + } + + fun lineBreakIfLineBreakInParent(numSpacesOtherwise: Int, allowBlankLines: Boolean = true) { + newRule { p, _, _ -> + Spacing.createDependentLFSpacing( + numSpacesOtherwise, numSpacesOtherwise, p.textRange, + commonCodeStyleSettings.KEEP_LINE_BREAKS, + if (allowBlankLines) commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE else 0 + ) + } + } + + fun emptyLinesIfLineBreakInLeft(emptyLines: Int, numberOfLineFeedsOtherwise: Int = 1, numSpacesOtherwise: Int = 0) { + newRule { _: ASTBlock, left: ASTBlock, _: ASTBlock -> + val lastChild = left.node?.psi?.lastChild + val leftEndsWithComment = lastChild is PsiComment && lastChild.tokenType == KtTokens.EOL_COMMENT + val dependentSpacingRule = DependentSpacingRule(Trigger.HAS_LINE_FEEDS).registerData(Anchor.MIN_LINE_FEEDS, emptyLines + 1) + val textRange = left.node + ?.startOfDeclaration() + ?.startOffset + ?.let { TextRange.create(it, left.textRange.endOffset) } + ?: left.textRange + + spacingBuilderUtil.createLineFeedDependentSpacing( + numSpacesOtherwise, + numSpacesOtherwise, + if (leftEndsWithComment) max(1, numberOfLineFeedsOtherwise) else numberOfLineFeedsOtherwise, + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_DECLARATIONS, + textRange, + dependentSpacingRule + ) + } + } + + fun spacing(spacing: Spacing) { + newRule { _, _, _ -> spacing } + } + + fun customRule(block: (parent: ASTBlock, left: ASTBlock, right: ASTBlock) -> Spacing?) { + newRule(block) + } + + private fun newRule(rule: (ASTBlock, ASTBlock, ASTBlock) -> Spacing?) { + val savedConditions = ArrayList(conditions) + rules.add(Rule(savedConditions, rule)) + conditions.clear() + } + } + + fun getSpacing(parent: Block, child1: Block?, child2: Block): Spacing? { + if (parent !is ASTBlock || child1 !is ASTBlock || child2 !is ASTBlock) { + return null + } + + for (builder in builders) { + val spacing = builder.getSpacing(parent, child1, child2) + + if (spacing != null) { + // TODO: it's a severe hack but I don't know how to implement it in other way + if (child1.requireNode().elementType == KtTokens.EOL_COMMENT && spacing.toString().contains("minLineFeeds=0")) { + val isBeforeBlock = + child2.requireNode().elementType == KtNodeTypes.BLOCK || child2.requireNode().firstChildNode + ?.elementType == KtNodeTypes.BLOCK + val keepBlankLines = if (isBeforeBlock) 0 else commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + return createSpacing(0, minLineFeeds = 1, keepLineBreaks = true, keepBlankLines = keepBlankLines) + } + return spacing + } + } + return null + } + + fun simple(init: BasicSpacingBuilder.() -> Unit) { + val builder = BasicSpacingBuilder() + builder.init() + builders.add(builder) + } + + fun custom(init: CustomSpacingBuilder.() -> Unit) { + val builder = CustomSpacingBuilder() + builder.init() + builders.add(builder) + } + + fun createSpacing( + minSpaces: Int, + maxSpaces: Int = minSpaces, + minLineFeeds: Int = 0, + keepLineBreaks: Boolean = commonCodeStyleSettings.KEEP_LINE_BREAKS, + keepBlankLines: Int = commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ): Spacing { + return Spacing.createSpacing(minSpaces, maxSpaces, minLineFeeds, keepLineBreaks, keepBlankLines) + } +} + +interface KotlinSpacingBuilderUtil { + fun createLineFeedDependentSpacing( + minSpaces: Int, + maxSpaces: Int, + minimumLineFeeds: Int, + keepLineBreaks: Boolean, + keepBlankLines: Int, + dependency: TextRange, + rule: DependentSpacingRule + ): Spacing + + fun getPreviousNonWhitespaceLeaf(node: ASTNode?): ASTNode? + + fun isWhitespaceOrEmpty(node: ASTNode?): Boolean +} + +fun rules( + commonCodeStyleSettings: CommonCodeStyleSettings, + builderUtil: KotlinSpacingBuilderUtil, + init: KotlinSpacingBuilder.() -> Unit +): KotlinSpacingBuilder { + val builder = KotlinSpacingBuilder(commonCodeStyleSettings, builderUtil) + builder.init() + return builder +} + +internal fun ASTNode.startOfDeclaration(): ASTNode? = children().firstOrNull { + val elementType = it.elementType + elementType !is KtModifierListElementType<*> && elementType !in KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET +} + +val MODIFIERS_LIST_ENTRIES = TokenSet.orSet(TokenSet.create(ANNOTATION_ENTRY, ANNOTATION), MODIFIER_KEYWORDS) + +val EXTEND_COLON_ELEMENTS = + TokenSet.create(TYPE_CONSTRAINT, CLASS, OBJECT_DECLARATION, TYPE_PARAMETER, ENUM_ENTRY, SECONDARY_CONSTRUCTOR) + +val DECLARATIONS = TokenSet.create(PROPERTY, FUN, CLASS, OBJECT_DECLARATION, ENUM_ENTRY, SECONDARY_CONSTRUCTOR, CLASS_INITIALIZER) + +fun SpacingBuilder.beforeInside(element: IElementType, tokenSet: TokenSet, spacingFun: RuleBuilder.() -> Unit) { + tokenSet.types.forEach { inType -> beforeInside(element, inType).spacingFun() } +} + +fun SpacingBuilder.afterInside(element: IElementType, tokenSet: TokenSet, spacingFun: RuleBuilder.() -> Unit) { + tokenSet.types.forEach { inType -> afterInside(element, inType).spacingFun() } +} + +fun RuleBuilder.spacesNoLineBreak(spaces: Int): SpacingBuilder? = + spacing(spaces, spaces, 0, false, 0) + +fun createSpacingBuilder(settings: CodeStyleSettings, builderUtil: KotlinSpacingBuilderUtil): KotlinSpacingBuilder { + val kotlinCommonSettings = settings.kotlinCommonSettings + val kotlinCustomSettings = settings.kotlinCustomSettings + return rules(kotlinCommonSettings, builderUtil) { + simple { + before(FILE_ANNOTATION_LIST).lineBreakInCode() + between(IMPORT_DIRECTIVE, IMPORT_DIRECTIVE).lineBreakInCode() + after(IMPORT_LIST).blankLines(1) + } + + custom { + fun commentSpacing(minSpaces: Int): Spacing { + if (kotlinCommonSettings.KEEP_FIRST_COLUMN_COMMENT) { + return Spacing.createKeepingFirstColumnSpacing( + minSpaces, + Int.MAX_VALUE, + settings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + } + return Spacing.createSpacing( + minSpaces, + Int.MAX_VALUE, + 0, + settings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + } + + // Several line comments happened to be generated in one line + inPosition(parent = null, left = EOL_COMMENT, right = EOL_COMMENT).customRule { _, _, right -> + val nodeBeforeRight = right.requireNode().treePrev + if (nodeBeforeRight is PsiWhiteSpace && !nodeBeforeRight.textContains('\n')) { + createSpacing(0, minLineFeeds = 1) + } else { + null + } + } + + inPosition(right = BLOCK_COMMENT).spacing(commentSpacing(0)) + inPosition(right = EOL_COMMENT).spacing(commentSpacing(1)) + inPosition(parent = FUNCTION_LITERAL, right = BLOCK).customRule { _, _, right -> + when (right.node?.children()?.firstOrNull()?.elementType) { + BLOCK_COMMENT -> commentSpacing(0) + EOL_COMMENT -> commentSpacing(1) + else -> null + } + } + } + + simple { + after(FILE_ANNOTATION_LIST).blankLines(1) + after(PACKAGE_DIRECTIVE).blankLines(1) + } + + custom { + inPosition(leftSet = DECLARATIONS, rightSet = DECLARATIONS).customRule(fun( + _: ASTBlock, + _: ASTBlock, + right: ASTBlock + ): Spacing? { + val node = right.node ?: return null + val elementStart = node.startOfDeclaration() ?: return null + return if (StringUtil.containsLineBreak( + node.text.subSequence(0, elementStart.startOffset - node.startOffset).trimStart() + ) + ) + createSpacing(0, minLineFeeds = 2) + else + null + }) + + inPosition(left = CLASS, right = CLASS).emptyLinesIfLineBreakInLeft(1) + inPosition(left = CLASS, right = OBJECT_DECLARATION).emptyLinesIfLineBreakInLeft(1) + inPosition(left = OBJECT_DECLARATION, right = OBJECT_DECLARATION).emptyLinesIfLineBreakInLeft(1) + inPosition(left = OBJECT_DECLARATION, right = CLASS).emptyLinesIfLineBreakInLeft(1) + inPosition(left = FUN, right = FUN).emptyLinesIfLineBreakInLeft(1) + inPosition(left = PROPERTY, right = FUN).emptyLinesIfLineBreakInLeft(1) + inPosition(left = FUN, right = PROPERTY).emptyLinesIfLineBreakInLeft(1) + inPosition(left = SECONDARY_CONSTRUCTOR, right = SECONDARY_CONSTRUCTOR).emptyLinesIfLineBreakInLeft(1) + inPosition(left = TYPEALIAS, right = TYPEALIAS).emptyLinesIfLineBreakInLeft(1) + + // Case left for alternative constructors + inPosition(left = FUN, right = CLASS).emptyLinesIfLineBreakInLeft(1) + + inPosition(left = ENUM_ENTRY, right = ENUM_ENTRY).emptyLinesIfLineBreakInLeft( + emptyLines = 0, numberOfLineFeedsOtherwise = 0, numSpacesOtherwise = 1 + ) + + inPosition(parent = CLASS_BODY, left = SEMICOLON).customRule { parent, _, right -> + val klass = parent.requireNode().treeParent.psi as? KtClass ?: return@customRule null + if (klass.isEnum() && right.requireNode().elementType in DECLARATIONS) { + createSpacing(0, minLineFeeds = 2, keepBlankLines = settings.KEEP_BLANK_LINES_IN_DECLARATIONS) + } else null + } + + inPosition(parent = CLASS_BODY, left = LBRACE).customRule { parent, left, right -> + if (right.requireNode().elementType == RBRACE) { + return@customRule createSpacing(0) + } + val classBody = parent.requireNode().psi as KtClassBody + val parentPsi = classBody.parent as? KtClassOrObject ?: return@customRule null + if (kotlinCommonSettings.BLANK_LINES_AFTER_CLASS_HEADER == 0 || parentPsi.isObjectLiteral()) { + null + } else { + val minLineFeeds = if (right.requireNode().elementType == FUN || right.requireNode().elementType == PROPERTY) + kotlinCommonSettings.BLANK_LINES_AFTER_CLASS_HEADER + 1 + else + 0 + + builderUtil.createLineFeedDependentSpacing( + 1, + 1, + minLineFeeds, + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_DECLARATIONS, + TextRange(parentPsi.textRange.startOffset, left.requireNode().psi.textRange.startOffset), + DependentSpacingRule(DependentSpacingRule.Trigger.HAS_LINE_FEEDS) + .registerData( + DependentSpacingRule.Anchor.MIN_LINE_FEEDS, + kotlinCommonSettings.BLANK_LINES_AFTER_CLASS_HEADER + 1 + ) + ) + } + } + + val parameterWithDocCommentRule = { _: ASTBlock, _: ASTBlock, right: ASTBlock -> + if (right.requireNode().firstChildNode.elementType == DOC_COMMENT) { + createSpacing(0, minLineFeeds = 1, keepLineBreaks = true, keepBlankLines = settings.KEEP_BLANK_LINES_IN_DECLARATIONS) + } else { + null + } + } + inPosition(parent = VALUE_PARAMETER_LIST, right = VALUE_PARAMETER).customRule(parameterWithDocCommentRule) + + inPosition(parent = PROPERTY, right = PROPERTY_ACCESSOR).customRule { parent, _, _ -> + val startNode = parent.requireNode().psi.firstChild + .siblings() + .dropWhile { it is PsiComment || it is PsiWhiteSpace }.firstOrNull() ?: parent.requireNode().psi + Spacing.createDependentLFSpacing( + 1, 1, + TextRange(startNode.textRange.startOffset, parent.textRange.endOffset), + false, 0 + ) + } + + if (!kotlinCustomSettings.ALLOW_TRAILING_COMMA) { + inPosition(parent = VALUE_ARGUMENT_LIST, left = LPAR).customRule { parent, _, _ -> + if (kotlinCommonSettings.CALL_PARAMETERS_LPAREN_ON_NEXT_LINE && org.jetbrains.kotlin.idea.formatter.needWrapArgumentList( + parent.requireNode().psi + ) + ) { + Spacing.createDependentLFSpacing( + 0, 0, + excludeLambdasAndObjects(parent), + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ) + } else { + createSpacing(0) + } + } + + inPosition(parent = VALUE_ARGUMENT_LIST, right = RPAR).customRule { parent, left, _ -> + when { + kotlinCommonSettings.CALL_PARAMETERS_RPAREN_ON_NEXT_LINE -> + Spacing.createDependentLFSpacing( + 0, 0, + excludeLambdasAndObjects(parent), + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ) + left.requireNode().elementType == COMMA -> // incomplete call being edited + createSpacing(1) + else -> + createSpacing(0) + } + } + } + + inPosition(left = CONDITION, right = RPAR).customRule { _, left, _ -> + if (kotlinCustomSettings.IF_RPAREN_ON_NEW_LINE) { + Spacing.createDependentLFSpacing( + 0, 0, + excludeLambdasAndObjects(left), + commonCodeStyleSettings.KEEP_LINE_BREAKS, + commonCodeStyleSettings.KEEP_BLANK_LINES_IN_CODE + ) + } else { + createSpacing(0) + } + } + + inPosition(left = VALUE_PARAMETER, right = COMMA).customRule { _, left, _ -> + if (left.node?.lastChildNode?.elementType === EOL_COMMENT) + createSpacing(0, minLineFeeds = 1) + else + null + } + + inPosition(parent = LONG_STRING_TEMPLATE_ENTRY, right = LONG_TEMPLATE_ENTRY_END).lineBreakIfLineBreakInParent(0) + inPosition(parent = LONG_STRING_TEMPLATE_ENTRY, left = LONG_TEMPLATE_ENTRY_START).lineBreakIfLineBreakInParent(0) + } + + simple { + // ============ Line breaks ============== + before(DOC_COMMENT).lineBreakInCode() + between(PROPERTY, PROPERTY).lineBreakInCode() + + // CLASS - CLASS, CLASS - OBJECT_DECLARATION are exception + between(CLASS, DECLARATIONS).blankLines(1) + + // FUN - FUN, FUN - PROPERTY, FUN - CLASS are exceptions + between(FUN, DECLARATIONS).blankLines(1) + + // PROPERTY - PROPERTY, PROPERTY - FUN are exceptions + between(PROPERTY, DECLARATIONS).blankLines(1) + + // OBJECT_DECLARATION - OBJECT_DECLARATION, CLASS - OBJECT_DECLARATION are exception + between(OBJECT_DECLARATION, DECLARATIONS).blankLines(1) + between(SECONDARY_CONSTRUCTOR, DECLARATIONS).blankLines(1) + between(CLASS_INITIALIZER, DECLARATIONS).blankLines(1) + + // TYPEALIAS - TYPEALIAS is an exception + between(TYPEALIAS, DECLARATIONS).blankLines(1) + + // ENUM_ENTRY - ENUM_ENTRY is exception + between(ENUM_ENTRY, DECLARATIONS).blankLines(1) + + between(ENUM_ENTRY, SEMICOLON).spaces(0) + + between(COMMA, SEMICOLON).lineBreakInCodeIf(kotlinCustomSettings.ALLOW_TRAILING_COMMA) + + beforeInside(FUN, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(SECONDARY_CONSTRUCTOR, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(CLASS, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(OBJECT_DECLARATION, TokenSet.create(BODY, CLASS_BODY)).lineBreakInCode() + beforeInside(PROPERTY, WHEN).spaces(0) + beforeInside(PROPERTY, LABELED_EXPRESSION).spacesNoLineBreak(1) + before(PROPERTY).lineBreakInCode() + + after(DOC_COMMENT).lineBreakInCode() + + // =============== Spacing ================ + between(EOL_COMMENT, COMMA).lineBreakInCode() + before(COMMA).spacesNoLineBreak(if (kotlinCommonSettings.SPACE_BEFORE_COMMA) 1 else 0) + after(COMMA).spaceIf(kotlinCommonSettings.SPACE_AFTER_COMMA) + + val spacesAroundAssignment = if (kotlinCommonSettings.SPACE_AROUND_ASSIGNMENT_OPERATORS) 1 else 0 + beforeInside(EQ, PROPERTY).spacesNoLineBreak(spacesAroundAssignment) + beforeInside(EQ, FUN).spacing(spacesAroundAssignment, spacesAroundAssignment, 0, false, 0) + + around( + TokenSet.create(EQ, MULTEQ, DIVEQ, PLUSEQ, MINUSEQ, PERCEQ) + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_ASSIGNMENT_OPERATORS) + around(TokenSet.create(ANDAND, OROR)).spaceIf(kotlinCommonSettings.SPACE_AROUND_LOGICAL_OPERATORS) + around(TokenSet.create(EQEQ, EXCLEQ, EQEQEQ, EXCLEQEQEQ)).spaceIf(kotlinCommonSettings.SPACE_AROUND_EQUALITY_OPERATORS) + aroundInside( + TokenSet.create(LT, GT, LTEQ, GTEQ), BINARY_EXPRESSION + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_RELATIONAL_OPERATORS) + aroundInside(TokenSet.create(PLUS, MINUS), BINARY_EXPRESSION).spaceIf(kotlinCommonSettings.SPACE_AROUND_ADDITIVE_OPERATORS) + aroundInside( + TokenSet.create(MUL, DIV, PERC), BINARY_EXPRESSION + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_MULTIPLICATIVE_OPERATORS) + around( + TokenSet.create(PLUSPLUS, MINUSMINUS, EXCLEXCL, MINUS, PLUS, EXCL) + ).spaceIf(kotlinCommonSettings.SPACE_AROUND_UNARY_OPERATOR) + before(ELVIS).spaces(1) + after(ELVIS).spacesNoLineBreak(1) + around(RANGE).spaceIf(kotlinCustomSettings.SPACE_AROUND_RANGE) + + after(MODIFIER_LIST).spaces(1) + + beforeInside(IDENTIFIER, CLASS).spaces(1) + beforeInside(IDENTIFIER, OBJECT_DECLARATION).spaces(1) + + after(VAL_KEYWORD).spaces(1) + after(VAR_KEYWORD).spaces(1) + betweenInside(TYPE_PARAMETER_LIST, IDENTIFIER, PROPERTY).spaces(1) + betweenInside(TYPE_REFERENCE, DOT, PROPERTY).spacing(0, 0, 0, false, 0) + betweenInside(DOT, IDENTIFIER, PROPERTY).spacing(0, 0, 0, false, 0) + + betweenInside(RETURN_KEYWORD, LABEL_QUALIFIER, RETURN).spaces(0) + afterInside(RETURN_KEYWORD, RETURN).spaces(1) + afterInside(LABEL_QUALIFIER, RETURN).spaces(1) + betweenInside(LABEL_QUALIFIER, EOL_COMMENT, LABELED_EXPRESSION).spacing( + 0, Int.MAX_VALUE, 0, true, kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + betweenInside(LABEL_QUALIFIER, BLOCK_COMMENT, LABELED_EXPRESSION).spacing( + 0, Int.MAX_VALUE, 0, true, kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + betweenInside(LABEL_QUALIFIER, LAMBDA_EXPRESSION, LABELED_EXPRESSION).spaces(0) + afterInside(LABEL_QUALIFIER, LABELED_EXPRESSION).spaces(1) + + betweenInside(FUN_KEYWORD, VALUE_PARAMETER_LIST, FUN).spacing(0, 0, 0, false, 0) + after(FUN_KEYWORD).spaces(1) + betweenInside(TYPE_PARAMETER_LIST, TYPE_REFERENCE, FUN).spaces(1) + betweenInside(TYPE_PARAMETER_LIST, IDENTIFIER, FUN).spaces(1) + betweenInside(TYPE_REFERENCE, DOT, FUN).spacing(0, 0, 0, false, 0) + betweenInside(DOT, IDENTIFIER, FUN).spacing(0, 0, 0, false, 0) + afterInside(IDENTIFIER, FUN).spacing(0, 0, 0, false, 0) + aroundInside(DOT, USER_TYPE).spaces(0) + + around(AS_KEYWORD).spaces(1) + around(AS_SAFE).spaces(1) + around(IS_KEYWORD).spaces(1) + around(NOT_IS).spaces(1) + around(IN_KEYWORD).spaces(1) + around(NOT_IN).spaces(1) + aroundInside(IDENTIFIER, BINARY_EXPRESSION).spaces(1) + + // before LPAR in constructor(): this() {} + after(CONSTRUCTOR_DELEGATION_REFERENCE).spacing(0, 0, 0, false, 0) + + // class A() - no space before LPAR of PRIMARY_CONSTRUCTOR + // class A private() - one space before modifier + custom { + inPosition(right = PRIMARY_CONSTRUCTOR).customRule { _, _, r -> + val spacesCount = if (r.requireNode().findLeafElementAt(0)?.elementType != LPAR) 1 else 0 + createSpacing(spacesCount, minLineFeeds = 0, keepLineBreaks = true, keepBlankLines = 0) + } + } + + afterInside(CONSTRUCTOR_KEYWORD, PRIMARY_CONSTRUCTOR).spaces(0) + betweenInside(IDENTIFIER, TYPE_PARAMETER_LIST, CLASS).spaces(0) + + beforeInside(DOT, DOT_QUALIFIED_EXPRESSION).spaces(0) + afterInside(DOT, DOT_QUALIFIED_EXPRESSION).spacesNoLineBreak(0) + beforeInside(SAFE_ACCESS, SAFE_ACCESS_EXPRESSION).spaces(0) + afterInside(SAFE_ACCESS, SAFE_ACCESS_EXPRESSION).spacesNoLineBreak(0) + + between(MODIFIERS_LIST_ENTRIES, MODIFIERS_LIST_ENTRIES).spaces(1) + + after(LBRACKET).spaces(0) + before(RBRACKET).spaces(0) + + afterInside(LPAR, VALUE_PARAMETER_LIST).spaces(0, kotlinCommonSettings.METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE) + beforeInside(RPAR, VALUE_PARAMETER_LIST).spaces(0, kotlinCommonSettings.METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE) + afterInside(LT, TYPE_PARAMETER_LIST).spaces(0) + beforeInside(GT, TYPE_PARAMETER_LIST).spaces(0) + afterInside(LT, TYPE_ARGUMENT_LIST).spaces(0) + beforeInside(GT, TYPE_ARGUMENT_LIST).spaces(0) + before(TYPE_ARGUMENT_LIST).spaces(0) + + after(LPAR).spaces(0) + before(RPAR).spaces(0) + + betweenInside(FOR_KEYWORD, LPAR, FOR).spaceIf(kotlinCommonSettings.SPACE_BEFORE_FOR_PARENTHESES) + betweenInside(IF_KEYWORD, LPAR, IF).spaceIf(kotlinCommonSettings.SPACE_BEFORE_IF_PARENTHESES) + betweenInside(WHILE_KEYWORD, LPAR, WHILE).spaceIf(kotlinCommonSettings.SPACE_BEFORE_WHILE_PARENTHESES) + betweenInside(WHILE_KEYWORD, LPAR, DO_WHILE).spaceIf(kotlinCommonSettings.SPACE_BEFORE_WHILE_PARENTHESES) + betweenInside(WHEN_KEYWORD, LPAR, WHEN).spaceIf(kotlinCustomSettings.SPACE_BEFORE_WHEN_PARENTHESES) + betweenInside(CATCH_KEYWORD, VALUE_PARAMETER_LIST, CATCH).spaceIf(kotlinCommonSettings.SPACE_BEFORE_CATCH_PARENTHESES) + + betweenInside(LPAR, VALUE_PARAMETER, FOR).spaces(0) + betweenInside(LPAR, DESTRUCTURING_DECLARATION, FOR).spaces(0) + betweenInside(LOOP_RANGE, RPAR, FOR).spaces(0) + + afterInside(ANNOTATION_ENTRY, ANNOTATED_EXPRESSION).spaces(1) + + before(SEMICOLON).spaces(0) + + beforeInside(INITIALIZER_LIST, ENUM_ENTRY).spaces(0) + + beforeInside(QUEST, NULLABLE_TYPE).spaces(0) + + val TYPE_COLON_ELEMENTS = TokenSet.create(PROPERTY, FUN, VALUE_PARAMETER, DESTRUCTURING_DECLARATION_ENTRY, FUNCTION_LITERAL) + beforeInside(COLON, TYPE_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_BEFORE_TYPE_COLON) } + afterInside(COLON, TYPE_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_AFTER_TYPE_COLON) } + + afterInside(COLON, EXTEND_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_AFTER_EXTEND_COLON) } + + beforeInside(ARROW, FUNCTION_LITERAL).spaceIf(kotlinCustomSettings.SPACE_BEFORE_LAMBDA_ARROW) + + aroundInside(ARROW, FUNCTION_TYPE).spaceIf(kotlinCustomSettings.SPACE_AROUND_FUNCTION_TYPE_ARROW) + + before(VALUE_ARGUMENT_LIST).spaces(0) + between(VALUE_ARGUMENT_LIST, LAMBDA_ARGUMENT).spaces(1) + betweenInside(REFERENCE_EXPRESSION, LAMBDA_ARGUMENT, CALL_EXPRESSION).spaces(1) + betweenInside(TYPE_ARGUMENT_LIST, LAMBDA_ARGUMENT, CALL_EXPRESSION).spaces(1) + + around(COLONCOLON).spaces(0) + + around(BY_KEYWORD).spaces(1) + betweenInside(IDENTIFIER, PROPERTY_DELEGATE, PROPERTY).spaces(1) + betweenInside(TYPE_REFERENCE, PROPERTY_DELEGATE, PROPERTY).spaces(1) + + before(INDICES).spaces(0) + before(WHERE_KEYWORD).spaces(1) + + afterInside(GET_KEYWORD, PROPERTY_ACCESSOR).spaces(0) + afterInside(SET_KEYWORD, PROPERTY_ACCESSOR).spaces(0) + } + custom { + + fun KotlinSpacingBuilder.CustomSpacingBuilder.ruleForKeywordOnNewLine( + shouldBeOnNewLine: Boolean, + keyword: IElementType, + parent: IElementType, + afterBlockFilter: (wordParent: ASTNode, block: ASTNode) -> Boolean = { _, _ -> true } + ) { + if (shouldBeOnNewLine) { + inPosition(parent = parent, right = keyword) + .lineBreakIfLineBreakInParent(numSpacesOtherwise = 1, allowBlankLines = false) + } else { + inPosition(parent = parent, right = keyword).customRule { _, _, right -> + + val previousLeaf = builderUtil.getPreviousNonWhitespaceLeaf(right.requireNode()) + val leftBlock = if ( + previousLeaf != null && + previousLeaf.elementType == RBRACE && + previousLeaf.treeParent?.elementType == BLOCK + ) { + previousLeaf.treeParent!! + } else null + + val removeLineBreaks = leftBlock != null && afterBlockFilter(right.node?.treeParent!!, leftBlock) + createSpacing(1, minLineFeeds = 0, keepLineBreaks = !removeLineBreaks, keepBlankLines = 0) + } + } + } + + ruleForKeywordOnNewLine(kotlinCommonSettings.ELSE_ON_NEW_LINE, keyword = ELSE_KEYWORD, parent = IF) { keywordParent, block -> + block.treeParent?.elementType == THEN && block.treeParent?.treeParent == keywordParent + } + ruleForKeywordOnNewLine( + kotlinCommonSettings.WHILE_ON_NEW_LINE, + keyword = WHILE_KEYWORD, + parent = DO_WHILE + ) { keywordParent, block -> + block.treeParent?.elementType == BODY && block.treeParent?.treeParent == keywordParent + } + ruleForKeywordOnNewLine(kotlinCommonSettings.CATCH_ON_NEW_LINE, keyword = CATCH, parent = TRY) + ruleForKeywordOnNewLine(kotlinCommonSettings.FINALLY_ON_NEW_LINE, keyword = FINALLY, parent = TRY) + + + fun spacingForLeftBrace(block: ASTNode?, blockType: IElementType = BLOCK): Spacing? { + if (block != null && block.elementType == blockType) { + val leftBrace = block.findChildByType(LBRACE) + if (leftBrace != null) { + val previousLeaf = builderUtil.getPreviousNonWhitespaceLeaf(leftBrace) + val isAfterEolComment = previousLeaf != null && (previousLeaf.elementType == EOL_COMMENT) + val keepLineBreaks = kotlinCustomSettings.LBRACE_ON_NEXT_LINE || isAfterEolComment + val minimumLF = if (kotlinCustomSettings.LBRACE_ON_NEXT_LINE) 1 else 0 + return createSpacing(1, minLineFeeds = minimumLF, keepLineBreaks = keepLineBreaks, keepBlankLines = 0) + } + } + return createSpacing(1) + } + + fun leftBraceRule(blockType: IElementType = BLOCK) = { _: ASTBlock, _: ASTBlock, right: ASTBlock -> + spacingForLeftBrace(right.node, blockType) + } + + val leftBraceRuleIfBlockIsWrapped = { _: ASTBlock, _: ASTBlock, right: ASTBlock -> + spacingForLeftBrace(right.requireNode().firstChildNode) + } + + // Add space after a semicolon if there is another child at the same line + inPosition(left = SEMICOLON).customRule { _, left, _ -> + val nodeAfterLeft = left.requireNode().treeNext + if (nodeAfterLeft is PsiWhiteSpace && !nodeAfterLeft.textContains('\n')) { + createSpacing(1) + } else { + null + } + } + + inPosition(parent = IF, right = THEN).customRule(leftBraceRuleIfBlockIsWrapped) + inPosition(parent = IF, right = ELSE).customRule(leftBraceRuleIfBlockIsWrapped) + + inPosition(parent = FOR, right = BODY).customRule(leftBraceRuleIfBlockIsWrapped) + inPosition(parent = WHILE, right = BODY).customRule(leftBraceRuleIfBlockIsWrapped) + inPosition(parent = DO_WHILE, right = BODY).customRule(leftBraceRuleIfBlockIsWrapped) + + inPosition(parent = TRY, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = CATCH, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = FINALLY, right = BLOCK).customRule(leftBraceRule()) + + inPosition(parent = FUN, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = SECONDARY_CONSTRUCTOR, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = CLASS_INITIALIZER, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = PROPERTY_ACCESSOR, right = BLOCK).customRule(leftBraceRule()) + + inPosition(right = CLASS_BODY).customRule(leftBraceRule(blockType = CLASS_BODY)) + + inPosition(left = WHEN_ENTRY, right = WHEN_ENTRY).customRule { _, left, right -> + val leftEntry = left.requireNode().psi as KtWhenEntry + val rightEntry = right.requireNode().psi as KtWhenEntry + val blankLines = if (leftEntry.expression is KtBlockExpression || rightEntry.expression is KtBlockExpression) + settings.kotlinCustomSettings.BLANK_LINES_AROUND_BLOCK_WHEN_BRANCHES + else + 0 + + createSpacing(0, minLineFeeds = blankLines + 1) + } + + inPosition(parent = WHEN_ENTRY, right = BLOCK).customRule(leftBraceRule()) + inPosition(parent = WHEN, right = LBRACE).customRule { parent, _, _ -> + spacingForLeftBrace(block = parent.requireNode(), blockType = WHEN) + } + + inPosition(left = LBRACE, right = WHEN_ENTRY).customRule { _, _, _ -> + createSpacing(0, minLineFeeds = 1) + } + + val spacesInSimpleFunction = if (kotlinCustomSettings.INSERT_WHITESPACES_IN_SIMPLE_ONE_LINE_METHOD) 1 else 0 + inPosition( + parent = FUNCTION_LITERAL, + left = LBRACE, + right = BLOCK + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = spacesInSimpleFunction) + + inPosition( + parent = FUNCTION_LITERAL, + left = ARROW, + right = BLOCK + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = 1) + + inPosition( + parent = FUNCTION_LITERAL, + left = LBRACE, + right = RBRACE + ).spacing(createSpacing(minSpaces = 0, maxSpaces = 1)) + + inPosition( + parent = FUNCTION_LITERAL, + right = RBRACE + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = spacesInSimpleFunction) + + inPosition( + parent = FUNCTION_LITERAL, + left = LBRACE + ).customRule { _, _, right -> + val rightNode = right.requireNode() + val rightType = rightNode.elementType + if (rightType == VALUE_PARAMETER_LIST) { + createSpacing(spacesInSimpleFunction, keepLineBreaks = false) + } else { + createSpacing(spacesInSimpleFunction) + } + } + + inPosition(parent = CLASS_BODY, right = RBRACE).customRule { parent, _, _ -> + kotlinCommonSettings.createSpaceBeforeRBrace(1, parent.textRange) + } + + inPosition(parent = BLOCK, right = RBRACE).customRule { block, left, _ -> + val psiElement = block.requireNode().treeParent.psi + + val empty = left.requireNode().elementType == LBRACE + + when (psiElement) { + is KtFunction -> { + if (psiElement.name != null && !empty) return@customRule null + } + is KtPropertyAccessor -> + if (!empty) return@customRule null + else -> + return@customRule null + } + + val spaces = if (empty) 0 else spacesInSimpleFunction + kotlinCommonSettings.createSpaceBeforeRBrace(spaces, psiElement.textRangeWithoutComments) + } + + inPosition(parent = BLOCK, left = LBRACE).customRule { parent, _, _ -> + val psiElement = parent.requireNode().treeParent.psi + val funNode = psiElement as? KtFunction ?: return@customRule null + + if (funNode.name != null) return@customRule null + + // Empty block is covered in above rule + Spacing.createDependentLFSpacing( + spacesInSimpleFunction, spacesInSimpleFunction, funNode.textRangeWithoutComments, + kotlinCommonSettings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE + ) + } + + inPosition(parentSet = EXTEND_COLON_ELEMENTS, left = PRIMARY_CONSTRUCTOR, right = COLON).customRule { _, left, _ -> + val primaryConstructor = left.requireNode().psi as KtPrimaryConstructor + val rightParenthesis = primaryConstructor.valueParameterList?.rightParenthesis + val prevSibling = rightParenthesis?.prevSibling + val spaces = if (kotlinCustomSettings.SPACE_BEFORE_EXTEND_COLON) 1 else 0 + // TODO This should use DependentSpacingRule, but it doesn't set keepLineBreaks to false if max LFs is 0 + if ((prevSibling as? PsiWhiteSpace)?.textContains('\n') == true || kotlinCommonSettings + .METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE + ) { + createSpacing(spaces, keepLineBreaks = false) + } else { + createSpacing(spaces) + } + } + + inPosition( + parent = CLASS_BODY, + left = LBRACE, + right = ENUM_ENTRY + ).lineBreakIfLineBreakInParent(numSpacesOtherwise = 1) + } + + simple { + afterInside(LBRACE, BLOCK).lineBreakInCode() + beforeInside(RBRACE, BLOCK).spacing( + 1, 0, 1, + kotlinCommonSettings.KEEP_LINE_BREAKS, + kotlinCommonSettings.KEEP_BLANK_LINES_BEFORE_RBRACE + ) + between(LBRACE, ENUM_ENTRY).spacing(1, 0, 0, true, kotlinCommonSettings.KEEP_BLANK_LINES_IN_CODE) + beforeInside(RBRACE, WHEN).lineBreakInCode() + between(RPAR, BODY).spaces(1) + + // if when entry has block, spacing after arrow should be set by lbrace rule + aroundInside(ARROW, WHEN_ENTRY).spaceIf(kotlinCustomSettings.SPACE_AROUND_WHEN_ARROW) + + beforeInside(COLON, EXTEND_COLON_ELEMENTS) { spaceIf(kotlinCustomSettings.SPACE_BEFORE_EXTEND_COLON) } + + after(EOL_COMMENT).lineBreakInCode() + } + } +} + +private fun excludeLambdasAndObjects(parent: ASTBlock): List { + val rangesToExclude = mutableListOf() + parent.requireNode().psi.accept(object : KtTreeVisitorVoid() { + override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { + super.visitLambdaExpression(lambdaExpression) + rangesToExclude.add(lambdaExpression.textRange) + } + + override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) { + super.visitObjectLiteralExpression(expression) + rangesToExclude.add(expression.textRange) + } + + override fun visitNamedFunction(function: KtNamedFunction) { + super.visitNamedFunction(function) + if (function.name == null) { + rangesToExclude.add(function.textRange) + } + } + }) + return TextRangeUtil.excludeRanges(parent.textRange, rangesToExclude).toList() +} + +private val QUALIFIED_OPERATION = TokenSet.create(DOT, SAFE_ACCESS) +private val QUALIFIED_EXPRESSIONS = TokenSet.create(DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION) +private val ELVIS_SET = TokenSet.create(ELVIS) +private val QUALIFIED_EXPRESSIONS_WITHOUT_WRAP = TokenSet.create(IMPORT_DIRECTIVE, PACKAGE_DIRECTIVE) + +private const val KDOC_COMMENT_INDENT = 1 + +private val BINARY_EXPRESSIONS = TokenSet.create(BINARY_EXPRESSION, BINARY_WITH_TYPE, IS_EXPRESSION) +private val KDOC_CONTENT = TokenSet.create(KDocTokens.KDOC, KDocElementTypes.KDOC_SECTION, KDocElementTypes.KDOC_TAG) + +private val CODE_BLOCKS = TokenSet.create(BLOCK, CLASS_BODY, FUNCTION_LITERAL) + +private val ALIGN_FOR_BINARY_OPERATIONS = TokenSet.create(MUL, DIV, PERC, PLUS, MINUS, ELVIS, LT, GT, LTEQ, GTEQ, ANDAND, OROR) +private val ANNOTATIONS = TokenSet.create(ANNOTATION_ENTRY, ANNOTATION) + +typealias WrappingStrategy = (childElement: ASTNode) -> Wrap? + +fun noWrapping(childElement: ASTNode): Wrap? = null + +abstract class KotlinCommonBlock( + private val node: ASTNode, + private val settings: CodeStyleSettings, + private val spacingBuilder: KotlinSpacingBuilder, + private val alignmentStrategy: CommonAlignmentStrategy, + private val overrideChildren: Sequence? = null, +) { + @Volatile + private var mySubBlocks: List? = null + + fun getTextRange(): TextRange { + if (overrideChildren != null) { + return TextRange(overrideChildren.first().startOffset, overrideChildren.last().textRange.endOffset) + } + return node.textRange + } + + protected abstract fun createBlock( + node: ASTNode, + alignmentStrategy: CommonAlignmentStrategy, + indent: Indent?, + wrap: Wrap?, + settings: CodeStyleSettings, + spacingBuilder: KotlinSpacingBuilder, + overrideChildren: Sequence? = null, + ): ASTBlock + + protected abstract fun createSyntheticSpacingNodeBlock(node: ASTNode): ASTBlock + + protected abstract fun getSubBlocks(): List + + protected abstract fun getSuperChildAttributes(newChildIndex: Int): ChildAttributes + + protected abstract fun isIncompleteInSuper(): Boolean + + protected abstract fun getAlignmentForCaseBranch(shouldAlignInColumns: Boolean): CommonAlignmentStrategy + + protected abstract fun getAlignment(): Alignment? + + protected abstract fun createAlignmentStrategy(alignOption: Boolean, defaultAlignment: Alignment?): CommonAlignmentStrategy + + protected abstract fun getNullAlignmentStrategy(): CommonAlignmentStrategy + + fun isLeaf(): Boolean = node.firstChildNode == null + + fun isIncomplete(): Boolean { + if (isIncompleteInSuper()) { + return true + } + + // An incomplete declaration is the reason when modifier list can become a class body child, otherwise + // it's going to be a declaration child. + return node.elementType == MODIFIER_LIST && node.treeParent?.elementType == CLASS_BODY + } + + fun buildChildren(): List { + if (mySubBlocks != null) { + return mySubBlocks!! + } + + var nodeSubBlocks = buildSubBlocks() + + if (node.elementType in QUALIFIED_EXPRESSIONS) { + nodeSubBlocks = splitSubBlocksOnDot(nodeSubBlocks) + } else { + val psi = node.psi + if (psi is KtBinaryExpression && psi.operationToken == ELVIS) { + nodeSubBlocks = splitSubBlocksOnElvis(nodeSubBlocks) + } + } + + mySubBlocks = nodeSubBlocks + + return nodeSubBlocks + } + + private fun splitSubBlocksOnDot(nodeSubBlocks: List): List { + if (node.treeParent?.isQualifier == true || node.isCallChainWithoutWrap) return nodeSubBlocks + + val operationBlockIndex = nodeSubBlocks.indexOfBlockWithType(QUALIFIED_OPERATION) + if (operationBlockIndex == -1) return nodeSubBlocks + + val block = nodeSubBlocks.first() + val wrap = createWrapForQualifierExpression(node) + val enforceIndentToChildren = anyCallInCallChainIsWrapped(node) + val indent = createIndentForQualifierExpression(enforceIndentToChildren) + val newBlock = block.processBlock(wrap, enforceIndentToChildren) + return nodeSubBlocks.replaceBlock(newBlock, 0).splitAtIndex(operationBlockIndex, indent, wrap) + } + + private fun ASTBlock.processBlock(wrap: Wrap?, enforceIndentToChildren: Boolean): ASTBlock { + val currentNode = requireNode() + val enforceIndent = enforceIndentToChildren && anyCallInCallChainIsWrapped(currentNode) + val indent = createIndentForQualifierExpression(enforceIndent) + + @Suppress("UNCHECKED_CAST") + val subBlocks = subBlocks as List + val elementType = currentNode.elementType + if (elementType != POSTFIX_EXPRESSION && elementType !in QUALIFIED_EXPRESSIONS) return this + + val index = 0 + val resultWrap = if (currentNode.wrapForFirstCallInChainIsAllowed) + wrap ?: createWrapForQualifierExpression(currentNode) + else + null + + val newBlock = subBlocks.elementAt(index).processBlock(resultWrap, enforceIndent) + return subBlocks.replaceBlock(newBlock, index).let { + val operationIndex = subBlocks.indexOfBlockWithType(QUALIFIED_OPERATION) + if (operationIndex != -1) + it.splitAtIndex(operationIndex, indent, resultWrap) + else + it + }.wrapToBlock(currentNode, this) + } + + private fun List.replaceBlock(block: ASTBlock, index: Int = 0): List = toMutableList().apply { this[index] = block } + + private val ASTNode.wrapForFirstCallInChainIsAllowed: Boolean + get() { + if (unwrapQualifier()?.isCall != true) return false + return settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN || receiverIsCall() + } + + private fun createWrapForQualifierExpression(node: ASTNode): Wrap? = + if (node.wrapForFirstCallInChainIsAllowed && node.receiverIsCall()) + Wrap.createWrap( + settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP, + true /* wrapFirstElement */, + ) + else + null + + // enforce indent to children when there's a line break before the dot in any call in the chain (meaning that + // the call chain following that call is indented) + private fun createIndentForQualifierExpression(enforceIndentToChildren: Boolean): Indent { + val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) { + if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST + } else { + Indent.Type.NORMAL + } + + return Indent.getIndent( + indentType, false, + enforceIndentToChildren, + ) + } + + private fun List.wrapToBlock( + anchor: ASTNode?, + parentBlock: ASTBlock?, + ): ASTBlock = splitAtIndex(0, null, null, anchor, parentBlock).single() + + private fun List.splitAtIndex( + index: Int, + indent: Indent?, + wrap: Wrap?, + anchor: ASTNode? = null, + parentBlock: ASTBlock? = null, + ): List { + val operationBlock = this[index] + val createParentSyntheticSpacingBlock: (ASTNode) -> ASTBlock = if (parentBlock != null) { + { parentBlock } + } else { + { + val parent = it.treeParent ?: node + val skipOperationNodeParent = if (parent.elementType === OPERATION_REFERENCE) { + parent.treeParent ?: parent + } else { + parent + } + createSyntheticSpacingNodeBlock(skipOperationNodeParent) + } + } + val operationSyntheticBlock = SyntheticKotlinBlock( + anchor ?: operationBlock.requireNode(), + subList(index, size), + null, indent, wrap, spacingBuilder, createParentSyntheticSpacingBlock, + ) + + return subList(0, index) + operationSyntheticBlock + } + + private fun splitSubBlocksOnElvis(nodeSubBlocks: List): List { + val elvisIndex = nodeSubBlocks.indexOfBlockWithType(ELVIS_SET) + if (elvisIndex >= 0) { + val indent = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_ELVIS) { + Indent.getContinuationIndent() + } else { + Indent.getNormalIndent() + } + + return nodeSubBlocks.splitAtIndex( + elvisIndex, + indent, + null, + ) + } + return nodeSubBlocks + } + + private fun createChildIndent(child: ASTNode): Indent? { + val childParent = child.treeParent + val childType = child.elementType + + if (childParent != null && isInCodeChunk(childParent)) { + return Indent.getNoneIndent() + } + + // do not indent child after heading comments inside declaration + if (childParent != null && childParent.psi is KtDeclaration) { + val prev = getPrevWithoutWhitespace(child) + if (prev != null && COMMENTS.contains(prev.elementType) && getSiblingWithoutWhitespaceAndComments(prev) == null) { + return Indent.getNoneIndent() + } + } + + for (strategy in INDENT_RULES) { + val indent = strategy.getIndent(child, settings) + if (indent != null) { + return indent + } + } + + // TODO: Try to rewrite other rules to declarative style + if (childParent != null) { + val parentType = childParent.elementType + + if (parentType === VALUE_PARAMETER_LIST || parentType === VALUE_ARGUMENT_LIST) { + val prev = getPrevWithoutWhitespace(child) + if (childType === RPAR && (prev == null || prev.elementType !== COMMA || !hasDoubleLineBreakBefore(child))) { + return Indent.getNoneIndent() + } + + return if (settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_ARGUMENT_LISTS) + Indent.getContinuationWithoutFirstIndent() + else + Indent.getNormalIndent() + } + + if (parentType === TYPE_PARAMETER_LIST || parentType === TYPE_ARGUMENT_LIST) { + return Indent.getContinuationWithoutFirstIndent() + } + } + + return Indent.getNoneIndent() + } + + private fun isInCodeChunk(node: ASTNode): Boolean { + val parent = node.treeParent ?: return false + + if (node.elementType != BLOCK) { + return false + } + + val parentType = parent.elementType + return parentType == SCRIPT + || parentType == BLOCK_CODE_FRAGMENT + || parentType == EXPRESSION_CODE_FRAGMENT + || parentType == TYPE_CODE_FRAGMENT + } + + fun getChildAttributes(newChildIndex: Int): ChildAttributes { + val type = node.elementType + + if (isInCodeChunk(node)) { + return ChildAttributes(Indent.getNoneIndent(), null) + } + + if (type == IF) { + val elseBlock = mySubBlocks?.getOrNull(newChildIndex) + if (elseBlock != null && elseBlock.requireNode().elementType == ELSE_KEYWORD) { + return ChildAttributes.DELEGATE_TO_NEXT_CHILD + } + } + + if (newChildIndex > 0) { + val prevBlock = mySubBlocks?.get(newChildIndex - 1) + if (prevBlock?.node?.elementType == MODIFIER_LIST) { + return ChildAttributes(Indent.getNoneIndent(), null) + } + } + + return when (type) { + in CODE_BLOCKS, WHEN, IF, FOR, WHILE, DO_WHILE, WHEN_ENTRY -> ChildAttributes( + Indent.getNormalIndent(), + null, + ) + + TRY -> ChildAttributes(Indent.getNoneIndent(), null) + + in QUALIFIED_EXPRESSIONS -> ChildAttributes(Indent.getContinuationWithoutFirstIndent(), null) + + VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> { + val subBlocks = getSubBlocks() + if (newChildIndex != 1 && newChildIndex != 0 && newChildIndex < subBlocks.size) { + val block = subBlocks[newChildIndex] + ChildAttributes(block.indent, block.alignment) + } else { + val indent = + if ((type == VALUE_PARAMETER_LIST && !settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_PARAMETER_LISTS) || + (type == VALUE_ARGUMENT_LIST && !settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_ARGUMENT_LISTS) + ) { + Indent.getNormalIndent() + } else { + Indent.getContinuationIndent() + } + ChildAttributes(indent, null) + } + } + + DOC_COMMENT -> ChildAttributes(Indent.getSpaceIndent(KDOC_COMMENT_INDENT), null) + + PARENTHESIZED -> getSuperChildAttributes(newChildIndex) + + else -> { + val blocks = getSubBlocks() + if (newChildIndex != 0) { + val isIncomplete = if (newChildIndex < blocks.size) blocks[newChildIndex - 1].isIncomplete else isIncompleteInSuper() + if (isIncomplete) { + if (blocks.size == newChildIndex && !settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_EXPRESSION_BODIES) { + val lastInParent = blocks.last() + if (lastInParent is ASTBlock && lastInParent.node?.elementType in ALL_ASSIGNMENTS) { + return ChildAttributes(Indent.getNormalIndent(), null) + } + } + + return getSuperChildAttributes(newChildIndex) + } + } + + if (blocks.size > newChildIndex) { + val block = blocks[newChildIndex] + return ChildAttributes(block.indent, block.alignment) + } + + ChildAttributes(Indent.getNoneIndent(), null) + } + } + } + + private fun getChildrenAlignmentStrategy(): CommonAlignmentStrategy { + val kotlinCommonSettings = settings.kotlinCommonSettings + val kotlinCustomSettings = settings.kotlinCustomSettings + val parentType = node.elementType + return when { + parentType === VALUE_PARAMETER_LIST -> + getAlignmentForChildInParenthesis( + kotlinCommonSettings.ALIGN_MULTILINE_PARAMETERS, VALUE_PARAMETER, COMMA, + kotlinCommonSettings.ALIGN_MULTILINE_METHOD_BRACKETS, LPAR, RPAR, + ) + + parentType === VALUE_ARGUMENT_LIST -> + getAlignmentForChildInParenthesis( + kotlinCommonSettings.ALIGN_MULTILINE_PARAMETERS_IN_CALLS, VALUE_ARGUMENT, COMMA, + kotlinCommonSettings.ALIGN_MULTILINE_METHOD_BRACKETS, LPAR, RPAR, + ) + + parentType === WHEN -> + getAlignmentForCaseBranch(kotlinCustomSettings.ALIGN_IN_COLUMNS_CASE_BRANCH) + + parentType === WHEN_ENTRY -> + alignmentStrategy + + parentType in BINARY_EXPRESSIONS && getOperationType(node) in ALIGN_FOR_BINARY_OPERATIONS -> + createAlignmentStrategy(kotlinCommonSettings.ALIGN_MULTILINE_BINARY_OPERATION, getAlignment()) + + parentType === SUPER_TYPE_LIST -> + createAlignmentStrategy(kotlinCommonSettings.ALIGN_MULTILINE_EXTENDS_LIST, getAlignment()) + + parentType === PARENTHESIZED -> + object : CommonAlignmentStrategy() { + private var bracketsAlignment: Alignment? = + if (kotlinCommonSettings.ALIGN_MULTILINE_BINARY_OPERATION) Alignment.createAlignment() else null + + override fun getAlignment(node: ASTNode): Alignment? { + val childNodeType = node.elementType + val prev = getPrevWithoutWhitespace(node) + + if (prev != null && prev.elementType === TokenType.ERROR_ELEMENT || childNodeType === TokenType.ERROR_ELEMENT) { + return bracketsAlignment + } + + if (childNodeType === LPAR || childNodeType === RPAR) { + return bracketsAlignment + } + + return null + } + } + + parentType == TYPE_CONSTRAINT_LIST -> + createAlignmentStrategy(true, getAlignment()) + + else -> + getNullAlignmentStrategy() + } + } + + + private fun buildSubBlock( + child: ASTNode, + alignmentStrategy: CommonAlignmentStrategy, + wrappingStrategy: WrappingStrategy, + overrideChildren: Sequence? = null, + ): ASTBlock { + val childWrap = wrappingStrategy(child) + + // Skip one sub-level for operators, so type of block node is an element type of operator + if (child.elementType === OPERATION_REFERENCE) { + val operationNode = child.firstChildNode + if (operationNode != null) { + return createBlock( + operationNode, + alignmentStrategy, + createChildIndent(child), + childWrap, + settings, + spacingBuilder, + overrideChildren, + ) + } + } + + return createBlock(child, alignmentStrategy, createChildIndent(child), childWrap, settings, spacingBuilder, overrideChildren) + } + + private fun buildSubBlocks(): List { + val childrenAlignmentStrategy = getChildrenAlignmentStrategy() + val wrappingStrategy = getWrappingStrategy() + + val childNodes = when { + overrideChildren != null -> overrideChildren.asSequence() + node.elementType == BINARY_EXPRESSION -> { + val binaryExpression = node.psi as? KtBinaryExpression + if (binaryExpression != null && ALL_ASSIGNMENTS.contains(binaryExpression.operationToken)) { + node.children() + } else { + val binaryExpressionChildren = mutableListOf() + collectBinaryExpressionChildren(node, binaryExpressionChildren) + binaryExpressionChildren.asSequence() + } + } + else -> node.children() + } + + return childNodes + .filter { it.textRange.length > 0 && it.elementType != TokenType.WHITE_SPACE } + .flatMap { buildSubBlocksForChildNode(it, childrenAlignmentStrategy, wrappingStrategy) } + .toList() + } + + private fun buildSubBlocksForChildNode( + node: ASTNode, + childrenAlignmentStrategy: CommonAlignmentStrategy, + wrappingStrategy: WrappingStrategy, + ): Sequence { + if (node.elementType == FUN && false /* TODO fix tests and restore */) { + val filteredChildren = node.children().filter { + it.textRange.length > 0 && it.elementType != TokenType.WHITE_SPACE + } + val significantChildren = filteredChildren.dropWhile { it.elementType == EOL_COMMENT } + val funIndent = extractIndent(significantChildren.first()) + val eolComments = filteredChildren.takeWhile { + it.elementType == EOL_COMMENT && extractIndent(it) != funIndent + }.toList() + val remainingChildren = filteredChildren.drop(eolComments.size) + + val blocks = eolComments.map { buildSubBlock(it, childrenAlignmentStrategy, wrappingStrategy) } + + sequenceOf(buildSubBlock(node, childrenAlignmentStrategy, wrappingStrategy, remainingChildren)) + val blockList = blocks.toList() + return blockList.asSequence() + } + + return sequenceOf(buildSubBlock(node, childrenAlignmentStrategy, wrappingStrategy)) + } + + private fun collectBinaryExpressionChildren(node: ASTNode, result: MutableList) { + for (child in node.children()) { + if (child.elementType == BINARY_EXPRESSION) { + collectBinaryExpressionChildren(child, result) + } else { + result.add(child) + } + } + } + + private fun getWrappingStrategy(): WrappingStrategy { + val commonSettings = settings.kotlinCommonSettings + val elementType = node.elementType + val parentElementType = node.treeParent?.elementType + val nodePsi = node.psi + + when { + elementType === VALUE_ARGUMENT_LIST -> { + val wrapSetting = commonSettings.CALL_PARAMETERS_WRAP + if (!node.addTrailingComma && + (wrapSetting == CommonCodeStyleSettings.WRAP_AS_NEEDED || wrapSetting == CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM) && + !needWrapArgumentList(nodePsi) + ) { + return ::noWrapping + } + return getWrappingStrategyForItemList( + wrapSetting, + VALUE_ARGUMENT, + node.addTrailingComma, + additionalWrap = trailingCommaWrappingStrategyWithMultiLineCheck(LPAR, RPAR), + ) + } + + elementType === VALUE_PARAMETER_LIST -> { + when (parentElementType) { + FUN, PRIMARY_CONSTRUCTOR, SECONDARY_CONSTRUCTOR -> return getWrappingStrategyForItemList( + commonSettings.METHOD_PARAMETERS_WRAP, + VALUE_PARAMETER, + node.addTrailingComma, + additionalWrap = trailingCommaWrappingStrategyWithMultiLineCheck(LPAR, RPAR), + ) + FUNCTION_TYPE -> return defaultTrailingCommaWrappingStrategy(LPAR, RPAR) + FUNCTION_LITERAL -> { + if (nodePsi.parent?.safeAs()?.needTrailingComma(settings) == true) { + val check = thisOrPrevIsMultiLineElement(COMMA, LBRACE /* not necessary */, ARROW /* not necessary */) + return { childElement -> + createWrapAlwaysIf(getSiblingWithoutWhitespaceAndComments(childElement) == null || check(childElement)) + } + } + } + } + } + + elementType === FUNCTION_LITERAL -> { + if (nodePsi.cast().needTrailingComma(settings)) + return trailingCommaWrappingStrategy(leftAnchor = LBRACE, rightAnchor = ARROW) + } + + elementType === WHEN_ENTRY -> { + // with argument + if (nodePsi.cast().needTrailingComma(settings)) { + val check = thisOrPrevIsMultiLineElement(COMMA, LBRACE /* not necessary */, ARROW /* not necessary */) + return trailingCommaWrappingStrategy(rightAnchor = ARROW) { + getSiblingWithoutWhitespaceAndComments(it, true) != null && check(it) + } + } + } + + elementType === DESTRUCTURING_DECLARATION -> { + nodePsi as KtDestructuringDeclaration + if (nodePsi.valOrVarKeyword == null) return defaultTrailingCommaWrappingStrategy(LPAR, RPAR) + else if (nodePsi.needTrailingComma(settings)) { + val check = thisOrPrevIsMultiLineElement(COMMA, LPAR, RPAR) + return trailingCommaWrappingStrategy(leftAnchor = LPAR, rightAnchor = RPAR, filter = { it.elementType !== EQ }) { + getSiblingWithoutWhitespaceAndComments(it, true) != null && check(it) + } + } + } + + elementType === INDICES -> return defaultTrailingCommaWrappingStrategy(LBRACKET, RBRACKET) + + elementType === TYPE_PARAMETER_LIST -> return defaultTrailingCommaWrappingStrategy(LT, GT) + + elementType === TYPE_ARGUMENT_LIST -> return defaultTrailingCommaWrappingStrategy(LT, GT) + + elementType === COLLECTION_LITERAL_EXPRESSION -> return defaultTrailingCommaWrappingStrategy(LBRACKET, RBRACKET) + + elementType === SUPER_TYPE_LIST -> { + val wrap = Wrap.createWrap(commonSettings.EXTENDS_LIST_WRAP, false) + return { childElement -> if (childElement.psi is KtSuperTypeListEntry) wrap else null } + } + + elementType === CLASS_BODY -> return getWrappingStrategyForItemList(commonSettings.ENUM_CONSTANTS_WRAP, ENUM_ENTRY) + + elementType === MODIFIER_LIST -> { + when (val parent = node.treeParent.psi) { + is KtParameter -> + return getWrappingStrategyForItemList( + commonSettings.PARAMETER_ANNOTATION_WRAP, + ANNOTATIONS, + !node.treeParent.isFirstParameter(), + ) + is KtClassOrObject, is KtTypeAlias -> + return getWrappingStrategyForItemList( + commonSettings.CLASS_ANNOTATION_WRAP, + ANNOTATIONS, + ) + + is KtNamedFunction, is KtSecondaryConstructor -> + return getWrappingStrategyForItemList( + commonSettings.METHOD_ANNOTATION_WRAP, + ANNOTATIONS, + ) + + is KtProperty -> + return getWrappingStrategyForItemList( + if (parent.isLocal) + commonSettings.VARIABLE_ANNOTATION_WRAP + else + commonSettings.FIELD_ANNOTATION_WRAP, + ANNOTATIONS, + ) + } + } + + elementType === VALUE_PARAMETER -> + return wrapAfterAnnotation(commonSettings.PARAMETER_ANNOTATION_WRAP) + + nodePsi is KtClassOrObject || nodePsi is KtTypeAlias -> + return wrapAfterAnnotation(commonSettings.CLASS_ANNOTATION_WRAP) + + nodePsi is KtNamedFunction || nodePsi is KtSecondaryConstructor -> + return wrap@{ childElement -> + getWrapAfterAnnotation(childElement, commonSettings.METHOD_ANNOTATION_WRAP)?.let { + return@wrap it + } + if (getSiblingWithoutWhitespaceAndComments(childElement)?.elementType == EQ) { + return@wrap Wrap.createWrap(settings.kotlinCustomSettings.WRAP_EXPRESSION_BODY_FUNCTIONS, true) + } + null + } + + nodePsi is KtProperty -> + return wrap@{ childElement -> + val wrapSetting = if (nodePsi.isLocal) commonSettings.VARIABLE_ANNOTATION_WRAP else commonSettings.FIELD_ANNOTATION_WRAP + getWrapAfterAnnotation(childElement, wrapSetting)?.let { + return@wrap it + } + if (getSiblingWithoutWhitespaceAndComments(childElement)?.elementType == EQ) { + return@wrap Wrap.createWrap(settings.kotlinCommonSettings.ASSIGNMENT_WRAP, true) + } + null + } + + nodePsi is KtBinaryExpression -> { + if (nodePsi.operationToken == EQ) { + return { childElement -> + if (getSiblingWithoutWhitespaceAndComments(childElement)?.elementType == OPERATION_REFERENCE) { + Wrap.createWrap(settings.kotlinCommonSettings.ASSIGNMENT_WRAP, true) + } else { + null + } + } + } + if (nodePsi.operationToken == ELVIS && nodePsi.getStrictParentOfType() == null) { + return { childElement -> + if (childElement.elementType == OPERATION_REFERENCE && (childElement.psi as? KtOperationReferenceExpression)?.operationSignTokenType == ELVIS) { + Wrap.createWrap(settings.kotlinCustomSettings.WRAP_ELVIS_EXPRESSIONS, true) + } else { + null + } + } + } + return ::noWrapping + } + } + + return ::noWrapping + } + + private fun defaultTrailingCommaWrappingStrategy(leftAnchor: IElementType, rightAnchor: IElementType): WrappingStrategy = + fun(childElement: ASTNode): Wrap? = trailingCommaWrappingStrategyWithMultiLineCheck(leftAnchor, rightAnchor)(childElement) + + private val ASTNode.addTrailingComma: Boolean + get() = (settings.kotlinCustomSettings.addTrailingCommaIsAllowedFor(this) || + lastChildNode?.let { getSiblingWithoutWhitespaceAndComments(it) }?.elementType === COMMA) && + psi?.let(PsiElement::isMultiline) == true + + + private fun ASTNode.notDelimiterSiblingNodeInSequence( + forward: Boolean, + delimiterType: IElementType, + typeOfLastElement: IElementType, + ): ASTNode? { + var sibling: ASTNode? = null + for (element in siblings(forward).filter { it.elementType != WHITE_SPACE }.takeWhile { it.elementType != typeOfLastElement }) { + val elementType = element.elementType + if (!forward) { + sibling = element + if (elementType != delimiterType && elementType !in COMMENTS) break + } else { + if (elementType !in COMMENTS) break + sibling = element + } + } + + return sibling + } + + private fun thisOrPrevIsMultiLineElement( + delimiterType: IElementType, + typeOfFirstElement: IElementType, + typeOfLastElement: IElementType, + ) = fun(childElement: ASTNode): Boolean { + when (childElement.elementType) { + typeOfFirstElement, + typeOfLastElement, + delimiterType, + in WHITE_SPACE_OR_COMMENT_BIT_SET, + -> return false + } + + val psi = childElement.psi ?: return false + if (psi.isMultiline()) return true + + val startOffset = childElement.notDelimiterSiblingNodeInSequence(false, delimiterType, typeOfFirstElement)?.startOffset + ?: psi.startOffset + val endOffset = childElement.notDelimiterSiblingNodeInSequence(true, delimiterType, typeOfLastElement)?.psi?.endOffset + ?: psi.endOffset + return psi.parent.containsLineBreakInThis(startOffset, endOffset) + } + + private fun trailingCommaWrappingStrategyWithMultiLineCheck( + leftAnchor: IElementType, + rightAnchor: IElementType, + ) = trailingCommaWrappingStrategy( + leftAnchor = leftAnchor, + rightAnchor = rightAnchor, + checkTrailingComma = true, + additionalCheck = thisOrPrevIsMultiLineElement(COMMA, leftAnchor, rightAnchor), + ) + + private fun trailingCommaWrappingStrategy( + leftAnchor: IElementType? = null, + rightAnchor: IElementType? = null, + checkTrailingComma: Boolean = false, + filter: (ASTNode) -> Boolean = { true }, + additionalCheck: (ASTNode) -> Boolean = { false }, + ): WrappingStrategy = fun(childElement: ASTNode): Wrap? { + if (!filter(childElement)) return null + val childElementType = childElement.elementType + return createWrapAlwaysIf( + (!checkTrailingComma || childElement.treeParent.addTrailingComma) && ( + rightAnchor != null && rightAnchor === childElementType || + leftAnchor != null && leftAnchor === getSiblingWithoutWhitespaceAndComments(childElement)?.elementType || + additionalCheck(childElement) + ), + ) + } +} + +private fun ASTNode.qualifierReceiver(): ASTNode? = unwrapQualifier()?.psi + ?.safeAs() + ?.receiverExpression + ?.node + ?.unwrapQualifier() + +private tailrec fun ASTNode.unwrapQualifier(): ASTNode? { + if (elementType in QUALIFIED_EXPRESSIONS) return this + + val psi = psi as? KtPostfixExpression ?: return null + if (psi.operationToken != EXCLEXCL) return null + + return psi.baseExpression?.node?.unwrapQualifier() +} + +private fun ASTNode.receiverIsCall(): Boolean = qualifierReceiver()?.isCall == true + +private val ASTNode.isCallChainWithoutWrap: Boolean + get() { + val callChainParent = parents().firstOrNull { !it.isQualifier } ?: return true + return callChainParent.elementType in QUALIFIED_EXPRESSIONS_WITHOUT_WRAP + } + +private val ASTNode.isQualifier: Boolean + get() { + var currentNode: ASTNode? = this + while (currentNode != null) { + if (currentNode.elementType in QUALIFIED_EXPRESSIONS) return true + if (currentNode.psi?.safeAs()?.operationToken != EXCLEXCL) return false + + currentNode = currentNode.treeParent + } + + return false + } + +private val ASTNode.isCall: Boolean + get() = unwrapQualifier()?.lastChildNode?.elementType == CALL_EXPRESSION + +private fun anyCallInCallChainIsWrapped(node: ASTNode): Boolean { + val sequentialNodes = generateSequence(node) { + when (it.elementType) { + POSTFIX_EXPRESSION, in QUALIFIED_EXPRESSIONS -> it.firstChildNode + PARENTHESIZED -> getSiblingWithoutWhitespaceAndComments(it.firstChildNode, true) + else -> null + } + } + + return sequentialNodes.any { + val checkedElement = when (it.elementType) { + in QUALIFIED_EXPRESSIONS -> it.findChildByType(QUALIFIED_OPERATION) + PARENTHESIZED -> it.lastChildNode + else -> null + } + + checkedElement != null && hasLineBreakBefore(checkedElement) + } +} + +private fun ASTNode.isFirstParameter(): Boolean = treePrev?.elementType == LPAR + +private fun wrapAfterAnnotation(wrapType: Int): WrappingStrategy { + return { childElement -> getWrapAfterAnnotation(childElement, wrapType) } +} + +private fun getWrapAfterAnnotation(childElement: ASTNode, wrapType: Int): Wrap? { + if (childElement.elementType in COMMENTS) return null + var prevLeaf = childElement.treePrev + while (prevLeaf?.elementType == TokenType.WHITE_SPACE) { + prevLeaf = prevLeaf.treePrev + } + if (prevLeaf?.elementType == MODIFIER_LIST) { + if (prevLeaf?.lastChildNode?.elementType in ANNOTATIONS) { + return Wrap.createWrap(wrapType, true) + } + } + return null +} + +fun needWrapArgumentList(psi: PsiElement): Boolean { + val args = (psi as? KtValueArgumentList)?.arguments + return args?.singleOrNull()?.getArgumentExpression() !is KtObjectLiteralExpression +} + +private fun hasLineBreakBefore(node: ASTNode): Boolean { + val prevSibling = node.leaves(false) + .dropWhile { it.psi is PsiComment } + .firstOrNull() + return prevSibling?.elementType == TokenType.WHITE_SPACE && prevSibling?.textContains('\n') == true +} + +private fun hasDoubleLineBreakBefore(node: ASTNode): Boolean { + val prevSibling = node.leaves(false).firstOrNull() ?: return false + + return prevSibling.text.count { it == '\n' } >= 2 +} + +fun NodeIndentStrategy.PositionStrategy.continuationIf( + option: (KotlinCodeStyleSettings) -> Boolean, + indentFirst: Boolean = false, +): NodeIndentStrategy { + return set { settings -> + if (option(settings.kotlinCustomSettings)) { + if (indentFirst) + Indent.getContinuationIndent() + else + Indent.getContinuationWithoutFirstIndent() + } else + Indent.getNormalIndent() + } +} + +private val INDENT_RULES = arrayOf( + strategy("No indent for braces in blocks") + .within(BLOCK, CLASS_BODY, FUNCTION_LITERAL) + .forType(RBRACE, LBRACE) + .set(Indent.getNoneIndent()), + + strategy("Indent for block content") + .within(BLOCK, CLASS_BODY, FUNCTION_LITERAL) + .notForType(RBRACE, LBRACE, BLOCK) + .set(Indent.getNormalIndent()), + + strategy("Indent for template content") + .within(LONG_STRING_TEMPLATE_ENTRY) + .notForType(LONG_TEMPLATE_ENTRY_START, LONG_TEMPLATE_ENTRY_END) + .set(Indent.getNormalIndent()), + + strategy("No indent for braces in template") + .within(LONG_STRING_TEMPLATE_ENTRY) + .forType(LONG_TEMPLATE_ENTRY_START, LONG_TEMPLATE_ENTRY_END) + .set(Indent.getNoneIndent()), + + strategy("Indent for property accessors") + .within(PROPERTY).forType(PROPERTY_ACCESSOR) + .set(Indent.getNormalIndent()), + + strategy("For a single statement in 'for'") + .within(BODY).notForType(BLOCK) + .set(Indent.getNormalIndent()), + + strategy("For WHEN content") + .within(WHEN) + .notForType(RBRACE, LBRACE, WHEN_KEYWORD) + .set(Indent.getNormalIndent()), + + strategy("For single statement in THEN and ELSE") + .within(THEN, ELSE).notForType(BLOCK) + .set(Indent.getNormalIndent()), + + strategy("Expression body") + .within(FUN) + .forElement { + (it.psi is KtExpression && it.psi !is KtBlockExpression) + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES, indentFirst = true), + + strategy("Line comment at expression body position") + .forElement { node -> + val psi = node.psi + val parent = psi.parent + if (psi is PsiComment && parent is KtDeclarationWithInitializer) { + psi.getNextSiblingIgnoringWhitespace() == parent.initializer + } else { + false + } + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES, indentFirst = true), + + strategy("If condition") + .within(CONDITION) + .set { settings -> + val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_IN_IF_CONDITIONS) + Indent.Type.CONTINUATION + else + Indent.Type.NORMAL + Indent.getIndent(indentType, false, true) + }, + + strategy("Property accessor expression body") + .within(PROPERTY_ACCESSOR) + .forElement { + it.psi is KtExpression && it.psi !is KtBlockExpression + } + .set(Indent.getNormalIndent()), + + strategy("Property initializer") + .within(PROPERTY) + .forElement { + it.psi is KtExpression + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES), + + strategy("Destructuring declaration") + .within(DESTRUCTURING_DECLARATION) + .forElement { + it.psi is KtExpression + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES), + + strategy("Assignment expressions") + .within(BINARY_EXPRESSION) + .within { + val binaryExpression = it.psi as? KtBinaryExpression + ?: return@within false + + return@within ALL_ASSIGNMENTS.contains(binaryExpression.operationToken) + } + .forElement { + val psi = it.psi + val binaryExpression = psi?.parent as? KtBinaryExpression + binaryExpression?.right == psi + } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES), + + strategy("Indent for parts") + .within(PROPERTY, FUN, DESTRUCTURING_DECLARATION, SECONDARY_CONSTRUCTOR) + .notForType( + BLOCK, FUN_KEYWORD, VAL_KEYWORD, VAR_KEYWORD, CONSTRUCTOR_KEYWORD, RPAR, + EOL_COMMENT, + ) + .set(Indent.getContinuationWithoutFirstIndent()), + + strategy("Chained calls") + .within(QUALIFIED_EXPRESSIONS) + .forType(EOL_COMMENT, BLOCK_COMMENT, DOC_COMMENT, SHEBANG_COMMENT) + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_CHAINED_CALLS), + + strategy("Colon of delegation list") + .within(CLASS, OBJECT_DECLARATION) + .forType(COLON) + .set(Indent.getNormalIndent()), + + strategy("Delegation list") + .within(SUPER_TYPE_LIST) + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_IN_SUPERTYPE_LISTS, indentFirst = true), + + strategy("Indices") + .within(INDICES) + .notForType(RBRACKET) + .set(Indent.getContinuationIndent(false)), + + strategy("Binary expressions") + .within(BINARY_EXPRESSIONS) + .forElement { node -> + !node.suppressBinaryExpressionIndent() + } + .set(Indent.getContinuationWithoutFirstIndent(false)), + + strategy("Parenthesized expression") + .within(PARENTHESIZED) + .set(Indent.getContinuationWithoutFirstIndent(false)), + + strategy("Opening parenthesis for conditions") + .forType(LPAR) + .within(IF, WHEN_ENTRY, WHILE, DO_WHILE) + .set(Indent.getContinuationWithoutFirstIndent(true)), + + strategy("Closing parenthesis for conditions") + .forType(RPAR) + .forElement { node -> !hasErrorElementBefore(node) } + .within(IF, WHEN_ENTRY, WHILE, DO_WHILE) + .set(Indent.getNoneIndent()), + + strategy("Closing parenthesis for incomplete conditions") + .forType(RPAR) + .forElement { node -> hasErrorElementBefore(node) } + .within(IF, WHEN_ENTRY, WHILE, DO_WHILE) + .set(Indent.getContinuationWithoutFirstIndent()), + + strategy("KDoc comment indent") + .within(KDOC_CONTENT) + .forType(KDocTokens.LEADING_ASTERISK, KDocTokens.END) + .set(Indent.getSpaceIndent(KDOC_COMMENT_INDENT)), + + strategy("Block in when entry") + .within(WHEN_ENTRY) + .notForType( + BLOCK, + WHEN_CONDITION_EXPRESSION, + WHEN_CONDITION_IN_RANGE, + WHEN_CONDITION_IS_PATTERN, + ELSE_KEYWORD, + ARROW, + ) + .set(Indent.getNormalIndent()), + + strategy("Parameter list") + .within(VALUE_PARAMETER_LIST) + .forElement { it.elementType == VALUE_PARAMETER && it.psi.prevSibling != null } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_IN_PARAMETER_LISTS, indentFirst = true), + + strategy("Where clause") + .within(CLASS, FUN, PROPERTY) + .forType(WHERE_KEYWORD) + .set(Indent.getContinuationIndent()), + + strategy("Array literals") + .within(COLLECTION_LITERAL_EXPRESSION) + .notForType(LBRACKET, RBRACKET) + .set(Indent.getNormalIndent()), + + strategy("Type aliases") + .within(TYPEALIAS) + .notForType( + TYPE_ALIAS_KEYWORD, EOL_COMMENT, MODIFIER_LIST, BLOCK_COMMENT, + DOC_COMMENT, + ) + .set(Indent.getContinuationIndent()), + + strategy("Default parameter values") + .within(VALUE_PARAMETER) + .forElement { node -> node.psi != null && node.psi == (node.psi.parent as? KtParameter)?.defaultValue } + .continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_EXPRESSION_BODIES, indentFirst = true), +) + + +private fun getOperationType(node: ASTNode): IElementType? = + node.findChildByType(OPERATION_REFERENCE)?.firstChildNode?.elementType + +fun hasErrorElementBefore(node: ASTNode): Boolean { + val prevSibling = getPrevWithoutWhitespace(node) + ?: return false + + if (prevSibling.elementType == TokenType.ERROR_ELEMENT) + return true + + val lastChild = TreeUtil.getLastChild(prevSibling) + return lastChild?.elementType == TokenType.ERROR_ELEMENT +} + +/** + * Suppress indent for binary expressions when there is a block higher in the tree that forces + * its indent to children ('if' condition or elvis). + */ +private fun ASTNode.suppressBinaryExpressionIndent(): Boolean { + var psi = psi.parent as? KtBinaryExpression ?: return false + while (psi.parent is KtBinaryExpression) { + psi = psi.parent as KtBinaryExpression + } + return psi.parent?.node?.elementType == CONDITION || psi.operationToken == ELVIS +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/formatter/TypingIndentationTestBaseGenerated.java b/idea/tests/org/jetbrains/kotlin/formatter/TypingIndentationTestBaseGenerated.java index 865634391f7..99266749f9b 100644 --- a/idea/tests/org/jetbrains/kotlin/formatter/TypingIndentationTestBaseGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/formatter/TypingIndentationTestBaseGenerated.java @@ -235,6 +235,11 @@ public class TypingIndentationTestBaseGenerated extends AbstractTypingIndentatio runTest("idea/testData/indentationOnNewline/KT20783.after.kt"); } + @TestMetadata("LargeFile.after.kt") + public void testLargeFile() throws Exception { + runTest("idea/testData/indentationOnNewline/LargeFile.after.kt"); + } + @TestMetadata("ModifierListInUnfinishedDeclaration.after.kt") public void testModifierListInUnfinishedDeclaration() throws Exception { runTest("idea/testData/indentationOnNewline/ModifierListInUnfinishedDeclaration.after.kt");