| /* |
| * 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<IElementType>() |
| private var withinCallback: ((ASTNode) -> Boolean)? = null |
| |
| private val notIn = ArrayList<IElementType>() |
| |
| private val forElement = ArrayList<IElementType>() |
| private val notForElement = ArrayList<IElementType>() |
| 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<IElementType>, singleType: IElementType, otherTypes: Array<out IElementType>) { |
| 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<IElementType>, |
| excludeElements: ArrayList<IElementType>, |
| 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<ASTBlock>, |
| 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<KtWhenExpression>().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 : PsiElement> 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,<caret>) |
| |
| 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<Builder>() |
| |
| 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<Condition>, |
| 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<Rule>() |
| private var conditions = ArrayList<Condition>() |
| |
| 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<TextRange> { |
| val rangesToExclude = mutableListOf<TextRange>() |
| 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<ASTNode>? = null, |
| ) { |
| @Volatile |
| private var mySubBlocks: List<ASTBlock>? = 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<ASTNode>? = null, |
| ): ASTBlock |
| |
| protected abstract fun createSyntheticSpacingNodeBlock(node: ASTNode): ASTBlock |
| |
| protected abstract fun getSubBlocks(): List<Block> |
| |
| 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<Block> { |
| 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<ASTBlock>): List<ASTBlock> { |
| 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<ASTBlock> |
| 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<ASTBlock>.replaceBlock(block: ASTBlock, index: Int = 0): List<ASTBlock> = 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<ASTBlock>.wrapToBlock( |
| anchor: ASTNode?, |
| parentBlock: ASTBlock?, |
| ): ASTBlock = splitAtIndex(0, null, null, anchor, parentBlock).single() |
| |
| private fun List<ASTBlock>.splitAtIndex( |
| index: Int, |
| indent: Indent?, |
| wrap: Wrap?, |
| anchor: ASTNode? = null, |
| parentBlock: ASTBlock? = null, |
| ): List<ASTBlock> { |
| 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<ASTBlock>): List<ASTBlock> { |
| 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<ASTNode>? = 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<ASTBlock> { |
| 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<ASTNode>() |
| 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<ASTBlock> { |
| 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<ASTNode>) { |
| 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<KtFunctionLiteral>()?.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<KtFunctionLiteral>().needTrailingComma(settings)) |
| return trailingCommaWrappingStrategy(leftAnchor = LBRACE, rightAnchor = ARROW) |
| } |
| |
| elementType === WHEN_ENTRY -> { |
| // with argument |
| if (nodePsi.cast<KtWhenEntry>().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<KtStringTemplateExpression>() == 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<KtQualifiedExpression>() |
| ?.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<KtPostfixExpression>()?.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 |
| } |
| |
| // WITHOUT_CUSTOM_LINE_INDENT_PROVIDER |